Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion src/Parser.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3777,6 +3777,7 @@ fn parseObjType(self: *Self, generic_types: ?std.AutoArrayHashMapUnmanaged(*obj.
try self.consume(.RightBrace, "Expected `}` after object body.");

object_type.optional = try self.match(.Question);
object_type.resolved_type.?.Object.is_tuple = obj_is_tuple;

try object_type.resolved_type.?.Object.sortFieldIndexes(self.gc.allocator);

Expand Down Expand Up @@ -5012,6 +5013,8 @@ fn anonymousObjectInit(self: *Self, _: bool) Error!Ast.Node.Index {
}
}

object_type.resolved_type.?.Object.is_tuple = obj_is_tuple;

try object_type.resolved_type.?.Object.sortFieldIndexes(self.gc.allocator);

try self.consume(.RightBrace, "Expected `}` after object initialization.");
Expand Down Expand Up @@ -5046,7 +5049,24 @@ fn dot(self: *Self, can_assign: bool, callee: Ast.Node.Index) Error!Ast.Node.Ind
);
}

try self.consume(.Identifier, "Expected property name after `.`");
if (try self.match(.IntegerValue)) {
const index_token = self.current_token.? - 1;
const index = self.ast.tokens.items(.lexeme)[index_token];

// Tuple shorthand is validated semantically by the typechecker once
// placeholders can resolve; the parser only rejects non-decimal forms.
if (index.len != 1 or index[0] < '0' or index[0] > '3') {
self.reporter.reportErrorAt(
.syntax,
self.ast.tokens.get(index_token),
self.ast.tokens.get(index_token),
"Tuple property shorthand only accepts indexes 0, 1, 2 or 3",
);
}
} else {
try self.consume(.Identifier, "Expected property name after `.`");
}

const member_name_token = self.current_token.? - 1;
const member_name = self.ast.tokens.items(.lexeme)[member_name_token];

Expand Down
30 changes: 28 additions & 2 deletions src/Scanner.zig
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,9 @@ fn atIdentifier(self: *Self) Token {
}

const string_token = self.string(false);
if (string_token.tag == .Error) {
return string_token;
}

self.token_index += 1;
return .{
Expand Down Expand Up @@ -609,6 +612,9 @@ pub fn highlight(self: *Self, out: *std.Io.Writer, true_color: bool) void {
var previous_offset: usize = 0;
var token = self.scanToken() catch unreachable;
while (token.tag != .Eof and token.tag != .Error) {
const token_end = self.current.offset;
const source_lexeme = self.source[token.offset..token_end];

// If there some whitespace or comments between tokens?
// In gray because either whitespace or comment
if (token.offset > previous_offset) {
Expand Down Expand Up @@ -754,12 +760,12 @@ pub fn highlight(self: *Self, out: *std.Io.Writer, true_color: bool) void {
.Docblock => if (true_color) Color.comment else Color.dim,
.Eof, .Error => unreachable,
},
token.lexeme,
source_lexeme,
Color.reset,
},
) catch unreachable;

previous_offset = token.offset + token.lexeme.len;
previous_offset = token_end;

token = self.scanToken() catch unreachable;
}
Expand All @@ -769,6 +775,26 @@ pub fn highlight(self: *Self, out: *std.Io.Writer, true_color: bool) void {
}
}

test "highlight preserves free identifier source spelling" {
var out = std.Io.Writer.Allocating.init(std.testing.allocator);
defer out.deinit();

var scanner = Self.init(std.testing.allocator, "test", "@\"hello\"");
scanner.highlight(&out.writer, false);

try std.testing.expectEqualStrings("@\"hello\"\x1b[0m", out.written());
}

test "highlight preserves unterminated free identifier source" {
var out = std.Io.Writer.Allocating.init(std.testing.allocator);
defer out.deinit();

var scanner = Self.init(std.testing.allocator, "test", "@\"hello");
scanner.highlight(&out.writer, false);

try std.testing.expectEqualStrings("@\"hello", out.written());
}

