diff --git a/docs/tutorial.md b/docs/tutorial.md index 0054e34..d06060b 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -224,6 +224,9 @@ This also prevents chain assignments so use parentheses: ## Nullability +Nullability is part of the type. `String` is non-null, `String?` is nullable. Use `!!` to assert non-null and throw +`NullReferenceException` if the value is `null`. + When the value is `null`, it might throws `NullReferenceException`, the name is somewhat a tradition. To avoid it one can check it against null or use _null coalescing_. The null coalescing means, if the operand (left) is null, the operation won't be performed and the result will be null. Here is the difference: @@ -242,6 +245,9 @@ the operation won't be performed and the result will be null. Here is the differ assert( ref?() == null ) >>> void +Note: `?.` is still a typed operation. The receiver must have a compile-time type that declares the member; if the +receiver is `Object`, cast it first or declare a more specific type. + There is also "elvis operator", null-coalesce infix operator '?:' that returns rvalue if lvalue is `null`: null ?: "nothing" @@ -425,8 +431,6 @@ Almost the same, using `val`: val foo = 1 foo += 1 // this will throw exception -# Constants - Same as in kotlin: val HalfPi = π / 2 @@ -434,6 +438,133 @@ Same as in kotlin: Note using greek characters in identifiers! All letters allowed, but remember who might try to read your script, most likely will know some English, the rest is the pure uncertainty. +# Types and inference + +Lyng uses Kotlin-style static types with inference. You can always write explicit types, but in most places the compiler +can infer them from literals, defaults, and flow analysis. + +## Type annotations + +Use `:` to specify a type: + + var x: Int = 10 + val label: String = "count" + fun clamp(x: Int, min: Int, max: Int): Int { ... } + +`Object` is the top type. If you omit a type and there is no default value, the parameter is `Object` by default: + + fun show(x) { println(x) } // x is Object + +For nullable types, add `?`: + + fun showMaybe(x: Object?) { ... } + fun parseInt(s: String?): Int? { ... } + +There is also a nullable shorthand for untyped parameters and constructor args: `x?` means `x: Object?`. +It cannot be combined with an explicit type annotation. + + class A(x?) { ... } // x: Object? + fun f(x?) { x == null } // x: Object? + +`void` is a singleton value of the class `Void`. `Void` can be used as an explicit return type: + + fun log(msg): Void { println(msg); void } + +`Null` is the class of `null`. It is a singleton type and mostly useful for type inference results. + +## Type inference + +The compiler infers types from: + +- literals: `1` is `Int`, `1.0` is `Real`, `"s"` is `String`, `'c'` is `Char` +- defaults: `fun f(x=1, name="n")` infers `x: Int`, `name: String` +- assignments: `val x = call()` uses the return type of `call` +- returns and branches: the result type of a block is the last expression, and if any branch is nullable, + the inferred type becomes nullable +- numeric ops: `Int` and `Real` stay `Int` when both sides are `Int`, and promote to `Real` on mixed arithmetic + +Examples: + + fun inc(x=0) = x + 1 // (Int)->Int + fun maybe(flag) { if(flag) 1 else null } // ()->Int? + +Untyped locals are allowed, but their type is fixed on the first assignment: + + var x + x = 1 // x becomes Int + x = "one" // compile-time error + + var y = null // y is Object? + val z = null // z is Null + +Empty list/map literals default to `List` and `Map` until a more specific type is known: + + val xs = [] // List + val ys: List = [] // List + +## Flow analysis + +Lyng uses flow analysis to narrow types inside branches: + + fun len(x: String?): Int { + if( x == null ) return 0 + // x is String (non-null) in this branch + return x.length + } + +`is` checks and `when` branches also narrow types: + + fun kind(x: Object) { + if( x is Int ) return "int" + if( x is String ) return "string" + return "other" + } + +Narrowing is local to the branch; after the branch, the original type is restored. + +## Casts and unknown types + +Use `as` for explicit casts. The compiler inserts casts only when it can be valid and necessary. If a cast fails at +runtime, it throws `ClassCastException`. If the value is nullable, `as T` implies a non-null assertion. + +Member access is resolved at compile time. Only members of `Object` are available on unknown types; non-Object members +require an explicit cast: + + fun f(x) { // x is Object + x.toString() // ok (Object member) + x.size() // compile-time error + (x as List).size() // ok + } + +This avoids runtime name-resolution fallbacks; all symbols must be known at compile time. + +## Generics and bounds + +Generic parameters are declared with `<...>`: + + fun id(x: T): T = x + class Box(val value: T) + +Bounds use `:` and can combine with `&` (intersection) and `|` (union): + + fun sum(x: T, y: T) = x + y + class Named(val data: T) + +Type arguments are usually inferred from call sites: + + val b = Box(10) // Box + val s = id("ok") // T is String + +## Variance + +Generic types are invariant by default, so `List` is not a `List`. +Use declaration-site variance when you need it: + + class Source(val value: T) + class Sink { fun accept(x: T) { ... } } + +`out` makes the type covariant (only produced), `in` makes it contravariant (only consumed). + # Defining functions fun check(amount) { @@ -1330,7 +1461,7 @@ than enum arrays, until `Lynon.encodeTyped` will be implemented. var result = null // here we will store the result >>> null -# Integral data types +# Built-in types | type | description | literal samples | |--------|---------------------------------|---------------------| @@ -1340,6 +1471,7 @@ than enum arrays, until `Lynon.encodeTyped` will be implemented. | Char | single unicode character | `'S'`, `'\n'` | | String | unicode string, no limits | "hello" (see below) | | List | mutable list | [1, "two", 3] | +| Object | top type for all values | | | Void | no value could exist, singleton | void | | Null | missing value, singleton | null | | Fn | callable type | | @@ -1731,4 +1863,4 @@ Example with custom accessors: Extension members are **scope-isolated**: they are visible only in the scope where they are defined and its children. This prevents name collisions and improves security. -To get details on OOP in Lyng, see [OOP notes](OOP.md). \ No newline at end of file +To get details on OOP in Lyng, see [OOP notes](OOP.md). diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 0bcaf81..74d1996 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -2641,8 +2641,31 @@ class Compiler( access } + // Nullable shorthand: "x?" means Object? (or makes explicit type nullable) + var nullableHint = false + val nullableHintPos = cc.savePos() + cc.skipWsTokens() + if (cc.peekNextNonWhitespace().type == Token.Type.QUESTION) { + cc.nextNonWhitespace() + nullableHint = true + } else { + cc.restorePos(nullableHintPos) + } + // type information (semantic + mini syntax) - val (typeInfo, miniType) = parseTypeDeclarationWithMini() + if (nullableHint) { + val afterHint = cc.savePos() + cc.skipWsTokens() + if (cc.peekNextNonWhitespace().type == Token.Type.COLON) { + throw ScriptError(t.pos, "nullable shorthand '?' cannot be combined with explicit type") + } + cc.restorePos(afterHint) + } + var (typeInfo, miniType) = parseTypeDeclarationWithMini() + if (nullableHint) { + typeInfo = makeTypeDeclNullable(typeInfo) + miniType = miniType?.let { makeMiniTypeNullable(it) } + } var defaultValue: Statement? = null cc.ifNextIs(Token.Type.ASSIGN) { @@ -2713,6 +2736,31 @@ class Compiler( return parseTypeUnionWithMini() } + private fun makeTypeDeclNullable(type: TypeDecl): TypeDecl { + if (type.isNullable) return type + return when (type) { + TypeDecl.TypeAny -> TypeDecl.TypeNullableAny + TypeDecl.TypeNullableAny -> type + is TypeDecl.Function -> type.copy(nullable = true) + is TypeDecl.TypeVar -> type.copy(nullable = true) + is TypeDecl.Union -> type.copy(nullable = true) + is TypeDecl.Intersection -> type.copy(nullable = true) + is TypeDecl.Simple -> TypeDecl.Simple(type.name, true) + is TypeDecl.Generic -> TypeDecl.Generic(type.name, type.args, true) + } + } + + private fun makeMiniTypeNullable(type: MiniTypeRef): MiniTypeRef { + return when (type) { + is MiniTypeName -> type.copy(nullable = true) + is MiniGenericType -> type.copy(nullable = true) + is MiniFunctionType -> type.copy(nullable = true) + is MiniTypeVar -> type.copy(nullable = true) + is MiniTypeUnion -> type.copy(nullable = true) + is MiniTypeIntersection -> type.copy(nullable = true) + } + } + private fun parseTypeUnionWithMini(): Pair { var left = parseTypeIntersectionWithMini() val options = mutableListOf(left) diff --git a/lynglib/src/commonTest/kotlin/TypesTest.kt b/lynglib/src/commonTest/kotlin/TypesTest.kt index 2c75463..c81bc09 100644 --- a/lynglib/src/commonTest/kotlin/TypesTest.kt +++ b/lynglib/src/commonTest/kotlin/TypesTest.kt @@ -146,4 +146,24 @@ class TypesTest { println("Result: "+limit) """.trimIndent()) } + + @Test + fun testNullableHints() = runTest { + eval(""" + // nullable, without type os Object? + class N(x=null) + assertEquals(null, N().x) + assertEquals("foo", N("foo").x) + // nullable shortcut (x?)is same as (var x: Object?) + class A(x?) + assertEquals(null, A(null).x) + assertEquals("ok", A("ok").x) + + // same in function: x? is a shortcut for (x: Object?) + fun f(x?) = x?.let { x + "!" } + assertEquals(null, f(null)) + assertEquals("ok!", f("ok")) + """.trimIndent() + ) + } }