Files
sx/src/parser.zig
agra 98526ab9b4 lang 1.2: parse pack-expansion forms in all four positions
Pack/tuple spread now parses in tuple-value `(..xs)` / `(..xs.field)`,
tuple-type `(..F(Ts))` / `(..F(Ts.Arg))`, call-arg `f(..xs)` (already),
and closure-sig `Closure(..Ts)` / `Closure(..sources.T)` positions.

Design: the uniform spread node is the existing `spread_expr` (its
operand sub-expression carries the projection `xs.field` and
type-application `F(Ts)` shapes) rather than a new PackExpansion node —
call-arg slice-spread (`..arr`) and pack-spread (`..pack`) are
syntactically identical, so they must share one node, and spread_expr
already serves it with working slice lowering. Closure-sig packs gain
`ClosureTypeExpr.pack_projection` alongside the existing `pack_name`.

Parser-only; sema/lowering land in Phase 2. 6 new parser unit tests +
examples/probes/pack-expansion-parses.sx. Build + 225-suite green.
2026-05-29 12:33:27 +03:00

3720 lines
167 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,
/// Type param names from enclosing generic struct (set while parsing methods)
struct_type_params: []const []const u8 = &.{},
/// When true (set while parsing methods inside `struct #compiler { ... }`),
/// a missing function body (just `name :: (params);`) is synthesized as
/// a `.compiler_expr` body so the per-method `#compiler` suffix can be
/// omitted.
struct_default_compiler: bool = false,
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"; or #import c { ... };
if (self.current.tag == .hash_import) {
self.advance();
// Check for #import c { ... } (C import block)
if (self.current.tag == .identifier and std.mem.eql(u8, self.tokenSlice(self.current), "c") and self.peekNext() == .l_brace) {
self.advance(); // consume 'c'
return self.parseCImportBlock(start, null);
}
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 #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 } });
}
// Top-level #framework directive: link against an Apple framework.
if (self.current.tag == .hash_framework) {
self.advance();
if (self.current.tag != .string_literal) {
return self.fail("expected string after '#framework'");
}
const raw = self.tokenSlice(self.current);
const fw_name = raw[1 .. raw.len - 1];
self.advance();
try self.expect(.semicolon);
return try self.createNode(start, .{ .framework_decl = .{ .name = fw_name } });
}
// impl Protocol for Type { methods }
if (self.current.tag == .kw_impl) {
return self.parseImplBlock(start);
}
// Top-level `inline if` — compile-time conditional
if (self.current.tag == .kw_inline) {
if (self.peekNext() == .kw_if) {
self.advance(); // skip 'inline'
const expr = try self.parseIfExpr();
if (expr.data == .if_expr) {
expr.data.if_expr.is_comptime = true;
} else if (expr.data == .match_expr) {
expr.data.match_expr.is_comptime = true;
}
return expr;
}
}
// All top-level declarations start with an identifier
if (!self.isIdentLike() and self.current.tag != .kw_Self) {
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"; or name :: #import c { ... };
if (self.current.tag == .hash_import) {
self.advance();
// Check for name :: #import c { ... }
if (self.current.tag == .identifier and std.mem.eql(u8, self.tokenSlice(self.current), "c") and self.peekNext() == .l_brace) {
self.advance(); // consume 'c'
return self.parseCImportBlock(start_pos, name);
}
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 } });
}
// Named library: name :: #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_pos, .{ .library_decl = .{ .lib_name = lib_name, .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);
}
// Protocol declaration
if (self.current.tag == .kw_protocol) {
return self.parseProtocolDecl(name, start_pos);
}
// Foreign-type binding with optional prefix modifiers:
// [#foreign | #jni_main]* (#jni_class / #jni_interface / #objc_class /
// #objc_protocol / #swift_class / #swift_struct / #swift_protocol) ("path") { body }
//
// Define-by-default: bare `#jni_class("...")` declares a new class (sx-defined).
// `#foreign` flips that to "reference an existing class on the foreign side."
// `#jni_main` flags the class as the launchable entry (Android Activity).
if (self.tryParseForeignClassPrefix()) |prefix| {
return self.parseForeignClassDecl(name, start_pos, prefix.runtime, prefix.is_foreign, prefix.is_main);
}
// C-style union declaration
if (self.current.tag == .kw_union) {
return self.parseUnionDecl(name, start_pos);
}
// UFCS alias: name :: ufcs target;
if (self.current.tag == .kw_ufcs) {
self.advance();
if (self.current.tag != .identifier) {
return self.fail("expected function name after 'ufcs'");
}
const target = self.tokenSlice(self.current);
self.advance();
try self.expect(.semicolon);
return try self.createNode(start_pos, .{ .ufcs_alias = .{ .name = name, .target = target } });
}
// 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 [lib] ["c_name"]; — foreign with type annotation
if (self.current.tag == .hash_foreign) {
const fi_start = self.current.loc.start;
self.advance();
// Optional: library reference (identifier). Omitted when the symbol
// resolves at link time from a framework or auto-detected library.
var lib_ref: ?[]const u8 = null;
if (self.current.tag == .identifier) {
lib_ref = self.tokenSlice(self.current);
self.advance();
}
// Optional: C symbol name (string literal)
var c_name: ?[]const u8 = null;
if (self.current.tag == .string_literal) {
const raw = self.tokenSlice(self.current);
c_name = raw[1 .. raw.len - 1];
self.advance();
}
try self.expect(.semicolon);
const fi = try self.createNode(fi_start, .{ .foreign_expr = .{
.library_ref = lib_ref,
.c_name = c_name,
} });
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 parseCImportBlock(self: *Parser, start: u32, name: ?[]const u8) anyerror!*Node {
try self.expect(.l_brace);
var includes = std.ArrayList([]const u8).empty;
var sources = std.ArrayList([]const u8).empty;
var defines = std.ArrayList([]const u8).empty;
var flags = std.ArrayList([]const u8).empty;
while (self.current.tag != .r_brace and self.current.tag != .eof) {
if (self.current.tag == .hash_include) {
self.advance();
if (self.current.tag != .string_literal) return self.fail("expected string after '#include'");
const raw = self.tokenSlice(self.current);
try includes.append(self.allocator, raw[1 .. raw.len - 1]);
self.advance();
try self.expect(.semicolon);
} else if (self.current.tag == .hash_source) {
self.advance();
if (self.current.tag != .string_literal) return self.fail("expected string after '#source'");
const raw = self.tokenSlice(self.current);
try sources.append(self.allocator, raw[1 .. raw.len - 1]);
self.advance();
try self.expect(.semicolon);
} else if (self.current.tag == .hash_define) {
self.advance();
if (self.current.tag != .string_literal) return self.fail("expected string after '#define'");
const raw = self.tokenSlice(self.current);
try defines.append(self.allocator, raw[1 .. raw.len - 1]);
self.advance();
try self.expect(.semicolon);
} else if (self.current.tag == .hash_flags) {
self.advance();
if (self.current.tag != .string_literal) return self.fail("expected string after '#flags'");
const raw = self.tokenSlice(self.current);
try flags.append(self.allocator, raw[1 .. raw.len - 1]);
self.advance();
try self.expect(.semicolon);
} else {
return self.fail("unexpected token inside '#import c { ... }'");
}
}
try self.expect(.r_brace);
try self.expect(.semicolon);
return try self.createNode(start, .{ .c_import_decl = .{
.includes = try includes.toOwnedSlice(self.allocator),
.sources = try sources.toOwnedSlice(self.allocator),
.defines = try defines.toOwnedSlice(self.allocator),
.flags = try flags.toOwnedSlice(self.allocator),
.name = name,
} });
}
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 } });
}
if (self.current.tag == .hash_foreign) {
// name : type #foreign [lib] ["c_name"]; (extern global from libsystem etc.)
self.advance();
var lib_ref: ?[]const u8 = null;
if (self.current.tag == .identifier) {
lib_ref = self.tokenSlice(self.current);
self.advance();
}
var c_name: ?[]const u8 = null;
if (self.current.tag == .string_literal) {
const raw = self.tokenSlice(self.current);
c_name = raw[1 .. raw.len - 1];
self.advance();
}
try self.expect(.semicolon);
return try self.createNode(start_pos, .{ .var_decl = .{
.name = name,
.type_annotation = type_node,
.value = null,
.is_foreign = true,
.foreign_lib = lib_ref,
.foreign_name = c_name,
} });
}
return self.fail("expected ':', '=', ';' or '#foreign' after type annotation");
}
fn parseTypeExpr(self: *Parser) anyerror!*Node {
const start = self.current.loc.start;
// Optional type: ?T
if (self.current.tag == .question) {
self.advance(); // skip '?'
const inner_type = try self.parseTypeExpr();
return try self.createNode(start, .{ .optional_type_expr = .{ .inner_type = inner_type } });
}
// 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 or $T/Protocol1/Protocol2.
// Also: pack-index type access $args[<int_literal>] — resolves to
// the i-th element type of the active pack binding (step 3 of
// the variadic heterogeneous type packs feature).
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();
// Pack-index access: $<pack_name>[<int_literal>]
if (self.current.tag == .l_bracket) {
self.advance(); // skip '['
if (self.current.tag != .int_literal) {
return self.fail("expected integer literal in pack index");
}
const idx_text = self.tokenSlice(self.current);
const idx_val = std.fmt.parseInt(i64, idx_text, 10) catch {
return self.fail("invalid integer literal in pack index");
};
if (idx_val < 0) return self.fail("pack index cannot be negative");
self.advance();
try self.expect(.r_bracket);
return try self.createNode(start, .{ .pack_index_type_expr = .{
.pack_name = name,
.index = @intCast(idx_val),
} });
}
// Parse optional protocol constraints: $T/Eq/Hashable
var constraints = std.ArrayList([]const u8).empty;
while (self.current.tag == .slash) {
self.advance(); // skip '/'
if (self.current.tag != .identifier) {
return self.fail("expected protocol name after '/'");
}
try constraints.append(self.allocator, self.tokenSlice(self.current));
self.advance();
}
const pc = try constraints.toOwnedSlice(self.allocator);
return try self.createNode(start, .{ .type_expr = .{ .name = name, .is_generic = true, .protocol_constraints = pc } });
}
// Function type: (ParamTypes) -> ReturnType
// Tuple type: (T1, T2) or (T1) — no '->' after ')'
// Named params (documentation only): (name: Type, ...) -> ReturnType
if (self.current.tag == .l_paren) {
self.advance(); // skip '('
var param_types = std.ArrayList(*Node).empty;
var param_names = std.ArrayList(?[]const u8).empty;
var has_names = false;
while (self.current.tag != .r_paren and self.current.tag != .eof) {
if (param_types.items.len > 0) {
try self.expect(.comma);
if (self.current.tag == .r_paren) break; // trailing comma ok
}
// Pack expansion in a tuple/function type: `(..F(Ts))` /
// `(..F(Ts.Arg))` / `(..Ts)`. Reuses `spread_expr`; its operand
// is the per-element type expression (e.g. `F(Ts)`), carrying any
// projection in `Ts.Arg` form.
if (self.current.tag == .dot_dot) {
const spread_start = self.current.loc.start;
self.advance(); // skip '..'
const operand = try self.parseTypeExpr();
const spread = try self.createNode(spread_start, .{ .spread_expr = .{ .operand = operand } });
try param_names.append(self.allocator, null);
try param_types.append(self.allocator, spread);
continue;
}
// Check for optional param name: `name: Type`
// An identifier followed by `:` (not `::` or `:=`) is a param name
if (self.isIdentLike() and self.peekNext() == .colon) {
const pname = self.tokenSlice(self.current);
self.advance(); // skip name
self.advance(); // skip ':'
try param_names.append(self.allocator, pname);
has_names = true;
} else {
try param_names.append(self.allocator, null);
}
try param_types.append(self.allocator, try self.parseTypeExpr());
}
try self.expect(.r_paren);
if (self.current.tag == .arrow) {
// '->' present: function type
self.advance(); // skip '->'
const return_type = try self.parseTypeExpr();
const call_conv = try self.parseOptionalCallConv();
return try self.createNode(start, .{ .function_type_expr = .{
.param_types = try param_types.toOwnedSlice(self.allocator),
.param_names = if (has_names) try param_names.toOwnedSlice(self.allocator) else null,
.return_type = return_type,
.call_conv = call_conv,
} });
}
// No '->': tuple type (even for single element)
return try self.createNode(start, .{ .tuple_type_expr = .{
.field_types = try param_types.toOwnedSlice(self.allocator),
.field_names = null,
} });
}
if (self.current.tag.isTypeKeyword() or self.isIdentLike()) {
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.isIdentLike() 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;
}
}
// Closure type: Closure(params...) -> R
// Variadic-pack trailing form: `Closure(Prefix..., ..$pack) -> R`
// binds `pack` to a heterogeneous comptime type list at impl
// match time (see plan: variadic heterogeneous type packs).
if (std.mem.eql(u8, name, "Closure") and self.current.tag == .l_paren) {
self.advance(); // skip '('
var param_types = std.ArrayList(*Node).empty;
var param_names = std.ArrayList(?[]const u8).empty;
var has_names = false;
var pack_name: ?[]const u8 = null;
var pack_projection: ?[]const u8 = null;
while (self.current.tag != .r_paren and self.current.tag != .eof) {
if (param_types.items.len > 0) {
try self.expect(.comma);
if (self.current.tag == .r_paren) break; // trailing comma ok
}
// Trailing pack marker: `..$name` or `..pack.Arg` (terminal only).
if (self.current.tag == .dot_dot) {
self.advance(); // skip '..'
if (self.current.tag == .dollar) self.advance(); // optional sigil
if (!self.isIdentLike()) {
return self.fail("expected pack name after '..' in Closure type");
}
pack_name = self.tokenSlice(self.current);
self.advance();
// Optional projection: `..sources.T` picks a type-arg per element.
if (self.current.tag == .dot) {
self.advance(); // skip '.'
if (!self.isIdentLike()) {
return self.fail("expected projection name after '.' in Closure pack");
}
pack_projection = self.tokenSlice(self.current);
self.advance();
}
// Pack must be the LAST item — only `)` accepted next.
if (self.current.tag != .r_paren) {
return self.fail("variadic pack must be the last parameter in Closure type");
}
break;
}
// Check for optional param name: `name: Type`
if (self.current.tag == .identifier and self.peekNext() == .colon) {
const pname = self.tokenSlice(self.current);
self.advance(); // skip name
self.advance(); // skip ':'
try param_names.append(self.allocator, pname);
has_names = true;
} else {
try param_names.append(self.allocator, null);
}
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();
return_type = try self.parseTypeExpr();
}
return try self.createNode(start, .{ .closure_type_expr = .{
.param_types = try param_types.toOwnedSlice(self.allocator),
.param_names = if (has_names) try param_names.toOwnedSlice(self.allocator) else null,
.return_type = return_type,
.pack_name = pack_name,
.pack_projection = pack_projection,
} });
}
// 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 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("invalid integer literal in type argument");
};
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),
} });
}
// Mark as generic if name matches an enclosing struct's type param
var is_struct_generic = false;
for (self.struct_type_params) |tp| {
if (std.mem.eql(u8, tp, name)) { is_struct_generic = true; break; }
}
return try self.createNode(start, .{ .type_expr = .{ .name = name, .is_generic = is_struct_generic } });
}
// 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 `#compiler` attribute: all methods inside this struct are
// implicitly compiler hooks (no per-method `#compiler` suffix needed).
// Mirrors `protocol #inline { ... }` shape.
var is_compiler_struct = false;
if (self.current.tag == .hash_compiler) {
is_compiler_struct = true;
self.advance();
}
// 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);
if (self.current.tag == .r_paren) break;
}
// 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();
// Parse optional protocol constraints: $T: Type/Eq/Hashable
var pc_list = std.ArrayList([]const u8).empty;
if (constraint.data == .type_expr and std.mem.eql(u8, constraint.data.type_expr.name, "Type")) {
while (self.current.tag == .slash) {
self.advance(); // skip '/'
if (self.current.tag != .identifier) {
return self.fail("expected protocol name after '/'");
}
try pc_list.append(self.allocator, self.tokenSlice(self.current));
self.advance();
}
}
const pc = try pc_list.toOwnedSlice(self.allocator);
try type_params.append(self.allocator, .{ .name = param_name, .constraint = constraint, .protocol_constraints = pc });
}
try self.expect(.r_paren);
}
try self.expect(.l_brace);
// Set struct type params context so method params can reference T without $
var tp_names = std.ArrayList([]const u8).empty;
for (type_params.items) |tp| try tp_names.append(self.allocator, tp.name);
const saved_struct_type_params = self.struct_type_params;
self.struct_type_params = tp_names.items;
defer self.struct_type_params = saved_struct_type_params;
// Propagate the struct-level `#compiler` flag to nested method
// parsing so a bodyless `name :: (params);` synthesizes a
// `.compiler_expr` body.
const saved_struct_default_compiler = self.struct_default_compiler;
self.struct_default_compiler = is_compiler_struct;
defer self.struct_default_compiler = saved_struct_default_compiler;
var field_names = std.ArrayList([]const u8).empty;
var field_types = std.ArrayList(*Node).empty;
var field_defaults = std.ArrayList(?*Node).empty;
var using_entries = std.ArrayList(ast.UsingEntry).empty;
var methods = std.ArrayList(*Node).empty;
var constants = std.ArrayList(*Node).empty;
while (self.current.tag != .r_brace and self.current.tag != .eof) {
// Check for #using directive
if (self.current.tag == .hash_using) {
self.advance(); // skip #using
if (self.current.tag != .identifier) {
return self.fail("expected type name after '#using'");
}
const used_type = self.tokenSlice(self.current);
self.advance();
try using_entries.append(self.allocator, .{
.insert_index = @intCast(field_names.items.len),
.type_name = used_type,
});
if (self.current.tag == .semicolon) self.advance();
continue;
}
// Method declaration: name :: (params) -> type { body }
if (self.current.tag == .identifier and self.peekNext() == .colon_colon) {
const method_start = self.current.loc.start;
const method_name = self.tokenSlice(self.current);
self.advance(); // skip name
self.advance(); // skip ::
if (self.current.tag == .l_paren and self.isFunctionDef()) {
try methods.append(self.allocator, try self.parseFnDecl(method_name, method_start));
} else {
// Non-function constant: name :: value;
const value = try self.parseExpr();
if (self.current.tag == .semicolon) self.advance();
try constants.append(self.allocator, try self.createNode(method_start, .{ .const_decl = .{
.name = method_name,
.type_annotation = null,
.value = value,
} }));
}
continue;
}
// Parse field group: name1, name2, ...: type (= default)?;
// Or typed constant: name :Type: value;
var group_names = std.ArrayList([]const u8).empty;
if (self.current.tag != .identifier) {
return self.fail("expected field name in struct");
}
const field_start = self.current.loc.start;
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();
// Typed constant: name :Type: value; (second colon after type)
if (self.current.tag == .colon and group_names.items.len == 1) {
self.advance(); // skip second ':'
const value = try self.parseExpr();
if (self.current.tag == .semicolon) self.advance();
try constants.append(self.allocator, try self.createNode(field_start, .{ .const_decl = .{
.name = group_names.items[0],
.type_annotation = field_type,
.value = value,
} }));
continue;
}
// 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),
.using_entries = try using_entries.toOwnedSlice(self.allocator),
.methods = try methods.toOwnedSlice(self.allocator),
.constants = try constants.toOwnedSlice(self.allocator),
} });
}
fn parseProtocolDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node {
self.advance(); // skip 'protocol'
// Optional type params: protocol(Target: Type, U: Type) { ... }
// Names are introduced without a `$` sigil (unlike struct's $T) because
// the parens after `protocol` already mark this as a parameter list.
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);
if (self.current.tag == .r_paren) break;
}
if (self.current.tag != .identifier) {
return self.fail("expected type parameter name in protocol header");
}
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);
}
// Check for #inline
var is_inline = false;
if (self.current.tag == .hash_inline) {
is_inline = true;
self.advance();
}
try self.expect(.l_brace);
// Push type-param names into scope so method signatures can refer to them
// bare (e.g. `convert :: () -> Target` resolves Target as a generic type expr).
var tp_names = std.ArrayList([]const u8).empty;
for (type_params.items) |tp| try tp_names.append(self.allocator, tp.name);
const saved_struct_type_params = self.struct_type_params;
self.struct_type_params = tp_names.items;
defer self.struct_type_params = saved_struct_type_params;
var methods = std.ArrayList(ast.ProtocolMethodDecl).empty;
while (self.current.tag != .r_brace and self.current.tag != .eof) {
// Method: name :: (params) -> type; or name :: (params) -> type { body }
if (self.current.tag != .identifier) {
return self.fail("expected method name in protocol body");
}
const method_name = self.tokenSlice(self.current);
self.advance();
try self.expect(.colon_colon);
try self.expect(.l_paren);
var param_types = std.ArrayList(*Node).empty;
var param_names = std.ArrayList([]const u8).empty;
while (self.current.tag != .r_paren and self.current.tag != .eof) {
if (param_types.items.len > 0) {
try self.expect(.comma);
if (self.current.tag == .r_paren) break;
}
// Parse: name: type
if (self.current.tag != .identifier and self.current.tag != .kw_Self) {
return self.fail("expected parameter name in protocol method");
}
const pname = self.tokenSlice(self.current);
self.advance();
try self.expect(.colon);
const ptype = try self.parseTypeExpr();
try param_names.append(self.allocator, pname);
try param_types.append(self.allocator, ptype);
}
try self.expect(.r_paren);
// Optional return type
var return_type: ?*Node = null;
if (self.current.tag == .arrow) {
self.advance();
return_type = try self.parseTypeExpr();
}
// Optional body (default method) or semicolon
var default_body: ?*Node = null;
if (self.current.tag == .l_brace) {
default_body = try self.parseBlock();
} else {
if (self.current.tag == .semicolon) self.advance();
}
try methods.append(self.allocator, .{
.name = method_name,
.params = try param_types.toOwnedSlice(self.allocator),
.param_names = try param_names.toOwnedSlice(self.allocator),
.return_type = return_type,
.default_body = default_body,
});
}
try self.expect(.r_brace);
return try self.createNode(start_pos, .{ .protocol_decl = .{
.name = name,
.methods = try methods.toOwnedSlice(self.allocator),
.is_inline = is_inline,
.type_params = try type_params.toOwnedSlice(self.allocator),
} });
}
fn foreignRuntimeForCurrent(self: *Parser) ?ast.ForeignRuntime {
return switch (self.current.tag) {
.hash_jni_class => .jni_class,
.hash_jni_interface => .jni_interface,
.hash_objc_class => .objc_class,
.hash_objc_protocol => .objc_protocol,
.hash_swift_class => .swift_class,
.hash_swift_struct => .swift_struct,
.hash_swift_protocol => .swift_protocol,
else => null,
};
}
const ForeignClassPrefix = struct {
runtime: ast.ForeignRuntime,
is_foreign: bool,
is_main: bool,
};
/// Recognise an optional sequence of `#foreign` / `#jni_main` modifiers
/// followed by a type-introducer directive (`#jni_class`, `#objc_class`,
/// ...). Returns null if the current position isn't a foreign-class
/// directive (possibly after modifiers). Consumes the modifier tokens
/// only when a runtime directive follows; otherwise leaves the parser
/// state untouched.
fn tryParseForeignClassPrefix(self: *Parser) ?ForeignClassPrefix {
// Peek ahead through modifier tokens to confirm a directive follows.
var lookahead_idx: usize = 0;
var is_foreign = false;
var is_main = false;
while (true) {
const tag = self.peekTag(lookahead_idx);
switch (tag) {
.hash_foreign => {
is_foreign = true;
lookahead_idx += 1;
},
.hash_jni_main => {
is_main = true;
lookahead_idx += 1;
},
else => break,
}
}
const runtime = self.foreignRuntimeForOffset(lookahead_idx) orelse return null;
// Commit: consume modifier tokens.
var i: usize = 0;
while (i < lookahead_idx) : (i += 1) self.advance();
return .{ .runtime = runtime, .is_foreign = is_foreign, .is_main = is_main };
}
fn peekTag(self: *Parser, offset: usize) Tag {
if (offset == 0) return self.current.tag;
var lexer_copy = self.lexer;
var tok: Token = undefined;
var i: usize = 0;
while (i < offset) : (i += 1) {
tok = lexer_copy.next();
}
return tok.tag;
}
fn foreignRuntimeForOffset(self: *Parser, offset: usize) ?ast.ForeignRuntime {
const tag = self.peekTag(offset);
return switch (tag) {
.hash_jni_class => .jni_class,
.hash_jni_interface => .jni_interface,
.hash_objc_class => .objc_class,
.hash_objc_protocol => .objc_protocol,
.hash_swift_class => .swift_class,
.hash_swift_struct => .swift_struct,
.hash_swift_protocol => .swift_protocol,
else => null,
};
}
fn parseForeignClassDecl(self: *Parser, name: []const u8, start_pos: u32, runtime: ast.ForeignRuntime, is_foreign: bool, is_main: bool) anyerror!*Node {
self.advance(); // skip directive token
try self.expect(.l_paren);
if (self.current.tag != .string_literal) {
return self.fail("expected string literal foreign-type path after directive");
}
const raw = self.tokenSlice(self.current);
const foreign_path = raw[1 .. raw.len - 1];
self.advance();
try self.expect(.r_paren);
try self.expect(.l_brace);
var members = std.ArrayList(ast.ForeignClassMember).empty;
while (self.current.tag != .r_brace and self.current.tag != .eof) {
// #extends Alias; or #implements Alias;
if (self.current.tag == .hash_extends or self.current.tag == .hash_implements) {
const is_extends = self.current.tag == .hash_extends;
self.advance();
if (self.current.tag != .identifier) {
return self.fail(if (is_extends) "expected superclass alias after '#extends'" else "expected interface alias after '#implements'");
}
const alias = self.tokenSlice(self.current);
self.advance();
try self.expect(.semicolon);
try members.append(self.allocator, if (is_extends)
.{ .extends = alias }
else
.{ .implements = alias });
continue;
}
// Field: name: Type; (instance field — JNI Get/Set<Type>Field)
// Method: name :: (args...) -> Ret;
if (self.current.tag != .identifier) {
return self.fail("expected member name in '#jni_class' body");
}
const member_name = self.tokenSlice(self.current);
self.advance();
if (self.current.tag == .colon) {
self.advance(); // consume `:`
const field_type = try self.parseTypeExpr();
// M2.2 — optional `#property[(modifier, modifier, ...)]`
// directive after the field type. Synthesizes Obj-C
// getter/setter dispatch at access sites.
var is_property = false;
var property_modifiers = std.ArrayList([]const u8).empty;
if (self.current.tag == .hash_property) {
is_property = true;
self.advance();
if (self.current.tag == .l_paren) {
self.advance(); // consume `(`
while (self.current.tag != .r_paren and self.current.tag != .eof) {
if (property_modifiers.items.len > 0) {
try self.expect(.comma);
if (self.current.tag == .r_paren) break;
}
if (self.current.tag != .identifier) {
return self.fail("expected property modifier name (strong, weak, copy, readonly, ...)");
}
const mod_name = self.tokenSlice(self.current);
self.advance();
// Optional argument: getter("name") / setter("name")
// — parsed but stored as part of the modifier string
// for now (M2.2 first pass; full attribute handling
// arrives with M4 ARC wiring).
if (self.current.tag == .l_paren) {
self.advance();
if (self.current.tag != .string_literal) {
return self.fail("expected string literal argument for property modifier");
}
self.advance();
try self.expect(.r_paren);
}
try property_modifiers.append(self.allocator, mod_name);
}
try self.expect(.r_paren);
}
}
try self.expect(.semicolon);
try members.append(self.allocator, .{ .field = .{
.name = member_name,
.field_type = field_type,
.is_property = is_property,
.property_modifiers = try property_modifiers.toOwnedSlice(self.allocator),
} });
continue;
}
try self.expect(.colon_colon);
// M2.1(a) — class-level constant `name :: Type = expr;` inside
// a `#objc_class` block. Reframed as a synthesized class method
// with an expression body (`name :: () -> Type => expr;`) so
// the rest of the M1.2 class-synthesis pipeline picks it up:
// a class-method IMP is emitted and registered on the metaclass.
// Apple's runtime calls the IMP from `[Cls foo]` — there's no
// runtime-level distinction between a class-level constant and
// a niladic class method, just a difference in source spelling.
if (self.current.tag != .l_paren) {
const ret_type = try self.parseTypeExpr();
try self.expect(.equal);
const expr_node = try self.parseExpr();
try self.expect(.semicolon);
const stmts = try self.allocator.alloc(*Node, 1);
stmts[0] = expr_node;
const block_node = try self.createNode(expr_node.span.start, .{ .block = .{ .stmts = stmts } });
try members.append(self.allocator, .{ .method = .{
.name = member_name,
.params = &.{},
.param_names = &.{},
.return_type = ret_type,
.is_static = true,
.jni_descriptor_override = null,
.selector_override = null,
.body = block_node,
} });
continue;
}
try self.expect(.l_paren);
var param_types = std.ArrayList(*Node).empty;
var param_names = std.ArrayList([]const u8).empty;
while (self.current.tag != .r_paren and self.current.tag != .eof) {
if (param_types.items.len > 0) {
try self.expect(.comma);
if (self.current.tag == .r_paren) break;
}
if (self.current.tag != .identifier and self.current.tag != .kw_Self) {
return self.fail("expected parameter name in '#jni_class' method");
}
const pname = self.tokenSlice(self.current);
self.advance();
try self.expect(.colon);
const ptype = try self.parseTypeExpr();
try param_names.append(self.allocator, pname);
try param_types.append(self.allocator, ptype);
}
try self.expect(.r_paren);
// Instance vs class method is determined by the first param's
// TYPE: `*Self` (pointer-to-Self) ⇒ instance method, anything
// else (including a method with no params at all) ⇒ class
// method. Keying on the type, not the param name, means the
// user can call the receiver whatever they like — `this`,
// `me`, etc. — without changing the dispatch shape.
const is_static = blk: {
if (param_types.items.len == 0) break :blk true;
const first = param_types.items[0];
if (first.data != .pointer_type_expr) break :blk true;
const pointee = first.data.pointer_type_expr.pointee_type;
if (pointee.data != .type_expr) break :blk true;
break :blk !std.mem.eql(u8, pointee.data.type_expr.name, "Self");
};
var return_type: ?*Node = null;
if (self.current.tag == .arrow) {
self.advance();
return_type = try self.parseTypeExpr();
}
// Optional `#jni_method_descriptor("(Sig)Ret")` — explicit JNI descriptor override.
var desc_override: ?[]const u8 = null;
if (self.current.tag == .hash_jni_method_descriptor) {
self.advance(); // skip `#jni_method_descriptor`
try self.expect(.l_paren);
if (self.current.tag != .string_literal) {
return self.fail("expected string literal JNI descriptor after '#jni_method_descriptor('");
}
const raw_desc = self.tokenSlice(self.current);
desc_override = raw_desc[1 .. raw_desc.len - 1];
self.advance();
try self.expect(.r_paren);
}
// Optional `#selector("explicit:string")` — explicit Obj-C selector override
// (Phase 3.2). Same slot as the JNI descriptor; they're not mutually
// exclusive at parse time though they belong to different runtimes.
var sel_override: ?[]const u8 = null;
if (self.current.tag == .hash_selector) {
self.advance(); // skip `#selector`
try self.expect(.l_paren);
if (self.current.tag != .string_literal) {
return self.fail("expected string literal selector after '#selector('");
}
const raw_sel = self.tokenSlice(self.current);
sel_override = raw_sel[1 .. raw_sel.len - 1];
self.advance();
try self.expect(.r_paren);
}
// Method body is optional: `;` → declaration (foreign or inherited
// method we just want to call); `{ ... }` → sx-side block body
// for sx-defined classes; `=> expr;` → expression-body form
// (M1.0), lowered as a single-statement block holding `expr`.
var body_node: ?*Node = null;
if (self.current.tag == .l_brace) {
body_node = try self.parseBlock();
} else if (self.current.tag == .fat_arrow) {
self.advance();
const expr = try self.parseExpr();
try self.expect(.semicolon);
const stmts = try self.allocator.alloc(*Node, 1);
stmts[0] = expr;
body_node = try self.createNode(expr.span.start, .{ .block = .{ .stmts = stmts } });
} else {
try self.expect(.semicolon);
}
try members.append(self.allocator, .{ .method = .{
.name = member_name,
.params = try param_types.toOwnedSlice(self.allocator),
.param_names = try param_names.toOwnedSlice(self.allocator),
.return_type = return_type,
.is_static = is_static,
.jni_descriptor_override = desc_override,
.selector_override = sel_override,
.body = body_node,
} });
}
try self.expect(.r_brace);
return try self.createNode(start_pos, .{ .foreign_class_decl = .{
.name = name,
.foreign_path = foreign_path,
.runtime = runtime,
.members = try members.toOwnedSlice(self.allocator),
.is_foreign = is_foreign,
.is_main = is_main,
} });
}
fn parseImplBlock(self: *Parser, start_pos: u32) anyerror!*Node {
self.advance(); // skip 'impl'
// Protocol name
if (self.current.tag != .identifier) {
return self.fail("expected protocol name after 'impl'");
}
const protocol_name = self.tokenSlice(self.current);
self.advance();
// Optional protocol type args: impl Into(Block) for ...
var protocol_type_args = std.ArrayList(*Node).empty;
if (self.current.tag == .l_paren) {
self.advance(); // skip '('
while (self.current.tag != .r_paren and self.current.tag != .eof) {
if (protocol_type_args.items.len > 0) {
try self.expect(.comma);
if (self.current.tag == .r_paren) break;
}
try protocol_type_args.append(self.allocator, try self.parseTypeExpr());
}
try self.expect(.r_paren);
}
// 'for' — note: 'for' is a keyword (kw_for), not an identifier
if (self.current.tag != .kw_for) {
return self.fail("expected 'for' after protocol name in impl block");
}
self.advance();
// Source-type spelling. For parameterised protocols we accept any TypeExpr
// (`Closure(...) -> R`, `*T`, etc.). For nullary protocols we keep the
// legacy identifier-only path so existing `impl P for SomeStruct` keeps
// working unchanged (the parser doesn't try to over-parse trailing tokens).
var target_type: []const u8 = "";
var target_type_expr: ?*Node = null;
var target_type_params = std.ArrayList(ast.StructTypeParam).empty;
if (protocol_type_args.items.len > 0) {
// Parameterised protocol — source is a general TypeExpr.
target_type_expr = try self.parseTypeExpr();
// Synthesize a string view of the source for back-compat consumers
// (LSP hover, etc.). The semantic key for the impl map uses
// structural mangling, not this string.
if (target_type_expr.?.data == .type_expr) {
target_type = target_type_expr.?.data.type_expr.name;
}
} else {
// Legacy nullary-protocol path: single identifier source.
if (self.current.tag != .identifier and !self.current.tag.isTypeKeyword()) {
return self.fail("expected type name after 'for'");
}
target_type = self.tokenSlice(self.current);
self.advance();
// Optional type params: impl Protocol for List($T)
if (self.current.tag == .l_paren) {
self.advance(); // skip '('
while (self.current.tag != .r_paren and self.current.tag != .eof) {
if (target_type_params.items.len > 0) {
try self.expect(.comma);
if (self.current.tag == .r_paren) break;
}
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();
// Optional constraint — for now just use Type
const constraint = try self.createNode(self.current.loc.start, .{ .type_expr = .{ .name = "Type" } });
try target_type_params.append(self.allocator, .{ .name = param_name, .constraint = constraint });
}
try self.expect(.r_paren);
}
}
try self.expect(.l_brace);
// Set struct type params context so method params can reference T without $
var tp_names = std.ArrayList([]const u8).empty;
for (target_type_params.items) |tp| try tp_names.append(self.allocator, tp.name);
const saved_struct_type_params = self.struct_type_params;
self.struct_type_params = tp_names.items;
defer self.struct_type_params = saved_struct_type_params;
var methods = std.ArrayList(*Node).empty;
while (self.current.tag != .r_brace and self.current.tag != .eof) {
// Method: name :: (params) -> type { body }
if (self.current.tag != .identifier) {
return self.fail("expected method name in impl block");
}
const method_start = self.current.loc.start;
const method_name = self.tokenSlice(self.current);
self.advance();
try self.expect(.colon_colon);
if (self.current.tag == .l_paren and self.isFunctionDef()) {
try methods.append(self.allocator, try self.parseFnDecl(method_name, method_start));
} else {
return self.fail("expected function declaration in impl block");
}
}
try self.expect(.r_brace);
return try self.createNode(start_pos, .{ .impl_block = .{
.protocol_name = protocol_name,
.target_type = target_type,
.target_type_params = try target_type_params.toOwnedSlice(self.allocator),
.methods = try methods.toOwnedSlice(self.allocator),
.protocol_type_args = try protocol_type_args.toOwnedSlice(self.allocator),
.target_type_expr = target_type_expr,
} });
}
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);
if (self.current.tag == .r_brace) break;
}
// 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);
// Optional init block: T.{ fields } { stmts }
const init_block: ?*Node = if (self.current.tag == .l_brace)
try self.parseBlock()
else
null;
return try self.createNode(start_pos, .{ .struct_literal = .{
.struct_name = struct_name,
.type_expr = type_expr,
.field_inits = try field_inits.toOwnedSlice(self.allocator),
.init_block = init_block,
} });
}
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);
if (self.current.tag == .r_paren) break;
}
// Leading `..` marks a variadic param at the binding site
// (e.g., `..$args` heterogeneous pack, or future homogeneous
// `..args: []$T`). The old `args: ..T` form keeps its marker
// after the colon (handled below).
var is_variadic = false;
if (self.current.tag == .dot_dot) {
is_variadic = true;
self.advance();
}
var is_ct_param = false;
if (self.current.tag == .dollar) {
is_ct_param = true;
self.advance();
}
if (!self.isIdentLike()) {
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();
// Optional type annotation: if no ':', infer type from context
if (self.current.tag != .colon) {
const inferred_node = try self.createNode(param_name_span.start, .{ .inferred_type = {} });
try params.append(self.allocator, .{ .name = param_name, .name_span = param_name_span, .type_expr = inferred_node, .is_variadic = is_variadic, .is_comptime = is_ct_param });
continue;
}
self.advance(); // consume ':'
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")) {
// Parse optional protocol constraints: $T: Type/Eq/Hashable
var constraints = std.ArrayList([]const u8).empty;
while (self.current.tag == .slash) {
self.advance(); // skip '/'
if (self.current.tag != .identifier) {
return self.fail("expected protocol name after '/'");
}
try constraints.append(self.allocator, self.tokenSlice(self.current));
self.advance();
}
const pc = try constraints.toOwnedSlice(self.allocator);
param_type.data = .{ .type_expr = .{ .name = param_name, .is_generic = true, .protocol_constraints = pc } };
} else {
is_comptime_param = true;
}
}
// Optional default value: `param: T = expr`. Stored on the Param
// node; lowering fills it in for callers that omit this positional arg.
var default_expr: ?*Node = null;
if (self.current.tag == .equal) {
self.advance(); // consume '='
default_expr = try self.parseExpr();
}
// Protocol-constrained variadic pack: `..xs: Protocol` — a bare
// type (not a slice/array) on a non-comptime variadic param. The
// trailing args each conform to the protocol with their own
// type-arg. Slice variadics (`..xs: []T`) keep `is_pack == false`.
const is_pack = is_variadic and !is_comptime_param and switch (param_type.data) {
.type_expr, .parameterized_type_expr => true,
else => false,
};
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, .is_pack = is_pack, .default_expr = default_expr });
}
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;
var seen = std.StringHashMap(void).init(self.allocator);
for (params) |param| {
if (param.is_comptime) {
if (!seen.contains(param.name)) {
try seen.put(param.name, {});
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| {
if (!seen.contains(gen_name)) {
try seen.put(gen_name, {});
// Propagate protocol constraints from the TypeExpr if present
const pc = if (param.type_expr.data == .type_expr)
param.type_expr.data.type_expr.protocol_constraints
else
&[_][]const u8{};
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, .protocol_constraints = pc }) 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();
}
// Optional calling convention: callconv(.c)
const call_conv = try self.parseOptionalCallConv();
// Body: block `{ ... }`, arrow `=> expr;`, #builtin, #compiler, or #foreign marker
var is_arrow = false;
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_compiler) blk: {
const ci_start = self.current.loc.start;
self.advance();
try self.expect(.semicolon);
break :blk try self.createNode(ci_start, .{ .compiler_expr = {} });
} else if (self.struct_default_compiler and self.current.tag == .semicolon) blk: {
// Inside `struct #compiler { ... }`: a bodyless method is
// implicitly a `#compiler` hook.
const ci_start = self.current.loc.start;
self.advance();
break :blk try self.createNode(ci_start, .{ .compiler_expr = {} });
} else if (self.current.tag == .hash_foreign) blk: {
const fi_start = self.current.loc.start;
self.advance();
// Required: library reference (identifier)
// Optional: library reference (identifier).
var lib_ref: ?[]const u8 = null;
if (self.current.tag == .identifier) {
lib_ref = self.tokenSlice(self.current);
self.advance();
}
// Optional: C symbol name (string literal)
var c_name: ?[]const u8 = null;
if (self.current.tag == .string_literal) {
const raw = self.tokenSlice(self.current);
c_name = raw[1 .. raw.len - 1];
self.advance();
}
try self.expect(.semicolon);
break :blk try self.createNode(fi_start, .{ .foreign_expr = .{
.library_ref = lib_ref,
.c_name = c_name,
} });
} else if (self.current.tag == .fat_arrow) blk: {
is_arrow = true;
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,
.is_arrow = is_arrow,
.call_conv = call_conv,
} });
}
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,
.jni_env_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.isIdentLike()) {
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);
}
// Multi-target assignment: ident, expr, ... = expr, expr, ...;
if (self.current.tag == .comma) {
const first_target = try self.createNode(start, .{ .identifier = .{ .name = name } });
return try self.parseMultiAssign(first_target, 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 } });
}
// `#import "path";` / `#framework "Name";` inside a block body.
// Only meaningful inside an `inline if OS == ... { ... }` arm —
// the imports.zig flatten pass (issue-0042) surfaces those
// declarations to the top level before resolution. Anywhere else
// these nodes survive into lowering and produce a clear error.
if (self.current.tag == .hash_import) {
const start = self.current.loc.start;
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 } });
}
if (self.current.tag == .hash_framework) {
const start = self.current.loc.start;
self.advance();
if (self.current.tag != .string_literal) {
return self.fail("expected string after '#framework'");
}
const raw = self.tokenSlice(self.current);
const fw_name = raw[1 .. raw.len - 1];
self.advance();
try self.expect(.semicolon);
return try self.createNode(start, .{ .framework_decl = .{ .name = fw_name } });
}
// inline if — compile-time conditional
if (self.current.tag == .kw_inline) {
if (self.peekNext() == .kw_if) {
self.advance(); // skip 'inline'
const expr = try self.parseIfExpr();
if (expr.data == .if_expr) {
expr.data.if_expr.is_comptime = true;
} else if (expr.data == .match_expr) {
expr.data.match_expr.is_comptime = true;
}
try self.expectSemicolonAfter(expr);
return expr;
}
}
// 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;
}
if (self.current.tag == .kw_push) {
return try self.parsePushStmt();
}
// Expression statement
const expr = try self.parseExpr();
// Multi-target assignment: expr, expr, ... = expr, expr, ...;
if (self.current.tag == .comma) {
return try self.parseMultiAssign(expr, expr.span.start);
}
// 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(Prec.none);
}
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) {
// Pipe operator: desugar a |> f(args) → f(a, args), a |> f → f(a)
if (self.current.tag == .pipe_arrow and Prec.pipe >= min_prec) {
self.advance();
// Parse the RHS as a full call expression (higher precedence)
const rhs = try self.parseBinary(Prec.pipe + 1);
// Desugar based on RHS shape
if (rhs.data == .call) {
// a |> f(args) → f(a, args...)
var new_args = std.ArrayList(*Node).empty;
try new_args.append(self.allocator, lhs);
for (rhs.data.call.args) |arg| {
try new_args.append(self.allocator, arg);
}
lhs = try self.createNode(lhs.span.start, .{ .call = .{
.callee = rhs.data.call.callee,
.args = try new_args.toOwnedSlice(self.allocator),
} });
} else {
// a |> f → f(a)
const args = try self.allocator.alloc(*Node, 1);
args[0] = lhs;
lhs = try self.createNode(lhs.span.start, .{ .call = .{
.callee = rhs,
.args = args,
} });
}
continue;
}
// Null coalescing: expr ?? default
if (self.current.tag == .question_question and Prec.null_coalesce >= min_prec) {
self.advance();
const rhs = try self.parseBinary(Prec.null_coalesce);
lhs = try self.createNode(lhs.span.start, .{ .null_coalesce = .{ .lhs = lhs, .rhs = rhs } });
continue;
}
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 == .tilde) {
const start = self.current.loc.start;
self.advance();
const operand = try self.parseUnary();
return try self.createNode(start, .{ .unary_op = .{ .op = .bit_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);
if (self.current.tag == .r_paren) break;
}
// 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);
if (self.current.tag == .r_bracket) break;
}
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 if (self.current.tag == .identifier) {
// Named field access: expr.field
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 == .int_literal) {
// Numeric field access: tuple.0, tuple.1
const field = self.tokenSlice(self.current);
self.advance();
expr = try self.createNode(expr.span.start, .{ .field_access = .{ .object = expr, .field = field } });
} else {
return self.fail("expected field name or index after '.'");
}
} else if (self.current.tag == .question_dot) {
// Optional chaining: expr?.field
self.advance();
if (self.current.tag == .identifier) {
const field = self.tokenSlice(self.current);
self.advance();
expr = try self.createNode(expr.span.start, .{ .field_access = .{ .object = expr, .field = field, .is_optional = true } });
} else if (self.current.tag == .int_literal) {
const field = self.tokenSlice(self.current);
self.advance();
expr = try self.createNode(expr.span.start, .{ .field_access = .{ .object = expr, .field = field, .is_optional = true } });
} else {
return self.fail("expected field name after '?.'");
}
} 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 if (self.current.tag == .bang) {
// Force unwrap: expr!
// Only if it's not != (bang_equal would have been lexed as a single token)
self.advance();
expr = try self.createNode(expr.span.start, .{ .force_unwrap = .{ .operand = expr } });
} else {
break;
}
}
return expr;
}
fn parsePrimary(self: *Parser) anyerror!*Node {
const start = self.current.loc.start;
// Pack references in expression position:
// `$<pack_name>[<int_literal>]` → `pack_index_type_expr`
// (single Type value, step 3 shape)
// `$<pack_name>` → `comptime_pack_ref`
// (whole pack as []Type value, step 4 final slice)
// Lowering routes each through `pack_arg_types` to either
// a `const_type(TypeId)` or a `[]Type` aggregate of them.
if (self.current.tag == .dollar) {
self.advance();
if (self.current.tag != .identifier) {
return self.fail("expected pack name after '$'");
}
const pname = self.tokenSlice(self.current);
self.advance();
if (self.current.tag == .l_bracket) {
self.advance(); // skip '['
if (self.current.tag != .int_literal) {
return self.fail("expected integer literal in pack index");
}
const idx_text = self.tokenSlice(self.current);
const idx_val = std.fmt.parseInt(i64, idx_text, 10) catch {
return self.fail("invalid integer literal in pack index");
};
if (idx_val < 0) return self.fail("pack index cannot be negative");
self.advance();
try self.expect(.r_bracket);
return try self.createNode(start, .{ .pack_index_type_expr = .{
.pack_name = pname,
.index = @intCast(idx_val),
} });
}
return try self.createNode(start, .{ .comptime_pack_ref = .{
.pack_name = pname,
} });
}
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 } });
},
.kw_closure, .kw_protocol, .kw_impl, .kw_ufcs => {
// Contextual keywords used as identifiers in expressions
const name = self.tokenSlice(self.current);
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);
if (self.current.tag == .r_bracket) break;
}
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: .variant_name — parsePostfix handles optional (...) as a call
return try self.createNode(start, .{ .enum_literal = .{ .name = name } });
},
.l_paren => {
// Lambda: (params) => expr
if (self.isLambda()) {
return self.parseLambda();
}
// Function-type literal: (T1, T2) -> R (no body — isLambda would have caught a body)
if (self.isFunctionTypeExprAtLParen()) {
return try self.parseTypeExpr();
}
self.advance(); // skip '('
// Check for named tuple: (name: expr, ...)
if (self.current.tag == .identifier and self.peekNext() == .colon) {
return self.parseTupleLiteralNamed(start);
}
// Empty parens or first expression
if (self.current.tag == .r_paren) {
self.advance();
// () — empty tuple
return try self.createNode(start, .{ .tuple_literal = .{ .elements = &.{} } });
}
// Leading pack/tuple spread: `(..xs)` / `(..xs.field)` materializes
// a tuple from a pack. The spread reuses `spread_expr`; its operand
// carries the projection (`xs.field`) shape.
if (self.current.tag == .dot_dot) {
const spread_start = self.current.loc.start;
self.advance(); // skip '..'
const operand = try self.parseExpr();
const spread = try self.createNode(spread_start, .{ .spread_expr = .{ .operand = operand } });
return self.finishTupleAfterFirst(start, spread);
}
const first = try self.parseExpr();
// Check for comma → tuple
if (self.current.tag == .comma) {
return self.finishTupleAfterFirst(start, first);
}
// No comma → grouping
try self.expect(.r_paren);
return first;
},
.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_push => {
return self.parsePushStmt();
},
.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, .star, .question => {
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 } });
},
.hash_objc_call, .hash_jni_call, .hash_jni_static_call => {
return try self.parseFfiIntrinsicCall();
},
.hash_jni_env => {
return try self.parseJniEnvBlock();
},
else => {
return self.fail("unexpected token in expression");
},
}
}
/// Parse `#objc_call(T)(recv, "sel:", args...)`,
/// `#jni_call(T)(env, target, "name", "(Sig)R", args...)`, or
/// `#jni_static_call(T)(class, "name", "(Sig)R", args...)`. The
/// return type sits in the first parens; the actual call args
/// follow in the second.
fn parseJniEnvBlock(self: *Parser) anyerror!*Node {
const start = self.current.loc.start;
self.advance(); // skip `#jni_env`
try self.expect(.l_paren);
const env_expr = try self.parseExpr();
try self.expect(.r_paren);
// Body is a brace-delimited block. The `-> ?T` annotation for
// exception bubbling lands with step 2.15 / 2.16 follow-ups.
if (self.current.tag != .l_brace) {
return self.fail("expected '{' after '#jni_env(env)'");
}
const body = try self.parseBlock();
return try self.createNode(start, .{ .jni_env_block = .{
.env = env_expr,
.body = body,
} });
}
fn parseFfiIntrinsicCall(self: *Parser) anyerror!*Node {
const start = self.current.loc.start;
const kind: ast.FfiIntrinsicKind = switch (self.current.tag) {
.hash_objc_call => .objc_call,
.hash_jni_call => .jni_call,
.hash_jni_static_call => .jni_static_call,
else => unreachable,
};
self.advance(); // skip the directive
try self.expect(.l_paren);
const ret_type = try self.parseTypeExpr();
try self.expect(.r_paren);
try self.expect(.l_paren);
var args = std.ArrayList(*Node).empty;
while (self.current.tag != .r_paren and self.current.tag != .eof) {
const arg = try self.parseExpr();
try args.append(self.allocator, arg);
if (self.current.tag == .comma) {
self.advance();
} else {
break;
}
}
try self.expect(.r_paren);
return try self.createNode(start, .{ .ffi_intrinsic_call = .{
.kind = kind,
.return_type = ret_type,
.args = try args.toOwnedSlice(self.allocator),
} });
}
fn parseIfExpr(self: *Parser) anyerror!*Node {
const start = self.current.loc.start;
self.advance(); // skip 'if'
// Optional binding: if val := expr { ... }
// Detect: identifier followed by :=
if (self.current.tag == .identifier and self.peekNext() == .colon_equal) {
const binding_name = self.tokenSlice(self.current);
self.advance(); // skip identifier
self.advance(); // skip :=
const source_expr = try self.parseExpr();
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 = source_expr,
.then_branch = then_branch,
.else_branch = else_branch,
.is_inline = false,
.binding_name = binding_name,
} });
}
// Parse condition above comparison level, leaving comparisons
// unconsumed for manual handling with match disambiguation.
var condition = try self.parseBinary(Prec.shift);
// 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(Prec.shift);
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(Prec.shift);
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, Prec.logical_or);
// 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'
// Optional binding: while val := expr { ... }
if (self.current.tag == .identifier and self.peekNext() == .colon_equal) {
const binding_name = self.tokenSlice(self.current);
self.advance(); // skip identifier
self.advance(); // skip :=
const source_expr = try self.parseExpr();
const body = try self.parseBlock();
return try self.createNode(start, .{ .while_expr = .{
.condition = source_expr,
.body = body,
.binding_name = binding_name,
} });
}
const condition = try self.parseExpr();
const body = try self.parseBlock();
return try self.createNode(start, .{ .while_expr = .{
.condition = condition,
.body = body,
} });
}
fn parsePushStmt(self: *Parser) anyerror!*Node {
const start = self.current.loc.start;
self.advance(); // skip 'push'
const context_expr = try self.parseExpr();
// push Context.{ ... } { body } — if parseExpr consumed the push body
// as a struct init block, steal it back as the push body.
// (if/while don't have this issue — they require bool/optional conditions)
const body = if (context_expr.data == .struct_literal and
context_expr.data.struct_literal.init_block != null)
body_blk: {
const ib = context_expr.data.struct_literal.init_block.?;
context_expr.data.struct_literal.init_block = null;
break :body_blk ib;
} else try self.parseBlock();
return try self.createNode(start, .{ .push_stmt = .{
.context_expr = context_expr,
.body = body,
} });
}
fn parseForExpr(self: *Parser) anyerror!*Node {
const start = self.current.loc.start;
self.advance(); // skip 'for'
const iterable = try self.parseExpr();
// Expect ': (' capture clause
try self.expect(.colon);
try self.expect(.l_paren);
// Capture variable name
if (self.current.tag != .identifier) return self.fail("expected capture variable name");
const capture_name = self.tokenSlice(self.current);
self.advance();
// Optional ', index_name'
var index_name: ?[]const u8 = null;
if (self.current.tag == .comma) {
self.advance();
if (self.current.tag != .identifier) return self.fail("expected index variable name");
index_name = self.tokenSlice(self.current);
self.advance();
}
try self.expect(.r_paren);
const body = try self.parseBlock();
return try self.createNode(start, .{ .for_expr = .{
.iterable = iterable,
.body = body,
.capture_name = capture_name,
.index_name = index_name,
} });
}
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) } });
}
/// Parse a named tuple literal: (name: expr, name: expr, ...)
/// Called after '(' has been consumed and we've verified identifier + colon pattern.
fn parseTupleLiteralNamed(self: *Parser, start: u32) anyerror!*Node {
var elements = std.ArrayList(ast.TupleElement).empty;
while (self.current.tag != .r_paren and self.current.tag != .eof) {
if (self.current.tag != .identifier) {
return self.fail("expected field name in named tuple");
}
const name = self.tokenSlice(self.current);
self.advance();
try self.expect(.colon);
const value = try self.parseExpr();
try elements.append(self.allocator, .{ .name = name, .value = value });
if (self.current.tag == .comma) {
self.advance();
if (self.current.tag == .r_paren) break;
} else break;
}
try self.expect(.r_paren);
return try self.createNode(start, .{ .tuple_literal = .{ .elements = try elements.toOwnedSlice(self.allocator) } });
}
/// Finish parsing a tuple after the first positional element and a comma.
/// Called with first element already parsed and current token is ','.
fn finishTupleAfterFirst(self: *Parser, start: u32, first: *Node) anyerror!*Node {
var elements = std.ArrayList(ast.TupleElement).empty;
try elements.append(self.allocator, .{ .name = null, .value = first });
while (self.current.tag == .comma) {
self.advance(); // skip ','
if (self.current.tag == .r_paren) break; // trailing comma: (42,)
// Spread element: `(a, ..xs, b)` — reuses `spread_expr`.
if (self.current.tag == .dot_dot) {
const spread_start = self.current.loc.start;
self.advance(); // skip '..'
const operand = try self.parseExpr();
const spread = try self.createNode(spread_start, .{ .spread_expr = .{ .operand = operand } });
try elements.append(self.allocator, .{ .name = null, .value = spread });
continue;
}
const value = try self.parseExpr();
try elements.append(self.allocator, .{ .name = null, .value = value });
}
try self.expect(.r_paren);
return try self.createNode(start, .{ .tuple_literal = .{ .elements = try elements.toOwnedSlice(self.allocator) } });
}
/// Save state, skip past matching parens, return the tag of the next token, then restore.
/// Returns null if no matching ')' found before EOF.
fn peekPastParens(self: *Parser) ?Tag {
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) return null;
self.advance(); // skip ')'
return self.current.tag;
}
/// Returns true when the current `(` opens a function-type literal `(T1, T2) -> R`
/// rather than a tuple/grouping/lambda. Only meaningful after `isLambda` has
/// returned false — at that point a trailing `->` after the matching `)` can
/// only be a function type, since any body (`=>` or `{`) would have made it
/// a lambda.
fn isFunctionTypeExprAtLParen(self: *Parser) bool {
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) return false;
self.advance(); // skip ')'
return self.current.tag == .arrow;
}
fn isLambda(self: *Parser) bool {
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;
}
// Check upfront if parens look like function params (for block-body disambiguation)
const has_param_parens = blk: {
self.advance(); // skip '('
if (self.current.tag == .r_paren) break :blk true; // empty parens
if (self.current.tag != .identifier) break :blk false;
self.advance();
break :blk self.current.tag == .colon or
self.current.tag == .comma or
self.current.tag == .r_paren;
};
// Restore to '(' and scan past parens inline (not via peekPastParens which restores state)
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) return false;
self.advance(); // skip ')' — now positioned on token after parens
const tag = self.current.tag;
// (params) => expr
if (tag == .fat_arrow) return true;
// (params) -> ReturnType => expr
// (params) -> ReturnType { stmts }
if (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;
if (self.current.tag == .l_brace) return true;
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 or
self.current.tag == .star or self.current.tag == .question)
{
self.advance();
} else break;
}
return false;
}
// (params) { stmts } — block-body lambda
// Only if contents look like function params (have `:` type annotations or is empty `()`)
if (tag == .l_brace) {
return has_param_parens;
}
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 OR (params) -> Type { stmts }
var return_type: ?*Node = null;
if (self.current.tag == .arrow) {
self.advance();
return_type = try self.parseTypeExpr();
}
// Optional calling convention: callconv(.c)
const call_conv = try self.parseOptionalCallConv();
// Two body forms:
// (params) => expr — expression lambda
// (params) { stmts } — block-body lambda
const body = if (self.current.tag == .l_brace)
try self.parseBlock()
else blk: {
try self.expect(.fat_arrow);
break :blk 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,
.call_conv = call_conv,
} });
}
// ---- Helpers ----
/// Returns true if the current token can be used as an identifier name.
/// Includes actual identifiers plus contextual keywords that are only
/// keywords in specific syntactic positions (e.g., `protocol`, `impl`).
fn isIdentLike(self: *const Parser) bool {
return switch (self.current.tag) {
.identifier, .kw_protocol, .kw_impl, .kw_ufcs, .kw_closure => true,
else => false,
};
}
fn isFunctionDef(self: *Parser) bool {
const tag = self.peekPastParens() orelse return false;
// Inside `struct #compiler { ... }`, a bodyless method declaration
// ends with `;` directly after the param list — recognise it as a
// function def (not a constant) so it goes through parseFnDecl.
if (self.struct_default_compiler and tag == .semicolon) return true;
// `(T1, T2) -> R` without a trailing body (`{`, `=>`, or a foreign/
// builtin marker) is a function-type literal, not a function def.
if (tag == .arrow) return self.hasFnBodyAfterArrow();
return tag == .l_brace or tag == .hash_builtin or tag == .hash_compiler or tag == .hash_foreign or tag == .fat_arrow or tag == .kw_callconv;
}
fn hasFnBodyAfterArrow(self: *Parser) bool {
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) return false;
self.advance(); // skip ')'
if (self.current.tag != .arrow) return false;
self.advance(); // skip '->'
while (self.current.tag != .eof) {
if (self.current.tag == .fat_arrow) return true;
if (self.current.tag == .l_brace) return true;
if (self.current.tag == .hash_builtin or self.current.tag == .hash_compiler or self.current.tag == .hash_foreign) return true;
if (self.current.tag == .kw_callconv) return true;
// Inside a `struct #compiler` block, a `(...) -> Ret;` ending
// with `;` after the return type is a `#compiler` method
// declaration (body implicit). Outside that context, the same
// shape is a function-type alias (no body) and falls through to
// const-decl parsing.
if (self.struct_default_compiler and self.current.tag == .semicolon) return true;
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 or
self.current.tag == .star or self.current.tag == .question or
self.current.tag == .colon or self.current.tag == .arrow)
{
self.advance();
} else break;
}
return false;
}
fn parseOptionalCallConv(self: *Parser) anyerror!ast.CallingConvention {
if (self.current.tag != .kw_callconv) return .default;
self.advance();
try self.expect(.l_paren);
try self.expect(.dot);
if (self.current.tag != .identifier)
return self.fail("expected calling convention name after '.'");
const cc_name = self.tokenSlice(self.current);
const cc: ast.CallingConvention = if (std.mem.eql(u8, cc_name, "c")) .c else return self.fail("unknown calling convention");
self.advance();
try self.expect(.r_paren);
return cc;
}
fn isAssignOp(self: *const Parser) bool {
return switch (self.current.tag) {
.equal, .plus_equal, .minus_equal, .star_equal, .slash_equal, .percent_equal,
.ampersand_equal, .pipe_equal, .caret_equal, .less_less_equal, .greater_greater_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,
.ampersand_equal => .and_assign,
.pipe_equal => .or_assign,
.caret_equal => .xor_assign,
.less_less_equal => .shl_assign,
.greater_greater_equal => .shr_assign,
else => unreachable,
};
}
fn parseMultiAssign(self: *Parser, first_target: *Node, start: u32) !*Node {
var targets = std.ArrayList(*Node).empty;
try targets.append(self.allocator, first_target);
// Consume remaining targets separated by commas
while (self.current.tag == .comma) {
self.advance();
const target = try self.parseExpr();
try targets.append(self.allocator, target);
}
// Destructuring declaration: a, b := expr;
if (self.current.tag == .colon_equal) {
self.advance();
// All targets must be plain identifiers
var names = std.ArrayList([]const u8).empty;
for (targets.items) |target| {
if (target.data != .identifier) {
return self.fail("destructuring targets must be identifiers");
}
try names.append(self.allocator, target.data.identifier.name);
}
const value = try self.parseExpr();
try self.expectSemicolonAfter(value);
return try self.createNode(start, .{ .destructure_decl = .{
.names = try names.toOwnedSlice(self.allocator),
.value = value,
} });
}
// Multi-target assignment: only plain '=' is allowed
if (self.current.tag != .equal) {
return self.fail("multi-target assignment requires '=' or ':='");
}
self.advance();
// Parse RHS values separated by commas
var values = std.ArrayList(*Node).empty;
const first_val = try self.parseExpr();
try values.append(self.allocator, first_val);
while (self.current.tag == .comma) {
self.advance();
const val = try self.parseExpr();
try values.append(self.allocator, val);
}
if (targets.items.len != values.items.len) {
return self.fail("multi-target assignment: target count does not match value count");
}
try self.expect(.semicolon);
return try self.createNode(start, .{ .multi_assign = .{
.targets = try targets.toOwnedSlice(self.allocator),
.values = try values.toOwnedSlice(self.allocator),
} });
}
const Prec = struct {
const none: u8 = 0;
const pipe: u8 = 1; // |>
const null_coalesce: u8 = 2; // ??
const logical_or: u8 = 3; // or
const logical_and: u8 = 4; // and
const bit_or: u8 = 5; // |
const bit_xor: u8 = 6; // ^
const bit_and: u8 = 7; // &
const comparison: u8 = 8; // == != < <= > >= in
const shift: u8 = 9; // << >>
const additive: u8 = 10; // + -
const multiplicative: u8 = 11; // * / %
};
fn binaryPrec(self: *const Parser) u8 {
return switch (self.current.tag) {
.kw_or => Prec.logical_or,
.kw_and => Prec.logical_and,
.pipe => Prec.bit_or,
.caret => Prec.bit_xor,
.ampersand => Prec.bit_and,
.equal_equal, .bang_equal, .less, .less_equal, .greater, .greater_equal, .kw_in => Prec.comparison,
.less_less, .greater_greater => Prec.shift,
.plus, .minus => Prec.additive,
.star, .slash, .percent => Prec.multiplicative,
else => Prec.none,
};
}
fn binaryOp(self: *const Parser) ?ast.BinaryOp.Op {
return switch (self.current.tag) {
.kw_and => .and_op,
.kw_or => .or_op,
.pipe => .bit_or,
.caret => .bit_xor,
.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,
.less_less => .shl,
.greater_greater => .shr,
.kw_in => .in_op,
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,
};
}
/// Peek at the next token's tag without consuming.
fn peekNext(self: *Parser) Tag {
const saved_lexer = self.lexer;
const tok = self.lexer.next();
self.lexer = saved_lexer;
return tok.tag;
}
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 = "rl :: #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);
try std.testing.expectEqualStrings("rl", decl.data.library_decl.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 rl;";
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.expectEqualStrings("rl", decl.data.fn_decl.body.data.foreign_expr.library_ref.?);
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.?);
}
test "parse pack-constrained variadic parameter (..xs: Protocol)" {
const source = "map :: (..sources: ValueListenable) => sources;";
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 params = root.data.root.decls[0].data.fn_decl.params;
try std.testing.expectEqual(@as(usize, 1), params.len);
const p = params[0];
try std.testing.expect(p.is_variadic);
try std.testing.expect(p.is_pack); // protocol-constrained pack
try std.testing.expect(!p.is_comptime);
try std.testing.expectEqualStrings("sources", p.name);
// The constraint is a bare type reference, not a slice.
try std.testing.expect(p.type_expr.data == .type_expr);
try std.testing.expectEqualStrings("ValueListenable", p.type_expr.data.type_expr.name);
}
test "parse slice variadic is NOT a pack (..xs: []T)" {
const source = "join :: (..parts: []string) => parts;";
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 p = root.data.root.decls[0].data.fn_decl.params[0];
try std.testing.expect(p.is_variadic);
try std.testing.expect(!p.is_pack); // slice variadic, not a pack
try std.testing.expect(p.type_expr.data == .slice_type_expr);
}
test "parse comptime type-pack is NOT a protocol pack (..$args)" {
const source = "foo :: (..$args) => args;";
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 p = root.data.root.decls[0].data.fn_decl.params[0];
try std.testing.expect(p.is_variadic);
try std.testing.expect(p.is_comptime); // comptime type pack
try std.testing.expect(!p.is_pack); // not the protocol-constrained form
}
// ── Step 1.2 — pack expansion in the four positions ───────────────────
// All spread forms reuse `spread_expr` (its operand carries any projection /
// type-application); closure-sig packs use ClosureTypeExpr.pack_name +
// pack_projection. Arrow bodies wrap the expression in a block.
test "parse pack expansion: tuple value (..xs)" {
const source = "f :: () => (..xs);";
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;
const tup = body.data.block.stmts[0];
try std.testing.expect(tup.data == .tuple_literal);
try std.testing.expectEqual(@as(usize, 1), tup.data.tuple_literal.elements.len);
const el = tup.data.tuple_literal.elements[0].value;
try std.testing.expect(el.data == .spread_expr);
try std.testing.expect(el.data.spread_expr.operand.data == .identifier);
try std.testing.expectEqualStrings("xs", el.data.spread_expr.operand.data.identifier.name);
}
test "parse pack expansion: tuple value projection (..xs.value)" {
const source = "f :: () => (..xs.value);";
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 tup = root.data.root.decls[0].data.fn_decl.body.data.block.stmts[0];
const el = tup.data.tuple_literal.elements[0].value;
try std.testing.expect(el.data == .spread_expr);
const op = el.data.spread_expr.operand;
try std.testing.expect(op.data == .field_access);
try std.testing.expectEqualStrings("value", op.data.field_access.field);
try std.testing.expect(op.data.field_access.object.data == .identifier);
try std.testing.expectEqualStrings("xs", op.data.field_access.object.data.identifier.name);
}
test "parse pack expansion: tuple type (..F(Ts))" {
const source = "g :: (x: (..F(Ts))) => 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 ty = root.data.root.decls[0].data.fn_decl.params[0].type_expr;
try std.testing.expect(ty.data == .tuple_type_expr);
try std.testing.expectEqual(@as(usize, 1), ty.data.tuple_type_expr.field_types.len);
const field = ty.data.tuple_type_expr.field_types[0];
try std.testing.expect(field.data == .spread_expr);
const op = field.data.spread_expr.operand;
try std.testing.expect(op.data == .parameterized_type_expr);
try std.testing.expectEqualStrings("F", op.data.parameterized_type_expr.name);
}
test "parse pack expansion: closure sig projection Closure(..sources.T)" {
const source = "h :: (cb: Closure(..sources.T) -> s32) => cb;";
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 ty = root.data.root.decls[0].data.fn_decl.params[0].type_expr;
try std.testing.expect(ty.data == .closure_type_expr);
try std.testing.expectEqualStrings("sources", ty.data.closure_type_expr.pack_name.?);
try std.testing.expectEqualStrings("T", ty.data.closure_type_expr.pack_projection.?);
}
test "parse closure sig bare pack Closure(..Ts) has no projection" {
const source = "j :: (cb: Closure(..Ts) -> s32) => cb;";
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 ty = root.data.root.decls[0].data.fn_decl.params[0].type_expr;
try std.testing.expect(ty.data == .closure_type_expr);
try std.testing.expectEqualStrings("Ts", ty.data.closure_type_expr.pack_name.?);
try std.testing.expect(ty.data.closure_type_expr.pack_projection == null);
}
test "parse pack expansion: call-arg spread q(..xs) reuses spread_expr" {
const source = "k :: () => q(..xs);";
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 call = root.data.root.decls[0].data.fn_decl.body.data.block.stmts[0];
try std.testing.expect(call.data == .call);
try std.testing.expectEqual(@as(usize, 1), call.data.call.args.len);
try std.testing.expect(call.data.call.args[0].data == .spread_expr);
}