From 468d1144382eab879c536f71e599714f24de8c6f Mon Sep 17 00:00:00 2001 From: Benoit Giannangeli Date: Mon, 3 Jun 2024 16:56:25 +0200 Subject: [PATCH] feat: Tuples Syntaxic sugar over anonymous objects closes #298 --- CHANGELOG.md | 6 + src/Parser.zig | 196 ++++++++++++++++++----- src/Reporter.zig | 2 + tests/042-anonymous-objects.buzz | 11 +- tests/073-tuples.buzz | 27 ++++ tests/compile_errors/031-long-tuple.buzz | 4 + tests/compile_errors/032-tuple-mix.buzz | 4 + 7 files changed, 210 insertions(+), 40 deletions(-) create mode 100644 tests/073-tuples.buzz create mode 100644 tests/compile_errors/031-long-tuple.buzz create mode 100644 tests/compile_errors/032-tuple-mix.buzz diff --git a/CHANGELOG.md b/CHANGELOG.md index 52a309a1..9efe577e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## Added - Object can have `const` properties (https://github.com/buzz-language/buzz/issues/13). A object with only `const` properties is considered itself `const`. Although we don't do anything yet with that concept. https://github.com/buzz-language/buzz/issues/114 is the objective but it requires being able to build objects and instances at compile time which is not yet possible. - `rg.subsetOf`, `rg.intersect`, `rg.union` +- Tuples (https://github.com/buzz-language/buzz/issues/298): syntaxic sugar over anonymous objects: +```buzz +const tuple = .{ "john", "james" }; + +tuples.@"0" == "john"; +``` ## Modified - Enum can now have `rg`, `ud`, `void`, `pat` has value type diff --git a/src/Parser.zig b/src/Parser.zig index e7f6c298..725aacf1 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -699,7 +699,7 @@ fn match(self: *Self, tag: Token.Type) !bool { return true; } -// Insert token in ast and advance over it to avoid confusing the parser +/// Insert token in ast and advance over it to avoid confusing the parser fn insertUtilityToken(self: *Self, token: Token) !Ast.TokenIndex { const current_token = self.ast.tokens.get(self.current_token.?); @@ -3004,15 +3004,61 @@ fn parseObjType(self: *Self, generic_types: ?std.AutoArrayHashMap(*obj.ObjString defer field_names.deinit(); var fields = std.ArrayList(Ast.AnonymousObjectType.Field).init(self.gc.allocator); defer fields.shrinkAndFree(fields.items.len); + var tuple_index: u8 = 0; + var obj_is_tuple = false; + var obj_is_not_tuple = false; while (!self.check(.RightBrace) and !self.check(.Eof)) { + const constant = try self.match(.Const); + const property_type = try self.parseTypeDef(generic_types, true); - const constant = try self.match(.Const); - try self.consume(.Identifier, "Expected property name."); - const property_name = self.current_token.? - 1; + const is_tuple = !(try self.match(.Identifier)); + const property_name = if (!is_tuple) + self.current_token.? - 1 + else + try self.insertUtilityToken( + Token.identifier( + switch (tuple_index) { + 0 => "0", + 1 => "1", + 2 => "2", + 3 => "3", + else => "invalid", + }, + ), + ); const property_name_lexeme = self.ast.tokens.items(.lexeme)[property_name]; - if (field_names.get(property_name_lexeme) != null) { + if (is_tuple) { + obj_is_tuple = true; + + if (obj_is_not_tuple) { + self.reportErrorAtCurrent( + .mix_tuple, + "Can't mix tuple notation and regular properties in anonymous object initialization", + ); + } + + if (tuple_index >= 4) { + self.reportErrorAtCurrent( + .tuple_limit, + "Tuples can't have more than 4 elements", + ); + } + + tuple_index += 1; + } else { + obj_is_not_tuple = true; + + if (obj_is_tuple) { + self.reportErrorAtCurrent( + .mix_tuple, + "Can't mix tuple notation and regular properties in anonymous object initialization", + ); + } + } + + if (!is_tuple and field_names.get(property_name_lexeme) != null) { self.reportError(.property_already_exists, "A property with that name already exists."); } @@ -3871,26 +3917,63 @@ fn anonymousObjectInit(self: *Self, _: bool) Error!Ast.Node.Index { var property_names = std.StringHashMap(Ast.Node.Index).init(self.gc.allocator); defer property_names.deinit(); + var tuple_index: u8 = 0; + var obj_is_tuple = false; + var obj_is_not_tuple = false; while (!self.check(.RightBrace) and !self.check(.Eof)) { - try self.consume(.Identifier, "Expected property name"); + // Unnamed: this expression is a little bit tricky: + // - either an identifier followed by something other than = + // - or not an identifier + if ((self.check(.Identifier) and !(try self.checkAhead(.Equal, 0))) or + !self.check(.Identifier)) + { + const expr = try self.expression(false); + const is_tuple = self.ast.nodes.items(.tag)[expr] != .NamedVariable; - const property_name = self.current_token.? - 1; - const property_name_lexeme = self.ast.tokens.items(.lexeme)[property_name]; - if (property_names.get(property_name_lexeme)) |previous_decl| { - self.reporter.reportWithOrigin( - .property_already_exists, - self.ast.tokens.get(property_name), - self.ast.tokens.get(previous_decl), - "Property `{s}` was already defined", - .{property_name_lexeme}, - "Defined here", - ); - } - try property_names.put(property_name_lexeme, property_name); + if (is_tuple) { + obj_is_tuple = true; - // Named variable with the same name as property - const expr = if (self.check(.Comma) or self.check(.RightBrace)) named: { - const expr = try self.expression(true); + if (obj_is_not_tuple) { + self.reportErrorAtCurrent( + .mix_tuple, + "Can't mix tuple notation and regular properties in anonymous object initialization", + ); + } + } + + if (is_tuple and tuple_index >= 4) { + self.reportErrorAtCurrent( + .tuple_limit, + "Tuples can't have more than 4 elements", + ); + } + + // Consume identifier if it exists + const property_name = if (!is_tuple) + self.current_token.? - 1 + else + try self.insertUtilityToken( + Token.identifier( + switch (tuple_index) { + 0 => "0", + 1 => "1", + 2 => "2", + 3 => "3", + else => "invalid", + }, + ), + ); + + if (is_tuple) { + tuple_index += 1; + } + + const property_name_lexeme = self.ast.tokens.items(.lexeme)[property_name]; + + try property_names.put( + property_name_lexeme, + property_name, + ); try properties.append( .{ @@ -3899,8 +3982,45 @@ fn anonymousObjectInit(self: *Self, _: bool) Error!Ast.Node.Index { }, ); - break :named expr; - } else regular: { + try object_type.resolved_type.?.Object.fields.put( + property_name_lexeme, + .{ + .name = property_name_lexeme, + .type_def = self.ast.nodes.items(.type_def)[expr].?, + .location = self.ast.tokens.get(property_name), + .static = false, + .method = false, + .constant = false, + .has_default = false, + }, + ); + } else { + try self.consume(.Identifier, "Expected property name"); + + obj_is_not_tuple = true; + + if (obj_is_tuple) { + self.reportErrorAtCurrent( + .mix_tuple, + "Can't mix tuple notation and regular properties in anonymous object initialization", + ); + } + + const property_name = self.current_token.? - 1; + const property_name_lexeme = self.ast.tokens.items(.lexeme)[property_name]; + if (property_names.get(property_name_lexeme)) |previous_decl| { + self.reporter.reportWithOrigin( + .property_already_exists, + self.ast.tokens.get(property_name), + self.ast.tokens.get(previous_decl), + "Property `{s}` was already defined", + .{property_name_lexeme}, + "Defined here", + ); + } + try property_names.put(property_name_lexeme, property_name); + + // Named variable with the same name as property or tuple notation try self.consume(.Equal, "Expected `=` after property name."); const expr = try self.expression(false); @@ -3912,21 +4032,19 @@ fn anonymousObjectInit(self: *Self, _: bool) Error!Ast.Node.Index { }, ); - break :regular expr; - }; - - try object_type.resolved_type.?.Object.fields.put( - property_name_lexeme, - .{ - .name = property_name_lexeme, - .type_def = self.ast.nodes.items(.type_def)[expr].?, - .location = self.ast.tokens.get(property_name), - .static = false, - .method = false, - .constant = false, - .has_default = false, - }, - ); + try object_type.resolved_type.?.Object.fields.put( + property_name_lexeme, + .{ + .name = property_name_lexeme, + .type_def = self.ast.nodes.items(.type_def)[expr].?, + .location = self.ast.tokens.get(property_name), + .static = false, + .method = false, + .constant = false, + .has_default = false, + }, + ); + } if (!self.check(.RightBrace) or self.check(.Comma)) { try self.consume(.Comma, "Expected `,` after field initialization."); diff --git a/src/Reporter.zig b/src/Reporter.zig index 9fa79b7f..457e7899 100644 --- a/src/Reporter.zig +++ b/src/Reporter.zig @@ -111,6 +111,8 @@ pub const Error = enum(u8) { unused_import = 94, label_does_not_exists = 95, constant_property = 96, + tuple_limit = 97, + mix_tuple = 98, }; // Inspired by https://github.com/zesterer/ariadne diff --git a/tests/042-anonymous-objects.buzz b/tests/042-anonymous-objects.buzz index e57c13aa..f573d417 100644 --- a/tests/042-anonymous-objects.buzz +++ b/tests/042-anonymous-objects.buzz @@ -16,6 +16,15 @@ test "Anonymous objects" { std.assert(info is obj{ str name, int age }, message: "Type safety works with anonymous object"); } +test "Named variable init" { + const name = "Joe"; + const age = 42; + + const person = .{ name, age }; + + std.assert(person.name == "Joe" and person.age == 42); +} + fun getPayload::(T data) > obj{ T data } { return .{ data = data @@ -26,4 +35,4 @@ test "Anonymous object with generics" { obj{ int data } payload = getPayload::(42); std.assert(payload.data == 42, message: "Could use anonymous object with generic"); -} \ No newline at end of file +} diff --git a/tests/073-tuples.buzz b/tests/073-tuples.buzz new file mode 100644 index 00000000..d5432833 --- /dev/null +++ b/tests/073-tuples.buzz @@ -0,0 +1,27 @@ +import "std"; + +fun pack([str] names) > obj{ str, str, str } { + return .{ + names[0], + names[1], + names[2], + }; +} + +test "Tuples" { + const tuple = .{ "joe", "doe" }; + + std.assert(tuple.@"0" == "joe" and tuple.@"1" == "doe"); + + const name = "Joe"; + const age = 42; + + | Forced tuple + const another = .{ (name), (age) }; + + std.assert(another.@"0" == "Joe" and another.@"1" == 42); + + const names = pack(["Joe", "John", "James"]); + + std.assert(names.@"0" == "Joe" and names.@"1" == "John"); +} diff --git a/tests/compile_errors/031-long-tuple.buzz b/tests/compile_errors/031-long-tuple.buzz new file mode 100644 index 00000000..255e576d --- /dev/null +++ b/tests/compile_errors/031-long-tuple.buzz @@ -0,0 +1,4 @@ +| Tuples can't have more than 4 elements +test "Long tuple" { + .{ 1, 2, 3, 4, 5 }; +} diff --git a/tests/compile_errors/032-tuple-mix.buzz b/tests/compile_errors/032-tuple-mix.buzz new file mode 100644 index 00000000..c4c53d39 --- /dev/null +++ b/tests/compile_errors/032-tuple-mix.buzz @@ -0,0 +1,4 @@ +| Can't mix tuple notation and regular properties in anonymous object initialization +test "Mixing tuple notation and regular anonymous object" { + .{ name = "Joe", 42 }; +}