From dc40940af1b520a0d68ab370961e3bd0456270f5 Mon Sep 17 00:00:00 2001 From: fubark Date: Mon, 2 Oct 2023 14:08:21 -0400 Subject: [PATCH] Update type system design. Reserve `auto` keyword. --- docs/hugo/content/docs/toc/syntax.md | 10 +- docs/hugo/content/docs/toc/type-system.md | 225 +++++++++++----- .../layouts/partials/docs/inject/body.html | 2 +- docs/hugo/static/style.css | 2 + exts/sublime/cyber.sublime-syntax | 2 +- src/cbindgen.cy | 247 +++++++++--------- src/cyber.zig | 10 + src/main.zig | 4 +- src/parser.zig | 6 +- src/sema.zig | 61 +++-- test/behavior_test.zig | 101 ++++--- test/import_test.cy | 6 +- test/static_func_test.cy | 6 +- test/staticvar_decl_test.cy | 8 +- test/test_mods/a.cy | 4 +- 15 files changed, 426 insertions(+), 268 deletions(-) diff --git a/docs/hugo/content/docs/toc/syntax.md b/docs/hugo/content/docs/toc/syntax.md index b18ab4d71..95bbd30f6 100644 --- a/docs/hugo/content/docs/toc/syntax.md +++ b/docs/hugo/content/docs/toc/syntax.md @@ -46,7 +46,9 @@ func foo(): ``` ## Variables. -In Cyber, there are local variables and static variables. +In Cyber, there are local variables and static variables. The following sections show how variables are declared with the dynamic type. + +For declaring typed variables, see [Typed variables]({{}}) and [`auto` declarations]({{}}). ### Local Variables. Local variables exist until the end of their scope. @@ -163,11 +165,11 @@ var myImage: The final resulting value that is assigned to the static variable is provided by a `break` statement. If a `break` statement is not provided, `none` is assigned instead. ## Keywords. -There are currently `33` keywords in Cyber. This list categorizes them and shows you when you might need them. +There are currently `34` keywords in Cyber. This list categorizes them and shows you when you might need them. -- [Control Flow]({{}}): `if` `else` `match` `while` `for` `each` `break` `continue` `pass` `some` +- [Control Flow]({{}}): `if` `else` `match` `case` `while` `for` `each` `break` `continue` `pass` `some` - [Operators](#operators): `or` `and` `not` `is` -- [Variables](#variables): `var` `as` +- [Variables](#variables): `var` `as` `auto` - [Functions]({{}}): `func` `return` - [Coroutines]({{}}): `coinit` `coyield`, `coresume` - [Data Types]({{}}): `type` `object` `enum` `true` `false` `none` diff --git a/docs/hugo/content/docs/toc/type-system.md b/docs/hugo/content/docs/toc/type-system.md index 8c3850163..1b96e7ed7 100644 --- a/docs/hugo/content/docs/toc/type-system.md +++ b/docs/hugo/content/docs/toc/type-system.md @@ -5,37 +5,84 @@ weight: 9 --- # Type System. -Cyber supports gradual typing which allows the use of both dynamically and statically typed code. -> _Incomplete: Types in general is in development. One of the goals of Cyber is to let dynamic code mix with typed code. At the moment, there are places where it works and other places where it won't. Keep that in mind when using types._ +Cyber supports the use of both dynamically and statically typed code. +## Dynamic typing. Dynamic typing can reduce the amount of friction when writing code, but it can also result in more runtime errors. -Gradual typing allows you to add static typing incrementally which provides compile-time guarantees and prevents runtime errors. -Static typing also makes it easier to maintain and refactor your code. -## Dynamic typing. -A variable with the `any` type can hold any value. It can only be copied to destinations that also accept the `any` type. An `any` value can be used as the callee for a function call or the receiver for a method call. It can be used with any operators. +### `dynamic` vs `any` +Variables without a type specifier are implicitly assigned the `dynamic` type. +`dynamic` values can be freely used and copied without any compile errors (if there is a chance it can succeed at runtime, see [Recent type inference](#recent-type-inference)): +```cy +var a = 123 + +func getFirstRune(s string): + return s[0] + +getFirstRune(a) -- RuntimeError. Expected `string`. +``` +Since `a` is dynamic, passing it to a typed function parameter is allowed at compile-time, but will fail when the function is invoked at runtime. + +The `any` type on the otherhand is a **static type** and must be explicitly declared as a variable's type specifier: +```cy +var a any = 123 + +func getFirstRune(s string): + return s[0] + +getFirstRune(a) -- CompileError. Expected `string`. +``` +This same setup will now fail at compile-time because `any` does not satisfy the destination's `string` type constraint. + +The use of the `dynamic` type effectively defers type checking to runtime while `any` is a static type and must adhere to type constraints at compile-time. + +A `dynamic` value can be used in any operation. It can be invoked as the callee, invoked as the receiver of a method call, or used with operators. + +### Invoking `dynamic` values. +When a `dynamic` value is invoked, checks on whether the callee is a function is deferred to runtime. +```cy +var op = 123 +print op(1, 2, 3) -- RuntimeError. Expected a function. +``` + +### Dynamic return value. +When the return type of a function is not specified, it defaults to the `dynamic` type. +This allows copying the return value to a typed destination without casting: +```cy +func getValue(): + return 123 + +func add(a int, b int): + return a + b + +print add(getValue(), 2) -- Prints "125" +``` +The `add` function defers type checking of `getValue()` to runtime because it has the `dynamic` type. -## Compile-time dynamic typing. -Cyber introduces the concept of compile-time dynamic typing. This allows a local variable to gain additional compile-time features while using it as a dynamic value. It can prevent inevitable runtime errors and avoid unnecessary type casts. +### Recent type inference. +Although a `dynamic` variable has the most flexibility, in some situations it is advantageous to know what type it could be. -Local variables declared without a type specifier start off with the type of their initializer. In the following, `a` is implicity declared as a `float` at compile-time because numeric literals default to the `float` type. +The compiler keeps a running record of a `dynamic` variable's most **recent type** to gain additional compile-time features without sacrificing flexibility. It can prevent inevitable runtime errors and avoid unnecessary type casts. + +When a `dynamic` variable is first initialized, it has a recent type inferred from its initializer. In the following, `a` has the recent type of `int` at compile-time because numeric literals default to the `int` type: ```cy var a = 123 ``` -The type can change at compile-time from another assignment. -If `a` is then assigned to a string literal, `a` from that point on becomes the `string` type at compile-time. +The recent type can change at compile-time from another assignment. +If `a` is then assigned to a string literal, `a` from that point on has the recent type of `string` at compile-time: ```cy var a = 123 foo(a) -- Valid call expression. a = 'hello' -foo(a) -- CompileError. Expected `float` argument, got `string`. +foo(a) -- CompileError. Expected `int` argument, got `string`. -func foo(n float): +func foo(n int): pass ``` +Even though `a` is `dynamic` and is usually allowed to defer type checking to runtime, the compiler knows that doing so in this context would **always** result in a runtime error, so it provides a compile error instead. This provides a quicker feedback to fix the problem. -The type of `a` can also change in branches. However, after the branch block, `a` will have a merged type determined by the types assigned to `a` from the two branched code paths. Currently, the `any` type is used if the types from the two branches differ. At the end of the following `if` block, `a` assumes the `any` type after merging the `float` and `string` types. +The recent type of `a` can also change in branches. However, after the branch block, `a` will have a recent type after merging the types assigned to `a` from the two branched code paths. Currently, the `any` type is used if the types from the two branches differ. At the end of the following `if` block, `a` has the recent type of `any` type after merging the `int` and `string` types: ```cy var a = 123 if a > 20: @@ -48,71 +95,72 @@ func foo(s string): pass ``` -## Default types. -Static variables without a type specifier will always default to the `any` type. In the following, `a` is compiled with the `any` type despite being initialized to a numeric literal. +## Static typing. +Static typing can be incrementally applied which provides compile-time guarantees and prevents runtime errors. +Static typing also makes it easier to maintain and refactor your code. +> _Incomplete: Static types in general is in development. One of the goals of Cyber is to let dynamic code mix with typed code. At the moment, there are places where it works and other places where it won't. Keep that in mind when using types._ + +### Builtin types. +The following builtin types are available in every module: `boolean`, `float`, `int`, `string`, `List`, `Map`, `error`, `fiber`, `any`. + +### Typed variables. +A typed local variable can be declared by attaching a type specifier after its name. The value assigned to the variable must satisfy the type constraint or a compile error is issued. +> _Incomplete: Only function parameter and object member type specifiers have meaning to the VM at the moment. Variable type specifiers have no meaning and will be discarded._ ```cy -var a: 123 -a = 'hello' +var a float = 123 + +var b int = 123.0 -- CompileError. Expected `int`, got `float`. ``` -Function parameters without a type specifier will default to the `any` type. The return type also defaults to `any`. In the following, both `a` and `b` have the `any` type despite being only used for arithmetic. +Any operation afterwards that violates the type constraint of the variable will result in a compile error. ```cy -func add(a, b): - return a + b - -print add(3, 4) +a = 'hello' -- CompileError. Expected `float`, got `string`. ``` -## Static typing. -In Cyber, types can be optionally declared with variables, parameters, and return values. -The following builtin types are available in every namespace: `bool`, `float`, `int`, `string`, `List`, `Map`, `error`, `fiber`, `any`. +Static variables are declared in a similar way except `:` is used instead of `=`: +```cy +var global Map: {} +``` -A `type object` declaration creates a new object type. +Type specifiers must be resolved at compile-time. ```cy -type Student object: -- Creates a new type named `Student` - name string - age int - gpa float +var foo Foo = none -- CompileError. Type `Foo` is not declared. ``` -When a type specifier follows a variable name, it declares the variable with the type. Any operation afterwards that violates the type constraint will result in a compile error. -> _Incomplete: Only function parameter and object member type specifiers have meaning to the VM at the moment. Variable type specifiers have no meaning and will be discarded._ +### `auto` declarations. +The `auto` declaration infers the type of the assigned value and initializes the variable with the same type. +> _Planned Feature_ ```cy -a float = 123 -a = 'hello' -- CompileError. Type mismatch. +-- Initialized as an `int` variable. +auto a = 123 ``` -Parameter and return type specifiers in a function signature follows the same syntax. +`auto` declarations are strictly for static typing. If the assigned value's type is `dynamic`, the variable's type becomes `any`. ```cy -func mul(a float, b float) float: - return a * b +func getValue(): + return ['a', 'list'] -print mul(3, 4) -print mul(3, '4') -- CompileError. Function signature mismatch. +-- Initialized as an `any` variable. +auto a = getValue() ``` -Type specifiers must be resolved at compile-time. +### Object types. +A `type object` declaration creates a new object type. Member types are declared with a type specifier after their name. ```cy -type Foo object: - a float - b string - c Bar -- CompileError. Bar is not declared. +type Student object: -- Creates a new type named `Student` + name string + age int + gpa float ``` Circular type references are allowed. ```cy type Node object: - val any + val any next Node -- Valid type specifier. ``` -## Union types. -> _Planned Feature_ - -## Traits. -> _Planned Feature_ - -## Type aliases. +### Type aliases. A type alias is declared from a single line `type` statement. This creates a new type symbol for an existing data type. ```cy import util './util.cy' @@ -122,29 +170,68 @@ type Vec3 util.Vec3 var v = Vec3{ x: 3, y: 4, z: 5 } ``` -## Type casting. -The `as` keyword can be used to cast a value to a specific type. Casting lets the compiler know what the expected type is and does not perform any conversions. -If the compiler knows the cast will always fail at runtime, a compile error is returned instead. -If the cast fails at runtime, a panic is returned. +### Functions. +Function parameter and return type specifiers follows a similiar syntax. ```cy -print('123' as float) -- CompileError. Can not cast `string` to `float`. +func mul(a float, b float) float: + return a * b -erased any = 123 -add(1, erased as float) -- Success. +print mul(3, 4) +print mul(3, '4') -- CompileError. Function signature mismatch. +``` -print(erased as string) -- Panic. Can not cast `float` to `string`. +### Traits. +> _Planned Feature_ -func add(a float, b float): +### Union types. +> _Planned Feature_ + +### `any` type. +A variable with the `any` type can hold any value, but copying it to narrowed type destination will result in a compile error: +```cy +func square(i int): + return i * i + +var a any = 123 +a = ['a', 'list'] -- Valid assignment to a value with a different type. +a = 10 + +print square(a) -- CompileError. Expected `int`, got `any`. +``` +`a` must be explicitly casted to satisfy the type constraint: +```cy +print square(a as int) -- Prints "100". +``` + +### Invoking `any` values. +Since `any` is a static type, invoking an `any` value must be explicitly casted to the appropriate function type. +> _Planned Feature: Casting to a function type is not currently supported._ + +```cy +func add(a int, b int) int: return a + b + +var op any = add +print op(1, 2) -- CompileError. Expected `func (int, int) any` + +auto opFunc = op as (func (int, int) int) +print opFunc(1, 2) -- Prints "3". +``` + +### Type casting. +The `as` keyword can be used to cast a value to a specific type. Casting lets the compiler know what the expected type is and does not perform any conversions. + +If the compiler knows the cast will always fail at runtime, a compile error is returned instead. +```cy +print('123' as int) -- CompileError. Can not cast `string` to `int`. ``` -## Runtime type checking. -Since Cyber allows invoking `any` function values, the callee's function signature is not always known at compile-time. To ensure type safety in this situation, type checking is done at runtime and with no additional overhead compared to calling an untyped function. +If the cast fails at runtime, a panic is returned. ```cy -op any = add -print op(1, 2) -- '3' -print op(1, '2') -- Panic. Function signature mismatch. +var erased any = 123 +add(1, erased as int) -- Success. +print(erased as string) -- Panic. Can not cast `int` to `string`. -func add(a float, b float) float: +func add(a int, b int): return a + b -``` +``` \ No newline at end of file diff --git a/docs/hugo/layouts/partials/docs/inject/body.html b/docs/hugo/layouts/partials/docs/inject/body.html index 9d398655e..2b292ac9c 100644 --- a/docs/hugo/layouts/partials/docs/inject/body.html +++ b/docs/hugo/layouts/partials/docs/inject/body.html @@ -7,7 +7,7 @@ 'func', 'import', 'for', 'coinit', 'coresume', 'coyield', 'return', 'if', 'else', 'as', 'each', 'while', 'var', 'object', 'break', 'continue', 'match', 'pass', 'or', 'and', 'not', 'is', 'some', 'error', - 'true', 'false', 'none', 'throw', 'try', 'catch', 'recover', 'enum', 'type', 'case' + 'true', 'false', 'none', 'throw', 'try', 'catch', 'recover', 'enum', 'type', 'case', 'auto' ], type: [ 'float', 'string', 'bool', 'any', 'int', 'List', 'Map', 'rawstring', 'symbol', 'pointer' diff --git a/docs/hugo/static/style.css b/docs/hugo/static/style.css index 3b5ffc6c2..4ce3d7c38 100644 --- a/docs/hugo/static/style.css +++ b/docs/hugo/static/style.css @@ -39,6 +39,8 @@ html, body { .markdown h3 { color: var(--accent-light); /* font-weight: bold; */ + font-size: 1.2em; + margin-top: 2.2em; } .markdown table tr:nth-child(2n) { diff --git a/exts/sublime/cyber.sublime-syntax b/exts/sublime/cyber.sublime-syntax index 2eb5024d7..475f757a7 100644 --- a/exts/sublime/cyber.sublime-syntax +++ b/exts/sublime/cyber.sublime-syntax @@ -33,7 +33,7 @@ contexts: - match: '\b(or|and|not|is)\b' scope: keyword.operator.cyber - - match: '\b(var|static|capture|as)\b' + - match: '\b(var|as|auto)\b' scope: keyword.variable.cyber - match: '\b(func|return)\b' diff --git a/src/cbindgen.cy b/src/cbindgen.cy index 7f6655951..5885526df 100755 --- a/src/cbindgen.cy +++ b/src/cbindgen.cy @@ -1,63 +1,67 @@ #!cyber import os -POST_HEADER = " +var POST_HEADER = " " -args = os.parseArgs([ +var args = os.parseArgs([ { name: 'update', type: string, default: none } { name: 'libpath', type: string, default: 'lib.dll' } ]) -skipMap = {} -existingLibPath = false +var skipMap = {} +var existingLibPath = false +var markerPos = none +var existing = none if args.update != none: -- Build skip map. - existing = readFile(args.update) + existing = os.readFile(args.update) markerPos = existing.find('\n-- CBINDGEN MARKER') if markerPos == none: markerPos = existing.len() - res = parseCyber(existing) + var res = parseCyber(existing) for res.decls each decl: if decl.pos < markerPos: - if decl.type == 'func': + match decl.type: + case 'func': skipMap[decl.name] = true - else decl.type == 'funcInit': + case 'funcInit': skipMap[decl.name] = true - else decl.type == 'object': + case 'object': skipMap[decl.name] = true - else decl.type == 'variable': + case 'variable': if decl.name == 'libPath': existingLibPath = true -headerPath = args.rest[2] +var headerPath = args.rest[2] -headerSrc = readFile(headerPath).utf8() -writeFile('temp.h', headerSrc.concat(POST_HEADER)) +var headerSrc = os.readFile(headerPath).utf8() +os.writeFile('temp.h', headerSrc.concat(POST_HEADER)) -res = execCmd(['/bin/bash', '-c', 'zig translate-c temp.h']) -zigSrc = res.out -writeFile('bindings.zig', zigSrc) +var res = os.execCmd(['/bin/bash', '-c', 'zig translate-c temp.h']) +var zigSrc = res.out +os.writeFile('bindings.zig', zigSrc) -- Build output string. -out = '' +var out = '' -- State. var structMap: {} -- structName -> list of fields (ctype syms) var structs: [] -var aliases: {} -- aliasName -> structName or binding symbol (eg: #voidPtr) +var aliases: {} -- aliasName -> structName or binding symbol (eg: .voidPtr) var arrays: {} -- [n]typeName -> true -state = #parseFirst -vars = {} -- varName -> bindingType -funcs = [] -skipFields = false +var state = .parseFirst +var vars = {} -- varName -> bindingType +var funcs = [] +var skipFields = false +var structFields = [] -lines = zigSrc.split('\n') +var lines = zigSrc.split('\n') for lines each line: - if state == #parseFirst: + if state == .parseFirst: if line.startsWith('pub const '): line = line['pub const '.len()..] if line.startsWith('__'): @@ -67,13 +71,13 @@ for lines each line: -- Skip zig keywords idents. continue - assignIdx = line.findRune(0u'=') + var assignIdx = line.findRune(0u'=') - beforeAssign = line[..assignIdx-1] - idx = beforeAssign.findRune(0u':') + var beforeAssign = line[..assignIdx-1] + var idx = beforeAssign.findRune(0u':') if idx == none: - name = beforeAssign - init = line[assignIdx+2..] + var name = beforeAssign + var init = line[assignIdx+2..] if init.startsWith('@'): -- Skip `= @import` continue @@ -88,12 +92,12 @@ for lines each line: continue if name.startsWith('struct_'): - structName = name['struct_'.len()..] + var structName = name['struct_'.len()..] if init == 'opaque \{\};': out = out.concat('type {structName} pointer\n') - aliases[structName] = #voidPtr + aliases[structName] = .voidPtr else: - state = #parseField + state = .parseField if skipMap[structName]: skipFields = true out = out.concat('-- Skip type {structName} object:\n') @@ -105,22 +109,22 @@ for lines each line: continue if init == 'c_uint;': - out = out.concat('type {name} number\n') - aliases[name] = #uint + out = out.concat('type {name} int\n') + aliases[name] = .uint continue else init.startsWith('?*const'): -- Function pointer type alias. out = out.concat('type {name} pointer\n') - aliases[name] = #voidPtr + aliases[name] = .voidPtr continue else init.startsWith('"'): -- String constant. - right = init.trim(#right, ';') + var right = init.trim(.right, ';') out = out.concat('var {name}: {right}\n') continue else: -- Type alias. - right = init.trim(#right, ';') + var right = init.trim(.right, ';') if structMap[right]: -- Found existing type. out = out.concat('type {name} {right}\n') @@ -132,35 +136,35 @@ for lines each line: print 'TODO {line}' else: -- Has var specifier. - name = beforeAssign[..idx] - spec = beforeAssign[idx+2..] - init = line[assignIdx+2..line.len()-1] + var name = beforeAssign[..idx] + var spec = beforeAssign[idx+2..] + var init = line[assignIdx+2..line.len()-1] if spec == 'c_int': - out = out.concat('var {name} number: {init}\n') - vars[name] = #int + out = out.concat('var {name} int: {init}\n') + vars[name] = .int else line.startsWith('pub extern fn '): line = line['pub extern fn '.len()..] -- Parse func signature. -- Find start. - idx = line.findRune(0u'(') - name = line[..idx] + var idx = line.findRune(0u'(') + var name = line[..idx] line = line[idx+1..] -- Find end. idx = line.findRune(0u')') - paramsStr = line[..idx] - ret = line[idx+2..].trim(#right, ';') + var paramsStr = line[..idx] + var ret = line[idx+2..].trim(.right, ';') - outFunc = 'func {name}(' + var outFunc = 'func {name}(' - fn = {} + var fn = {} fn['name'] = name -- Parse params. - fnParamTypes = [] - params = paramsStr.split(', ') + var fnParamTypes = [] + var params = paramsStr.split(', ') for params each param: if param.len() == 0: continue @@ -170,12 +174,12 @@ for lines each line: break idx = param.findRune(0u':') - paramName = param[..idx] + var paramName = param[..idx] if paramName.startsWith('@"'): paramName = paramName[2..paramName.len()-1] - paramSpec = param[idx+2..] + var paramSpec = param[idx+2..] - paramType = getBinding(paramSpec) + var paramType = getBinding(paramSpec) if paramType == none: outFunc = none break @@ -189,7 +193,7 @@ for lines each line: outFunc = outFunc.concat(') ') - retType = getBinding(ret) + var retType = getBinding(ret) if retType != none: outFunc = outFunc.concat('{getCyName(retType)} = lib.{name}') if skipMap[name]: @@ -203,25 +207,25 @@ for lines each line: else: -- print 'TODO {line}' pass - else state == #parseField: + else state == .parseField: if line == '};': - state = #parseFirst + state = .parseFirst out = out.concat('\n') skipFields = false continue - line = line.trim(#ends, ' ,') - idx = line.findRune(0u':') - name = line[..idx] - spec = line[idx+2..] + line = line.trim(.ends, ' ,') + var idx = line.findRune(0u':') + var name = line[..idx] + var spec = line[idx+2..] - fieldType = getBinding(spec) + var fieldType = getBinding(spec) if fieldType != none: if skipFields: out = out.concat('-- ') - if fieldType == #voidPtr: + if fieldType == .voidPtr: out = out.concat(' {name} pointer -- {spec}\n') - else typesym(fieldType) == #string and fieldType.startsWith('os.CArray'): + else typesym(fieldType) == .string and fieldType.startsWith('os.CArray'): out = out.concat(' {name} List -- {spec}\n') else: out = out.concat(' {name} {getCyName(fieldType)}\n') @@ -236,40 +240,41 @@ out = out.concat(" decls = []\n") for funcs each fn: out = out.concat(" decls.append(os.CFunc\{ sym: '{fn.name}', args: [{fn.params.joinString(', ')}], ret: {fn.ret} \})\n") for structs each name: - fields = structMap[name] + var fields = structMap[name] out = out.concat(" decls.append(os.CStruct\{ fields: [{fields.joinString(', ')}], type: {name} \})\n") -- libPath = if existingLibPath then 'libPath' else "'{args.libpath}'" -libPath = 'libPath' +var libPath = 'libPath' out = out.concat(" return os.bindLib({libPath}, decls, \{ genMap: true \})") -- Final output. if args.update != none: out = existing[0..markerPos].concat('\n-- CBINDGEN MARKER\n').concat(out) -writeFile('bindings.cy', out) + +os.writeFile('bindings.cy', out) func getCyName(nameOrSym): - if typesym(nameOrSym) == #symbol: - sym = nameOrSym - if sym == #voidPtr: + if typesym(nameOrSym) == .symbol: + match nameOrSym: + case .voidPtr: return 'pointer' - else sym == #bool: + case .bool: return 'boolean' - else sym == #int: - return 'number' - else sym == #uint: - return 'number' - else sym == #uchar: - return 'number' - else sym == #long: - return 'number' - else sym == #float: - return 'number' - else sym == #double: - return 'number' - else sym == #voidPtr: + case .int: + return 'int' + case .uint: + return 'int' + case .uchar: + return 'int' + case .long: + return 'int' + case .float: + return 'float' + case .double: + return 'float' + case .voidPtr: return 'pointer' - else sym == #void: + case .void: return 'none' else: print 'TODO getCyName {nameOrSym}' @@ -278,41 +283,43 @@ func getCyName(nameOrSym): return nameOrSym func getBinding(spec): - if spec == 'void': - return #void - else spec == 'bool': - return #bool - else spec == 'c_int': - return #int - else spec == 'c_uint': - return #uint - else spec == 'u8': - return #uchar - else spec == 'f32': - return #float - else spec == 'f64': - return #double - else spec == 'c_long': - return #long - else spec.startsWith('?*'): - return #voidPtr - else spec.startsWith('[*c]'): - return #voidPtr - else spec.startsWith('['): - idx = spec.findRune(0u']') - n = spec[1..idx] - elem = getBinding(spec[idx+1..]) - if elem == none: - print 'TODO getBinding {spec}' - return none - return 'os.CArray\{ n: {n}, elem: {elem}\}' + match spec: + case 'void': + return .void + case 'bool': + return .bool + case 'c_int': + return .int + case 'c_uint': + return .uint + case 'u8': + return .uchar + case 'f32': + return .float + case 'f64': + return .double + case 'c_long': + return .long else: - if structMap[spec]: - -- Valid type. - return spec - else aliases[spec]: - -- Valid alias. - return aliases[spec] + if spec.startsWith('?*'): + return .voidPtr + else spec.startsWith('[*c]'): + return .voidPtr + else spec.startsWith('['): + var idx = spec.findRune(0u']') + var n = spec[1..idx] + var elem = getBinding(spec[idx+1..]) + if elem == none: + print 'TODO getBinding {spec}' + return none + return 'os.CArray\{ n: {n}, elem: {elem}\}' else: - print 'TODO getBinding {spec}' - return none \ No newline at end of file + if structMap[spec]: + -- Valid type. + return spec + else aliases[spec]: + -- Valid alias. + return aliases[spec] + else: + print 'TODO getBinding {spec}' + return none \ No newline at end of file diff --git a/src/cyber.zig b/src/cyber.zig index 6e9788d5a..52a6a845f 100644 --- a/src/cyber.zig +++ b/src/cyber.zig @@ -277,6 +277,16 @@ pub const panic = utils.panic; pub const panicFmt = utils.panicFmt; pub const dassert = utils.dassert; +pub fn writeStderr(s: []const u8) void { + @setCold(true); + const w = fmt.lockStderrWriter(); + defer fmt.unlockPrint(); + _ = w.writeAll(s) catch |err| { + log.debug("{}", .{err}); + fatal(); + }; +} + pub inline fn unexpected() noreturn { panic("unexpected"); } diff --git a/src/main.zig b/src/main.zig index edee0a708..492e337a3 100644 --- a/src/main.zig +++ b/src/main.zig @@ -153,7 +153,7 @@ fn compilePath(alloc: std.mem.Allocator, path: []const u8) !void { if (!cy.silentError) { const report = try vm.allocLastErrorReport(); defer alloc.free(report); - fmt.printStderr(report, &.{}); + cy.writeStderr(report); } exit(1); }, @@ -185,7 +185,7 @@ fn evalPath(alloc: std.mem.Allocator, path: []const u8) !void { if (!cy.silentError) { const report = try vm.allocLastErrorReport(); defer alloc.free(report); - fmt.printStderr(report, &.{}); + cy.writeStderr(report); } exit(1); }, diff --git a/src/parser.zig b/src/parser.zig index 8decb7a83..91dfcace0 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -23,6 +23,7 @@ const annotations = std.ComptimeStringMap(AnnotationType, .{ const keywords = std.ComptimeStringMap(TokenType, .{ .{ "and", .and_k }, .{ "as", .as_k }, + .{ "auto", .auto_k }, // .{ "await", .await_k }, .{ "break", .break_k }, .{ "case", .case_k }, @@ -3334,6 +3335,7 @@ pub const TokenType = enum(u8) { throw_k, var_k, match_k, + auto_k, // Error token, returned if ignoreErrors = true. err, /// Used to indicate no token. @@ -4788,8 +4790,8 @@ test "parser internals." { } try t.eq(@sizeOf(TokenizeState), 4); - try t.eq(std.enums.values(TokenType).len, 60); - try t.eq(keywords.kvs.len, 33); + try t.eq(std.enums.values(TokenType).len, 61); + try t.eq(keywords.kvs.len, 34); } fn isRecedingIndent(p: *Parser, prevIndent: u32, curIndent: u32, indent: u32) !bool { diff --git a/src/sema.zig b/src/sema.zig index d5ad7cc9d..334d76559 100644 --- a/src/sema.zig +++ b/src/sema.zig @@ -484,12 +484,12 @@ pub fn semaStmt(c: *cy.Chunk, nodeId: cy.NodeId) !void { const leftT = try semaExpr(c, left.head.indexExpr.left); if (leftT == bt.List) { // Specialized. - _ = try semaExprType(c, left.head.indexExpr.right, bt.Integer); + _ = try semaExprCstr(c, left.head.indexExpr.right, bt.Integer, false); _ = try semaExpr(c, node.head.left_right.right); c.nodes[leftId].head.indexExpr.semaGenStrat = .specialized; } else if (leftT == bt.Map) { // Specialized. - _ = try semaExprType(c, left.head.indexExpr.right, bt.Any); + _ = try semaExprCstr(c, left.head.indexExpr.right, bt.Any, false); _ = try semaExpr(c, node.head.left_right.right); c.nodes[leftId].head.indexExpr.semaGenStrat = .specialized; } else { @@ -513,7 +513,7 @@ pub fn semaStmt(c: *cy.Chunk, nodeId: cy.NodeId) !void { } }, .staticDecl => { - try staticDecl(c, nodeId, true); + try staticDecl(c, nodeId); }, .typeAliasDecl => { const nameN = c.nodes[node.head.typeAliasDecl.name]; @@ -663,7 +663,7 @@ pub fn semaStmt(c: *cy.Chunk, nodeId: cy.NodeId) !void { .return_expr_stmt => { const block = curBlock(c); const retType = try block.getReturnType(c); - _ = try semaExprType(c, node.head.child_head, retType); + _ = try semaExprCstr(c, node.head.child_head, retType, true); }, .comptimeStmt => { return; @@ -1395,8 +1395,7 @@ fn localDecl(self: *cy.Chunk, nodeId: cy.NodeId) !void { } } -fn staticDecl(c: *cy.Chunk, nodeId: cy.NodeId, exported: bool) !void { - _ = exported; +fn staticDecl(c: *cy.Chunk, nodeId: cy.NodeId) !void { const node = c.nodes[nodeId]; const varSpec = c.nodes[node.head.staticDecl.varSpec]; const nameN = c.nodes[varSpec.head.varSpec.name]; @@ -1414,7 +1413,8 @@ fn staticDecl(c: *cy.Chunk, nodeId: cy.NodeId, exported: bool) !void { if (right.node_t == .matchBlock) { _ = try matchBlock(c, node.head.staticDecl.right, true); } else { - _ = semaExpr(c, node.head.staticDecl.right) catch |err| { + const symTypeId = getSymType(c.compiler, symId); + _ = semaExprCstr(c, node.head.staticDecl.right, symTypeId, true) catch |err| { if (err == error.CanNotUseLocal) { const local = c.nodes[c.compiler.errorPayload]; const localName = c.getNodeTokenString(local); @@ -1436,9 +1436,20 @@ fn semaExpr(c: *cy.Chunk, nodeId: cy.NodeId) anyerror!TypeId { } /// No preferred type when `preferType == bt.Any`. -fn semaExprType(c: *cy.Chunk, nodeId: cy.NodeId, preferType: TypeId) anyerror!TypeId { - const res = try semaExprInner(c, nodeId, preferType); +/// If the type constraint is required, the type check is performed here and not `semaExprInner`. +fn semaExprCstr(c: *cy.Chunk, nodeId: cy.NodeId, typeId: TypeId, typeRequired: bool) anyerror!TypeId { + const res = try semaExprInner(c, nodeId, typeId); c.nodeTypes[nodeId] = res; + if (typeRequired) { + // Dynamic is allowed. + if (res != bt.Dynamic) { + if (!types.isTypeSymCompat(c.compiler, res, typeId)) { + const cstrName = types.getTypeName(c.compiler, typeId); + const typeName = types.getTypeName(c.compiler, res); + return c.reportErrorAt("Expected type `{}`, got `{}`.", &.{v(cstrName), v(typeName)}, nodeId); + } + } + } return res; } @@ -1631,12 +1642,12 @@ fn semaExprInner(c: *cy.Chunk, nodeId: cy.NodeId, preferType: TypeId) anyerror!T const leftT = try semaExpr(c, node.head.indexExpr.left); if (leftT == bt.List) { // Specialized. - _ = try semaExprType(c, node.head.indexExpr.right, bt.Integer); + _ = try semaExprCstr(c, node.head.indexExpr.right, bt.Integer, false); c.nodes[nodeId].head.indexExpr.semaGenStrat = .specialized; return bt.Dynamic; } else if (leftT == bt.Map) { // Specialized. - _ = try semaExprType(c, node.head.indexExpr.right, bt.Any); + _ = try semaExprCstr(c, node.head.indexExpr.right, bt.Any, false); c.nodes[nodeId].head.indexExpr.semaGenStrat = .specialized; return bt.Dynamic; } else { @@ -1663,7 +1674,7 @@ fn semaExprInner(c: *cy.Chunk, nodeId: cy.NodeId, preferType: TypeId) anyerror!T switch (op) { .minus => { const childPreferT = if (preferType == bt.Integer or preferType == bt.Float) preferType else bt.Any; - const childT = try semaExprType(c, node.head.unary.child, childPreferT); + const childT = try semaExprCstr(c, node.head.unary.child, childPreferT, false); if (childT == bt.Integer or childT == bt.Float) { c.nodes[nodeId].head.unary.semaGenStrat = .specialized; return childT; @@ -1678,7 +1689,7 @@ fn semaExprInner(c: *cy.Chunk, nodeId: cy.NodeId, preferType: TypeId) anyerror!T }, .bitwiseNot => { const childPreferT = if (preferType == bt.Integer) preferType else bt.Any; - const childT = try semaExprType(c, node.head.unary.child, childPreferT); + const childT = try semaExprCstr(c, node.head.unary.child, childPreferT, false); if (childT == bt.Integer) { c.nodes[nodeId].head.unary.semaGenStrat = .specialized; return bt.Float; @@ -1712,16 +1723,16 @@ fn semaExprInner(c: *cy.Chunk, nodeId: cy.NodeId, preferType: TypeId) anyerror!T .plus, .minus => { const leftPreferT = if (preferType == bt.Float or preferType == bt.Integer) preferType else bt.Any; - const leftT = try semaExprType(c, left, leftPreferT); + const leftT = try semaExprCstr(c, left, leftPreferT, false); if (leftT == bt.Integer or leftT == bt.Float) { // Specialized. - _ = try semaExprType(c, right, leftT); + _ = try semaExprCstr(c, right, leftT, false); c.nodes[nodeId].head.binExpr.semaGenStrat = .specialized; return leftT; } else { // Generic callObjSym. - _ = try semaExprType(c, right, bt.Any); + _ = try semaExprCstr(c, right, bt.Any, false); c.nodes[nodeId].head.binExpr.semaGenStrat = .generic; // TODO: Check func syms. @@ -1734,14 +1745,14 @@ fn semaExprInner(c: *cy.Chunk, nodeId: cy.NodeId, preferType: TypeId) anyerror!T .bitwiseLeftShift, .bitwiseRightShift => { const leftPreferT = if (preferType == bt.Integer) preferType else bt.Any; - const leftT = try semaExprType(c, left, leftPreferT); + const leftT = try semaExprCstr(c, left, leftPreferT, false); if (leftT == bt.Integer) { - _ = try semaExprType(c, right, leftT); + _ = try semaExprCstr(c, right, leftT, false); c.nodes[nodeId].head.binExpr.semaGenStrat = .specialized; return leftT; } else { - _ = try semaExprType(c, right, bt.Any); + _ = try semaExprCstr(c, right, bt.Any, false); c.nodes[nodeId].head.binExpr.semaGenStrat = .generic; // TODO: Check func syms. @@ -1765,9 +1776,9 @@ fn semaExprInner(c: *cy.Chunk, nodeId: cy.NodeId, preferType: TypeId) anyerror!T .bang_equal, .equal_equal => { const leftPreferT = if (preferType == bt.Float or preferType == bt.Integer) preferType else bt.Any; - const leftT = try semaExprType(c, left, leftPreferT); + const leftT = try semaExprCstr(c, left, leftPreferT, false); if (leftT == bt.Integer or leftT == bt.Float) { - _ = try semaExprType(c, right, leftT); + _ = try semaExprCstr(c, right, leftT, false); } else { _ = try semaExpr(c, right); } @@ -1778,16 +1789,16 @@ fn semaExprInner(c: *cy.Chunk, nodeId: cy.NodeId, preferType: TypeId) anyerror!T .greater_equal, .less => { const leftPreferT = if (preferType == bt.Float or preferType == bt.Integer) preferType else bt.Any; - const leftT = try semaExprType(c, left, leftPreferT); + const leftT = try semaExprCstr(c, left, leftPreferT, false); if (leftT == bt.Integer or leftT == bt.Float) { // Specialized. - _ = try semaExprType(c, right, leftT); + _ = try semaExprCstr(c, right, leftT, false); c.nodes[nodeId].head.binExpr.semaGenStrat = .specialized; return bt.Boolean; } else { // Generic callObjSym. - _ = try semaExprType(c, right, bt.Any); + _ = try semaExprCstr(c, right, bt.Any, false); c.nodes[nodeId].head.binExpr.semaGenStrat = .generic; // TODO: Check func syms. @@ -4289,7 +4300,7 @@ fn pushCallArgsWithPreferredTypes(c: *cy.Chunk, argHead: cy.NodeId, preferredTyp preferredT = preferredTypes[i]; i += 1; } - const argT = try semaExprType(c, nodeId, preferredT); + const argT = try semaExprCstr(c, nodeId, preferredT, false); hasDynamicArg = hasDynamicArg or (argT == bt.Dynamic); try c.compiler.typeStack.append(c.alloc, argT); nodeId = arg.next; diff --git a/test/behavior_test.zig b/test/behavior_test.zig index 8ea4f8734..ecad100e4 100644 --- a/test/behavior_test.zig +++ b/test/behavior_test.zig @@ -487,23 +487,6 @@ test "Typed symbol." { ); } -test "Typed function params." { - // Can't resolve param type. - try eval(.{ .silent = true }, - \\func foo(a Vec2): - \\ pass - , struct { fn func(run: *VMrunner, res: EvalResult) !void { - try run.expectErrorReport(res, error.CompileError, - \\CompileError: Could not find type symbol `Vec2`. - \\ - \\main:1:12: - \\func foo(a Vec2): - \\ ^ - \\ - ); - }}.func); -} - test "Compile-time typed function calls." { // Error from param type mismatch. try eval(.{ .silent = true }, @@ -933,21 +916,21 @@ test "Imports." { _ = try run.evalExt(Config.initFileModules("./test/import_test.cy"), \\import a './test_mods/a.cy' \\import t 'test' - \\t.eq(a.varNum, 123) + \\t.eq(a.varInt, 123) ); // Import using implied relative path prefix. _ = try run.evalExt(Config.initFileModules("./test/import_test.cy"), \\import a 'test_mods/a.cy' \\import t 'test' - \\t.eq(a.varNum, 123) + \\t.eq(a.varInt, 123) ); // Import using unresolved relative path. _ = try run.evalExt(Config.initFileModules("./test/import_test.cy"), \\import a './test_mods/../test_mods/a.cy' \\import t 'test' - \\t.eq(a.varNum, 123) + \\t.eq(a.varInt, 123) ); // Import when running main script in the cwd. @@ -955,7 +938,7 @@ test "Imports." { _ = try run.evalExt(Config.initFileModules("./import_test.cy"), \\import a 'test_mods/a.cy' \\import t 'test' - \\t.eq(a.varNum, 123) + \\t.eq(a.varInt, 123) ); // Import when running main script in a child directory. @@ -963,7 +946,7 @@ test "Imports." { _ = try run.evalExt(Config.initFileModules("../import_test.cy"), \\import a 'test_mods/a.cy' \\import t 'test' - \\t.eq(a.varNum, 123) + \\t.eq(a.varInt, 123) ); run.deinit(); @@ -2087,20 +2070,40 @@ test "Undeclared variable references." { }}.func); } -test "Static variable declaration." { - const run = VMrunner.create(); - defer run.destroy(); +test "Typed static variable declaration." { + // Type check on initializer. + try eval(.{ .silent = true }, + \\var a float: [] + , struct { fn func(run: *VMrunner, res: EvalResult) !void { + try run.expectErrorReport(res, error.CompileError, + \\CompileError: Expected type `float`, got `List`. + \\ + \\main:1:14: + \\var a float: [] + \\ ^ + \\ + ); + }}.func); +} +test "Static variable declaration." { // Capturing a local variable in a static var initializer is not allowed. - var res = run.evalExt(.{ .silent = true }, + try eval(.{ .silent = true }, \\var b = 123 \\var a: b - ); - try t.expectError(res, error.CompileError); - try t.eqStr(run.vm.getCompileErrorMsg(), "The declaration of static variable `a` can not reference the local variable `b`."); + , struct { fn func(run: *VMrunner, res: EvalResult) !void { + try run.expectErrorReport(res, error.CompileError, + \\CompileError: The declaration of static variable `a` can not reference the local variable `b`. + \\ + \\main:2:1: + \\var a: b + \\^ + \\ + ); + }}.func); // Declaration with a circular reference. - _ = try run.eval( + _ = try evalPass(.{}, \\import t 'test' \\ \\var a: b @@ -2114,7 +2117,7 @@ test "Static variable declaration." { ); // Declaration that depends on another. - _ = try run.eval( + _ = try evalPass(.{}, \\import t 'test' \\var a: 123 \\var b: a + 321 @@ -2125,7 +2128,7 @@ test "Static variable declaration." { ); // Depends on and declared before another. - _ = try run.eval( + _ = try evalPass(.{}, \\import t 'test' \\var c: a + b \\var b: a + 321 @@ -2134,7 +2137,6 @@ test "Static variable declaration." { \\t.eq(b, 444) \\t.eq(c, 567) ); - run.deinit(); // Declaration over using builtin module. try evalPass(.{}, @@ -2491,8 +2493,39 @@ test "Function overloading." { try evalPass(.{}, @embedFile("function_overload_test.cy")); } -test "Static functions." { +test "Typed static functions." { + // Check return type. + try eval(.{ .silent = true }, + \\func foo() int: + \\ return 1.2 + , struct { fn func(run: *VMrunner, res: EvalResult) !void { + try run.expectErrorReport(res, error.CompileError, + \\CompileError: Expected type `int`, got `float`. + \\ + \\main:2:10: + \\ return 1.2 + \\ ^ + \\ + ); + }}.func); + + // Can't resolve param type. + try eval(.{ .silent = true }, + \\func foo(a Vec2): + \\ pass + , struct { fn func(run: *VMrunner, res: EvalResult) !void { + try run.expectErrorReport(res, error.CompileError, + \\CompileError: Could not find type symbol `Vec2`. + \\ + \\main:1:12: + \\func foo(a Vec2): + \\ ^ + \\ + ); + }}.func); +} +test "Static functions." { // Call with missing func sym. try eval(.{ .silent = true }, \\foo(1) diff --git a/test/import_test.cy b/test/import_test.cy index 2d399fb59..4d5286631 100644 --- a/test/import_test.cy +++ b/test/import_test.cy @@ -1,9 +1,9 @@ import a 'test_mods/a.cy' import t 'test' -t.eq(a.varNum, 123) -t.eq(checkNumberArg(a.varTypedNum), 123) -func checkNumberArg(a float): +t.eq(a.varInt, 123) +t.eq(checkIntArg(a.varTypedInt), 123) +func checkIntArg(a int): return a t.eqList(a.varList, [1, 2, 3]) t.eq(a.varMap.size(), 3) diff --git a/test/static_func_test.cy b/test/static_func_test.cy index f133c34e4..bbfbfc9a3 100644 --- a/test/static_func_test.cy +++ b/test/static_func_test.cy @@ -47,9 +47,9 @@ func foo5(): return 2 + 2 t.eq(foo5(), 4) -- Static func initializer assigns static function value. -func foo6a(val) float: - return val -func foo6(val) float = foo6a +func foo6a(val) int: + return val as int +func foo6(val) int = foo6a t.eq(foo6(123), 123) -- Static func initializer assigns function value. diff --git a/test/staticvar_decl_test.cy b/test/staticvar_decl_test.cy index 846845cee..3928be885 100644 --- a/test/staticvar_decl_test.cy +++ b/test/staticvar_decl_test.cy @@ -34,5 +34,9 @@ var a5: Object{ foo: 123 } t.eq(a5.foo, 123) -- Type specifier. -var a6 float: 123 -t.eq(a6, 123) \ No newline at end of file +var a6 float: 123.0 +t.eq(a6, 123.0) + +-- Type specifier infer type. +var a7 float: 123 +t.eq(a7, 123.0) \ No newline at end of file diff --git a/test/test_mods/a.cy b/test/test_mods/a.cy index b02ca8dba..d46af433f 100644 --- a/test/test_mods/a.cy +++ b/test/test_mods/a.cy @@ -1,5 +1,5 @@ -var varNum: 123 -var varTypedNum float: 123 +var varInt: 123 +var varTypedInt int: 123 var varList: [1, 2, 3] var varMap: { a: 1, b: 2, c: 3 } var varFunc: func():