diff --git a/CHANGELOG.md b/CHANGELOG.md index 9857fb76..40d9a1da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - `pattern.match` returns now a list of `obj{ start: int, end: int, capture: str }` and `matchAll` a list of those lists - Selective import erases the imported namespace: `import print from "std"; ... print("hello world");` - Common part of imported namespace gets erased: il imported file as namesapce `a\b\c` and importing script has namespace `a\b`, only `c\` remains +- Enum name can be omitted if it can be inferred (`final list: [Locale] = [ .fr, .it, .en ]`) (https://github.com/buzz-language/buzz/issues/360) ## Internal diff --git a/examples/game-of-life.buzz b/examples/game-of-life.buzz index b577681b..2ed50c0b 100644 --- a/examples/game-of-life.buzz +++ b/examples/game-of-life.buzz @@ -125,23 +125,23 @@ fun loop(world: mut World, sdl: SDL, renderer: Renderer, texture: Texture) > voi } fun main(args: [str]) > void !> SDLError { - final sdl = SDL.init([ Subsystem.Video ]); + final sdl = SDL.init([ .Video ]); final window = Window.init( "buzz example: Game of Life", width: 800, height: 600, - flags: [ WindowFlag.OpenGL, WindowFlag.AllowHighDPI ], + flags: [ .OpenGL, .AllowHighDPI ], ); final renderer = window.createRenderer( index: -1, - flags: [ RendererFlag.Accelerated, RendererFlag.TargetTexture ], + flags: [ .Accelerated, .TargetTexture ], ); final texture = renderer.createTexture( - format: PixelFormat.RGBA8888, - access: TextureAccess.Target, + format: .RGBA8888, + access: .Target, width: 800, height: 600, ); diff --git a/examples/sdl.buzz b/examples/sdl.buzz index 6674634f..a68ee9af 100644 --- a/examples/sdl.buzz +++ b/examples/sdl.buzz @@ -2,23 +2,23 @@ import "examples/sdl-wrapped" as _; /// buzz -L/path/to/SDL2.dylib/so/dll examples/sdl.buzz fun main() > int !> SDLError { - final sdl = SDL.init([ Subsystem.Video ]); + final sdl = SDL.init([ .Video ]); final window = Window.init( "SDL FFI test", width: 800, height: 600, - flags: [ WindowFlag.OpenGL, WindowFlag.AllowHighDPI ], + flags: [ .OpenGL, .AllowHighDPI ], ); final renderer = window.createRenderer( index: -1, - flags: [ RendererFlag.Accelerated, RendererFlag.TargetTexture ], + flags: [ .Accelerated, .TargetTexture ], ); final texture = renderer.createTexture( - format: PixelFormat.RGBA8888, - access: TextureAccess.Target, + format: .RGBA8888, + access: .Target, width: 800, height: 600, ); diff --git a/examples/sqlite.buzz b/examples/sqlite.buzz index ae2e43d7..e9e8ac4d 100644 --- a/examples/sqlite.buzz +++ b/examples/sqlite.buzz @@ -4,6 +4,7 @@ import "ffi"; import "std"; import "buffer"; import "serialize" as _; +import "errors"; zdef( "sqlite3", @@ -29,37 +30,68 @@ zdef( ); enum ResultCode { - Ok = 0, // Successful result - Error = 1, // Generic error - Internal = 2, // Internal logic error in SQLite - Perm = 3, // Access permission denied - Abort = 4, // Callback routine requested an abort - Busy = 5, // The database file is locked - Locked = 6, // A table in the database is locked - NoMem = 7, // A malloc() failed - Readonly = 8, // Attempt to write a readonly database - Interrupt = 9, // Operation terminated by sqlite3_interrupt( - IoErr = 10, // Some kind of disk I/O error occurred - Corrupt = 11, // The database disk image is malformed - NotFound = 12, // Unknown opcode in sqlite3_file_control() - Full = 13, // Insertion failed because database is full - CantOpen = 14, // Unable to open the database file - Protocol = 15, // Database lock protocol error - Empty = 16, // Internal use only - Schema = 17, // The database schema changed - TooBig = 18, // String or BLOB exceeds size limit - Constraint = 19, // Abort due to finalraint violation - Mismatch = 20, // Data type mismatch - Misuse = 21, // Library used incorrectly - NoLfs = 22, // Uses OS features not supported on host - Auth = 23, // Authorization denied - Format = 24, // Not used - Range = 25, // 2nd parameter to sqlite3_bind out of range - NotADB = 26, // File opened that is not a database file - Notice = 27, // Notifications from sqlite3_log() - Warning = 28, // Warnings from sqlite3_log() - Row = 100, // sqlite3_step() has another row ready - Done = 101, // sqlite3_step() has finished executing + /// Successful result + Ok = 0, + /// Generic error + Error = 1, + /// Internal logic error in SQLite + Internal = 2, + /// Access permission denied + Perm = 3, + /// Callback routine requested an abort + Abort = 4, + /// The database file is locked + Busy = 5, + /// A table in the database is locked + Locked = 6, + /// A malloc() failed + NoMem = 7, + /// Attempt to write a readonly database + Readonly = 8, + /// Operation terminated by sqlite3_interrupt( + Interrupt = 9, + /// Some kind of disk I/O error occurred + IoErr = 10, + /// The database disk image is malformed + Corrupt = 11, + /// Unknown opcode in sqlite3_file_control() + NotFound = 12, + /// Insertion failed because database is full + Full = 13, + /// Unable to open the database file + CantOpen = 14, + /// Database lock protocol error + Protocol = 15, + /// Internal use only + Empty = 16, + /// The database schema changed + Schema = 17, + /// String or BLOB exceeds size limit + TooBig = 18, + /// Abort due to finalraint violation + Constraint = 19, + /// Data type mismatch + Mismatch = 20, + /// Library used incorrectly + Misuse = 21, + /// Uses OS features not supported on host + NoLfs = 22, + /// Authorization denied + Auth = 23, + /// Not used + Format = 24, + /// 2nd parameter to sqlite3_bind out of range + Range = 25, + /// File opened that is not a database file + NotADB = 26, + /// Notifications from sqlite3_log() + Notice = 27, + /// Warnings from sqlite3_log() + Warning = 28, + /// sqlite3_step() has another row ready + Row = 100, + /// sqlite3_step() has finished executing + Done = 101, } export object SQLiteError { @@ -139,11 +171,11 @@ export object Query { return this.branch("values", expression: "({values.join(", ")})"); } - fun prepare(db: Database) > Statement !> SQLiteError { + fun prepare(db: Database) > Statement !> SQLiteError, errors\ReadWriteError { return db.prepare(this); } - fun execute(db: Database) > [[Boxed]] *> [Boxed]? !> SQLiteError { + fun execute(db: Database) > [[Boxed]] *> [Boxed]? !> SQLiteError, errors\ReadWriteError { return db.prepare(this).execute(); } } @@ -155,34 +187,34 @@ export object Statement { fun execute() > [[Boxed]] *> [Boxed]? !> SQLiteError { std\print("Executing query `{this.query}`"); - var code: ResultCode = ResultCode.Ok; + var code: ResultCode = .Ok; final rows: mut [[Boxed]] = mut []; - while (code != ResultCode.Done) { - code = ResultCode(sqlite3_step(this.stmt)) ?? ResultCode.Error; - if (code != ResultCode.Ok and code != ResultCode.Row and code != ResultCode.Done) { + while (code != .Done) { + code = ResultCode(sqlite3_step(this.stmt)) ?? .Error; + if (code != .Ok and code != .Row and code != .Done) { throw SQLiteError{ code = code, message = "Could not execute statement `{this.query}`: {code} {sqlite3_errmsg(this.db)}", }; } - if (code == ResultCode.Done) { + if (code == .Done) { break; } final columnCount = sqlite3_column_count(this.stmt); final row: mut [any] = mut []; for (i: int = 0; i < columnCount; i = i + 1) { - final columnType = Type(sqlite3_column_type(this.stmt, col: i)) ?? Type.Null; + final columnType: Type = Type(sqlite3_column_type(this.stmt, col: i)) ?? .Null; - if (columnType == Type.Integer) { + if (columnType == .Integer) { row.append(sqlite3_column_int(this.stmt, col: i)); - } else if (columnType == Type.Double) { + } else if (columnType == .Double) { row.append(sqlite3_column_double(this.stmt, col: i)); - } else if (columnType == Type.Text) { + } else if (columnType == .Text) { row.append(sqlite3_column_text(this.stmt, col: i)); - } else if (columnType == Type.Blob) { + } else if (columnType == .Blob) { row.append(sqlite3_column_blob(this.stmt, col: i)); } else { row.append(null); @@ -207,14 +239,14 @@ export object Database { db: ud, fun collect() > void { - _ = ResultCode(sqlite3_close_v2(this.db)) ?? ResultCode.Error; + _ = ResultCode(sqlite3_close_v2(this.db)) ?? .Error; } - fun prepare(query: Query) > Statement !> SQLiteError { + fun prepare(query: Query) > Statement !> SQLiteError, errors\ReadWriteError { return this.prepareRaw(query.query); } - fun prepareRaw(query: str) > Statement !> SQLiteError { + fun prepareRaw(query: str) > Statement !> SQLiteError, errors\ReadWriteError { final buffer = buffer\Buffer.init(); buffer.writeUserData(std\toUd(0)) catch void; @@ -226,8 +258,8 @@ export object Database { ppStmt: buffer.ptr(), pzTail: null, ), - ) ?? ResultCode.Error; - if (code != ResultCode.Ok) { + ) ?? .Error; + if (code != .Ok) { throw SQLiteError{ code = code, message = "Could not prepareRaw query `{query}`: {code} {sqlite3_errmsg(this.db)}" }; } @@ -241,7 +273,7 @@ export object Database { export object SQLite { static fun init() > SQLite !> SQLiteError { - final code = ResultCode(sqlite3_initialize()) ?? ResultCode.Error; + final code = ResultCode(sqlite3_initialize()) ?? .Error; if (code != ResultCode.Ok) { throw SQLiteError{ code = code, @@ -260,7 +292,7 @@ export object SQLite { _ = sqlite3_shutdown(); } - fun open(filename: str, flags: [OpenFlag]) > Database !> SQLiteError { + fun open(filename: str, flags: [OpenFlag]) > Database !> SQLiteError, errors\ReadWriteError { var cFlags = 0; foreach (flag in flags) { cFlags = cFlags | flag.value; @@ -269,9 +301,9 @@ export object SQLite { final buffer = buffer\Buffer.init(); buffer.writeUserData(std\toUd(0)) catch void; - final code = ResultCode(sqlite3_open_v2(filename: ffi\cstr(filename), ppDb: buffer.ptr(), flags: cFlags, zVfs: null)) ?? ResultCode.Error; + final code = ResultCode(sqlite3_open_v2(filename: ffi\cstr(filename), ppDb: buffer.ptr(), flags: cFlags, zVfs: null)) ?? .Error; - if (code != ResultCode.Ok) { + if (code != .Ok) { throw SQLiteError{ code = code, message = "Could not open database `{filename}`: {code}" }; } @@ -286,7 +318,7 @@ export object SQLite { test "Testing sqlite" { final sqlite = SQLite.init(); - final database = sqlite.open("./test.db", flags: [ OpenFlag.ReadWrite, OpenFlag.Create ]); + final database = sqlite.open("./test.db", flags: [ .ReadWrite, .Create ]); final statement = database.prepareRaw(` CREATE TABLE IF NOT EXISTS test ( diff --git a/examples/voronoi-diagram.buzz b/examples/voronoi-diagram.buzz index 72cadd1b..d75cf281 100644 --- a/examples/voronoi-diagram.buzz +++ b/examples/voronoi-diagram.buzz @@ -71,23 +71,23 @@ fun main() > void !> SDLError { final height = 400; final numCells = 50; - final sdl = SDL.init([ Subsystem.Video ]); + final sdl = SDL.init([ .Video ]); final window = Window.init( "buzz example: Voronoi Diagram", width, height, - flags: [ WindowFlag.OpenGL, WindowFlag.AllowHighDPI ], + flags: [ .OpenGL, .AllowHighDPI ], ); final renderer = window.createRenderer( index: -1, - flags: [ RendererFlag.Accelerated, RendererFlag.TargetTexture ], + flags: [ .Accelerated, .TargetTexture ], ); final texture = renderer.createTexture( - format: PixelFormat.RGBA8888, - access: TextureAccess.Target, + format: .RGBA8888, + access: .Target, width, height, ); diff --git a/src/Ast.zig b/src/Ast.zig index 1e0eec1d..f2a895cd 100644 --- a/src/Ast.zig +++ b/src/Ast.zig @@ -299,6 +299,7 @@ pub const Slice = struct { try node_queue.append(allocator, comp.While.body); }, .Yield => try node_queue.append(allocator, comp.Yield), + .AnonymousEnumCase, .Boolean, .Break, .Continue, @@ -355,6 +356,7 @@ pub const Slice = struct { ) (std.mem.Allocator.Error || std.fmt.BufPrintError)!bool { switch (ast.nodes.items(.tag)[node]) { .AnonymousObjectType, + .AnonymousEnumCase, .FiberType, .FunctionType, .GenericResolveType, @@ -848,6 +850,29 @@ pub const Slice = struct { .Double => Value.fromDouble(components[node].Double), .Integer => Value.fromInteger(components[node].Integer), .Boolean => Value.fromBoolean(components[node].Boolean), + .AnonymousEnumCase => enum_case: { + const components_ptr = &components[node].AnonymousEnumCase; + const type_def = self.nodes.items(.type_def)[node].?; + const case_name = self.tokens.items(.lexeme)[components_ptr.case_name]; + const enum_type_def = type_def.resolved_type.?.EnumInstance + .of + .resolved_type.?.Enum; + + for (enum_type_def.cases, 0..) |case, idx| { + if (std.mem.eql(u8, case, case_name)) { + components_ptr.resolved_case = @intCast(idx); + + break :enum_case (try gc.allocateObject( + obj.ObjEnumInstance{ + .enum_ref = enum_type_def.value.?, + .case = @intCast(idx), + }, + )).toValue(); + } + } + + @panic("Could not constant fold anonymous enum case"); + }, .As => try self.toValue(components[node].As.left, gc), .Is => is: { const is_components = components[node].Is; @@ -1120,6 +1145,7 @@ pub const Node = struct { pub const Tag = enum(u8) { AnonymousObjectType, + AnonymousEnumCase, As, AsyncCall, Binary, @@ -1196,6 +1222,7 @@ pub const Node = struct { pub const Components = union(Tag) { AnonymousObjectType: AnonymousObjectType, + AnonymousEnumCase: AnonymousEnumCase, As: IsAs, AsyncCall: Node.Index, Binary: Binary, @@ -1415,6 +1442,11 @@ pub const AnonymousObjectType = struct { }; }; +pub const AnonymousEnumCase = struct { + case_name: TokenIndex, + resolved_case: ?u32 = null, +}; + pub const Binary = struct { left: Node.Index, right: Node.Index, diff --git a/src/Codegen.zig b/src/Codegen.zig index a2b912b9..b412f620 100644 --- a/src/Codegen.zig +++ b/src/Codegen.zig @@ -79,8 +79,9 @@ debugging: bool, reporter: Reporter, -const generators = [_]?NodeGen{ +const generators = [@typeInfo(Ast.Node.Tag).@"enum".fields.len]?NodeGen{ null, // AnonymousObjectType, + generateAnonymousEnumCase, // AnonymousEnumCase generateAs, // As, generateAsyncCall, // AsyncCall, generateBinary, // Binary, @@ -1400,6 +1401,67 @@ fn generateEnum(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!?*obj.ObjF return null; } +fn generateAnonymousEnumCase(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!?*obj.ObjFunction { + const locations = self.ast.nodes.items(.location); + const components = &self.ast.nodes.items(.components)[node].AnonymousEnumCase; + const expected_case = self.ast.tokens.items(.lexeme)[components.case_name]; + const type_def = self.ast.nodes.items(.type_def)[node]; + if (type_def == null or type_def.?.def_type != .EnumInstance) { + self.reporter.reportErrorAt( + .inferred_type, + self.ast.tokens.get(locations[node]), + self.ast.tokens.get(locations[node]), + "Could not infer type for enum case.", + ); + + return null; + } + + const enum_type_def = type_def.?.resolved_type.?.EnumInstance + .of + .resolved_type.?.Enum; + const enum_value = enum_type_def.value.?; + + const enum_case = case: { + for (enum_type_def.cases, 0..) |case, idx| { + if (std.mem.eql(u8, case, expected_case)) { + break :case idx; + } + } + + break :case null; + }; + + if (enum_case) |resolved_case| { + components.resolved_case = @intCast(resolved_case); + + try self.emitConstant( + locations[node], + enum_value.toValue(), + ); + + try self.OP_GET_ENUM_CASE( + locations[node], + @intCast(resolved_case), + ); + } else { + self.reporter.reportErrorFmt( + .inferred_type, + self.ast.tokens.get(locations[node]), + self.ast.tokens.get(locations[node]), + "Could not infer type for enum case `{s}`.", + .{ + expected_case, + }, + ); + } + + try self.patchOptJumps(node); + try self.endScope(node); + + return null; +} + fn generateExport(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { const components = self.ast.nodes.items(.components)[node].Export; diff --git a/src/Jit.zig b/src/Jit.zig index 333a820f..325f4b18 100644 --- a/src/Jit.zig +++ b/src/Jit.zig @@ -906,6 +906,7 @@ fn generateNode(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { .ForEach => try self.generateForEach(node), .TypeExpression => try self.generateTypeExpression(node), .TypeOfExpression => try self.generateTypeOfExpression(node), + .AnonymousEnumCase => try self.generateAnonymousEnumCase(node), .AsyncCall, .Resume, .Resolve, @@ -4245,20 +4246,30 @@ fn generateDot(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { }, .Enum => { + const enum_value = (try self.generateNode(components.callee)).?; + const case_name = m.MIR_new_uint_op(self.ctx, (try self.getString(member_lexeme)).toValue().val); const res = m.MIR_new_reg_op( self.ctx, try self.REG("res", m.MIR_T_I64), ); + + // Enum case lookup allocates the case instance; keep lookup inputs alive across the call. + try self.buildPush(enum_value); + try self.buildPush(case_name); + try self.buildExternApiCall( .bz_getEnumCase, res, &.{ - (try self.generateNode(components.callee)).?, - m.MIR_new_uint_op(self.ctx, (try self.getString(member_lexeme)).toValue().val), + enum_value, + case_name, m.MIR_new_reg_op(self.ctx, self.state.?.vm_reg.?), }, ); + try self.buildPop(null); + try self.buildPop(null); + return res; }, @@ -4330,6 +4341,36 @@ fn generateDot(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { } } +fn generateAnonymousEnumCase(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { + const components = self.state.?.ast.nodes.items(.components)[node].AnonymousEnumCase; + const member_lexeme = self.state.?.ast.tokens.items(.lexeme)[components.case_name]; + const type_defs = self.state.?.ast.nodes.items(.type_def); + if (type_defs[node] == null or type_defs[node].?.def_type != .EnumInstance) { + return error.CantCompile; + } + + const enum_value = type_defs[node].?.resolved_type.?.EnumInstance + .of + .resolved_type.?.Enum + .value.?; + + const res = m.MIR_new_reg_op( + self.ctx, + try self.REG("res", m.MIR_T_I64), + ); + try self.buildExternApiCall( + .bz_getEnumCase, + res, + &.{ + m.MIR_new_uint_op(self.ctx, enum_value.toValue().val), + m.MIR_new_uint_op(self.ctx, (try self.getString(member_lexeme)).toValue().val), + m.MIR_new_reg_op(self.ctx, self.state.?.vm_reg.?), + }, + ); + + return res; +} + fn generateSubscript(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { const components = self.state.?.ast.nodes.items(.components)[node].Subscript; const type_defs = self.state.?.ast.nodes.items(.type_def); diff --git a/src/Parser.zig b/src/Parser.zig index cc1362b1..d6ec1ab8 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -508,7 +508,7 @@ const rules = [_]ParseRule{ .{}, // RightParen .{ .prefix = map, .infix = objectInit, .precedence = .Primary }, // LeftBrace .{}, // RightBrace - .{ .prefix = anonymousObjectInit, .infix = dot, .precedence = .Call }, // Dot + .{ .prefix = anonymousEnumCaseOrObjectInit, .infix = dot, .precedence = .Call }, // Dot .{}, // Comma .{}, // Semicolon .{ .infix = binary, .precedence = .Comparison }, // Greater @@ -4650,9 +4650,47 @@ fn objectInit(self: *Self, _: bool, object: Ast.Node.Index) Error!Ast.Node.Index ); } -fn anonymousObjectInit(self: *Self, _: bool) Error!Ast.Node.Index { +fn anonymousEnumCaseOrObjectInit(self: *Self, can_assign: bool) Error!Ast.Node.Index { + if (try self.match(.LeftBrace)) { + return self.anonymousObjectInit(can_assign); + } + + return self.anonymousEnumCase(can_assign); +} + +fn anonymousEnumCase(self: *Self, _: bool) Error!Ast.Node.Index { const start_location = self.current_token.? - 1; - try self.consume(.LeftBrace, "Expected `{` after `.`"); + try self.consume(.Identifier, "Expected enum case identifier"); + const case = self.current_token.? - 1; + + return self.ast.appendNode( + .{ + .tag = .AnonymousEnumCase, + .location = start_location, + .end_location = case, + .type_def = try self.gc.type_registry.getTypeDef( + .{ + .def_type = .Placeholder, + .resolved_type = .{ + .Placeholder = .init( + case, + case, + false, + ), + }, + }, + ), + .components = .{ + .AnonymousEnumCase = .{ + .case_name = case, + }, + }, + }, + ); +} + +fn anonymousObjectInit(self: *Self, _: bool) Error!Ast.Node.Index { + const start_location = self.current_token.? - 2; const qualifier = try std.mem.replaceOwned( u8, @@ -7105,6 +7143,14 @@ fn binary(self: *Self, _: bool, left: Ast.Node.Index) Error!Ast.Node.Index { const type_defs = self.ast.nodes.items(.type_def); const right_type_def = type_defs[right]; const left_type_def = type_defs[left]; + // `a ?? b` evaluates to the fallback type, but an inferred fallback may + // need the non-optional left type as context before typechecking runs. + const null_coalescing_type_def = if (right_type_def != null and right_type_def.?.def_type != .Placeholder) + right_type_def + else if (left_type_def != null and left_type_def.?.def_type != .Placeholder) + try left_type_def.?.cloneNonOptional(&self.gc.type_registry) + else + right_type_def; return try self.ast.appendNode( .{ @@ -7112,7 +7158,7 @@ fn binary(self: *Self, _: bool, left: Ast.Node.Index) Error!Ast.Node.Index { .location = start_location, .end_location = self.current_token.? - 1, .type_def = switch (operator) { - .QuestionQuestion => right_type_def, + .QuestionQuestion => null_coalescing_type_def, .Greater, .Less, diff --git a/src/TypeChecker.zig b/src/TypeChecker.zig index a17238f1..4adfa4c3 100644 --- a/src/TypeChecker.zig +++ b/src/TypeChecker.zig @@ -14,70 +14,71 @@ const NodeCheck = *const fn ( node: Ast.Node.Index, ) error{OutOfMemory}!bool; -const checkers = [_]?NodeCheck{ - null, // AnonymousObjectType, - null, // As, +const checkers = [@typeInfo(Ast.Node.Tag).@"enum".fields.len]?NodeCheck{ + null, // AnonymousObjectType + null, // AnonymousEnumCase + null, // As checkAsyncCall, checkBinary, - null, // Block, - null, // BlockExpression, - null, // Boolean, - null, // Break, + null, // Block + null, // BlockExpression + null, // Boolean + null, // Break checkCall, - null, // Continue, + null, // Continue checkDot, checkDoUntil, checkEnum, - null, // Export, - null, // Expression, - null, // FiberType, - null, // Double, + null, // Export + null, // Expression + null, // FiberType + null, // Double checkFor, checkForceUnwrap, checkForEach, checkFunction, - null, // FunctionType, - null, // FunDeclaration, + null, // FunctionType + null, // FunDeclaration checkGenericResolve, - null, // GenericResolveType, - null, // GenericType, - null, // Grouping, + null, // GenericResolveType + null, // GenericType + null, // Grouping checkIf, - null, // Import, - null, // Integer, - null, // Is, + null, // Import + null, // Integer + null, // Is checkList, - null, // ListType, + null, // ListType checkMap, - null, // MapType, - null, // Namespace, + null, // MapType + null, // Namespace checkNamedVariable, - null, // Null, + null, // Null checkObjectDeclaration, checkObjectInit, - null, // Out, - null, // Pattern, - null, // ProtocolDeclaration, + null, // Out + null, // Pattern + null, // ProtocolDeclaration checkRange, checkResolve, checkResume, checkReturn, - null, // SimpleType, - null, // String, - null, // StringLiteral, + null, // SimpleType + null, // String + null, // StringLiteral checkSubscript, - null, // Throw, - null, // Try, - null, // TypeExpression, - null, // TypeOfExpression, + null, // Throw + null, // Try + null, // TypeExpression + null, // TypeOfExpression checkUnary, checkUnwrap, - null, // UserType, + null, // UserType checkVarDeclaration, - null, // Void, + null, // Void checkWhile, checkYield, - null, // Zdef, + null, // Zdef }; /// Typecheck the node (but does not typecheck its leaf) @@ -88,37 +89,126 @@ pub fn check(ast: Ast.Slice, reporter: *Reporter, gc: *GC, current_function_node false; } -pub fn populateEmptyCollectionType(ast: Ast.Slice, gc: *GC, value: Ast.Node.Index, target_type: *o.ObjTypeDef) error{OutOfMemory}!void { +fn inferType(ast: Ast.Slice, reporter: *Reporter, gc: *GC, value_node: Ast.Node.Index, target_type: *o.ObjTypeDef) error{OutOfMemory}!bool { const tags = ast.nodes.items(.tag); + + return switch (tags[value_node]) { + .AnonymousEnumCase => populateAnonymousEnumCase(ast, reporter, value_node, target_type), + .List => try inferListType(ast, reporter, gc, value_node, target_type), + .Map => try inferMapType(ast, reporter, gc, value_node, target_type), + else => false, + }; +} + +fn inferListType(ast: Ast.Slice, reporter: *Reporter, gc: *GC, value: Ast.Node.Index, target_type: *o.ObjTypeDef) error{OutOfMemory}!bool { const components = ast.nodes.items(.components); const type_defs = ast.nodes.items(.type_def); - // variable: [T] = [] -> variable: [T] = [] - // variable: mut [T] = [] -> keep immutable [T], do not infer mutability - if (target_type.def_type == .List and - tags[value] == .List and - components[value].List.explicit_item_type == null and - components[value].List.items.len == 0) - { + if (target_type.def_type != .List) { + return false; + } + + const list = components[value].List; + const item_type = target_type.resolved_type.?.List.item_type; + var inferred_item = false; + + for (list.items) |item| { + // A contextual list type propagates to nested inferred item expressions. + inferred_item = (try inferType(ast, reporter, gc, item, item_type)) or inferred_item; + } + + // variable: [T] = [] -> variable: [T] = []. + // When contextual inference resolved an item, the literal's own parser-inferred + // placeholder type can be replaced with the contextual list type. + if (list.explicit_item_type == null and (list.items.len == 0 or inferred_item)) { type_defs[value] = target_type.cloneMutable( &gc.type_registry, type_defs[value].?.isMutable(), ) catch return error.OutOfMemory; + + return true; + } + + return false; +} + +fn inferMapType(ast: Ast.Slice, reporter: *Reporter, gc: *GC, value: Ast.Node.Index, target_type: *o.ObjTypeDef) error{OutOfMemory}!bool { + const components = ast.nodes.items(.components); + const type_defs = ast.nodes.items(.type_def); + + if (target_type.def_type != .Map) { + return false; + } + + const map = components[value].Map; + const key_type = target_type.resolved_type.?.Map.key_type; + const value_type = target_type.resolved_type.?.Map.value_type; + var inferred_entry = false; + + for (map.entries) |entry| { + // A contextual map type propagates to nested inferred key/value expressions. + inferred_entry = (try inferType(ast, reporter, gc, entry.key, key_type)) or inferred_entry; + inferred_entry = (try inferType(ast, reporter, gc, entry.value, value_type)) or inferred_entry; } - // variable: {K: V} = {} -> variable: {K: V} = {} - // variable: mut {K: V} = {} -> keep immutable {K: V}, do not infer mutability - if (target_type.def_type == .Map and - tags[value] == .Map and - components[value].Map.explicit_key_type == null and - components[value].Map.explicit_value_type == null and - components[value].Map.entries.len == 0) + // variable: {K: V} = {} -> variable: {K: V} = {}. + if (map.explicit_key_type == null and + map.explicit_value_type == null and + (map.entries.len == 0 or inferred_entry)) { type_defs[value] = target_type.cloneMutable( &gc.type_registry, type_defs[value].?.isMutable(), ) catch return error.OutOfMemory; + return true; + } + + return false; +} + +fn populateAnonymousEnumCase(ast: Ast.Slice, reporter: *Reporter, value: Ast.Node.Index, target_type: *o.ObjTypeDef) bool { + const type_defs = ast.nodes.items(.type_def); + + if (target_type.def_type == .EnumInstance) { + const locations = ast.nodes.items(.location); + const end_locations = ast.nodes.items(.end_location); + const components = ast.nodes.items(.components)[value].AnonymousEnumCase; + const case_name = ast.tokens.items(.lexeme)[components.case_name]; + const enum_type_def = target_type.resolved_type.?.EnumInstance.of.resolved_type.?.Enum; + + for (enum_type_def.cases) |case| { + if (std.mem.eql(u8, case, case_name)) { + type_defs[value] = target_type; + + return true; + } + } + + reporter.reportErrorFmt( + .inferred_type, + ast.tokens.get(locations[value]), + ast.tokens.get(end_locations[value]), + "Could not infer type for enum case `{s}`.", + .{case_name}, + ); + + return true; + } + + if (type_defs[value].?.def_type == .Placeholder) { + const locations = ast.nodes.items(.location); + const end_locations = ast.nodes.items(.end_location); + reporter.reportErrorAt( + .inferred_type, + ast.tokens.get(locations[value]), + ast.tokens.get(end_locations[value]), + "Could not infer type for enum case.", + ); + + return true; } + + return false; } fn checkBinary(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.Index, node: Ast.Node.Index) error{OutOfMemory}!bool { @@ -129,11 +219,35 @@ fn checkBinary(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.Index, const node_location = locations[node]; const end_locations = ast.nodes.items(.end_location); const node_end_location = end_locations[node]; - const left_type = type_defs[node_components.Binary.left] orelse gc.type_registry.any_type; - const right_type = type_defs[node_components.Binary.right] orelse gc.type_registry.any_type; + var left_type = type_defs[node_components.Binary.left] orelse gc.type_registry.any_type; + var right_type = type_defs[node_components.Binary.right] orelse gc.type_registry.any_type; var had_error = false; + if (node_components.Binary.operator == .QuestionQuestion and left_type.def_type != .Placeholder) { + const fallback_type = left_type.cloneNonOptional(&gc.type_registry) catch return error.OutOfMemory; + // `a ?? b` gives `b` the non-optional type of `a`. + if (try inferType(ast, reporter, gc, node_components.Binary.right, fallback_type)) { + right_type = type_defs[node_components.Binary.right] orelse gc.type_registry.any_type; + } + + type_defs[node] = right_type; + } else if (left_type.def_type != .Placeholder) { + // A concrete left operand can provide context for an inferred right operand. + if (try inferType(ast, reporter, gc, node_components.Binary.right, left_type)) { + right_type = type_defs[node_components.Binary.right] orelse gc.type_registry.any_type; + } + } + + if (node_components.Binary.operator != .QuestionQuestion and + right_type.def_type != .Placeholder) + { + // A concrete right operand can provide context for an inferred left operand. + if (try inferType(ast, reporter, gc, node_components.Binary.left, right_type)) { + left_type = type_defs[node_components.Binary.left] orelse gc.type_registry.any_type; + } + } + switch (node_components.Binary.operator) { .QuestionQuestion, .Ampersand, @@ -465,7 +579,8 @@ fn checkCall(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.Index, n const def_arg_type = args.get(actual_arg_key); if (def_arg_type) |arg_type| { - try populateEmptyCollectionType(ast, gc, argument.value, arg_type); + // Function signatures provide contextual types for inferred arguments. + _ = try inferType(ast, reporter, gc, argument.value, arg_type); argument_type_def = type_defs[argument.value].?; if (!arg_type.eql(argument_type_def)) { @@ -545,6 +660,9 @@ fn checkCall(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.Index, n // Catch clause const error_types = callee_type.?.resolved_type.?.Function.error_types; if (components.catch_default) |catch_default| { + const node_type_def = type_defs[node].?; + // Inline catch defaults must produce the same value type as the call. + _ = try inferType(ast, reporter, gc, catch_default, node_type_def); const catch_default_type_def = type_defs[catch_default].?; if (error_types == null or error_types.?.len == 0) { reporter.reportErrorAt( @@ -555,7 +673,6 @@ fn checkCall(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.Index, n ); had_error = true; } else if (error_types != null) { - const node_type_def = type_defs[node].?; // Expression if (!node_type_def.eql(catch_default_type_def) and !(node_type_def.cloneOptional(&gc.type_registry) catch return error.OutOfMemory) @@ -736,7 +853,8 @@ fn checkDot(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.Index, no had_error = true; } - try populateEmptyCollectionType(ast, gc, value, field.?.type_def); + // Field assignments provide the declared field type as context. + _ = try inferType(ast, reporter, gc, value, field.?.type_def); value_type_def = type_defs[value].?; if (!field.?.type_def.eql(value_type_def)) { @@ -1502,7 +1620,7 @@ fn checkIf(ast: Ast.Slice, reporter: *Reporter, _: *GC, _: ?Ast.Node.Index, node return had_error; } -fn checkList(ast: Ast.Slice, reporter: *Reporter, _: *GC, _: ?Ast.Node.Index, node: Ast.Node.Index) error{OutOfMemory}!bool { +fn checkList(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.Index, node: Ast.Node.Index) error{OutOfMemory}!bool { const locations = ast.nodes.items(.location); const end_locations = ast.nodes.items(.end_location); const components = ast.nodes.items(.components)[node].List; @@ -1514,7 +1632,13 @@ fn checkList(ast: Ast.Slice, reporter: *Reporter, _: *GC, _: ?Ast.Node.Index, no for (components.items) |item| { if (item_type.def_type == .Placeholder) { reporter.reportPlaceholder(ast, type_defs[item].?.resolved_type.?.Placeholder); - } else if (!item_type.eql(type_defs[item].?)) { + } else { + // The list item type provides context for inferred item expressions. + _ = try inferType(ast, reporter, gc, item, item_type); + } + + const actual_item_type = type_defs[item].?; + if (item_type.def_type != .Placeholder and !item_type.eql(actual_item_type)) { reporter.reportTypeCheck( .list_item_type, ast.tokens.get(locations[node]), @@ -1522,7 +1646,7 @@ fn checkList(ast: Ast.Slice, reporter: *Reporter, _: *GC, _: ?Ast.Node.Index, no item_type, ast.tokens.get(locations[item]), ast.tokens.get(end_locations[item]), - type_defs[item].?, + actual_item_type, "Bad list type", ); had_error = true; @@ -1532,31 +1656,37 @@ fn checkList(ast: Ast.Slice, reporter: *Reporter, _: *GC, _: ?Ast.Node.Index, no return had_error; } -fn checkMap(ast: Ast.Slice, reporter: *Reporter, _: *GC, _: ?Ast.Node.Index, node: Ast.Node.Index) error{OutOfMemory}!bool { +fn checkMap(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.Index, node: Ast.Node.Index) error{OutOfMemory}!bool { const locations = ast.nodes.items(.location); const end_locations = ast.nodes.items(.end_location); const components = ast.nodes.items(.components)[node].Map; const type_defs = ast.nodes.items(.type_def); + const map_type = type_defs[node].?.resolved_type.?.Map; const key_type = if (components.explicit_key_type) |kt| - type_defs[kt] + type_defs[kt].? else - null; + map_type.key_type; const value_type = if (components.explicit_value_type) |vt| - type_defs[vt] + type_defs[vt].? else - null; + map_type.value_type; var had_error = false; for (components.entries) |entry| { - if (key_type != null and !key_type.?.eql(type_defs[entry.key].?)) { + if (key_type.def_type != .Placeholder) { + // Map key declarations provide context for inferred key expressions. + _ = try inferType(ast, reporter, gc, entry.key, key_type); + } + + if (!key_type.eql(type_defs[entry.key].?)) { reporter.reportTypeCheck( .map_key_type, ast.tokens.get(locations[node]), ast.tokens.get(end_locations[node]), - key_type.?, + key_type, ast.tokens.get(locations[entry.key]), ast.tokens.get(end_locations[entry.key]), type_defs[entry.key].?, @@ -1565,12 +1695,17 @@ fn checkMap(ast: Ast.Slice, reporter: *Reporter, _: *GC, _: ?Ast.Node.Index, nod had_error = true; } - if (value_type != null and !value_type.?.eql(type_defs[entry.value].?)) { + if (value_type.def_type != .Placeholder) { + // Map value declarations provide context for inferred value expressions. + _ = try inferType(ast, reporter, gc, entry.value, value_type); + } + + if (!value_type.eql(type_defs[entry.value].?)) { reporter.reportTypeCheck( .map_value_type, ast.tokens.get(locations[node]), ast.tokens.get(end_locations[node]), - value_type.?, + value_type, ast.tokens.get(locations[entry.value]), ast.tokens.get(end_locations[entry.value]), type_defs[entry.value].?, @@ -1583,7 +1718,7 @@ fn checkMap(ast: Ast.Slice, reporter: *Reporter, _: *GC, _: ?Ast.Node.Index, nod return had_error; } -fn checkNamedVariable(ast: Ast.Slice, reporter: *Reporter, _: *GC, _: ?Ast.Node.Index, node: Ast.Node.Index) error{OutOfMemory}!bool { +fn checkNamedVariable(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.Index, node: Ast.Node.Index) error{OutOfMemory}!bool { const components = ast.nodes.items(.components)[node].NamedVariable; const locations = ast.nodes.items(.location); const end_locations = ast.nodes.items(.end_location); @@ -1593,7 +1728,11 @@ fn checkNamedVariable(ast: Ast.Slice, reporter: *Reporter, _: *GC, _: ?Ast.Node. var had_error = false; if (components.value) |value| { - if (!type_defs[node].?.eql(type_defs[value].?)) { + // Assignment targets provide context for inferred assigned values. + _ = try inferType(ast, reporter, gc, value, type_defs[node].?); + const value_type_def = type_defs[value].?; + + if (!type_defs[node].?.eql(value_type_def)) { reporter.reportTypeCheck( .assignment_value_type, ast.tokens.get(locations[node]), @@ -1601,7 +1740,7 @@ fn checkNamedVariable(ast: Ast.Slice, reporter: *Reporter, _: *GC, _: ?Ast.Node. type_defs[node].?, ast.tokens.get(locations[value]), ast.tokens.get(end_locations[value]), - type_defs[value].?, + value_type_def, "Bad value type", ); had_error = true; @@ -1796,7 +1935,8 @@ fn checkObjectDeclaration(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast. // Create property default value if (member.method_or_default_value) |default| { - try populateEmptyCollectionType(ast, gc, default, property_type); + // Property declarations provide context for inferred default values. + _ = try inferType(ast, reporter, gc, default, property_type); const default_type_def = type_defs[default].?; if (default_type_def.isMutable()) { @@ -1881,7 +2021,8 @@ fn checkObjectInit(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.In for (components.properties) |property| { const property_name = lexemes[property.name]; if (fields.get(property_name)) |prop| { - try populateEmptyCollectionType(ast, gc, property.value, prop); + // Object initializer fields provide context for inferred property values. + _ = try inferType(ast, reporter, gc, property.value, prop); const value_type_def = type_defs[property.value].?; if (!prop.eql(value_type_def)) { @@ -2073,6 +2214,8 @@ fn checkReturn(ast: Ast.Slice, reporter: *Reporter, gc: *GC, current_function_no var had_error = false; if (components.value) |value| { + // Function return types provide context for inferred return values. + _ = try inferType(ast, reporter, gc, value, current_function_type_def.return_type); const value_type_def = type_defs[value]; if (value_type_def == null) { reporter.reportErrorAt( @@ -2121,7 +2264,7 @@ fn checkSubscript(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.Ind const subscripted_type_def = type_defs[components.subscripted].?; const index_type_def = type_defs[components.index].?; - const value_type_def = if (components.value) |value| type_defs[value] else null; + var value_type_def = if (components.value) |value| type_defs[value] else null; var had_error = false; @@ -2188,6 +2331,10 @@ fn checkSubscript(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.Ind had_error = true; } + // List element assignments provide the list item type as context. + _ = try inferType(ast, reporter, gc, value, subscripted_type_def.resolved_type.?.List.item_type); + value_type_def = type_defs[value]; + if (!subscripted_type_def.resolved_type.?.List.item_type.eql(value_type_def.?)) { reporter.reportTypeCheck( .subscript_value_type, @@ -2234,6 +2381,10 @@ fn checkSubscript(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.Ind had_error = true; } + // Map element assignments provide the map value type as context. + _ = try inferType(ast, reporter, gc, value, subscripted_type_def.resolved_type.?.Map.value_type); + value_type_def = type_defs[value]; + if (!subscripted_type_def.resolved_type.?.Map.value_type.eql(value_type_def.?)) { reporter.reportTypeCheck( .subscript_value_type, @@ -2337,7 +2488,7 @@ fn checkVarDeclaration(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Nod const components = ast.nodes.items(.components)[node].VarDeclaration; const type_defs = ast.nodes.items(.type_def); const type_def = type_defs[node].?; - const value_type_def = if (components.value) |value| + var value_type_def = if (components.value) |value| ast.nodes.items(.type_def)[value] else null; @@ -2346,6 +2497,10 @@ fn checkVarDeclaration(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Nod const location = locations[node]; if (components.value) |value| { + // Declared variable types provide context for inferred initializer values. + _ = try inferType(ast, reporter, gc, value, type_def); + value_type_def = ast.nodes.items(.type_def)[value]; + if (!(type_def.toInstance(&gc.type_registry, type_def.isMutable()) catch return error.OutOfMemory) .eql(value_type_def.?) and !((type_def.toInstance(&gc.type_registry, type_def.isMutable()) catch return error.OutOfMemory) @@ -2391,9 +2546,9 @@ fn checkWhile(ast: Ast.Slice, reporter: *Reporter, _: *GC, _: ?Ast.Node.Index, n return false; } -fn checkYield(ast: Ast.Slice, reporter: *Reporter, _: *GC, current_function_node: ?Ast.Node.Index, node: Ast.Node.Index) error{OutOfMemory}!bool { +fn checkYield(ast: Ast.Slice, reporter: *Reporter, gc: *GC, current_function_node: ?Ast.Node.Index, node: Ast.Node.Index) error{OutOfMemory}!bool { const type_defs = ast.nodes.items(.type_def); - const type_def = type_defs[node]; + const components = ast.nodes.items(.components)[node].Yield; const locations = ast.nodes.items(.location); const end_locations = ast.nodes.items(.end_location); const location = locations[node]; @@ -2421,6 +2576,11 @@ fn checkYield(ast: Ast.Slice, reporter: *Reporter, _: *GC, current_function_node else => {}, } + // Fiber yield types provide context for inferred yielded values. + _ = try inferType(ast, reporter, gc, components, current_function_typedef.yield_type); + type_defs[node] = type_defs[components]; + const type_def = type_defs[node]; + if (type_def == null) { reporter.reportErrorAt( .unknown, diff --git a/src/lib/toml.buzz b/src/lib/toml.buzz index 1cb13696..c2096ae2 100644 --- a/src/lib/toml.buzz +++ b/src/lib/toml.buzz @@ -256,30 +256,30 @@ object Token { } fun literal() > any { - if (this.tag == TokenTag.LiteralString) { + if (this.tag == .LiteralString) { return this.stripStringDelimiters(1); } - if (this.tag == TokenTag.MultilineLiteralString) { + if (this.tag == .MultilineLiteralString) { return this.trimFirstMultilineNewline(this.stripStringDelimiters(3)); } - if (this.tag == TokenTag.BasicString) { + if (this.tag == .BasicString) { return this.decodeBasicString(this.stripStringDelimiters(1)); } - if (this.tag == TokenTag.MultilineBasicString) { + if (this.tag == .MultilineBasicString) { return this.decodeBasicString( this.trimFirstMultilineNewline(this.stripStringDelimiters(3)), multiline: true, ); } - if (this.tag == TokenTag.Integer) { + if (this.tag == .Integer) { return this.integerLiteral(); } - if (this.tag == TokenTag.Float) { + if (this.tag == .Float) { final normalized = this.floatLexeme(); if (normalized == "inf" or normalized == "+inf" or normalized == "-inf" or normalized == "nan" or normalized == "+nan" or normalized == "-nan") { return 0.0; @@ -461,7 +461,7 @@ object Scanner { return this.tokens[this.tokens.len() - 1]; } - fun makeError(error: str) => this.makeToken(TokenTag.Error, error); + fun makeError(error: str) => this.makeToken(.Error, error); mut fun skipComment() > Token? { _ = this.advance(); @@ -499,7 +499,7 @@ object Scanner { } if (this.isEOF()) { - return this.makeToken(TokenTag.Eof); + return this.makeToken(.Eof); } final char = this.advance(); @@ -508,7 +508,7 @@ object Scanner { this.current.line += 1; this.current.column = 0; - return this.makeToken(TokenTag.Newline); + return this.makeToken(.Newline); } else if (char == '\r') { if (!this.match('\n')) { return this.makeError("Expected LF after CR"); @@ -517,21 +517,21 @@ object Scanner { this.current.line += 1; this.current.column = 0; - return this.makeToken(TokenTag.Newline); + return this.makeToken(.Newline); } else if (char == '=') { - return this.makeToken(TokenTag.Equal); + return this.makeToken(.Equal); } else if (char == '.') { - return this.makeToken(TokenTag.Dot); + return this.makeToken(.Dot); } else if (char == ',') { - return this.makeToken(TokenTag.Comma); + return this.makeToken(.Comma); } else if (char == '[') { - return this.makeToken(TokenTag.LeftBracket); + return this.makeToken(.LeftBracket); } else if (char == ']') { - return this.makeToken(TokenTag.RightBracket); + return this.makeToken(.RightBracket); } else if (char == '{') { - return this.makeToken(TokenTag.LeftBrace); + return this.makeToken(.LeftBrace); } else if (char == '}') { - return this.makeToken(TokenTag.RightBrace); + return this.makeToken(.RightBrace); } else if (this.isLetter(char) or char == '_' or (char == '-' and !this.isNumber(peeked) and peeked != 'i' and peeked != 'n')) { return this.bareKey(); } else if (this.isNumber(char) or ((char == '+' or char == '-') and (this.isNumber(peeked) or peeked == 'i' or peeked == 'n'))) { @@ -566,14 +566,14 @@ object Scanner { final lexeme = this.source.sub(this.current.start, len: this.current.offset - this.current.start); if (lexeme == "true" or lexeme == "false") { - return this.makeToken(TokenTag.Boolean); + return this.makeToken(.Boolean); } if (lexeme == "inf" or lexeme == "nan") { - return this.makeToken(TokenTag.Float); + return this.makeToken(.Float); } - return this.makeToken(TokenTag.BareKey); + return this.makeToken(.BareKey); } fun scannedCanBeBareKey() > bool { @@ -747,7 +747,7 @@ object Scanner { if (timeLength == 0) { if (afterDate == ' ') { - return this.makeTokenWithLength(TokenTag.LocalDate, length: dateLength); + return this.makeTokenWithLength(.LocalDate, length: dateLength); } return null; @@ -767,7 +767,7 @@ object Scanner { return null; } - return this.makeTokenWithLength(TokenTag.OffsetDateTime, length: length); + return this.makeTokenWithLength(.OffsetDateTime, length: length); } if (afterTime == '+' or afterTime == '-') { @@ -784,7 +784,7 @@ object Scanner { return null; } - return this.makeTokenWithLength(TokenTag.OffsetDateTime, length: length); + return this.makeTokenWithLength(.OffsetDateTime, length: length); } return null; @@ -794,14 +794,14 @@ object Scanner { return null; } - return this.makeTokenWithLength(TokenTag.LocalDateTime, length: length); + return this.makeTokenWithLength(.LocalDateTime, length: length); } if (this.isBareKeyChar(afterDate)) { return null; } - return this.makeTokenWithLength(TokenTag.LocalDate, length: dateLength); + return this.makeTokenWithLength(.LocalDate, length: dateLength); } final timeLength = this.partialTimeLength(this.current.start); @@ -810,7 +810,7 @@ object Scanner { return this.makeError("Invalid time"); } - return this.makeTokenWithLength(TokenTag.LocalTime, length: timeLength); + return this.makeTokenWithLength(.LocalTime, length: timeLength); } return null; @@ -831,7 +831,7 @@ object Scanner { return this.makeError("Invalid float"); } - return this.makeToken(TokenTag.Float); + return this.makeToken(.Float); } return this.makeError("Invalid float"); @@ -873,7 +873,7 @@ object Scanner { return this.bareKey(); } - return this.makeToken(TokenTag.Integer); + return this.makeToken(.Integer); } var valid = true; @@ -1061,7 +1061,7 @@ object Scanner { if (peeked == '\"') { _ = this.advance(); - return this.makeToken(TokenTag.BasicString); + return this.makeToken(.BasicString); } if (peeked == '\n' or peeked == '\r') { @@ -1091,7 +1091,7 @@ object Scanner { final peeked = this.peek(); if (peeked == '\"') { - final token = this.consumeMultilineQuoteRun('\"', tag: TokenTag.MultilineBasicString); + final token = this.consumeMultilineQuoteRun('\"', tag: .MultilineBasicString); if (token != null) { return token!; } @@ -1132,7 +1132,7 @@ object Scanner { if (peeked == '\'') { _ = this.advance(); - return this.makeToken(TokenTag.LiteralString); + return this.makeToken(.LiteralString); } if (peeked == '\n' or peeked == '\r') { @@ -1157,7 +1157,7 @@ object Scanner { final next = this.peek(1); if (peeked == '\'') { - final token = this.consumeMultilineQuoteRun('\'', tag: TokenTag.MultilineLiteralString); + final token = this.consumeMultilineQuoteRun('\'', tag: .MultilineLiteralString); if (token != null) { return token!; } @@ -1207,23 +1207,23 @@ object PathPart { index: int?, static fun keyPart(key: str) => PathPart{ - tag = PathPartTag.Key, + tag = .Key, key, }; static fun indexPart(index: int) => PathPart{ - tag = PathPartTag.Index, + tag = .Index, index, }; - fun display() => if (this.tag == PathPartTag.Index) "{this.index!}" else this.key!; + fun display() => if (this.tag == .Index) "{this.index!}" else this.key!; fun same(other: PathPart) > bool { if (this.tag != other.tag) { return false; } - if (this.tag == PathPartTag.Key) { + if (this.tag == .Key) { return this.key! == other.key!; } @@ -1299,11 +1299,11 @@ object Path { while (true) { if (logical) { - while (left < this.parts.len() and this.parts[left].tag == PathPartTag.Index) { + while (left < this.parts.len() and this.parts[left].tag == .Index) { left += 1; } - while (right < other.parts.len() and other.parts[right].tag == PathPartTag.Index) { + while (right < other.parts.len() and other.parts[right].tag == .Index) { right += 1; } } @@ -1373,7 +1373,7 @@ object PathSet { } fun scalarFromToken(token: Token) > any { - if (token.tag == TokenTag.Boolean) { + if (token.tag == .Boolean) { return token.lexeme == "true"; } @@ -1419,8 +1419,8 @@ object Parser { parser.advance(); parser.skipLineIgnorable(); - while (!parser.match(TokenTag.Eof)) { - if (parser.match(TokenTag.LeftBracket)) { + while (!parser.match(.Eof)) { + if (parser.match(.LeftBracket)) { final firstLeft = parser.scanner.tokens[parser.currentToken! - 1]; // pop previous section if (parser.section -> section) { @@ -1429,7 +1429,7 @@ object Parser { parser.section = parser.currentPath.len(); - if (parser.check(TokenTag.LeftBracket) and parser.scanner.tokens[parser.currentToken!].offset == firstLeft.offset + 1) { + if (parser.check(.LeftBracket) and parser.scanner.tokens[parser.currentToken!].offset == firstLeft.offset + 1) { parser.advance(); // [[section]] parser.currentPath.appendKeys(parser.parseKey()); @@ -1439,9 +1439,9 @@ object Parser { throw Error.Invalid; } - parser.consume(TokenTag.RightBracket, message: "Expected `]`"); + parser.consume(.RightBracket, message: "Expected `]`"); final firstRight = parser.scanner.tokens[parser.currentToken! - 1]; - if (!parser.check(TokenTag.RightBracket) or parser.scanner.tokens[parser.currentToken!].offset != firstRight.offset + 1) { + if (!parser.check(.RightBracket) or parser.scanner.tokens[parser.currentToken!].offset != firstRight.offset + 1) { parser.report("Expected adjacent `]`"); throw Error.Invalid; @@ -1456,7 +1456,7 @@ object Parser { } } - parser.consume(TokenTag.RightBracket, message: "Expected `]`"); + parser.consume(.RightBracket, message: "Expected `]`"); } else { if (!parser.parseKeyValue()) { parser.report("Invalid key/value"); @@ -1475,19 +1475,19 @@ object Parser { } mut fun skipLineIgnorable() !> Error { - while (this.match(TokenTag.Newline)) {} + while (this.match(.Newline)) {} } mut fun skipNewlines() !> Error { - while (this.match(TokenTag.Newline)) {} + while (this.match(.Newline)) {} } mut fun consumeLineEnd() !> Error { - if (this.match(TokenTag.Eof)) { + if (this.match(.Eof)) { return; } - this.consume(TokenTag.Newline, message: "Expected new line"); + this.consume(.Newline, message: "Expected new line"); } mut fun resolveTablePath() > bool { @@ -1500,7 +1500,7 @@ object Parser { final last = i == this.currentPath.len() - 1; if (current as? mut {str: any} -> map) { - if (part.tag != PathPartTag.Key) { + if (part.tag != .Key) { return false; } @@ -1571,7 +1571,7 @@ object Parser { final last = i == this.currentPath.len() - 1; if (current as? mut {str: any} -> map) { - if (part.tag != PathPartTag.Key) { + if (part.tag != .Key) { return false; } @@ -1652,7 +1652,7 @@ object Parser { var current: any = this.parsed; foreach (i, part in this.currentPath.parts) { if (current as? mut {str: any} -> map) { - if (part.tag != PathPartTag.Key) { + if (part.tag != .Key) { return false; } @@ -1672,7 +1672,7 @@ object Parser { current = map[key]; } } else if (current as? mut [any] -> list) { - if (part.tag == PathPartTag.Index) { + if (part.tag == .Index) { if (i == this.currentPath.len() - 1) { list.append(value); } else { @@ -1712,7 +1712,7 @@ object Parser { var current: any = this.parsed; foreach (i, part in this.currentPath.parts) { if (current as? mut {str: any} -> map) { - if (part.tag != PathPartTag.Key) { + if (part.tag != .Key) { return null; } @@ -1727,7 +1727,7 @@ object Parser { current = map[key]; } } else if (current as? [any] -> list) { - if (part.tag == PathPartTag.Index) { + if (part.tag == .Index) { if (i == this.currentPath.len() - 1) { return list[?part.index!]; } else { @@ -1750,7 +1750,7 @@ object Parser { } mut fun advance() !> Error { - if (this.currentToken != null and this.scanner.tokens[this.currentToken!].tag == TokenTag.Eof) { + if (this.currentToken != null and this.scanner.tokens[this.currentToken!].tag == .Eof) { return; } @@ -1763,7 +1763,7 @@ object Parser { if (this.currentToken! >= this.scanner.tokens.len()) { final newToken = this.scanner.scan(); - if (newToken.tag == TokenTag.Error) { + if (newToken.tag == .Error) { this.report("Syntax error: {newToken.error ?? "unexpected character"}"); throw Error.Invalid; @@ -1797,7 +1797,7 @@ object Parser { fun check(tag: TokenTag) => this.currentToken != null and this.scanner.tokens[this.currentToken!].tag == tag; - fun isKeyStartTag(tag: TokenTag) => tag == TokenTag.LeftBracket or tag == TokenTag.BareKey or tag == TokenTag.LiteralString or tag == TokenTag.BasicString or tag == TokenTag.Boolean or tag == TokenTag.Integer or tag == TokenTag.Float or tag == TokenTag.LocalDate; + fun isKeyStartTag(tag: TokenTag) => tag == .LeftBracket or tag == .BareKey or tag == .LiteralString or tag == .BasicString or tag == .Boolean or tag == .Integer or tag == .Float or tag == .LocalDate; mut fun match(tag: TokenTag) > bool !> Error { if (!this.check(tag)) { @@ -1824,7 +1824,7 @@ object Parser { this.panic_mode = false; // Advance until we see a token from which we can recover - while (this.currentToken != null and this.scanner.tokens[this.currentToken!].tag != TokenTag.Eof) { + while (this.currentToken != null and this.scanner.tokens[this.currentToken!].tag != .Eof) { if (this.currentToken == 0) { return; } @@ -1859,22 +1859,22 @@ object Parser { mut fun parseKey() > [str] !> Error { final result = mut []; - while (!this.match(TokenTag.Eof)) { + while (!this.match(.Eof)) { this.consumeAnyOf( [ - TokenTag.BareKey, - TokenTag.LiteralString, - TokenTag.BasicString, - TokenTag.Boolean, - TokenTag.Integer, - TokenTag.Float, - TokenTag.LocalDate, + .BareKey, + .LiteralString, + .BasicString, + .Boolean, + .Integer, + .Float, + .LocalDate, ], message: "Expected bare key", ); final token = this.scanner.tokens[this.currentToken! - 1]; - if (token.tag == TokenTag.LiteralString or token.tag == TokenTag.BasicString) { + if (token.tag == .LiteralString or token.tag == .BasicString) { final literal = token.literal(); if (literal as? str -> key) { result.append(key); @@ -1888,7 +1888,7 @@ object Parser { } - if (!this.match(TokenTag.Dot)) { + if (!this.match(.Dot)) { break; } } @@ -1897,7 +1897,7 @@ object Parser { } mut fun parseValue() > bool !> Error { - if (this.match(TokenTag.Boolean)) { + if (this.match(.Boolean)) { final token = this.scanner.tokens[this.currentToken! - 1]; final scalar = scalarFromToken(token); @@ -1910,19 +1910,19 @@ object Parser { return this.setValue(scalar); } - if (this.match(TokenTag.LeftBrace)) { + if (this.match(.LeftBrace)) { return this.parseInlineTable(); } if (this.matchAnyOf([ - TokenTag.LiteralString, - TokenTag.MultilineLiteralString, - TokenTag.BasicString, - TokenTag.MultilineBasicString, - TokenTag.OffsetDateTime, - TokenTag.LocalDateTime, - TokenTag.LocalTime, - TokenTag.LocalDate, + .LiteralString, + .MultilineLiteralString, + .BasicString, + .MultilineBasicString, + .OffsetDateTime, + .LocalDateTime, + .LocalTime, + .LocalDate, ])) { final token = this.scanner.tokens[this.currentToken! - 1]; final scalar = scalarFromToken(token); @@ -1936,7 +1936,7 @@ object Parser { return this.setValue(scalar); } - if (this.match(TokenTag.Integer)) { + if (this.match(.Integer)) { final token = this.scanner.tokens[this.currentToken! - 1]; final value = scalarFromToken(token); @@ -1949,7 +1949,7 @@ object Parser { return this.setValue(value); } - if (this.match(TokenTag.Float)) { + if (this.match(.Float)) { final token = this.scanner.tokens[this.currentToken! - 1]; final value = scalarFromToken(token); @@ -1962,7 +1962,7 @@ object Parser { return this.setValue(value); } - if (this.match(TokenTag.LeftBracket)) { + if (this.match(.LeftBracket)) { return this.parseArray(); } @@ -1976,11 +1976,11 @@ object Parser { } while (true) { - if (this.match(TokenTag.RightBrace)) { + if (this.match(.RightBrace)) { break; } - if (this.match(TokenTag.Eof)) { + if (this.match(.Eof)) { this.report("Unfinished inline table"); throw Error.Invalid; @@ -1990,12 +1990,12 @@ object Parser { return false; } - if (!this.match(TokenTag.Comma)) { - this.consume(TokenTag.RightBrace, message: "Expected `}`"); + if (!this.match(.Comma)) { + this.consume(.RightBrace, message: "Expected `}`"); break; } - if (this.check(TokenTag.RightBrace)) { + if (this.check(.RightBrace)) { this.report("Trailing comma in inline table"); throw Error.Invalid; @@ -2015,11 +2015,11 @@ object Parser { var i = 0; while (true) { - if (this.match(TokenTag.RightBracket)) { + if (this.match(.RightBracket)) { break; } - if (this.match(TokenTag.Eof)) { + if (this.match(.Eof)) { this.report("Unfinished array"); throw Error.Invalid; @@ -2036,9 +2036,9 @@ object Parser { this.skipNewlines(); - if (!this.match(TokenTag.Comma)) { + if (!this.match(.Comma)) { this.skipNewlines(); - this.consume(TokenTag.RightBracket, message: "Expected `]`"); + this.consume(.RightBracket, message: "Expected `]`"); break; } @@ -2056,7 +2056,7 @@ object Parser { this.currentKeyBaseLen = baseLen; this.currentPath.appendKeys(key); - this.consume(TokenTag.Equal, message: "Expected `=`"); + this.consume(.Equal, message: "Expected `=`"); final ok = this.parseValue(); diff --git a/src/obj.zig b/src/obj.zig index 3f8a58c7..297f7a98 100644 --- a/src/obj.zig +++ b/src/obj.zig @@ -5672,7 +5672,7 @@ pub const PlaceholderDef = struct { /// What's the relation with the parent? parent_relation: ?Relation = null, /// Children adds themselves here - children: std.ArrayList(*ObjTypeDef), + children: std.ArrayList(*ObjTypeDef) = .empty, mutable: ?bool, /// If the placeholder is a function return, we need to remember eventual generic types defined in that call @@ -5682,7 +5682,6 @@ pub const PlaceholderDef = struct { return Self{ .where = where, .where_end = where_end, - .children = std.ArrayList(*ObjTypeDef).empty, .mutable = mutable, }; } diff --git a/src/renderer.zig b/src/renderer.zig index bfb8d234..97921e7f 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -118,6 +118,7 @@ pub const Renderer = struct { const renderers = [_]RenderNode{ renderAnonymousObjectType, + renderAnonymousEnumCase, renderAs, renderAsyncCall, renderBinary, @@ -182,6 +183,17 @@ pub const Renderer = struct { renderZdef, }; + fn renderAnonymousEnumCase(self: *Self, node: Ast.Node.Index, space: Space) Error!void { + const locations = self.ast.nodes.items(.location); + const components = self.ast.nodes.items(.components)[node].AnonymousEnumCase; + + // . + try self.renderExpectedToken(locations[node], .Dot, .None); + + // case + try self.renderExpectedToken(components.case_name, .Identifier, space); + } + fn renderExpectedTokenSequence(self: *Self, start_token: Ast.TokenIndex, comptime expected: []const Token.Type, space: Space) Error!void { for (expected, 0..) |tag, offset| { try self.renderExpectedToken( diff --git a/src/vm.zig b/src/vm.zig index 2d40ae0c..521d5967 100644 --- a/src/vm.zig +++ b/src/vm.zig @@ -5279,6 +5279,7 @@ pub const VM = struct { _ = self.pop(); // Default value in case of error + self.current_fiber.stack_top = self.current_fiber.stack_top - arg_count - 1; self.push(catch_value.?); return; } diff --git a/tests/behavior/crypto.buzz b/tests/behavior/crypto.buzz index 1f17f1e5..5d7c951e 100644 --- a/tests/behavior/crypto.buzz +++ b/tests/behavior/crypto.buzz @@ -3,22 +3,22 @@ import "crypto" as _; test "hash" { std\assert( - "c3fcd3d76192e4007dfb496cca67e13b" == hash(HashAlgorithm.Md5, data: "abcdefghijklmnopqrstuvwxyz").hex(), + "c3fcd3d76192e4007dfb496cca67e13b" == hash(.Md5, data: "abcdefghijklmnopqrstuvwxyz").hex(), message: "md5", ); std\assert( - "a9993e364706816aba3e25717850c26c9cd0d89d" == hash(HashAlgorithm.Sha1, data: "abc").hex(), + "a9993e364706816aba3e25717850c26c9cd0d89d" == hash(.Sha1, data: "abc").hex(), message: "sha1", ); std\assert( - "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" == hash(HashAlgorithm.Sha256, data: "abc").hex(), + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" == hash(.Sha256, data: "abc").hex(), message: "sha256", ); std\assert( - "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532" == hash(HashAlgorithm.Sha3256, data: "abc").hex(), + "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532" == hash(.Sha3256, data: "abc").hex(), message: "sha3-256", ); } diff --git a/tests/behavior/inferred-enum-case.buzz b/tests/behavior/inferred-enum-case.buzz new file mode 100644 index 00000000..e8f3422b --- /dev/null +++ b/tests/behavior/inferred-enum-case.buzz @@ -0,0 +1,105 @@ +import "std"; + +enum Locale { + fr, + en, + it, +} + +object HasLocalField { + locale: Locale, + defaultLocale: Locale = .it, +} + +fun identity(locale: Locale) > Locale { + return locale; +} + +fun defaultLocale() > Locale { + return .it; +} + +fun maybeLocale() > Locale? { + return null; +} + +fun failLocale() > Locale !> str { + throw "no locale"; + return .fr; +} + +fun localeFiber() > void *> Locale? { + _ = yield .en; +} + +fun acceptsLocaleList(locales: [Locale]) > bool { + foreach (locale in locales) { + if (locale == .en) { + return true; + } + } + + return false; +} + +test "Can infer enum type" { + final value: Locale = .fr; + + std\assert(value == Locale.fr, message: "Infer var declaration"); + std\assert(.fr == Locale.fr, message: "Infer in reversed comparison"); + std\assert(identity(.en) == Locale.en, message: "Infer call argument"); + std\assert(defaultLocale() == Locale.it, message: "Infer return value"); + std\assert((maybeLocale() ?? .en) == Locale.en, message: "Infer null coalescing fallback"); + std\assert((failLocale() catch .en) == Locale.en, message: "Infer inline catch default"); + + final coalesced = maybeLocale() ?? .it; + + std\assert(coalesced == Locale.it, message: "Infer null coalescing variable"); + + final instance = HasLocalField{ + locale = .en, + }; + + std\assert(instance.locale == Locale.en, message: "Infer object field"); + std\assert(instance.defaultLocale == Locale.it, message: "Infer object default value"); + + std\assert(Locale.fr == .fr, message: "Infer in comparison"); + + final list: [Locale] = [ .fr, .en ]; + + std\assert(list[0] == Locale.fr, message: "Infer in list"); + std\assert(list[1] == Locale.en, message: "Infer in list"); + + final explicitList = [, .it ]; + + std\assert(explicitList[0] == Locale.it, message: "Infer in explicit list"); + + final mutableList: mut [Locale] = mut [ .fr ]; + mutableList[0] = .it; + + std\assert(mutableList[0] == Locale.it, message: "Infer list subscript assignment"); + + final map: {str: Locale} = { + "fr": .fr, + }; + + std\assert(map["fr"] == Locale.fr, message: "Infer in map"); + + final explicitMap = {, "it": .it }; + + std\assert(explicitMap["it"] == Locale.it, message: "Infer in explicit map"); + + var assigned: Locale = Locale.fr; + assigned = .en; + + std\assert(assigned == Locale.en, message: "Infer named assignment"); + + final fiber = &localeFiber(); + + std\assert(resume fiber == Locale.en, message: "Infer yield value"); + + std\assert( + acceptsLocaleList([ .fr, .en ]), + message: "Infer enum case in constant-foldable list argument", + ); +} diff --git a/tests/behavior/io.buzz b/tests/behavior/io.buzz index 0a55914a..7a79f797 100644 --- a/tests/behavior/io.buzz +++ b/tests/behavior/io.buzz @@ -3,14 +3,14 @@ import "io"; import "fs"; test "Write & read a file" { - final file = io\File.open("./hello.txt", mode: io\FileMode.write); + final file = io\File.open("./hello.txt", mode: .write); file.write("Hello World"); file.close(); // Now read it - final fileB = io\File.open("./hello.txt", mode: io\FileMode.read); + final fileB = io\File.open("./hello.txt", mode: .read); std\assert(fileB.readAll() == "Hello World", message: "Could write and read a file"); @@ -24,7 +24,7 @@ test "Write on stdout" { } test "Read by lines" { - final file = io\File.open("./README.md", mode: io\FileMode.read); + final file = io\File.open("./README.md", mode: .read); for (lines: int = 0, line: str? = ""; line != null; line = file.readLine(), lines = lines + 1) { std\print("{lines}: {line}"); @@ -34,7 +34,7 @@ test "Read by lines" { } test "Read" { - final file = io\File.open("./README.md", mode: io\FileMode.read); + final file = io\File.open("./README.md", mode: .read); std\assert(file.read(18) == "

", message: "Can read n bytes"); diff --git a/tests/compile_errors/inferred-enum-case-invalid.buzz b/tests/compile_errors/inferred-enum-case-invalid.buzz new file mode 100644 index 00000000..23c2dc95 --- /dev/null +++ b/tests/compile_errors/inferred-enum-case-invalid.buzz @@ -0,0 +1,8 @@ +// Could not infer type for enum case `es`. +enum Locale { + fr, +} + +test "invalid inferred enum case" { + final value: Locale = .es; +} diff --git a/tests/manual/http-client.buzz b/tests/manual/http-client.buzz index 2083deb9..1187718c 100644 --- a/tests/manual/http-client.buzz +++ b/tests/manual/http-client.buzz @@ -6,7 +6,7 @@ fun main() > void !> any { final client = Client.init(); final request = mut Request{ - method = Method.GET, + method = .GET, headers = { "accept": "*/*", }, diff --git a/tests/manual/tcp-client.buzz b/tests/manual/tcp-client.buzz index 66aec80a..4b1e972f 100644 --- a/tests/manual/tcp-client.buzz +++ b/tests/manual/tcp-client.buzz @@ -5,7 +5,7 @@ fun main() > void !> any { final socket = os\Socket.connect( address: "127.0.0.1", port: 8080, - netProtocol: os\SocketProtocol.tcp + netProtocol: .tcp ); std\print("socket fd: {socket.fd}"); diff --git a/tests/manual/write-file.buzz b/tests/manual/write-file.buzz index 9e8e4ccc..d814fcd9 100644 --- a/tests/manual/write-file.buzz +++ b/tests/manual/write-file.buzz @@ -13,7 +13,7 @@ fun writeFile(file: io\File, a: str, b: int, c: bool) > void !> any { fun main() > void !> any { final file = io\File.open( "/Users/giann/git/buzz/ooout.txt", - mode: io\FileMode.write, + mode: .write, ); writeFile(file, a: "hey", b: 12, c: true);