pub const Color = struct {
pub const black = "\x1b[30m";
pub const red = "\x1b[31m";
Expand Down
23 changes: 20 additions & 3 deletions src/TypeChecker.zig
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,7 @@ fn checkDot(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.Index, no
const locations = ast.nodes.items(.location);
const end_locations = ast.nodes.items(.end_location);
const tags = ast.tokens.items(.tag);
const lexemes = ast.tokens.items(.lexeme);

var had_error = false;

Expand Down Expand Up @@ -907,10 +908,26 @@ fn checkDot(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.Index, no
had_error = true;
}

if (tags[components.identifier] == .IntegerValue and
(callee_type.def_type != .ObjectInstance or
!callee_type.resolved_type.?.ObjectInstance.of
.resolved_type.?.Object
.is_tuple) and
callee_type.def_type != .Placeholder)
{
reporter.reportErrorAt(
.field_access,
ast.tokens.get(components.identifier),
ast.tokens.get(components.identifier),
"Tuple index shorthand is only allowed on tuples",
);
had_error = true;
}

switch (callee_type.def_type) {
.Fiber, .Pattern, .String => {},
.ForeignContainer, .ObjectInstance, .Object => {
const field_name = ast.tokens.items(.lexeme)[components.identifier];
const field_name = lexemes[components.identifier];
const field = switch (callee_type.def_type) {
.ObjectInstance => callee_type.resolved_type.?.ObjectInstance.of
.resolved_type.?.Object
Expand Down Expand Up @@ -1135,7 +1152,7 @@ fn checkDot(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.Index, no
}
},
.ProtocolInstance => if (components.member_kind == .Call) {
const field_name = ast.tokens.items(.lexeme)[components.identifier];
const field_name = lexemes[components.identifier];
const field = callee_type.resolved_type.?.ProtocolInstance.of
.resolved_type.?.Protocol
.methods
Expand Down Expand Up @@ -1166,7 +1183,7 @@ fn checkDot(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.Index, no
.Enum => {},
.EnumInstance => {},
.List, .Map, .Range => if (components.member_kind == .Call) {
const identifier = ast.tokens.items(.lexeme)[components.identifier];
const identifier = lexemes[components.identifier];

switch (callee_type.def_type) {
.List => if (callee_type.resolved_type.?.List.methods.get(identifier)) |member| {
Expand Down
1 change: 1 addition & 0 deletions src/TypeRegistry.zig
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ fn hashHelper(hasher: *std.hash.Wyhash, type_def: *const o.ObjTypeDef) void {
if (resolved.Object.anonymous) {
// If anonymous, we must take the whole type into account
// But since it'type_def anonymous, we only need to worry about fields type knowing there'type_def no method, static, etc.
std.hash.autoHash(hasher, resolved.Object.is_tuple);
var it = resolved.Object.fields.iterator();
while (it.next()) |kv| {
std.hash.autoHash(
Expand Down
5 changes: 5 additions & 0 deletions src/obj.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1796,6 +1796,8 @@ pub const ObjObject = struct {
placeholders: std.StringHashMapUnmanaged(Placeholder),
static_placeholders: std.StringHashMapUnmanaged(Placeholder),
anonymous: bool,
/// True when an anonymous object was parsed with tuple notation.
is_tuple: bool,
conforms_to: std.AutoHashMapUnmanaged(*ObjTypeDef, void),

generic_types: std.AutoArrayHashMapUnmanaged(*ObjString, *ObjTypeDef),
Expand All @@ -1822,6 +1824,7 @@ pub const ObjObject = struct {
.placeholders = .{},
.static_placeholders = .{},
.anonymous = anonymous,
.is_tuple = false,
.conforms_to = .{},
.generic_types = .{},
};
Expand Down Expand Up @@ -4566,6 +4569,7 @@ pub const ObjTypeDef = struct {
old_object_def.qualified_name,
old_object_def.anonymous,
);
resolved.is_tuple = old_object_def.is_tuple;

resolved.generic_types.deinit(type_registry.gc.allocator);
resolved.generic_types = try old_object_def.generic_types.clone(type_registry.gc.allocator);
Expand Down Expand Up @@ -5462,6 +5466,7 @@ pub const ObjTypeDef = struct {
// If both are anonymous object type, we can deeply compare them
if (!expected.Object.anonymous or
!actual.Object.anonymous or
expected.Object.is_tuple != actual.Object.is_tuple or
expected.Object.fields.count() != actual.Object.fields.count())
{
return false;
Expand Down
4 changes: 2 additions & 2 deletions src/renderer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2298,9 +2298,9 @@ pub const Renderer = struct {
);

// identifier
try self.renderExpectedToken(
try self.renderOneOfExpectedToken(
components.identifier,
.Identifier,
&.{ .Identifier, .IntegerValue },
switch (components.member_kind) {
.Ref, .EnumCase => space,
.Value => .Space,
Expand Down
22 changes: 19 additions & 3 deletions tests/behavior/tuples.buzz
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,33 @@ fun pack(names: [str]) > obj{ :str, :str, :str } {
test "Tuples" {
final tuple = .{ "joe", "doe" };

std\assert(tuple.@"0" == "joe" and tuple.@"1" == "doe");
std\assert(
tuple.@"0" == "joe" and tuple.0 == "joe" and tuple.@"1" == "doe"
and tuple.1 == "doe"
);

final name = "Joe";
final age = 42;

// Forced tuple
final another = .{ (name), (age) };

std\assert(another.@"0" == "Joe" and another.@"1" == 42);
std\assert(
another.@"0" == "Joe" and another.0 == "Joe" and another.@"1" == 42
and another.1 == 42
);

final names = pack([ "Joe", "John", "James" ]);

std\assert(names.@"0" == "Joe" and names.@"1" == "John");
std\assert(
names.@"0" == "Joe" and names.0 == "Joe" and names.@"1" == "John"
and names.1 == "John"
);

final positions = .{ 0, 1, 2, 3 };

std\assert(
positions.0 == 0 and positions.1 == 1 and positions.2 == 2
and positions.3 == 3
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Tuple index shorthand is only allowed on tuples
test "Tuple shorthand rejects anonymous numeric fields" {
final value = .{
@"0" = 1,
};

_ = value.0;
}
6 changes: 6 additions & 0 deletions tests/compile_errors/tuple-shorthand-binary-index.buzz
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Tuple property shorthand only accepts indexes 0, 1, 2 or 3
test "Tuple shorthand rejects binary index" {
final tuple = .{ 1, 2, 3, 4 };

_ = tuple.0b10;
}
6 changes: 6 additions & 0 deletions tests/compile_errors/tuple-shorthand-hex-index.buzz
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Tuple property shorthand only accepts indexes 0, 1, 2 or 3
test "Tuple shorthand rejects hexadecimal index" {
final tuple = .{ 1, 2, 3, 4 };

_ = tuple.0x10;
}
18 changes: 18 additions & 0 deletions tests/compile_errors/tuple-shorthand-named-object.buzz
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Tuple index shorthand is only allowed on tuples

/// Object used to verify tuple shorthand does not apply to named numeric fields.
object NumericField {
/// Returns the numeric field using invalid tuple shorthand.
fun get() > int => this.0;

/// Field intentionally named like a tuple property.
@"0": int,
}

test "Tuple shorthand is tuple-only" {
final value = NumericField{
@"0" = 1,
};

_ = value.get();
}
6 changes: 6 additions & 0 deletions tests/compile_errors/tuple-shorthand-out-of-range.buzz
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Tuple property shorthand only accepts indexes 0, 1, 2 or 3
test "Tuple shorthand rejects out-of-range index" {
final tuple = .{ 1, 2, 3, 4 };

_ = tuple.4;
}
6 changes: 6 additions & 0 deletions tests/compile_errors/tuple-shorthand-underscore-index.buzz
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Tuple property shorthand only accepts indexes 0, 1, 2 or 3
test "Tuple shorthand rejects underscore index" {
final tuple = .{ 1, 2, 3, 4 };

_ = tuple.0_0;
}
Loading