1920 lines
82 KiB
Zig
1920 lines
82 KiB
Zig
const std = @import("std");
|
|
const Token = @import("token.zig").Token;
|
|
const Tag = @import("token.zig").Tag;
|
|
const Lexer = @import("lexer.zig").Lexer;
|
|
const ast = @import("ast.zig");
|
|
const Node = ast.Node;
|
|
const Type = @import("types.zig").Type;
|
|
const errors = @import("errors.zig");
|
|
|
|
pub const Parser = struct {
|
|
lexer: Lexer,
|
|
current: Token,
|
|
source: [:0]const u8,
|
|
allocator: std.mem.Allocator,
|
|
err_msg: ?[]const u8,
|
|
err_offset: ?u32 = null,
|
|
prev_end: u32 = 0,
|
|
diagnostics: ?*errors.DiagnosticList = null,
|
|
|
|
pub fn init(allocator: std.mem.Allocator, source: [:0]const u8) Parser {
|
|
var lexer = Lexer.init(source);
|
|
const first = lexer.next();
|
|
return .{
|
|
.lexer = lexer,
|
|
.current = first,
|
|
.source = source,
|
|
.allocator = allocator,
|
|
.err_msg = null,
|
|
.err_offset = null,
|
|
};
|
|
}
|
|
|
|
fn createNode(self: *Parser, start: u32, data: Node.Data) !*Node {
|
|
const node = try self.allocator.create(Node);
|
|
node.* = .{ .span = .{ .start = start, .end = self.prev_end }, .data = data };
|
|
return node;
|
|
}
|
|
|
|
pub fn parse(self: *Parser) anyerror!*Node {
|
|
var decls = std.ArrayList(*Node).empty;
|
|
while (self.current.tag != .eof) {
|
|
const decl = try self.parseTopLevel();
|
|
try decls.append(self.allocator, decl);
|
|
}
|
|
const node = try self.createNode(0, .{ .root = .{ .decls = try decls.toOwnedSlice(self.allocator) } });
|
|
return node;
|
|
}
|
|
|
|
fn parseTopLevel(self: *Parser) anyerror!*Node {
|
|
const start = self.current.loc.start;
|
|
|
|
// Top-level flat import: #import "path";
|
|
if (self.current.tag == .hash_import) {
|
|
self.advance();
|
|
if (self.current.tag != .string_literal) {
|
|
return self.fail("expected string path after '#import'");
|
|
}
|
|
const raw = self.tokenSlice(self.current);
|
|
const path = raw[1 .. raw.len - 1];
|
|
self.advance();
|
|
try self.expect(.semicolon);
|
|
return try self.createNode(start, .{ .import_decl = .{ .path = path, .name = null } });
|
|
}
|
|
|
|
// Top-level #library directive: #library "libname";
|
|
if (self.current.tag == .hash_library) {
|
|
self.advance();
|
|
if (self.current.tag != .string_literal) {
|
|
return self.fail("expected string after '#library'");
|
|
}
|
|
const raw = self.tokenSlice(self.current);
|
|
const lib_name = raw[1 .. raw.len - 1];
|
|
self.advance();
|
|
try self.expect(.semicolon);
|
|
return try self.createNode(start, .{ .library_decl = .{ .lib_name = lib_name } });
|
|
}
|
|
|
|
// Top-level #run directive
|
|
if (self.current.tag == .hash_run) {
|
|
self.advance();
|
|
const expr = try self.parseExpr();
|
|
try self.expect(.semicolon);
|
|
return try self.createNode(start, .{ .comptime_expr = .{ .expr = expr } });
|
|
}
|
|
|
|
// All top-level declarations start with an identifier
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail("expected identifier at top level");
|
|
}
|
|
const name = self.tokenSlice(self.current);
|
|
self.advance();
|
|
|
|
// IDENT :: ...
|
|
if (self.current.tag == .colon_colon) {
|
|
self.advance();
|
|
return self.parseConstBinding(name, start);
|
|
}
|
|
|
|
// IDENT : type : value; (typed constant)
|
|
// IDENT : type = value; (typed variable)
|
|
if (self.current.tag == .colon) {
|
|
self.advance();
|
|
return self.parseTypedBinding(name, start);
|
|
}
|
|
|
|
// IDENT := value; (variable)
|
|
if (self.current.tag == .colon_equal) {
|
|
self.advance();
|
|
const value = try self.parseExpr();
|
|
try self.expectSemicolonAfter(value);
|
|
return try self.createNode(start, .{ .var_decl = .{ .name = name, .type_annotation = null, .value = value } });
|
|
}
|
|
|
|
return self.fail("expected '::', ':=', or ':' after identifier");
|
|
}
|
|
|
|
fn parseConstBinding(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node {
|
|
// After `::`
|
|
// Could be: #run expr, enum { ... }, (params) -> type { body }, or expr;
|
|
|
|
// Namespaced import: name :: #import "path";
|
|
if (self.current.tag == .hash_import) {
|
|
self.advance();
|
|
if (self.current.tag != .string_literal) {
|
|
return self.fail("expected string path after '#import'");
|
|
}
|
|
const raw = self.tokenSlice(self.current);
|
|
const path = raw[1 .. raw.len - 1];
|
|
self.advance();
|
|
try self.expect(.semicolon);
|
|
return try self.createNode(start_pos, .{ .import_decl = .{ .path = path, .name = name } });
|
|
}
|
|
|
|
// Compile-time evaluation: name :: #run expr;
|
|
if (self.current.tag == .hash_run) {
|
|
const run_start = self.current.loc.start;
|
|
self.advance();
|
|
const inner = try self.parseExpr();
|
|
try self.expect(.semicolon);
|
|
const ct = try self.createNode(run_start, .{ .comptime_expr = .{ .expr = inner } });
|
|
return try self.createNode(start_pos, .{ .const_decl = .{ .name = name, .type_annotation = null, .value = ct } });
|
|
}
|
|
|
|
// Built-in declaration: name :: #builtin;
|
|
if (self.current.tag == .hash_builtin) {
|
|
const bi_start = self.current.loc.start;
|
|
self.advance();
|
|
try self.expect(.semicolon);
|
|
const bi = try self.createNode(bi_start, .{ .builtin_expr = {} });
|
|
return try self.createNode(start_pos, .{ .const_decl = .{ .name = name, .type_annotation = null, .value = bi } });
|
|
}
|
|
|
|
// Enum declaration
|
|
if (self.current.tag == .kw_enum) {
|
|
return self.parseEnumDecl(name, start_pos);
|
|
}
|
|
|
|
// Struct declaration
|
|
if (self.current.tag == .kw_struct) {
|
|
return self.parseStructDecl(name, start_pos);
|
|
}
|
|
|
|
// C-style union declaration
|
|
if (self.current.tag == .kw_union) {
|
|
return self.parseUnionDecl(name, start_pos);
|
|
}
|
|
|
|
// Function declaration: (params) -> type { body } or () { body }
|
|
if (self.current.tag == .l_paren) {
|
|
// Look ahead: is this a function or an expression starting with `(`?
|
|
// Heuristic: if after matching parens we see `{` or `->`, it's a function.
|
|
if (self.isFunctionDef()) {
|
|
return self.parseFnDecl(name, start_pos);
|
|
}
|
|
}
|
|
|
|
// Bare block shorthand: name :: { body } is equivalent to name :: () { body }
|
|
if (self.current.tag == .l_brace) {
|
|
const body = try self.parseBlock();
|
|
return try self.createNode(start_pos, .{ .fn_decl = .{ .name = name, .params = &.{}, .return_type = null, .body = body } });
|
|
}
|
|
|
|
// Otherwise it's a constant expression
|
|
const value = try self.parseExpr();
|
|
|
|
// name :: type_expr #builtin; — builtin with type annotation
|
|
if (self.current.tag == .hash_builtin) {
|
|
const bi_start = self.current.loc.start;
|
|
self.advance();
|
|
try self.expect(.semicolon);
|
|
const bi = try self.createNode(bi_start, .{ .builtin_expr = {} });
|
|
return try self.createNode(start_pos, .{ .const_decl = .{ .name = name, .type_annotation = value, .value = bi } });
|
|
}
|
|
|
|
// name :: type_expr #foreign; — foreign with type annotation
|
|
if (self.current.tag == .hash_foreign) {
|
|
const fi_start = self.current.loc.start;
|
|
self.advance();
|
|
try self.expect(.semicolon);
|
|
const fi = try self.createNode(fi_start, .{ .foreign_expr = {} });
|
|
return try self.createNode(start_pos, .{ .const_decl = .{ .name = name, .type_annotation = value, .value = fi } });
|
|
}
|
|
|
|
try self.expect(.semicolon);
|
|
return try self.createNode(start_pos, .{ .const_decl = .{ .name = name, .type_annotation = null, .value = value } });
|
|
}
|
|
|
|
fn parseTypedBinding(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node {
|
|
// After `name :`
|
|
// Parse type
|
|
const type_node = try self.parseTypeExpr();
|
|
|
|
if (self.current.tag == .colon) {
|
|
// name : type : value; (typed constant)
|
|
self.advance();
|
|
const value = try self.parseExpr();
|
|
try self.expectSemicolonAfter(value);
|
|
return try self.createNode(start_pos, .{ .const_decl = .{ .name = name, .type_annotation = type_node, .value = value } });
|
|
}
|
|
|
|
if (self.current.tag == .equal) {
|
|
// name : type = value; (typed variable)
|
|
self.advance();
|
|
const value = try self.parseExpr();
|
|
try self.expectSemicolonAfter(value);
|
|
return try self.createNode(start_pos, .{ .var_decl = .{ .name = name, .type_annotation = type_node, .value = value } });
|
|
}
|
|
|
|
if (self.current.tag == .semicolon) {
|
|
// name : type; (default-initialized variable)
|
|
self.advance();
|
|
return try self.createNode(start_pos, .{ .var_decl = .{ .name = name, .type_annotation = type_node, .value = null } });
|
|
}
|
|
|
|
return self.fail("expected ':', '=' or ';' after type annotation");
|
|
}
|
|
|
|
fn parseTypeExpr(self: *Parser) anyerror!*Node {
|
|
const start = self.current.loc.start;
|
|
|
|
// Pointer type: *T
|
|
if (self.current.tag == .star) {
|
|
self.advance(); // skip '*'
|
|
const pointee_type = try self.parseTypeExpr();
|
|
return try self.createNode(start, .{ .pointer_type_expr = .{ .pointee_type = pointee_type } });
|
|
}
|
|
|
|
// Array type: [N]T, Slice type: []T, Many-pointer type: [*]T, Sentinel slice: [:0]T
|
|
if (self.current.tag == .l_bracket) {
|
|
self.advance(); // skip '['
|
|
if (self.current.tag == .colon) {
|
|
// Sentinel-terminated slice: [:0]T
|
|
self.advance(); // skip ':'
|
|
if (self.current.tag != .int_literal) {
|
|
return self.fail("expected sentinel value after ':'");
|
|
}
|
|
const sentinel_str = self.tokenSlice(self.current);
|
|
self.advance(); // skip sentinel value
|
|
try self.expect(.r_bracket); // expect ']'
|
|
const elem_type = try self.parseTypeExpr();
|
|
// Build name like "[:0]u8" for type resolution
|
|
const elem_name = if (elem_type.data == .type_expr) elem_type.data.type_expr.name else "?";
|
|
const name = try std.fmt.allocPrint(self.allocator, "[:{s}]{s}", .{ sentinel_str, elem_name });
|
|
return try self.createNode(start, .{ .type_expr = .{ .name = name } });
|
|
}
|
|
if (self.current.tag == .r_bracket) {
|
|
// Slice type: []T
|
|
self.advance(); // skip ']'
|
|
const elem_type = try self.parseTypeExpr();
|
|
return try self.createNode(start, .{ .slice_type_expr = .{ .element_type = elem_type } });
|
|
}
|
|
if (self.current.tag == .star) {
|
|
// Many-pointer type: [*]T
|
|
self.advance(); // skip '*'
|
|
try self.expect(.r_bracket); // expect ']'
|
|
const elem_type = try self.parseTypeExpr();
|
|
return try self.createNode(start, .{ .many_pointer_type_expr = .{ .element_type = elem_type } });
|
|
}
|
|
const len_node = try self.parseExpr();
|
|
try self.expect(.r_bracket);
|
|
const elem_type = try self.parseTypeExpr();
|
|
return try self.createNode(start, .{ .array_type_expr = .{ .length = len_node, .element_type = elem_type } });
|
|
}
|
|
|
|
// Generic type parameter introduction: $T
|
|
if (self.current.tag == .dollar) {
|
|
self.advance();
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail("expected type parameter name after '$'");
|
|
}
|
|
const name = self.tokenSlice(self.current);
|
|
self.advance();
|
|
return try self.createNode(start, .{ .type_expr = .{ .name = name, .is_generic = true } });
|
|
}
|
|
// Function pointer type: (ParamTypes) -> ReturnType
|
|
if (self.current.tag == .l_paren) {
|
|
self.advance(); // skip '('
|
|
var param_types = std.ArrayList(*Node).empty;
|
|
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
|
if (param_types.items.len > 0) {
|
|
try self.expect(.comma);
|
|
}
|
|
try param_types.append(self.allocator, try self.parseTypeExpr());
|
|
}
|
|
try self.expect(.r_paren);
|
|
var return_type: ?*Node = null;
|
|
if (self.current.tag == .arrow) {
|
|
self.advance(); // skip '->'
|
|
return_type = try self.parseTypeExpr();
|
|
}
|
|
return try self.createNode(start, .{ .function_type_expr = .{
|
|
.param_types = try param_types.toOwnedSlice(self.allocator),
|
|
.return_type = return_type,
|
|
} });
|
|
}
|
|
|
|
if (self.current.tag.isTypeKeyword() or self.current.tag == .identifier) {
|
|
var name = self.tokenSlice(self.current);
|
|
self.advance();
|
|
|
|
// Qualified name: ns.Type or ns.Type(args)
|
|
while (self.current.tag == .dot) {
|
|
const dot_lexer = self.lexer;
|
|
const dot_current = self.current;
|
|
const dot_prev_end = self.prev_end;
|
|
self.advance();
|
|
if (self.current.tag == .identifier or self.current.tag.isTypeKeyword()) {
|
|
name = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ name, self.tokenSlice(self.current) });
|
|
self.advance();
|
|
} else {
|
|
// Not a qualified name continuation — restore the dot
|
|
self.lexer = dot_lexer;
|
|
self.current = dot_current;
|
|
self.prev_end = dot_prev_end;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Parameterized type: Vector(N, T) or later generic struct instantiation
|
|
if (self.current.tag == .l_paren) {
|
|
self.advance(); // skip '('
|
|
var args = std.ArrayList(*Node).empty;
|
|
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
|
if (args.items.len > 0) {
|
|
try self.expect(.comma);
|
|
}
|
|
// Args can be int literals (for lengths) or type expressions
|
|
if (self.current.tag == .int_literal) {
|
|
const arg_start = self.current.loc.start;
|
|
const text = self.tokenSlice(self.current);
|
|
const value = std.fmt.parseInt(i64, text, 10) catch 0;
|
|
self.advance();
|
|
try args.append(self.allocator, try self.createNode(arg_start, .{ .int_literal = .{ .value = value } }));
|
|
} else {
|
|
try args.append(self.allocator, try self.parseTypeExpr());
|
|
}
|
|
}
|
|
try self.expect(.r_paren);
|
|
return try self.createNode(start, .{ .parameterized_type_expr = .{
|
|
.name = name,
|
|
.args = try args.toOwnedSlice(self.allocator),
|
|
} });
|
|
}
|
|
|
|
return try self.createNode(start, .{ .type_expr = .{ .name = name } });
|
|
}
|
|
// Inline struct type in type position: struct { ... }
|
|
if (self.current.tag == .kw_struct) {
|
|
return try self.parseStructDecl("__anon", start);
|
|
}
|
|
// Inline C-style union in type position: union { ... }
|
|
if (self.current.tag == .kw_union) {
|
|
return try self.parseUnionDecl("__anon", start);
|
|
}
|
|
// Inline enum type in type position: enum { ... }
|
|
if (self.current.tag == .kw_enum) {
|
|
return try self.parseEnumDecl("__anon", start);
|
|
}
|
|
return self.fail("expected type name");
|
|
}
|
|
|
|
fn parseEnumDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node {
|
|
self.advance(); // skip 'enum'
|
|
|
|
// Check for 'flags' modifier: enum flags { ... }
|
|
var is_flags = false;
|
|
if (self.current.tag == .identifier and std.mem.eql(u8, self.tokenSlice(self.current), "flags")) {
|
|
is_flags = true;
|
|
self.advance();
|
|
}
|
|
|
|
// Check for optional backing type: enum u8 { ... } or enum flags u32 { ... }
|
|
var backing_type: ?*Node = null;
|
|
if (self.current.tag != .l_brace) {
|
|
backing_type = try self.parseTypeExpr();
|
|
}
|
|
|
|
try self.expect(.l_brace);
|
|
var variant_names = std.ArrayList([]const u8).empty;
|
|
var variant_types = std.ArrayList(?*Node).empty;
|
|
var variant_values = std.ArrayList(?*Node).empty;
|
|
var has_any_type = false;
|
|
var has_any_value = false;
|
|
while (self.current.tag != .r_brace and self.current.tag != .eof) {
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail("expected variant name");
|
|
}
|
|
try variant_names.append(self.allocator, self.tokenSlice(self.current));
|
|
self.advance();
|
|
if (self.current.tag == .colon_colon) {
|
|
// Explicit value: name :: expr; or name :: expr: type;
|
|
self.advance();
|
|
const val_expr = try self.parseExpr();
|
|
try variant_values.append(self.allocator, val_expr);
|
|
has_any_value = true;
|
|
// Check for payload type after value: name :: 0x300: KeyData
|
|
if (self.current.tag == .colon) {
|
|
if (is_flags) {
|
|
return self.fail("flags enum variants cannot have payloads");
|
|
}
|
|
self.advance();
|
|
const vtype = try self.parseTypeExpr();
|
|
try variant_types.append(self.allocator, vtype);
|
|
has_any_type = true;
|
|
} else {
|
|
try variant_types.append(self.allocator, null);
|
|
}
|
|
} else if (self.current.tag == .colon) {
|
|
// Typed variant: name: type;
|
|
if (is_flags) {
|
|
return self.fail("flags enum variants cannot have payloads");
|
|
}
|
|
self.advance();
|
|
const vtype = try self.parseTypeExpr();
|
|
try variant_types.append(self.allocator, vtype);
|
|
try variant_values.append(self.allocator, null);
|
|
has_any_type = true;
|
|
} else {
|
|
// Void variant: name;
|
|
try variant_types.append(self.allocator, null);
|
|
try variant_values.append(self.allocator, null);
|
|
}
|
|
if (self.current.tag == .semicolon) {
|
|
self.advance();
|
|
}
|
|
}
|
|
try self.expect(.r_brace);
|
|
// Always produce enum_decl; variant_types distinguishes payload-less from tagged
|
|
return try self.createNode(start_pos, .{ .enum_decl = .{
|
|
.name = name,
|
|
.variant_names = try variant_names.toOwnedSlice(self.allocator),
|
|
.variant_types = if (has_any_type) try variant_types.toOwnedSlice(self.allocator) else &.{},
|
|
.is_flags = is_flags,
|
|
.variant_values = if (has_any_value) try variant_values.toOwnedSlice(self.allocator) else &.{},
|
|
.backing_type = backing_type,
|
|
} });
|
|
}
|
|
|
|
fn parseUnionDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node {
|
|
self.advance(); // skip 'union'
|
|
try self.expect(.l_brace);
|
|
var field_names = std.ArrayList([]const u8).empty;
|
|
var field_types = std.ArrayList(*Node).empty;
|
|
var anon_idx: u32 = 0;
|
|
while (self.current.tag != .r_brace and self.current.tag != .eof) {
|
|
// Anonymous struct field: struct { x, y: f32; };
|
|
if (self.current.tag == .kw_struct) {
|
|
const anon_field = try std.fmt.allocPrint(self.allocator, "__anon_{d}", .{anon_idx});
|
|
anon_idx += 1;
|
|
const anon_struct_name = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ name, anon_field });
|
|
const struct_node = try self.parseStructDecl(anon_struct_name, self.current.loc.start);
|
|
try field_names.append(self.allocator, anon_field);
|
|
try field_types.append(self.allocator, struct_node);
|
|
if (self.current.tag == .semicolon) {
|
|
self.advance();
|
|
}
|
|
continue;
|
|
}
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail("expected field name or 'struct'");
|
|
}
|
|
try field_names.append(self.allocator, self.tokenSlice(self.current));
|
|
self.advance();
|
|
if (self.current.tag != .colon) {
|
|
return self.fail("union fields must have a type");
|
|
}
|
|
self.advance();
|
|
const ftype = try self.parseTypeExpr();
|
|
try field_types.append(self.allocator, ftype);
|
|
if (self.current.tag == .semicolon) {
|
|
self.advance();
|
|
}
|
|
}
|
|
try self.expect(.r_brace);
|
|
return try self.createNode(start_pos, .{ .union_decl = .{
|
|
.name = name,
|
|
.field_names = try field_names.toOwnedSlice(self.allocator),
|
|
.field_types = try field_types.toOwnedSlice(self.allocator),
|
|
} });
|
|
}
|
|
|
|
fn parseStructDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node {
|
|
self.advance(); // skip 'struct'
|
|
|
|
// Optional type params: struct($N: u32, $T: Type) { ... }
|
|
var type_params = std.ArrayList(ast.StructTypeParam).empty;
|
|
if (self.current.tag == .l_paren) {
|
|
self.advance(); // skip '('
|
|
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
|
if (type_params.items.len > 0) {
|
|
try self.expect(.comma);
|
|
}
|
|
// Expect $name : constraint
|
|
try self.expect(.dollar);
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail("expected type parameter name after '$'");
|
|
}
|
|
const param_name = self.tokenSlice(self.current);
|
|
self.advance();
|
|
try self.expect(.colon);
|
|
const constraint = try self.parseTypeExpr();
|
|
try type_params.append(self.allocator, .{ .name = param_name, .constraint = constraint });
|
|
}
|
|
try self.expect(.r_paren);
|
|
}
|
|
|
|
try self.expect(.l_brace);
|
|
|
|
var field_names = std.ArrayList([]const u8).empty;
|
|
var field_types = std.ArrayList(*Node).empty;
|
|
var field_defaults = std.ArrayList(?*Node).empty;
|
|
|
|
while (self.current.tag != .r_brace and self.current.tag != .eof) {
|
|
// Parse field group: name1, name2, ...: type (= default)?;
|
|
var group_names = std.ArrayList([]const u8).empty;
|
|
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail("expected field name in struct");
|
|
}
|
|
try group_names.append(self.allocator, self.tokenSlice(self.current));
|
|
self.advance();
|
|
|
|
while (self.current.tag == .comma) {
|
|
self.advance(); // skip ','
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail("expected field name after ','");
|
|
}
|
|
try group_names.append(self.allocator, self.tokenSlice(self.current));
|
|
self.advance();
|
|
}
|
|
|
|
try self.expect(.colon);
|
|
const field_type = try self.parseTypeExpr();
|
|
|
|
// Check for default value: = expr
|
|
var default_val: ?*Node = null;
|
|
if (self.current.tag == .equal) {
|
|
self.advance();
|
|
default_val = try self.parseExpr();
|
|
}
|
|
|
|
// All names in the group share the same type and default
|
|
for (group_names.items) |fname| {
|
|
// `_` is an ignore identifier — auto-rename to unique internal name
|
|
const actual_name = if (std.mem.eql(u8, fname, "_"))
|
|
try std.fmt.allocPrint(self.allocator, "_{d}", .{field_names.items.len})
|
|
else
|
|
fname;
|
|
try field_names.append(self.allocator, actual_name);
|
|
try field_types.append(self.allocator, field_type);
|
|
try field_defaults.append(self.allocator, default_val);
|
|
}
|
|
|
|
if (self.current.tag == .semicolon) {
|
|
self.advance();
|
|
}
|
|
}
|
|
try self.expect(.r_brace);
|
|
|
|
return try self.createNode(start_pos, .{ .struct_decl = .{
|
|
.name = name,
|
|
.field_names = try field_names.toOwnedSlice(self.allocator),
|
|
.field_types = try field_types.toOwnedSlice(self.allocator),
|
|
.field_defaults = try field_defaults.toOwnedSlice(self.allocator),
|
|
.type_params = try type_params.toOwnedSlice(self.allocator),
|
|
} });
|
|
}
|
|
|
|
fn parseStructLiteral(self: *Parser, struct_name: ?[]const u8, type_expr: ?*Node, start_pos: u32) anyerror!*Node {
|
|
try self.expect(.l_brace);
|
|
|
|
var field_inits = std.ArrayList(ast.StructFieldInit).empty;
|
|
|
|
while (self.current.tag != .r_brace and self.current.tag != .eof) {
|
|
if (field_inits.items.len > 0) {
|
|
try self.expect(.comma);
|
|
}
|
|
|
|
// Check if this is a named field: identifier followed by '='
|
|
if (self.current.tag == .identifier) {
|
|
const saved_lexer = self.lexer;
|
|
const saved_current = self.current;
|
|
const saved_prev_end = self.prev_end;
|
|
const fname = self.tokenSlice(self.current);
|
|
const ident_start = self.current.loc.start;
|
|
self.advance();
|
|
|
|
if (self.current.tag == .equal) {
|
|
// Named field: name = expr
|
|
self.advance(); // skip '='
|
|
const value = try self.parseExpr();
|
|
try field_inits.append(self.allocator, .{ .name = fname, .value = value });
|
|
continue;
|
|
} else if (self.current.tag == .comma or self.current.tag == .r_brace) {
|
|
// Shorthand: just an identifier (name = identifier with same name)
|
|
const ident_node = try self.createNode(ident_start, .{ .identifier = .{ .name = fname } });
|
|
try field_inits.append(self.allocator, .{ .name = fname, .value = ident_node });
|
|
continue;
|
|
}
|
|
|
|
// Not named — backtrack and parse as positional expression
|
|
self.lexer = saved_lexer;
|
|
self.current = saved_current;
|
|
self.prev_end = saved_prev_end;
|
|
}
|
|
|
|
// Positional field: just an expression
|
|
const value = try self.parseExpr();
|
|
try field_inits.append(self.allocator, .{ .name = null, .value = value });
|
|
}
|
|
try self.expect(.r_brace);
|
|
|
|
return try self.createNode(start_pos, .{ .struct_literal = .{
|
|
.struct_name = struct_name,
|
|
.type_expr = type_expr,
|
|
.field_inits = try field_inits.toOwnedSlice(self.allocator),
|
|
} });
|
|
}
|
|
|
|
fn reconstructQualifiedName(self: *Parser, node: *Node) ![]const u8 {
|
|
if (node.data == .identifier) return node.data.identifier.name;
|
|
if (node.data == .field_access) {
|
|
const obj_name = try self.reconstructQualifiedName(node.data.field_access.object);
|
|
return std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ obj_name, node.data.field_access.field });
|
|
}
|
|
return error.ParseError;
|
|
}
|
|
|
|
/// Parse a parenthesized parameter list: `(name: type, $T: Type, args: ..Any)`
|
|
/// Handles `$` generic params, `..` variadic marker, and comptime detection.
|
|
/// Expects opening `(` already NOT consumed — this function consumes `(` through `)`.
|
|
fn parseParams(self: *Parser) anyerror![]const ast.Param {
|
|
try self.expect(.l_paren);
|
|
var params = std.ArrayList(ast.Param).empty;
|
|
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
|
if (params.items.len > 0) {
|
|
try self.expect(.comma);
|
|
}
|
|
var is_ct_param = false;
|
|
if (self.current.tag == .dollar) {
|
|
is_ct_param = true;
|
|
self.advance();
|
|
}
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail("expected parameter name");
|
|
}
|
|
const param_name = self.tokenSlice(self.current);
|
|
const param_name_span = ast.Span{ .start = self.current.loc.start, .end = self.current.loc.end };
|
|
self.advance();
|
|
try self.expect(.colon);
|
|
const is_variadic = self.current.tag == .dot_dot;
|
|
if (is_variadic) self.advance();
|
|
const param_type = try self.parseTypeExpr();
|
|
var is_comptime_param = false;
|
|
if (is_ct_param and param_type.data == .type_expr) {
|
|
const constraint_name = param_type.data.type_expr.name;
|
|
if (std.mem.eql(u8, constraint_name, "Type")) {
|
|
param_type.data = .{ .type_expr = .{ .name = param_name, .is_generic = true } };
|
|
} else {
|
|
is_comptime_param = true;
|
|
}
|
|
}
|
|
try params.append(self.allocator, .{ .name = param_name, .name_span = param_name_span, .type_expr = param_type, .is_variadic = is_variadic, .is_comptime = is_comptime_param });
|
|
}
|
|
for (params.items, 0..) |param, i| {
|
|
if (param.is_variadic and i != params.items.len - 1) {
|
|
return self.fail("variadic parameter must be the last parameter");
|
|
}
|
|
}
|
|
try self.expect(.r_paren);
|
|
return try params.toOwnedSlice(self.allocator);
|
|
}
|
|
|
|
/// Recursively find all generic type names ($T) in a type expression tree.
|
|
fn collectGenericNames(node: *Node, list: *std.ArrayList([]const u8), allocator: std.mem.Allocator) void {
|
|
switch (node.data) {
|
|
.type_expr => |te| {
|
|
if (te.is_generic) list.append(allocator, te.name) catch {};
|
|
},
|
|
.pointer_type_expr => |pte| collectGenericNames(pte.pointee_type, list, allocator),
|
|
.many_pointer_type_expr => |mpte| collectGenericNames(mpte.element_type, list, allocator),
|
|
.slice_type_expr => |ste| collectGenericNames(ste.element_type, list, allocator),
|
|
.array_type_expr => |ate| collectGenericNames(ate.element_type, list, allocator),
|
|
.parameterized_type_expr => |pte| {
|
|
for (pte.args) |arg| collectGenericNames(arg, list, allocator);
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
/// Collect generic type params and comptime value params from parameter annotations.
|
|
fn collectTypeParams(self: *Parser, params: []const ast.Param) ![]const ast.StructTypeParam {
|
|
var type_params = std.ArrayList(ast.StructTypeParam).empty;
|
|
for (params) |param| {
|
|
if (param.is_comptime) {
|
|
var found = false;
|
|
for (type_params.items) |existing| {
|
|
if (std.mem.eql(u8, existing.name, param.name)) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
try type_params.append(self.allocator, .{ .name = param.name, .constraint = param.type_expr });
|
|
}
|
|
} else {
|
|
// Collect all generic type params found anywhere in the type expression
|
|
var generic_names = std.ArrayList([]const u8).empty;
|
|
collectGenericNames(param.type_expr, &generic_names, self.allocator);
|
|
for (generic_names.items) |gen_name| {
|
|
var found = false;
|
|
for (type_params.items) |existing| {
|
|
if (std.mem.eql(u8, existing.name, gen_name)) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
const type_constraint = self.createNode(param.type_expr.span.start, .{ .type_expr = .{ .name = "Type" } }) catch continue;
|
|
type_params.append(self.allocator, .{ .name = gen_name, .constraint = type_constraint }) catch {};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return try type_params.toOwnedSlice(self.allocator);
|
|
}
|
|
|
|
fn parseFnDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node {
|
|
const params = try self.parseParams();
|
|
|
|
// Optional return type
|
|
var return_type: ?*Node = null;
|
|
if (self.current.tag == .arrow) {
|
|
self.advance();
|
|
return_type = try self.parseTypeExpr();
|
|
}
|
|
|
|
// Body: block `{ ... }`, arrow `=> expr;`, #builtin, or #foreign marker
|
|
const body = if (self.current.tag == .hash_builtin) blk: {
|
|
const bi_start = self.current.loc.start;
|
|
self.advance();
|
|
try self.expect(.semicolon);
|
|
break :blk try self.createNode(bi_start, .{ .builtin_expr = {} });
|
|
} else if (self.current.tag == .hash_foreign) blk: {
|
|
const fi_start = self.current.loc.start;
|
|
self.advance();
|
|
try self.expect(.semicolon);
|
|
break :blk try self.createNode(fi_start, .{ .foreign_expr = {} });
|
|
} else if (self.current.tag == .fat_arrow) blk: {
|
|
self.advance();
|
|
const expr = try self.parseExpr();
|
|
try self.expect(.semicolon);
|
|
const stmts = try self.allocator.alloc(*Node, 1);
|
|
stmts[0] = expr;
|
|
const block_start = expr.span.start;
|
|
const block = try self.createNode(block_start, .{ .block = .{ .stmts = stmts } });
|
|
break :blk block;
|
|
} else try self.parseBlock();
|
|
|
|
const type_params = try self.collectTypeParams(params);
|
|
|
|
return try self.createNode(start_pos, .{ .fn_decl = .{
|
|
.name = name,
|
|
.params = params,
|
|
.return_type = return_type,
|
|
.body = body,
|
|
.type_params = type_params,
|
|
} });
|
|
}
|
|
|
|
fn parseBlock(self: *Parser) anyerror!*Node {
|
|
const start = self.current.loc.start;
|
|
try self.expect(.l_brace);
|
|
var stmts = std.ArrayList(*Node).empty;
|
|
while (self.current.tag != .r_brace and self.current.tag != .eof) {
|
|
const stmt = try self.parseStmt();
|
|
try stmts.append(self.allocator, stmt);
|
|
}
|
|
try self.expect(.r_brace);
|
|
return try self.createNode(start, .{ .block = .{ .stmts = try stmts.toOwnedSlice(self.allocator) } });
|
|
}
|
|
|
|
/// Block-form if/match/while/bare blocks don't require trailing semicolon.
|
|
fn expectSemicolonAfter(self: *Parser, expr: *Node) anyerror!void {
|
|
const needs_semi = switch (expr.data) {
|
|
.if_expr => |ie| ie.is_inline,
|
|
.match_expr => false,
|
|
.while_expr => false,
|
|
.for_expr => false,
|
|
.block => false,
|
|
else => true,
|
|
};
|
|
if (needs_semi) {
|
|
try self.expect(.semicolon);
|
|
} else if (self.current.tag == .semicolon) {
|
|
self.advance(); // consume optional ;
|
|
}
|
|
}
|
|
|
|
pub fn parseStmt(self: *Parser) anyerror!*Node {
|
|
// Check if this is a declaration (IDENT followed by ::, :=, or : type)
|
|
if (self.current.tag == .identifier) {
|
|
const saved_lexer = self.lexer;
|
|
const saved_current = self.current;
|
|
const saved_prev_end = self.prev_end;
|
|
const start = self.current.loc.start;
|
|
const name = self.tokenSlice(self.current);
|
|
self.advance();
|
|
|
|
if (self.current.tag == .colon_colon) {
|
|
self.advance();
|
|
return self.parseConstBinding(name, start);
|
|
}
|
|
if (self.current.tag == .colon_equal) {
|
|
self.advance();
|
|
const value = try self.parseExpr();
|
|
try self.expectSemicolonAfter(value);
|
|
return try self.createNode(start, .{ .var_decl = .{ .name = name, .type_annotation = null, .value = value } });
|
|
}
|
|
if (self.current.tag == .colon) {
|
|
self.advance();
|
|
return self.parseTypedBinding(name, start);
|
|
}
|
|
|
|
// Check for assignment operators
|
|
if (self.isAssignOp()) {
|
|
const op = self.assignOp();
|
|
self.advance();
|
|
const value = try self.parseExpr();
|
|
try self.expect(.semicolon);
|
|
const target = try self.createNode(start, .{ .identifier = .{ .name = name } });
|
|
return try self.createNode(start, .{ .assignment = .{ .target = target, .op = op, .value = value } });
|
|
}
|
|
|
|
// Not a declaration or assignment — backtrack and parse as expression
|
|
self.lexer = saved_lexer;
|
|
self.current = saved_current;
|
|
self.prev_end = saved_prev_end;
|
|
}
|
|
|
|
// Return statement: return expr; or return;
|
|
if (self.current.tag == .kw_return) {
|
|
const start = self.current.loc.start;
|
|
self.advance();
|
|
if (self.current.tag == .semicolon) {
|
|
self.advance();
|
|
return try self.createNode(start, .{ .return_stmt = .{ .value = null } });
|
|
}
|
|
const value = try self.parseExpr();
|
|
try self.expect(.semicolon);
|
|
return try self.createNode(start, .{ .return_stmt = .{ .value = value } });
|
|
}
|
|
|
|
// Defer statement: defer <expr>;
|
|
if (self.current.tag == .kw_defer) {
|
|
const start = self.current.loc.start;
|
|
self.advance();
|
|
const deferred = try self.parseExpr();
|
|
try self.expect(.semicolon);
|
|
return try self.createNode(start, .{ .defer_stmt = .{ .expr = deferred } });
|
|
}
|
|
|
|
// Break statement: break;
|
|
if (self.current.tag == .kw_break) {
|
|
const start = self.current.loc.start;
|
|
self.advance();
|
|
try self.expect(.semicolon);
|
|
return try self.createNode(start, .{ .break_expr = {} });
|
|
}
|
|
|
|
// Continue statement: continue;
|
|
if (self.current.tag == .kw_continue) {
|
|
const start = self.current.loc.start;
|
|
self.advance();
|
|
try self.expect(.semicolon);
|
|
return try self.createNode(start, .{ .continue_expr = {} });
|
|
}
|
|
|
|
// Insert directive: #insert <expr>;
|
|
if (self.current.tag == .hash_insert) {
|
|
const start = self.current.loc.start;
|
|
self.advance();
|
|
const inner = try self.parseExpr();
|
|
try self.expect(.semicolon);
|
|
return try self.createNode(start, .{ .insert_expr = .{ .expr = inner } });
|
|
}
|
|
|
|
// Block-form if/while/for as statements — parse directly to prevent
|
|
// postfix chaining (e.g. `if cond { ... }.field` being misparsed)
|
|
if (self.current.tag == .kw_if) {
|
|
const expr = try self.parseIfExpr();
|
|
try self.expectSemicolonAfter(expr);
|
|
return expr;
|
|
}
|
|
if (self.current.tag == .kw_while) {
|
|
const expr = try self.parsePrimary();
|
|
try self.expectSemicolonAfter(expr);
|
|
return expr;
|
|
}
|
|
if (self.current.tag == .kw_for) {
|
|
const expr = try self.parsePrimary();
|
|
try self.expectSemicolonAfter(expr);
|
|
return expr;
|
|
}
|
|
|
|
// Expression statement
|
|
const expr = try self.parseExpr();
|
|
|
|
// Check for field assignment: expr = value; (e.g. a.b = 1;)
|
|
if (self.isAssignOp()) {
|
|
const op = self.assignOp();
|
|
self.advance();
|
|
const value = try self.parseExpr();
|
|
try self.expect(.semicolon);
|
|
return try self.createNode(expr.span.start, .{ .assignment = .{ .target = expr, .op = op, .value = value } });
|
|
}
|
|
|
|
// Block-form if/match/while/bare blocks don't require trailing semicolon
|
|
try self.expectSemicolonAfter(expr);
|
|
return expr;
|
|
}
|
|
|
|
// ---- Expression parsing (Pratt / precedence climbing) ----
|
|
|
|
pub fn parseExpr(self: *Parser) anyerror!*Node {
|
|
return self.parseBinary(0);
|
|
}
|
|
|
|
fn parseBinary(self: *Parser, min_prec: u8) anyerror!*Node {
|
|
const lhs = try self.parseUnary();
|
|
return self.parseBinaryRhs(lhs, min_prec);
|
|
}
|
|
|
|
fn parseBinaryRhs(self: *Parser, initial_lhs: *Node, min_prec: u8) anyerror!*Node {
|
|
var lhs = initial_lhs;
|
|
|
|
while (true) {
|
|
const prec = self.binaryPrec();
|
|
if (prec == 0 or prec < min_prec) break;
|
|
|
|
const op = self.binaryOp() orelse break;
|
|
self.advance();
|
|
|
|
const rhs = try self.parseBinary(prec + 1);
|
|
|
|
// Chained comparison detection: if op is a comparison and the next
|
|
// token is also a comparison at the same precedence, accumulate
|
|
// into a ChainedComparison node.
|
|
if (isComparisonOp(op) and self.binaryPrec() == prec and self.isComparisonToken()) {
|
|
var operands = std.ArrayList(*Node).empty;
|
|
var ops = std.ArrayList(ast.BinaryOp.Op).empty;
|
|
try operands.append(self.allocator, lhs);
|
|
try operands.append(self.allocator, rhs);
|
|
try ops.append(self.allocator, op);
|
|
|
|
while (self.binaryPrec() == prec and self.isComparisonToken()) {
|
|
const chain_op = self.binaryOp() orelse break;
|
|
self.advance();
|
|
const chain_rhs = try self.parseBinary(prec + 1);
|
|
try operands.append(self.allocator, chain_rhs);
|
|
try ops.append(self.allocator, chain_op);
|
|
}
|
|
|
|
lhs = try self.createNode(lhs.span.start, .{ .chained_comparison = .{
|
|
.operands = try operands.toOwnedSlice(self.allocator),
|
|
.ops = try ops.toOwnedSlice(self.allocator),
|
|
} });
|
|
} else {
|
|
lhs = try self.createNode(lhs.span.start, .{ .binary_op = .{ .op = op, .lhs = lhs, .rhs = rhs } });
|
|
}
|
|
}
|
|
|
|
return lhs;
|
|
}
|
|
|
|
fn parseUnary(self: *Parser) anyerror!*Node {
|
|
if (self.current.tag == .minus) {
|
|
const start = self.current.loc.start;
|
|
self.advance();
|
|
const operand = try self.parseUnary();
|
|
return try self.createNode(start, .{ .unary_op = .{ .op = .negate, .operand = operand } });
|
|
}
|
|
if (self.current.tag == .bang) {
|
|
const start = self.current.loc.start;
|
|
self.advance();
|
|
const operand = try self.parseUnary();
|
|
return try self.createNode(start, .{ .unary_op = .{ .op = .not, .operand = operand } });
|
|
}
|
|
if (self.current.tag == .kw_xx) {
|
|
const start = self.current.loc.start;
|
|
self.advance();
|
|
const operand = try self.parseUnary();
|
|
return try self.createNode(start, .{ .unary_op = .{ .op = .xx, .operand = operand } });
|
|
}
|
|
if (self.current.tag == .at) {
|
|
const start = self.current.loc.start;
|
|
self.advance();
|
|
const operand = try self.parseUnary();
|
|
return try self.createNode(start, .{ .unary_op = .{ .op = .address_of, .operand = operand } });
|
|
}
|
|
// cast(Type) expr — prefix operator with type parameter
|
|
if (self.current.tag == .identifier and std.mem.eql(u8, self.tokenSlice(self.current), "cast")) {
|
|
const saved_lexer = self.lexer;
|
|
const next_tok = self.lexer.next();
|
|
self.lexer = saved_lexer;
|
|
if (next_tok.tag == .l_paren) {
|
|
const start = self.current.loc.start;
|
|
self.advance(); // consume 'cast'
|
|
self.advance(); // consume '('
|
|
const type_arg = try self.parseExpr();
|
|
try self.expect(.r_paren);
|
|
const operand = try self.parseUnary();
|
|
const callee = try self.createNode(start, .{ .identifier = .{ .name = "cast" } });
|
|
const args = try self.allocator.alloc(*Node, 2);
|
|
args[0] = type_arg;
|
|
args[1] = operand;
|
|
return try self.createNode(start, .{ .call = .{ .callee = callee, .args = args } });
|
|
}
|
|
}
|
|
return self.parsePostfix();
|
|
}
|
|
|
|
fn parsePostfix(self: *Parser) anyerror!*Node {
|
|
var expr = try self.parsePrimary();
|
|
|
|
while (true) {
|
|
if (self.current.tag == .l_paren) {
|
|
// Call
|
|
self.advance();
|
|
var args = std.ArrayList(*Node).empty;
|
|
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
|
if (args.items.len > 0) {
|
|
try self.expect(.comma);
|
|
}
|
|
// Spread operator: ..expr
|
|
if (self.current.tag == .dot_dot) {
|
|
const spread_start = self.current.loc.start;
|
|
self.advance();
|
|
const operand = try self.parseExpr();
|
|
try args.append(self.allocator, try self.createNode(spread_start, .{ .spread_expr = .{ .operand = operand } }));
|
|
} else {
|
|
try args.append(self.allocator, try self.parseExpr());
|
|
}
|
|
}
|
|
try self.expect(.r_paren);
|
|
expr = try self.createNode(expr.span.start, .{ .call = .{ .callee = expr, .args = try args.toOwnedSlice(self.allocator) } });
|
|
} else if (self.current.tag == .dot) {
|
|
self.advance();
|
|
if (self.current.tag == .l_brace) {
|
|
// Struct literal: Type.{ ... }
|
|
if (expr.data == .identifier) {
|
|
// Simple name: Vec4.{ ... }
|
|
expr = try self.parseStructLiteral(expr.data.identifier.name, null, expr.span.start);
|
|
} else if (expr.data == .field_access) {
|
|
// Qualified name: std.Vec4.{ ... }
|
|
const qname = try self.reconstructQualifiedName(expr);
|
|
expr = try self.parseStructLiteral(qname, null, expr.span.start);
|
|
} else {
|
|
// Expression type: Vec(3, f32).{ ... }
|
|
expr = try self.parseStructLiteral(null, expr, expr.span.start);
|
|
}
|
|
} else if (self.current.tag == .l_bracket) {
|
|
// Typed array/vector literal: Type.[elem, ...]
|
|
self.advance(); // skip '['
|
|
var elements = std.ArrayList(*Node).empty;
|
|
while (self.current.tag != .r_bracket and self.current.tag != .eof) {
|
|
if (elements.items.len > 0) {
|
|
try self.expect(.comma);
|
|
}
|
|
const elem = try self.parseExpr();
|
|
try elements.append(self.allocator, elem);
|
|
}
|
|
try self.expect(.r_bracket);
|
|
expr = try self.createNode(expr.span.start, .{ .array_literal = .{
|
|
.elements = try elements.toOwnedSlice(self.allocator),
|
|
.type_expr = expr,
|
|
} });
|
|
} else if (self.current.tag == .star) {
|
|
// Dereference: expr.*
|
|
self.advance();
|
|
expr = try self.createNode(expr.span.start, .{ .deref_expr = .{ .operand = expr } });
|
|
} else {
|
|
// Field access
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail("expected field name after '.'");
|
|
}
|
|
const field = self.tokenSlice(self.current);
|
|
self.advance();
|
|
expr = try self.createNode(expr.span.start, .{ .field_access = .{ .object = expr, .field = field } });
|
|
}
|
|
} else if (self.current.tag == .l_bracket) {
|
|
// Index or slice access: expr[expr] or expr[start..end]
|
|
self.advance();
|
|
if (self.current.tag == .dot_dot) {
|
|
// [..end]
|
|
self.advance();
|
|
const end_expr = try self.parseExpr();
|
|
try self.expect(.r_bracket);
|
|
expr = try self.createNode(expr.span.start, .{ .slice_expr = .{
|
|
.object = expr, .start = null, .end = end_expr,
|
|
} });
|
|
} else {
|
|
const first = try self.parseExpr();
|
|
if (self.current.tag == .dot_dot) {
|
|
// [start..end] or [start..]
|
|
self.advance();
|
|
const end_expr: ?*ast.Node = if (self.current.tag != .r_bracket)
|
|
try self.parseExpr()
|
|
else
|
|
null;
|
|
try self.expect(.r_bracket);
|
|
expr = try self.createNode(expr.span.start, .{ .slice_expr = .{
|
|
.object = expr, .start = first, .end = end_expr,
|
|
} });
|
|
} else {
|
|
// [index] — normal index access
|
|
try self.expect(.r_bracket);
|
|
expr = try self.createNode(expr.span.start, .{ .index_expr = .{
|
|
.object = expr, .index = first,
|
|
} });
|
|
}
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return expr;
|
|
}
|
|
|
|
fn parsePrimary(self: *Parser) anyerror!*Node {
|
|
const start = self.current.loc.start;
|
|
switch (self.current.tag) {
|
|
.int_literal => {
|
|
const text = self.tokenSlice(self.current);
|
|
const base: u8 = if (text.len > 2 and text[0] == '0' and (text[1] == 'x' or text[1] == 'X'))
|
|
16
|
|
else if (text.len > 2 and text[0] == '0' and (text[1] == 'b' or text[1] == 'B'))
|
|
2
|
|
else
|
|
10;
|
|
const digits = if (base != 10) text[2..] else text;
|
|
const value = std.fmt.parseInt(i64, digits, base) catch {
|
|
return self.fail("integer literal overflow");
|
|
};
|
|
self.advance();
|
|
return try self.createNode(start, .{ .int_literal = .{ .value = value } });
|
|
},
|
|
.float_literal => {
|
|
const text = self.tokenSlice(self.current);
|
|
const value = std.fmt.parseFloat(f64, text) catch {
|
|
return self.fail("float literal overflow");
|
|
};
|
|
self.advance();
|
|
return try self.createNode(start, .{ .float_literal = .{ .value = value } });
|
|
},
|
|
.string_literal => {
|
|
// raw includes quotes
|
|
const raw = self.tokenSlice(self.current);
|
|
self.advance();
|
|
return try self.createNode(start, .{ .string_literal = .{ .raw = raw[1 .. raw.len - 1] } });
|
|
},
|
|
.raw_string_literal => {
|
|
// #string heredoc — token span is content only, no stripping needed
|
|
const raw = self.tokenSlice(self.current);
|
|
self.advance();
|
|
return try self.createNode(start, .{ .string_literal = .{ .raw = raw, .is_raw = true } });
|
|
},
|
|
.kw_true => {
|
|
self.advance();
|
|
return try self.createNode(start, .{ .bool_literal = .{ .value = true } });
|
|
},
|
|
.kw_false => {
|
|
self.advance();
|
|
return try self.createNode(start, .{ .bool_literal = .{ .value = false } });
|
|
},
|
|
.kw_null => {
|
|
self.advance();
|
|
return try self.createNode(start, .{ .null_literal = {} });
|
|
},
|
|
.identifier => {
|
|
const name = self.tokenSlice(self.current);
|
|
// Check if this identifier is a type name (e.g. s32, u8, s128)
|
|
if (Type.fromName(name) != null) {
|
|
self.advance();
|
|
return try self.createNode(start, .{ .type_expr = .{ .name = name } });
|
|
}
|
|
self.advance();
|
|
return try self.createNode(start, .{ .identifier = .{ .name = name } });
|
|
},
|
|
.dot => {
|
|
self.advance();
|
|
// Anonymous struct literal: .{ ... }
|
|
if (self.current.tag == .l_brace) {
|
|
return self.parseStructLiteral(null, null, start);
|
|
}
|
|
// Array literal: .[expr, expr, ...]
|
|
if (self.current.tag == .l_bracket) {
|
|
self.advance(); // skip '['
|
|
var elements = std.ArrayList(*Node).empty;
|
|
while (self.current.tag != .r_bracket and self.current.tag != .eof) {
|
|
if (elements.items.len > 0) {
|
|
try self.expect(.comma);
|
|
}
|
|
const elem = try self.parseExpr();
|
|
try elements.append(self.allocator, elem);
|
|
}
|
|
try self.expect(.r_bracket);
|
|
return try self.createNode(start, .{ .array_literal = .{ .elements = try elements.toOwnedSlice(self.allocator) } });
|
|
}
|
|
// Enum literal: .variant_name
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail("expected variant name, '{', or '[' after '.'");
|
|
}
|
|
const name = self.tokenSlice(self.current);
|
|
self.advance();
|
|
// Enum literal with payload: .variant(payload) — tagged enum (formerly union literal)
|
|
if (self.current.tag == .l_paren) {
|
|
self.advance(); // skip '('
|
|
const payload = try self.parseExpr();
|
|
try self.expect(.r_paren);
|
|
return try self.createNode(start, .{ .enum_literal = .{
|
|
.name = name,
|
|
.payload = payload,
|
|
} });
|
|
}
|
|
return try self.createNode(start, .{ .enum_literal = .{ .name = name } });
|
|
},
|
|
.l_paren => {
|
|
// Lambda: (params) => expr
|
|
if (self.isLambda()) {
|
|
return self.parseLambda();
|
|
}
|
|
// Grouped expression
|
|
self.advance();
|
|
const expr = try self.parseExpr();
|
|
try self.expect(.r_paren);
|
|
return expr;
|
|
},
|
|
.kw_f32, .kw_f64, .kw_Type => {
|
|
// Type keyword used as expression (for type aliases: SOME_TYPE :: f64;)
|
|
const name = self.tokenSlice(self.current);
|
|
self.advance();
|
|
return try self.createNode(start, .{ .type_expr = .{ .name = name } });
|
|
},
|
|
.kw_struct => {
|
|
// Anonymous struct expression: struct { value: T; count: u32; }
|
|
return try self.parseStructDecl("__anon", start);
|
|
},
|
|
.kw_enum => {
|
|
// Anonymous enum expression: enum { variant: T; other: u32; }
|
|
return try self.parseEnumDecl("__anon", start);
|
|
},
|
|
.kw_union => {
|
|
// Anonymous C-style union expression: union { f: f32; i: s32; }
|
|
return try self.parseUnionDecl("__anon", start);
|
|
},
|
|
.kw_if => {
|
|
return self.parseIfExpr();
|
|
},
|
|
.kw_while => {
|
|
return self.parseWhileExpr();
|
|
},
|
|
.kw_for => {
|
|
return self.parseForExpr();
|
|
},
|
|
.kw_break => {
|
|
self.advance();
|
|
return try self.createNode(start, .{ .break_expr = {} });
|
|
},
|
|
.kw_continue => {
|
|
self.advance();
|
|
return try self.createNode(start, .{ .continue_expr = {} });
|
|
},
|
|
.kw_return => {
|
|
self.advance();
|
|
// return with optional value
|
|
const value = if (self.current.tag != .semicolon and self.current.tag != .eof)
|
|
try self.parseExpr()
|
|
else
|
|
null;
|
|
return try self.createNode(start, .{ .return_stmt = .{ .value = value } });
|
|
},
|
|
.l_bracket => {
|
|
// Type expression in expression position: []T.[...] or [N]T.[...]
|
|
return try self.parseTypeExpr();
|
|
},
|
|
.l_brace => {
|
|
return self.parseBlock();
|
|
},
|
|
.triple_minus => {
|
|
self.advance();
|
|
return try self.createNode(start, .{ .undef_literal = {} });
|
|
},
|
|
.hash_run => {
|
|
self.advance(); // skip '#run'
|
|
const inner = try self.parseExpr();
|
|
return try self.createNode(start, .{ .comptime_expr = .{ .expr = inner } });
|
|
},
|
|
else => {
|
|
return self.fail("unexpected token in expression");
|
|
},
|
|
}
|
|
}
|
|
|
|
fn parseIfExpr(self: *Parser) anyerror!*Node {
|
|
const start = self.current.loc.start;
|
|
self.advance(); // skip 'if'
|
|
|
|
// Parse condition at prec 5 (arithmetic+), leaving all comparisons
|
|
// unconsumed for manual handling with match disambiguation.
|
|
var condition = try self.parseBinary(5);
|
|
|
|
// Handle comparisons with chain detection and match disambiguation.
|
|
// All comparisons (< <= > >= == !=) are at the same precedence.
|
|
if (self.isComparisonToken()) {
|
|
var operands = std.ArrayList(*Node).empty;
|
|
var ops = std.ArrayList(ast.BinaryOp.Op).empty;
|
|
try operands.append(self.allocator, condition);
|
|
|
|
while (self.isComparisonToken()) {
|
|
// Match disambiguation: == followed by { is a match expression
|
|
if (self.current.tag == .equal_equal) {
|
|
self.advance();
|
|
if (self.current.tag == .l_brace) {
|
|
// Match expression: if expr == { case ... }
|
|
// Only valid as the first comparison (no chain before it)
|
|
if (ops.items.len == 0) {
|
|
return self.parseMatchBody(condition, start);
|
|
}
|
|
// Chain followed by == { is an error — fall through to
|
|
// regular comparison (will likely fail at parse time)
|
|
}
|
|
const rhs = try self.parseBinary(5);
|
|
try operands.append(self.allocator, rhs);
|
|
try ops.append(self.allocator, .eq);
|
|
} else {
|
|
const cmp_op = self.binaryOp() orelse break;
|
|
self.advance();
|
|
const rhs = try self.parseBinary(5);
|
|
try operands.append(self.allocator, rhs);
|
|
try ops.append(self.allocator, cmp_op);
|
|
}
|
|
}
|
|
|
|
if (ops.items.len == 1) {
|
|
// Single comparison — regular binary_op
|
|
condition = try self.createNode(condition.span.start, .{ .binary_op = .{
|
|
.op = ops.items[0],
|
|
.lhs = operands.items[0],
|
|
.rhs = operands.items[1],
|
|
} });
|
|
} else {
|
|
// Chained comparison
|
|
condition = try self.createNode(condition.span.start, .{ .chained_comparison = .{
|
|
.operands = try operands.toOwnedSlice(self.allocator),
|
|
.ops = try ops.toOwnedSlice(self.allocator),
|
|
} });
|
|
}
|
|
}
|
|
|
|
// Handle and/or with proper Pratt precedence
|
|
condition = try self.parseBinaryRhs(condition, 1);
|
|
|
|
// Inline form: if cond then expr [else expr]
|
|
if (self.current.tag == .kw_then) {
|
|
self.advance();
|
|
const then_branch = try self.parseExpr();
|
|
var else_branch: ?*Node = null;
|
|
if (self.current.tag == .kw_else) {
|
|
self.advance();
|
|
else_branch = try self.parseExpr();
|
|
}
|
|
return try self.createNode(start, .{ .if_expr = .{
|
|
.condition = condition,
|
|
.then_branch = then_branch,
|
|
.else_branch = else_branch,
|
|
.is_inline = true,
|
|
} });
|
|
}
|
|
|
|
// Block form: if cond { ... } else { ... }
|
|
const then_branch = try self.parseBlock();
|
|
var else_branch: ?*Node = null;
|
|
if (self.current.tag == .kw_else) {
|
|
self.advance();
|
|
if (self.current.tag == .kw_if) {
|
|
else_branch = try self.parseIfExpr();
|
|
} else {
|
|
else_branch = try self.parseBlock();
|
|
}
|
|
}
|
|
return try self.createNode(start, .{ .if_expr = .{
|
|
.condition = condition,
|
|
.then_branch = then_branch,
|
|
.else_branch = else_branch,
|
|
.is_inline = false,
|
|
} });
|
|
}
|
|
|
|
fn parseWhileExpr(self: *Parser) anyerror!*Node {
|
|
const start = self.current.loc.start;
|
|
self.advance(); // skip 'while'
|
|
|
|
const condition = try self.parseExpr();
|
|
const body = try self.parseBlock();
|
|
|
|
return try self.createNode(start, .{ .while_expr = .{
|
|
.condition = condition,
|
|
.body = body,
|
|
} });
|
|
}
|
|
|
|
fn parseForExpr(self: *Parser) anyerror!*Node {
|
|
const start = self.current.loc.start;
|
|
self.advance(); // skip 'for'
|
|
|
|
const iterable = try self.parseExpr();
|
|
const body = try self.parseBlock();
|
|
|
|
return try self.createNode(start, .{ .for_expr = .{
|
|
.iterable = iterable,
|
|
.body = body,
|
|
} });
|
|
}
|
|
|
|
fn parseMatchBody(self: *Parser, subject: *Node, start_pos: u32) anyerror!*Node {
|
|
try self.expect(.l_brace);
|
|
var arms = std.ArrayList(ast.MatchArm).empty;
|
|
while (self.current.tag == .kw_case) {
|
|
const arm_start = self.current.loc.start;
|
|
self.advance(); // skip 'case'
|
|
// Allow keyword tokens (struct, enum, union) as type category names in match arms
|
|
const pattern: *Node = if (self.current.tag == .kw_struct or self.current.tag == .kw_enum or self.current.tag == .kw_union) blk: {
|
|
const name = self.tokenSlice(self.current);
|
|
self.advance();
|
|
break :blk try self.createNode(arm_start, .{ .identifier = .{ .name = name } });
|
|
} else try self.parsePrimary(); // .variant
|
|
try self.expect(.colon);
|
|
|
|
// Optional payload capture: (ident)
|
|
var capture: ?[]const u8 = null;
|
|
if (self.current.tag == .l_paren) {
|
|
self.advance();
|
|
if (self.current.tag != .identifier) return self.fail("expected capture name");
|
|
capture = self.tokenSlice(self.current);
|
|
self.advance();
|
|
try self.expect(.r_paren);
|
|
}
|
|
|
|
if (self.current.tag == .kw_break) {
|
|
self.advance();
|
|
try self.expect(.semicolon);
|
|
const body = try self.createNode(arm_start, .{ .block = .{ .stmts = &.{} } });
|
|
try arms.append(self.allocator, .{ .pattern = pattern, .body = body, .is_break = true, .capture = capture });
|
|
} else if (self.current.tag == .fat_arrow) {
|
|
// Short form: (ident) => expr;
|
|
self.advance();
|
|
const expr = try self.parseExpr();
|
|
try self.expect(.semicolon);
|
|
const body = try self.createNode(arm_start, .{ .block = .{ .stmts = try self.allocator.dupe(*Node, &.{expr}) } });
|
|
try arms.append(self.allocator, .{ .pattern = pattern, .body = body, .is_break = false, .capture = capture });
|
|
} else {
|
|
const stmts_start = self.current.loc.start;
|
|
var stmts = std.ArrayList(*Node).empty;
|
|
while (self.current.tag != .kw_case and self.current.tag != .kw_else and self.current.tag != .r_brace and self.current.tag != .eof) {
|
|
try stmts.append(self.allocator, try self.parseStmt());
|
|
}
|
|
const body = try self.createNode(stmts_start, .{ .block = .{ .stmts = try stmts.toOwnedSlice(self.allocator) } });
|
|
try arms.append(self.allocator, .{ .pattern = pattern, .body = body, .is_break = false, .capture = capture });
|
|
}
|
|
}
|
|
// Optional else arm (default)
|
|
if (self.current.tag == .kw_else) {
|
|
const else_start = self.current.loc.start;
|
|
self.advance(); // skip 'else'
|
|
try self.expect(.colon);
|
|
var stmts = std.ArrayList(*Node).empty;
|
|
while (self.current.tag != .r_brace and self.current.tag != .eof) {
|
|
try stmts.append(self.allocator, try self.parseStmt());
|
|
}
|
|
const body = try self.createNode(else_start, .{ .block = .{ .stmts = try stmts.toOwnedSlice(self.allocator) } });
|
|
try arms.append(self.allocator, .{ .pattern = null, .body = body, .is_break = false });
|
|
}
|
|
try self.expect(.r_brace);
|
|
return try self.createNode(start_pos, .{ .match_expr = .{ .subject = subject, .arms = try arms.toOwnedSlice(self.allocator) } });
|
|
}
|
|
|
|
fn isLambda(self: *Parser) bool {
|
|
// Peek ahead: save state, scan to matching ), check if => or -> ... => follows
|
|
const saved_lexer = self.lexer;
|
|
const saved_current = self.current;
|
|
const saved_prev_end = self.prev_end;
|
|
defer {
|
|
self.lexer = saved_lexer;
|
|
self.current = saved_current;
|
|
self.prev_end = saved_prev_end;
|
|
}
|
|
|
|
self.advance(); // skip '('
|
|
var depth: u32 = 1;
|
|
while (depth > 0 and self.current.tag != .eof) {
|
|
if (self.current.tag == .l_paren) depth += 1;
|
|
if (self.current.tag == .r_paren) depth -= 1;
|
|
if (depth > 0) self.advance();
|
|
}
|
|
if (self.current.tag == .r_paren) {
|
|
self.advance(); // skip ')'
|
|
if (self.current.tag == .fat_arrow) return true;
|
|
// (params) -> ReturnType => expr
|
|
if (self.current.tag == .arrow) {
|
|
self.advance(); // skip '->'
|
|
// Skip past the return type tokens until we see '=>' or something unexpected
|
|
while (self.current.tag != .eof) {
|
|
if (self.current.tag == .fat_arrow) return true;
|
|
// Return type tokens: identifiers, dots, parens, type keywords, dollar, brackets
|
|
if (self.current.tag == .identifier or self.current.tag.isTypeKeyword() or
|
|
self.current.tag == .dot or self.current.tag == .dollar or
|
|
self.current.tag == .l_bracket or self.current.tag == .r_bracket or
|
|
self.current.tag == .l_paren or self.current.tag == .r_paren or
|
|
self.current.tag == .comma or self.current.tag == .int_literal)
|
|
{
|
|
self.advance();
|
|
} else break;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
fn parseLambda(self: *Parser) anyerror!*Node {
|
|
const start = self.current.loc.start;
|
|
const params = try self.parseParams();
|
|
|
|
// Optional return type: (params) -> Type => expr
|
|
var return_type: ?*Node = null;
|
|
if (self.current.tag == .arrow) {
|
|
self.advance();
|
|
return_type = try self.parseTypeExpr();
|
|
}
|
|
|
|
try self.expect(.fat_arrow);
|
|
const body = try self.parseExpr();
|
|
const type_params = try self.collectTypeParams(params);
|
|
return try self.createNode(start, .{ .lambda = .{
|
|
.params = params,
|
|
.return_type = return_type,
|
|
.body = body,
|
|
.type_params = type_params,
|
|
} });
|
|
}
|
|
|
|
// ---- Helpers ----
|
|
|
|
fn isFunctionDef(self: *Parser) bool {
|
|
// Peek ahead: save state, scan to matching ), check what follows
|
|
const saved_lexer = self.lexer;
|
|
const saved_current = self.current;
|
|
const saved_prev_end = self.prev_end;
|
|
defer {
|
|
self.lexer = saved_lexer;
|
|
self.current = saved_current;
|
|
self.prev_end = saved_prev_end;
|
|
}
|
|
|
|
self.advance(); // skip '('
|
|
var depth: u32 = 1;
|
|
while (depth > 0 and self.current.tag != .eof) {
|
|
if (self.current.tag == .l_paren) depth += 1;
|
|
if (self.current.tag == .r_paren) depth -= 1;
|
|
if (depth > 0) self.advance();
|
|
}
|
|
if (self.current.tag == .r_paren) {
|
|
self.advance(); // skip ')'
|
|
// Function if followed by '{', '->', '#builtin', '#foreign', or '=>'
|
|
return self.current.tag == .l_brace or self.current.tag == .arrow or self.current.tag == .hash_builtin or self.current.tag == .hash_foreign or self.current.tag == .fat_arrow;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
fn isAssignOp(self: *const Parser) bool {
|
|
return switch (self.current.tag) {
|
|
.equal, .plus_equal, .minus_equal, .star_equal, .slash_equal, .percent_equal => true,
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
fn assignOp(self: *const Parser) ast.Assignment.Op {
|
|
return switch (self.current.tag) {
|
|
.equal => .assign,
|
|
.plus_equal => .add_assign,
|
|
.minus_equal => .sub_assign,
|
|
.star_equal => .mul_assign,
|
|
.slash_equal => .div_assign,
|
|
.percent_equal => .mod_assign,
|
|
else => unreachable,
|
|
};
|
|
}
|
|
|
|
fn binaryPrec(self: *const Parser) u8 {
|
|
return switch (self.current.tag) {
|
|
.kw_or => 1,
|
|
.kw_and => 2,
|
|
.pipe => 3,
|
|
.ampersand => 3,
|
|
.equal_equal, .bang_equal, .less, .less_equal, .greater, .greater_equal => 4,
|
|
.plus, .minus => 5,
|
|
.star, .slash, .percent => 6,
|
|
else => 0,
|
|
};
|
|
}
|
|
|
|
fn binaryOp(self: *const Parser) ?ast.BinaryOp.Op {
|
|
return switch (self.current.tag) {
|
|
.kw_and => .and_op,
|
|
.kw_or => .or_op,
|
|
.pipe => .bit_or,
|
|
.ampersand => .bit_and,
|
|
.plus => .add,
|
|
.minus => .sub,
|
|
.star => .mul,
|
|
.slash => .div,
|
|
.percent => .mod,
|
|
.equal_equal => .eq,
|
|
.bang_equal => .neq,
|
|
.less => .lt,
|
|
.less_equal => .lte,
|
|
.greater => .gt,
|
|
.greater_equal => .gte,
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
fn isComparisonOp(op: ast.BinaryOp.Op) bool {
|
|
return switch (op) {
|
|
.lt, .lte, .gt, .gte, .eq, .neq => true,
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
fn isComparisonToken(self: *const Parser) bool {
|
|
return switch (self.current.tag) {
|
|
.less, .less_equal, .greater, .greater_equal, .equal_equal, .bang_equal => true,
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
fn advance(self: *Parser) void {
|
|
self.prev_end = self.current.loc.end;
|
|
self.current = self.lexer.next();
|
|
}
|
|
|
|
fn expect(self: *Parser, tag: Tag) !void {
|
|
if (self.current.tag != tag) {
|
|
const expected = tag.lexeme() orelse @tagName(tag);
|
|
return self.failFmt("expected '{s}'", .{expected});
|
|
}
|
|
self.advance();
|
|
}
|
|
|
|
fn failFmt(self: *Parser, comptime fmt: []const u8, args: anytype) error{ParseError} {
|
|
const msg = std.fmt.allocPrint(self.allocator, fmt, args) catch return error.ParseError;
|
|
return self.fail(msg);
|
|
}
|
|
|
|
fn tokenSlice(self: *const Parser, token: Token) []const u8 {
|
|
return self.source[token.loc.start..token.loc.end];
|
|
}
|
|
|
|
fn fail(self: *Parser, msg: []const u8) error{ParseError} {
|
|
self.err_msg = msg;
|
|
self.err_offset = self.current.loc.start;
|
|
if (self.diagnostics) |diags| {
|
|
diags.add(.err, msg, .{ .start = self.current.loc.start, .end = self.current.loc.end });
|
|
}
|
|
return error.ParseError;
|
|
}
|
|
};
|
|
|
|
test "parse minimal main" {
|
|
const source = "main :: () { 42; }";
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), source);
|
|
const root = try parser.parse();
|
|
try std.testing.expect(root.data == .root);
|
|
try std.testing.expectEqual(@as(usize, 1), root.data.root.decls.len);
|
|
const decl = root.data.root.decls[0];
|
|
try std.testing.expect(decl.data == .fn_decl);
|
|
try std.testing.expectEqualStrings("main", decl.data.fn_decl.name);
|
|
const body = decl.data.fn_decl.body;
|
|
try std.testing.expect(body.data == .block);
|
|
try std.testing.expectEqual(@as(usize, 1), body.data.block.stmts.len);
|
|
try std.testing.expect(body.data.block.stmts[0].data == .int_literal);
|
|
try std.testing.expectEqual(@as(i64, 42), body.data.block.stmts[0].data.int_literal.value);
|
|
}
|
|
|
|
test "parse #run const binding" {
|
|
const source = "x :: #run compute(5);";
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), source);
|
|
const root = try parser.parse();
|
|
try std.testing.expectEqual(@as(usize, 1), root.data.root.decls.len);
|
|
const decl = root.data.root.decls[0];
|
|
try std.testing.expect(decl.data == .const_decl);
|
|
try std.testing.expectEqualStrings("x", decl.data.const_decl.name);
|
|
try std.testing.expect(decl.data.const_decl.value.data == .comptime_expr);
|
|
// inner expr is a call
|
|
try std.testing.expect(decl.data.const_decl.value.data.comptime_expr.expr.data == .call);
|
|
}
|
|
|
|
test "parse top-level #run" {
|
|
const source = "#run main();";
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), source);
|
|
const root = try parser.parse();
|
|
try std.testing.expectEqual(@as(usize, 1), root.data.root.decls.len);
|
|
const decl = root.data.root.decls[0];
|
|
try std.testing.expect(decl.data == .comptime_expr);
|
|
// inner expr is a call
|
|
try std.testing.expect(decl.data.comptime_expr.expr.data == .call);
|
|
}
|
|
|
|
test "parse flat import" {
|
|
const source = "#import \"modules/std/math.sx\";";
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), source);
|
|
const root = try parser.parse();
|
|
try std.testing.expectEqual(@as(usize, 1), root.data.root.decls.len);
|
|
const decl = root.data.root.decls[0];
|
|
try std.testing.expect(decl.data == .import_decl);
|
|
try std.testing.expectEqualStrings("modules/std/math.sx", decl.data.import_decl.path);
|
|
try std.testing.expect(decl.data.import_decl.name == null);
|
|
}
|
|
|
|
test "parse namespaced import" {
|
|
const source = "std :: #import \"modules/std/std.sx\";";
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), source);
|
|
const root = try parser.parse();
|
|
try std.testing.expectEqual(@as(usize, 1), root.data.root.decls.len);
|
|
const decl = root.data.root.decls[0];
|
|
try std.testing.expect(decl.data == .import_decl);
|
|
try std.testing.expectEqualStrings("modules/std/std.sx", decl.data.import_decl.path);
|
|
try std.testing.expectEqualStrings("std", decl.data.import_decl.name.?);
|
|
}
|
|
|
|
test "parse library declaration" {
|
|
const source = "#library \"raylib\";";
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), source);
|
|
const root = try parser.parse();
|
|
try std.testing.expectEqual(@as(usize, 1), root.data.root.decls.len);
|
|
const decl = root.data.root.decls[0];
|
|
try std.testing.expect(decl.data == .library_decl);
|
|
try std.testing.expectEqualStrings("raylib", decl.data.library_decl.lib_name);
|
|
}
|
|
|
|
test "parse void function with builtin body" {
|
|
const source = "foo :: () #builtin;";
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), source);
|
|
const root = try parser.parse();
|
|
try std.testing.expectEqual(@as(usize, 1), root.data.root.decls.len);
|
|
const decl = root.data.root.decls[0];
|
|
try std.testing.expect(decl.data == .fn_decl);
|
|
try std.testing.expectEqualStrings("foo", decl.data.fn_decl.name);
|
|
try std.testing.expect(decl.data.fn_decl.body.data == .builtin_expr);
|
|
}
|
|
|
|
test "parse void function with foreign body" {
|
|
const source = "InitWindow :: (width: s32, height: s32, title: *u8) -> void #foreign;";
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), source);
|
|
const root = try parser.parse();
|
|
try std.testing.expectEqual(@as(usize, 1), root.data.root.decls.len);
|
|
const decl = root.data.root.decls[0];
|
|
try std.testing.expect(decl.data == .fn_decl);
|
|
try std.testing.expectEqualStrings("InitWindow", decl.data.fn_decl.name);
|
|
try std.testing.expect(decl.data.fn_decl.body.data == .foreign_expr);
|
|
try std.testing.expectEqual(@as(usize, 3), decl.data.fn_decl.params.len);
|
|
}
|
|
|
|
test "parse void function with arrow body" {
|
|
const source = "foo :: () => 42;";
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), source);
|
|
const root = try parser.parse();
|
|
try std.testing.expectEqual(@as(usize, 1), root.data.root.decls.len);
|
|
const decl = root.data.root.decls[0];
|
|
try std.testing.expect(decl.data == .fn_decl);
|
|
try std.testing.expectEqualStrings("foo", decl.data.fn_decl.name);
|
|
try std.testing.expect(decl.data.fn_decl.body.data == .int_literal);
|
|
try std.testing.expectEqual(@as(i64, 42), decl.data.fn_decl.body.data.int_literal.value);
|
|
}
|
|
|
|
test "parse hex and binary literals" {
|
|
const source = "main :: () { 0xFF; 0b1010; }";
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), source);
|
|
const root = try parser.parse();
|
|
const body = root.data.root.decls[0].data.fn_decl.body;
|
|
try std.testing.expectEqual(@as(usize, 2), body.data.block.stmts.len);
|
|
try std.testing.expectEqual(@as(i64, 255), body.data.block.stmts[0].data.int_literal.value);
|
|
try std.testing.expectEqual(@as(i64, 10), body.data.block.stmts[1].data.int_literal.value);
|
|
}
|
|
|
|
test "parse array type with identifier length" {
|
|
const source = "foo :: (arr: [N]f32) => arr;";
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), source);
|
|
const root = try parser.parse();
|
|
const decl = root.data.root.decls[0];
|
|
try std.testing.expect(decl.data == .fn_decl);
|
|
const param_type = decl.data.fn_decl.params[0].type_expr;
|
|
try std.testing.expect(param_type.data == .array_type_expr);
|
|
// length is an identifier "N", not an int literal
|
|
try std.testing.expect(param_type.data.array_type_expr.length.data == .identifier);
|
|
try std.testing.expectEqualStrings("N", param_type.data.array_type_expr.length.data.identifier.name);
|
|
try std.testing.expect(param_type.data.array_type_expr.element_type.data == .type_expr);
|
|
}
|
|
|
|
test "parse lambda with generic params" {
|
|
const source = "f :: (x: $T) => x;";
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), source);
|
|
const root = try parser.parse();
|
|
const decl = root.data.root.decls[0];
|
|
try std.testing.expect(decl.data == .const_decl);
|
|
const lambda = decl.data.const_decl.value;
|
|
try std.testing.expect(lambda.data == .lambda);
|
|
try std.testing.expectEqual(@as(usize, 1), lambda.data.lambda.params.len);
|
|
try std.testing.expectEqualStrings("x", lambda.data.lambda.params[0].name);
|
|
// has generic type param
|
|
try std.testing.expectEqual(@as(usize, 1), lambda.data.lambda.type_params.len);
|
|
try std.testing.expectEqualStrings("T", lambda.data.lambda.type_params[0].name);
|
|
}
|
|
|
|
test "parse lambda with return type" {
|
|
const source = "f :: (x: s32) -> s32 => x;";
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), source);
|
|
const root = try parser.parse();
|
|
const decl = root.data.root.decls[0];
|
|
try std.testing.expect(decl.data == .const_decl);
|
|
const lambda = decl.data.const_decl.value;
|
|
try std.testing.expect(lambda.data == .lambda);
|
|
try std.testing.expect(lambda.data.lambda.return_type != null);
|
|
try std.testing.expect(lambda.data.lambda.return_type.?.data == .type_expr);
|
|
try std.testing.expectEqualStrings("s32", lambda.data.lambda.return_type.?.data.type_expr.name);
|
|
}
|
|
|
|
test "parse match with else arm" {
|
|
const source =
|
|
\\main :: () {
|
|
\\ x := 5;
|
|
\\ if x == {
|
|
\\ case 1: 10;
|
|
\\ else: 99;
|
|
\\ };
|
|
\\}
|
|
;
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), source);
|
|
const root = try parser.parse();
|
|
const body = root.data.root.decls[0].data.fn_decl.body;
|
|
// second stmt is the match expr (after var decl)
|
|
const match_node = body.data.block.stmts[1];
|
|
try std.testing.expect(match_node.data == .match_expr);
|
|
const arms = match_node.data.match_expr.arms;
|
|
try std.testing.expectEqual(@as(usize, 2), arms.len);
|
|
// first arm has a pattern
|
|
try std.testing.expect(arms[0].pattern != null);
|
|
// second arm is the else arm (null pattern)
|
|
try std.testing.expect(arms[1].pattern == null);
|
|
}
|
|
|
|
test "integer literal overflow error" {
|
|
const source = "main :: () { 99999999999999999999; }";
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), source);
|
|
const result = parser.parse();
|
|
try std.testing.expectError(error.ParseError, result);
|
|
try std.testing.expectEqualStrings("integer literal overflow", parser.err_msg.?);
|
|
}
|