From 1650800e3f9faa56e66423168260d08afdbdb093 Mon Sep 17 00:00:00 2001 From: Benoit Giannangeli Date: Mon, 22 Jan 2024 16:19:32 +0100 Subject: [PATCH] feat: block expression closes #105 --- CHANGELOG.md | 9 ++ src/Ast.zig | 6 ++ src/Codegen.zig | 22 +++++ src/Jit.zig | 16 ++++ src/Parser.zig | 92 ++++++++++++++++++- src/Reporter.zig | 3 + src/Token.zig | 3 + src/main.zig | 18 +--- src/scanner.zig | 4 + tests/070-block-expression.buzz | 12 +++ tests/compile_errors/024-misplaced-out.buzz | 4 + tests/compile_errors/025-multiple-out.buzz | 7 ++ .../026-out-last-statement.buzz | 8 ++ 13 files changed, 187 insertions(+), 17 deletions(-) create mode 100644 tests/070-block-expression.buzz create mode 100644 tests/compile_errors/024-misplaced-out.buzz create mode 100644 tests/compile_errors/025-multiple-out.buzz create mode 100644 tests/compile_errors/026-out-last-statement.buzz diff --git a/CHANGELOG.md b/CHANGELOG.md index d5adcc92..8efe63d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,19 @@ - REPL (https://github.com/buzz-language/buzz/issues/17) available by running buzz without any argument - Function argument names and object property names can be ommitted if the provided value is a named variable with the same name (https://github.com/buzz-language/buzz/issues/204) - Sandboxing build options `memory_limit` and `cycle_limit` (https://github.com/buzz-language/buzz/issues/182) +- Block expression (https://github.com/buzz-language/buzz/issues/105): +```buzz +var value = <{ + | ... + + out result; +} +``` ## Changed - Map type notation has changed from `{K, V}` to `{K: V}`. Similarly map expression with specified typed went from `{, ...}` to `{, ...}` (https://github.com/buzz-language/buzz/issues/253) - `File.readLine`, `File.readAll`, `Socket.readLine`, `Socket.readAll` have now an optional `maxSize` argument +- Overriding variable from upper scope is not allowed anymore ## Fixed diff --git a/src/Ast.zig b/src/Ast.zig index d4744c0a..eb4f3ca1 100644 --- a/src/Ast.zig +++ b/src/Ast.zig @@ -79,6 +79,7 @@ pub const Node = struct { AsyncCall, Binary, Block, + BlockExpression, Boolean, Break, Call, @@ -112,6 +113,7 @@ pub const Node = struct { Null, ObjectDeclaration, ObjectInit, + Out, Pattern, ProtocolDeclaration, Range, @@ -142,6 +144,7 @@ pub const Node = struct { AsyncCall: Node.Index, Binary: Binary, Block: []Node.Index, + BlockExpression: []Node.Index, Boolean: bool, Break: void, Call: Call, @@ -175,6 +178,7 @@ pub const Node = struct { Null: void, ObjectDeclaration: ObjectDeclaration, ObjectInit: ObjectInit, + Out: Node.Index, Pattern: *obj.ObjPattern, ProtocolDeclaration: ProtocolDeclaration, Range: Range, @@ -585,6 +589,8 @@ pub fn isConstant(self: Self, node: Node.Index) bool { .While, .Yield, .Zdef, + .BlockExpression, + .Out, => false, .As => self.isConstant(self.nodes.items(.components)[node].As.left), .Is => self.isConstant(self.nodes.items(.components)[node].Is.left), diff --git a/src/Codegen.zig b/src/Codegen.zig index d0e2f42b..bbbc7770 100644 --- a/src/Codegen.zig +++ b/src/Codegen.zig @@ -62,6 +62,7 @@ const generators = [_]NodeGen{ generateAsyncCall, // AsyncCall, generateBinary, // Binary, generateBlock, // Block, + generateBlockExpression, // BlockExpression, generateBoolean, // Boolean, generateBreak, // Break, generateCall, // Call, @@ -95,6 +96,7 @@ const generators = [_]NodeGen{ generateNull, // Null, generateObjectDeclaration, // ObjectDeclaration, generateObjectInit, // ObjectInit, + generateOut, // Out, generatePattern, // Pattern, generateProtocolDeclaration, // ProtocolDeclaration, generateRange, // Range, @@ -836,6 +838,26 @@ fn generateBlock(self: *Self, node: Ast.Node.Index, breaks: ?*std.ArrayList(usiz return null; } +fn generateBlockExpression(self: *Self, node: Ast.Node.Index, breaks: ?*std.ArrayList(usize)) Error!?*obj.ObjFunction { + for (self.ast.nodes.items(.components)[node].BlockExpression) |statement| { + _ = try self.generateNode(statement, breaks); + } + + try self.patchOptJumps(node); + try self.endScope(node); + + return null; +} + +fn generateOut(self: *Self, node: Ast.Node.Index, breaks: ?*std.ArrayList(usize)) Error!?*obj.ObjFunction { + _ = try self.generateNode(self.ast.nodes.items(.components)[node].Out, breaks); + + try self.patchOptJumps(node); + try self.endScope(node); + + return null; +} + fn generateBoolean(self: *Self, node: Ast.Node.Index, _: ?*std.ArrayList(usize)) Error!?*obj.ObjFunction { try self.emitOpCode( self.ast.nodes.items(.location)[node], diff --git a/src/Jit.zig b/src/Jit.zig index 05b05c54..f9ecfd47 100644 --- a/src/Jit.zig +++ b/src/Jit.zig @@ -343,6 +343,8 @@ fn generateNode(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { .FunDeclaration => try self.generateFunDeclaration(node), .VarDeclaration => try self.generateVarDeclaration(node), .Block => try self.generateBlock(node), + .BlockExpression => try self.generateBlockExpression(node), + .Out => try self.generateNode(components[node].Out), .Call => try self.generateCall(node), .NamedVariable => try self.generateNamedVariable(node), .Return => try self.generateReturn(node), @@ -4032,6 +4034,20 @@ fn generateBlock(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { return null; } +fn generateBlockExpression(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { + const statements = self.state.?.ast.nodes.items(.components)[node].BlockExpression; + + var out_statement: ?m.MIR_op_t = null; + for (statements) |statement| { + out_statement = try self.generateNode(statement); + } + + return if (statements.len > 0 and self.state.?.ast.nodes.items(.tag)[statements[statements.len - 1]] == .Out) + out_statement.? + else + null; +} + fn generateFunDeclaration(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { return try self.generateFunction( self.state.?.ast.nodes.items(.components)[node].FunDeclaration.function, diff --git a/src/Parser.zig b/src/Parser.zig index afb01bf6..427b2428 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -121,6 +121,7 @@ pub const Frame = struct { constants: std.ArrayList(Value), in_try: bool = false, + in_block_expression: bool = false, pub fn resolveGeneric(self: Frame, name: *obj.ObjString) ?*obj.ObjTypeDef { if (self.generics) |generics| { @@ -342,6 +343,8 @@ const rules = [_]ParseRule{ .{ .prefix = null, .infix = null, .precedence = .None }, // zdef .{ .prefix = typeOfExpression, .infix = null, .precedence = .Unary }, // typeof .{ .prefix = null, .infix = null, .precedence = .None }, // var + .{ .prefix = blockExpression, .infix = null, .precedence = .None }, // <{ + .{ .prefix = null, .infix = null, .precedence = .None }, // out }; ast: Ast, @@ -1310,6 +1313,9 @@ fn statement(self: *Self, hanging: bool, loop_scope: ?LoopScope) !?Ast.Node.Inde } else if (try self.match(.Import)) { std.debug.assert(!hanging); return try self.importStatement(); + } else if (try self.match(.Out)) { + std.debug.assert(!hanging); + return try self.outStatement(); } else if (try self.match(.Throw)) { const start_location = self.current_token.? - 1; // For now we don't care about the type. Later if we have `Error` type of data, we'll type check this @@ -1948,7 +1954,7 @@ fn declareVariable(self: *Self, variable_type: *obj.ObjTypeDef, name_token: ?Ast break; } - if (!std.mem.eql(u8, name_lexeme, "_") and std.mem.eql(u8, name_lexeme, local.name.string)) { + if (!std.mem.eql(u8, name_lexeme, "_") and !std.mem.startsWith(u8, name_lexeme, "$") and std.mem.eql(u8, name_lexeme, local.name.string)) { self.reporter.reportWithOrigin( .variable_already_exists, self.ast.tokens.get(name), @@ -5378,6 +5384,63 @@ fn typeOfExpression(self: *Self, _: bool) Error!Ast.Node.Index { ); } +fn blockExpression(self: *Self, _: bool) Error!Ast.Node.Index { + const start_location = self.current_token.? - 1; + + self.current.?.in_block_expression = true; + + self.beginScope(); + + var statements = std.ArrayList(Ast.Node.Index).init(self.gc.allocator); + + var out: ?Ast.Node.Index = null; + while (!self.check(.RightBrace) and !self.check(.Eof)) { + if (try self.declarationOrStatement(null)) |stmt| { + try statements.append(stmt); + + if (self.ast.nodes.items(.tag)[stmt] == .Out) { + if (out != null) { + self.reportError( + .multiple_out, + "Only one `out` statement is allowed in block expression", + ); + } + + out = stmt; + } + } + } + + if (out != null and statements.getLastOrNull() != out) { + self.reportError( + .missing_out, + "Last block expression statement must be `out`", + ); + } + + try self.consume(.RightBrace, "Expected `}` at end of block expression"); + + self.current.?.in_block_expression = false; + + statements.shrinkAndFree(statements.items.len); + + return try self.ast.appendNode( + .{ + .tag = .BlockExpression, + .location = start_location, + .end_location = self.current_token.? - 1, + .type_def = if (out) |o| + self.ast.nodes.items(.type_def)[o] + else + try self.gc.type_registry.getTypeDef(.{ .def_type = .Void }), + .components = .{ + .BlockExpression = statements.items, + }, + .ends_scope = try self.endScope(), + }, + ); +} + fn binary(self: *Self, _: bool, left: Ast.Node.Index) Error!Ast.Node.Index { const start_location = self.ast.nodes.items(.location)[left]; @@ -7633,6 +7696,33 @@ fn returnStatement(self: *Self) Error!Ast.Node.Index { ); } +fn outStatement(self: *Self) Error!Ast.Node.Index { + const start_location = self.current_token.? - 1; + + if (!self.current.?.in_block_expression) { + self.reportError( + .out_not_allowed, + "`out` statement is only allowed inside a block expression", + ); + } + + const expr = try self.expression(false); + + try self.consume(.Semicolon, "Expected `;` after `out` statement."); + + return try self.ast.appendNode( + .{ + .tag = .Out, + .location = start_location, + .end_location = self.current_token.? - 1, + .type_def = self.ast.nodes.items(.type_def)[expr], + .components = .{ + .Out = expr, + }, + }, + ); +} + fn tryStatement(self: *Self) Error!Ast.Node.Index { const start_location = self.current_token.? - 1; diff --git a/src/Reporter.zig b/src/Reporter.zig index 38b300c5..5ec9add9 100644 --- a/src/Reporter.zig +++ b/src/Reporter.zig @@ -104,6 +104,9 @@ pub const Error = enum(u8) { inferred_type = 90, empty_import = 91, import_already_exists = 92, + multiple_out = 93, + missing_out = 94, + out_not_allowed = 95, }; // Inspired by https://github.com/zesterer/ariadne diff --git a/src/Token.zig b/src/Token.zig index fe666085..c0de2430 100644 --- a/src/Token.zig +++ b/src/Token.zig @@ -175,6 +175,8 @@ pub const Type = enum { Zdef, // zdef TypeOf, // typeof Var, // var + Blk, // <{ + Out, // out }; pub const keywords = std.ComptimeStringMap( @@ -232,5 +234,6 @@ pub const keywords = std.ComptimeStringMap( .{ "type", .Type }, .{ "typeof", .TypeOf }, .{ "var", .Var }, + .{ "out", .Out }, }, ); diff --git a/src/main.zig b/src/main.zig index 03e3a987..292f929c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -263,28 +263,14 @@ pub fn main() !void { .Run; if (flavor == .Repl) { - repl(allocator) catch |err| { - std.io.getStdErr().writer().print( - "Failed with error {s}\n", - .{@errorName(err)}, - ) catch unreachable; - - std.os.exit(1); - }; + repl(allocator) catch std.os.exit(1); } else { runFile( allocator, res.positionals[0], positionals.items[1..], flavor, - ) catch |err| { - std.io.getStdErr().writer().print( - "Failed with error {s}\n", - .{@errorName(err)}, - ) catch unreachable; - - std.os.exit(1); - }; + ) catch std.os.exit(1); } std.os.exit(0); diff --git a/src/scanner.zig b/src/scanner.zig index 044d84b5..0a386973 100644 --- a/src/scanner.zig +++ b/src/scanner.zig @@ -92,6 +92,8 @@ pub const Scanner = struct { self.makeToken(.ShiftLeft, null, null, null) else if (self.match('=')) self.makeToken(.LessEqual, null, null, null) + else if (self.match('{')) + self.makeToken(.Blk, null, null, null) else self.makeToken(.Less, null, null, null), '~' => self.makeToken(.Bnot, null, null, null), @@ -668,6 +670,7 @@ pub const Scanner = struct { .Var, .Question, .AsQuestion, + .Out, => if (true_color) Color.keyword else "\x1b[94m", // Punctuation .LeftBracket, @@ -685,6 +688,7 @@ pub const Scanner = struct { .Arrow, .Ampersand, .Spread, + .Blk, => if (true_color) Color.punctuation else Color.magenta, .IntegerValue, .FloatValue, diff --git a/tests/070-block-expression.buzz b/tests/070-block-expression.buzz new file mode 100644 index 00000000..4f7dd746 --- /dev/null +++ b/tests/070-block-expression.buzz @@ -0,0 +1,12 @@ +import "std"; + +test "block expression" { + var value = <{ + print("doing stuff in my block..."); + + out "my value"; + }; + + assert(value == "my value", message: "Could use block expression"); + assert(value is str, message: "Could infer block expression type properly"); +} \ No newline at end of file diff --git a/tests/compile_errors/024-misplaced-out.buzz b/tests/compile_errors/024-misplaced-out.buzz new file mode 100644 index 00000000..7fc7abf8 --- /dev/null +++ b/tests/compile_errors/024-misplaced-out.buzz @@ -0,0 +1,4 @@ +| `out` statement is only allowed inside a block expression +test "misplaced out statement" { + out "not allowed"; +} \ No newline at end of file diff --git a/tests/compile_errors/025-multiple-out.buzz b/tests/compile_errors/025-multiple-out.buzz new file mode 100644 index 00000000..01d4a7dd --- /dev/null +++ b/tests/compile_errors/025-multiple-out.buzz @@ -0,0 +1,7 @@ +| Only one `out` statement is allowed in block expression +test "multiple out statements" { + <{ + out "one"; + out "two"; + }; +} \ No newline at end of file diff --git a/tests/compile_errors/026-out-last-statement.buzz b/tests/compile_errors/026-out-last-statement.buzz new file mode 100644 index 00000000..c3eb4cb7 --- /dev/null +++ b/tests/compile_errors/026-out-last-statement.buzz @@ -0,0 +1,8 @@ +| Last block expression statement must be `out` +test "out must be last statement" { + <{ + out "i should be last"; + + "hello world"; + }; +} \ No newline at end of file