Skip to content

Commit

Permalink
feat: Tuples
Browse files Browse the repository at this point in the history
Syntaxic sugar over anonymous objects

closes #298
  • Loading branch information
giann committed Jun 4, 2024
1 parent 5918881 commit 468d114
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 40 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
196 changes: 157 additions & 39 deletions src/Parser.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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.?);

Expand Down Expand Up @@ -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.");
}

Expand Down Expand Up @@ -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(
.{
Expand All @@ -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);
Expand All @@ -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.");
Expand Down
2 changes: 2 additions & 0 deletions src/Reporter.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion tests/042-anonymous-objects.buzz
Original file line number Diff line number Diff line change
Expand Up @@ -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>(T data) > obj{ T data } {
return .{
data = data
Expand All @@ -26,4 +35,4 @@ test "Anonymous object with generics" {
obj{ int data } payload = getPayload::<int>(42);

std.assert(payload.data == 42, message: "Could use anonymous object with generic");
}
}
27 changes: 27 additions & 0 deletions tests/073-tuples.buzz
Original file line number Diff line number Diff line change
@@ -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");
}
4 changes: 4 additions & 0 deletions tests/compile_errors/031-long-tuple.buzz
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
| Tuples can't have more than 4 elements
test "Long tuple" {
.{ 1, 2, 3, 4, 5 };
}
4 changes: 4 additions & 0 deletions tests/compile_errors/032-tuple-mix.buzz
Original file line number Diff line number Diff line change
@@ -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 };
}

0 comments on commit 468d114

Please sign in to comment.