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[] — 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: $[] 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/SetField) // 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 ; 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 ; 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_index_type_expr` // (single Type value, step 3 shape) // `$` → `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); }