A match arm `case PAT: (expr)` — e.g. `case 0: (5)` — failed to parse:
parseMatchBody unconditionally consumed an `(` after `case PAT:` as a
payload-capture `(ident)`, so a non-identifier first token produced
"expected capture name".
Disambiguate: treat `(` as a capture only when it encloses exactly a lone
identifier — `( ident )` — via a new isLoneIdentParen() helper (peekTag-based
two-token lookahead). Otherwise the parens belong to the arm-body expression.
Payload capture (`case .b: (v) { ... }`, examples/128) still binds.
This fixes the scalar paren arm value (`case 0: (5)` now parses and runs).
The tuple arm-value form (`case .X: (a, b)`) additionally needs a tuple
literal in statement/binding position, tracked separately as issue 0059.
Tests: two inline parser unit tests (paren arm value is not a capture; lone
`(ident)` still binds). Gates: zig build, zig build test, 273/273 examples.
4431 lines
202 KiB
Zig
4431 lines
202 KiB
Zig
const std = @import("std");
|
|
const Token = @import("token.zig").Token;
|
|
const Tag = @import("token.zig").Tag;
|
|
const Lexer = @import("lexer.zig").Lexer;
|
|
const ast = @import("ast.zig");
|
|
const Node = ast.Node;
|
|
const Type = @import("types.zig").Type;
|
|
const errors = @import("errors.zig");
|
|
const print = @import("print.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,
|
|
/// When true, parsePostfix does not treat a trailing `(` as a call. Set
|
|
/// while parsing a `for` range bound so `for 0..N (i)` reads `N` as the
|
|
/// end and leaves `(i)` for the cursor rather than parsing `N(i)`.
|
|
suppress_call: bool = false,
|
|
/// When true (set while parsing an `onfail` body), a `raise` statement is
|
|
/// rejected — an error during cleanup has no propagation target. E1.7
|
|
/// extends this to the full {try, return, break, continue} set.
|
|
in_onfail_body: bool = false,
|
|
/// When true (set while parsing a `defer` body), a `raise` statement is
|
|
/// rejected — same reason as `onfail`: cleanup runs while the function is
|
|
/// already exiting, so there is nothing to propagate to. E1.7 extends this
|
|
/// to the full {try, return, break, continue} set.
|
|
in_defer_body: 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);
|
|
}
|
|
|
|
// Error-set declaration: name :: error { TagA, TagB }
|
|
if (self.current.tag == .kw_error) {
|
|
return self.parseErrorSetDecl(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;
|
|
|
|
// Error channel type: bare `!` (inferred set) or `!Named` (named set).
|
|
// Legal only as the trailing element of a multi-return result list
|
|
// (enforced by the parenthesized-list loop below) or as a bare
|
|
// failable return type. Sema (E1) restricts it to return positions.
|
|
if (self.current.tag == .bang) {
|
|
self.advance(); // skip '!'
|
|
var set_name: ?[]const u8 = null;
|
|
if (self.current.tag == .identifier) {
|
|
set_name = self.tokenSlice(self.current);
|
|
self.advance();
|
|
}
|
|
return try self.createNode(start, .{ .error_type_expr = .{ .name = set_name } });
|
|
}
|
|
|
|
// Optional type: ?T
|
|
if (self.current.tag == .question) {
|
|
self.advance(); // skip '?'
|
|
const inner_type = try self.parseTypeExpr();
|
|
return try self.createNode(start, .{ .optional_type_expr = .{ .inner_type = inner_type } });
|
|
}
|
|
|
|
// Pointer type: *T
|
|
if (self.current.tag == .star) {
|
|
self.advance(); // skip '*'
|
|
const pointee_type = try self.parseTypeExpr();
|
|
return try self.createNode(start, .{ .pointer_type_expr = .{ .pointee_type = pointee_type } });
|
|
}
|
|
|
|
// Array type: [N]T, Slice type: []T, Many-pointer type: [*]T, Sentinel slice: [:0]T
|
|
if (self.current.tag == .l_bracket) {
|
|
self.advance(); // skip '['
|
|
if (self.current.tag == .colon) {
|
|
// Sentinel-terminated slice: [:0]T
|
|
self.advance(); // skip ':'
|
|
if (self.current.tag != .int_literal) {
|
|
return self.fail("expected sentinel value after ':'");
|
|
}
|
|
const sentinel_str = self.tokenSlice(self.current);
|
|
self.advance(); // skip sentinel value
|
|
try self.expect(.r_bracket); // expect ']'
|
|
const elem_type = try self.parseTypeExpr();
|
|
// Build name like "[:0]u8" for type resolution
|
|
const elem_name = if (elem_type.data == .type_expr) elem_type.data.type_expr.name else "?";
|
|
const name = try std.fmt.allocPrint(self.allocator, "[:{s}]{s}", .{ sentinel_str, elem_name });
|
|
return try self.createNode(start, .{ .type_expr = .{ .name = name } });
|
|
}
|
|
if (self.current.tag == .r_bracket) {
|
|
// Slice type: []T
|
|
self.advance(); // skip ']'
|
|
const elem_type = try self.parseTypeExpr();
|
|
return try self.createNode(start, .{ .slice_type_expr = .{ .element_type = elem_type } });
|
|
}
|
|
if (self.current.tag == .star) {
|
|
// Many-pointer type: [*]T
|
|
self.advance(); // skip '*'
|
|
try self.expect(.r_bracket); // expect ']'
|
|
const elem_type = try self.parseTypeExpr();
|
|
return try self.createNode(start, .{ .many_pointer_type_expr = .{ .element_type = elem_type } });
|
|
}
|
|
const len_node = try self.parseExpr();
|
|
try self.expect(.r_bracket);
|
|
const elem_type = try self.parseTypeExpr();
|
|
return try self.createNode(start, .{ .array_type_expr = .{ .length = len_node, .element_type = elem_type } });
|
|
}
|
|
|
|
// Generic type parameter introduction: $T or $T/Protocol1/Protocol2.
|
|
// Also: pack-index type access $args[<int_literal>] — resolves to
|
|
// the i-th element type of the active pack binding (step 3 of
|
|
// the variadic heterogeneous type packs feature).
|
|
if (self.current.tag == .dollar) {
|
|
self.advance();
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail("expected type parameter name after '$'");
|
|
}
|
|
const name = self.tokenSlice(self.current);
|
|
self.advance();
|
|
// Pack-index access: $<pack_name>[<int_literal>]
|
|
if (self.current.tag == .l_bracket) {
|
|
self.advance(); // skip '['
|
|
if (self.current.tag != .int_literal) {
|
|
return self.fail("expected integer literal in pack index");
|
|
}
|
|
const idx_text = self.tokenSlice(self.current);
|
|
const idx_val = std.fmt.parseInt(i64, idx_text, 10) catch {
|
|
return self.fail("invalid integer literal in pack index");
|
|
};
|
|
if (idx_val < 0) return self.fail("pack index cannot be negative");
|
|
self.advance();
|
|
try self.expect(.r_bracket);
|
|
return try self.createNode(start, .{ .pack_index_type_expr = .{
|
|
.pack_name = name,
|
|
.index = @intCast(idx_val),
|
|
} });
|
|
}
|
|
// Parse optional protocol constraints: $T/Eq/Hashable
|
|
var constraints = std.ArrayList([]const u8).empty;
|
|
while (self.current.tag == .slash) {
|
|
self.advance(); // skip '/'
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail("expected protocol name after '/'");
|
|
}
|
|
try constraints.append(self.allocator, self.tokenSlice(self.current));
|
|
self.advance();
|
|
}
|
|
const pc = try constraints.toOwnedSlice(self.allocator);
|
|
return try self.createNode(start, .{ .type_expr = .{ .name = name, .is_generic = true, .protocol_constraints = pc } });
|
|
}
|
|
// Function type: (ParamTypes) -> ReturnType
|
|
// Tuple type: (T1, T2) or (T1) — no '->' after ')'
|
|
// Named params (documentation only): (name: Type, ...) -> ReturnType
|
|
if (self.current.tag == .l_paren) {
|
|
self.advance(); // skip '('
|
|
var param_types = std.ArrayList(*Node).empty;
|
|
var param_names = std.ArrayList(?[]const u8).empty;
|
|
var has_names = false;
|
|
// An error channel type (`!` / `!Named`) is only valid as the
|
|
// trailing element of a result list. Reject any element after it.
|
|
var saw_error_type = 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
|
|
}
|
|
if (saw_error_type) {
|
|
return self.fail("error type '!' must be the last element of a result list");
|
|
}
|
|
// 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);
|
|
}
|
|
const elem = try self.parseTypeExpr();
|
|
if (elem.data == .error_type_expr) saw_error_type = true;
|
|
try param_types.append(self.allocator, elem);
|
|
}
|
|
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). Keep field names
|
|
// for a named tuple `(x: T, y: U)` so `t.x` resolves. `field_names`
|
|
// is non-optional per slot, so synthesize `_<i>` for any unnamed one.
|
|
var field_names: ?[]const []const u8 = null;
|
|
if (has_names) {
|
|
var fns = std.ArrayList([]const u8).empty;
|
|
for (param_names.items, 0..) |pn, i| {
|
|
try fns.append(self.allocator, pn orelse try std.fmt.allocPrint(self.allocator, "_{d}", .{i}));
|
|
}
|
|
field_names = try fns.toOwnedSlice(self.allocator);
|
|
}
|
|
return try self.createNode(start, .{ .tuple_type_expr = .{
|
|
.field_types = try param_types.toOwnedSlice(self.allocator),
|
|
.field_names = field_names,
|
|
} });
|
|
}
|
|
|
|
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 if (self.current.tag == .dot_dot) {
|
|
// Pack-spread type arg: `Combined($R, ..sources.T)`.
|
|
const sp_start = self.current.loc.start;
|
|
self.advance(); // skip '..'
|
|
const operand = try self.parseTypeExpr();
|
|
try args.append(self.allocator, try self.createNode(sp_start, .{ .spread_expr = .{ .operand = operand } }));
|
|
} 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 parseErrorSetDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node {
|
|
self.advance(); // skip 'error'
|
|
try self.expect(.l_brace);
|
|
var tag_names = std.ArrayList([]const u8).empty;
|
|
while (self.current.tag != .r_brace and self.current.tag != .eof) {
|
|
if (tag_names.items.len > 0) {
|
|
try self.expect(.comma);
|
|
if (self.current.tag == .r_brace) break; // trailing comma ok
|
|
}
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail("expected error tag name");
|
|
}
|
|
try tag_names.append(self.allocator, self.tokenSlice(self.current));
|
|
self.advance();
|
|
}
|
|
try self.expect(.r_brace);
|
|
// Accept an optional trailing `;` — error-set decls read like value
|
|
// bindings and are commonly written `Foo :: error { ... };`.
|
|
if (self.current.tag == .semicolon) self.advance();
|
|
return try self.createNode(start_pos, .{ .error_set_decl = .{
|
|
.name = name,
|
|
.tag_names = try tag_names.toOwnedSlice(self.allocator),
|
|
} });
|
|
}
|
|
|
|
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;
|
|
}
|
|
// Optional leading `..` — a pack type-param `..$Ts: []Type`
|
|
// (must be the last param; binds the remaining type args).
|
|
var is_variadic = false;
|
|
if (self.current.tag == .dot_dot) {
|
|
is_variadic = true;
|
|
self.advance();
|
|
}
|
|
// 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, .is_variadic = is_variadic });
|
|
}
|
|
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;
|
|
}
|
|
|
|
/// With `self.current` at `(`, true iff the parens enclose exactly a single
|
|
/// identifier — `( ident )`. Distinguishes a match-arm payload capture from
|
|
/// a parenthesized / tuple arm-value expression (`(5)`, `(a, b)`).
|
|
fn isLoneIdentParen(self: *Parser) bool {
|
|
return self.peekTag(1) == .identifier and self.peekTag(2) == .r_paren;
|
|
}
|
|
|
|
fn foreignRuntimeForOffset(self: *Parser, offset: usize) ?ast.ForeignRuntime {
|
|
const tag = self.peekTag(offset);
|
|
return switch (tag) {
|
|
.hash_jni_class => .jni_class,
|
|
.hash_jni_interface => .jni_interface,
|
|
.hash_objc_class => .objc_class,
|
|
.hash_objc_protocol => .objc_protocol,
|
|
.hash_swift_class => .swift_class,
|
|
.hash_swift_struct => .swift_struct,
|
|
.hash_swift_protocol => .swift_protocol,
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
fn parseForeignClassDecl(self: *Parser, name: []const u8, start_pos: u32, runtime: ast.ForeignRuntime, is_foreign: bool, is_main: bool) anyerror!*Node {
|
|
self.advance(); // skip directive token
|
|
|
|
try self.expect(.l_paren);
|
|
if (self.current.tag != .string_literal) {
|
|
return self.fail("expected string literal foreign-type path after directive");
|
|
}
|
|
const raw = self.tokenSlice(self.current);
|
|
const foreign_path = raw[1 .. raw.len - 1];
|
|
self.advance();
|
|
try self.expect(.r_paren);
|
|
|
|
try self.expect(.l_brace);
|
|
|
|
var members = std.ArrayList(ast.ForeignClassMember).empty;
|
|
while (self.current.tag != .r_brace and self.current.tag != .eof) {
|
|
// #extends Alias; or #implements Alias;
|
|
if (self.current.tag == .hash_extends or self.current.tag == .hash_implements) {
|
|
const is_extends = self.current.tag == .hash_extends;
|
|
self.advance();
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail(if (is_extends) "expected superclass alias after '#extends'" else "expected interface alias after '#implements'");
|
|
}
|
|
const alias = self.tokenSlice(self.current);
|
|
self.advance();
|
|
try self.expect(.semicolon);
|
|
try members.append(self.allocator, if (is_extends)
|
|
.{ .extends = alias }
|
|
else
|
|
.{ .implements = alias });
|
|
continue;
|
|
}
|
|
|
|
// Field: name: Type; (instance field — JNI Get/Set<Type>Field)
|
|
// Method: name :: (args...) -> Ret;
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail("expected member name in '#jni_class' body");
|
|
}
|
|
const member_name = self.tokenSlice(self.current);
|
|
self.advance();
|
|
|
|
if (self.current.tag == .colon) {
|
|
self.advance(); // consume `:`
|
|
const field_type = try self.parseTypeExpr();
|
|
|
|
// M2.2 — optional `#property[(modifier, modifier, ...)]`
|
|
// directive after the field type. Synthesizes Obj-C
|
|
// getter/setter dispatch at access sites.
|
|
var is_property = false;
|
|
var property_modifiers = std.ArrayList([]const u8).empty;
|
|
if (self.current.tag == .hash_property) {
|
|
is_property = true;
|
|
self.advance();
|
|
if (self.current.tag == .l_paren) {
|
|
self.advance(); // consume `(`
|
|
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
|
if (property_modifiers.items.len > 0) {
|
|
try self.expect(.comma);
|
|
if (self.current.tag == .r_paren) break;
|
|
}
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail("expected property modifier name (strong, weak, copy, readonly, ...)");
|
|
}
|
|
const mod_name = self.tokenSlice(self.current);
|
|
self.advance();
|
|
// Optional argument: getter("name") / setter("name")
|
|
// — parsed but stored as part of the modifier string
|
|
// for now (M2.2 first pass; full attribute handling
|
|
// arrives with M4 ARC wiring).
|
|
if (self.current.tag == .l_paren) {
|
|
self.advance();
|
|
if (self.current.tag != .string_literal) {
|
|
return self.fail("expected string literal argument for property modifier");
|
|
}
|
|
self.advance();
|
|
try self.expect(.r_paren);
|
|
}
|
|
try property_modifiers.append(self.allocator, mod_name);
|
|
}
|
|
try self.expect(.r_paren);
|
|
}
|
|
}
|
|
|
|
try self.expect(.semicolon);
|
|
try members.append(self.allocator, .{ .field = .{
|
|
.name = member_name,
|
|
.field_type = field_type,
|
|
.is_property = is_property,
|
|
.property_modifiers = try property_modifiers.toOwnedSlice(self.allocator),
|
|
} });
|
|
continue;
|
|
}
|
|
|
|
try self.expect(.colon_colon);
|
|
|
|
// M2.1(a) — class-level constant `name :: Type = expr;` inside
|
|
// a `#objc_class` block. Reframed as a synthesized class method
|
|
// with an expression body (`name :: () -> Type => expr;`) so
|
|
// the rest of the M1.2 class-synthesis pipeline picks it up:
|
|
// a class-method IMP is emitted and registered on the metaclass.
|
|
// Apple's runtime calls the IMP from `[Cls foo]` — there's no
|
|
// runtime-level distinction between a class-level constant and
|
|
// a niladic class method, just a difference in source spelling.
|
|
if (self.current.tag != .l_paren) {
|
|
const ret_type = try self.parseTypeExpr();
|
|
try self.expect(.equal);
|
|
const expr_node = try self.parseExpr();
|
|
try self.expect(.semicolon);
|
|
const stmts = try self.allocator.alloc(*Node, 1);
|
|
stmts[0] = expr_node;
|
|
const block_node = try self.createNode(expr_node.span.start, .{ .block = .{ .stmts = stmts } });
|
|
try members.append(self.allocator, .{ .method = .{
|
|
.name = member_name,
|
|
.params = &.{},
|
|
.param_names = &.{},
|
|
.return_type = ret_type,
|
|
.is_static = true,
|
|
.jni_descriptor_override = null,
|
|
.selector_override = null,
|
|
.body = block_node,
|
|
} });
|
|
continue;
|
|
}
|
|
|
|
try self.expect(.l_paren);
|
|
|
|
var param_types = std.ArrayList(*Node).empty;
|
|
var param_names = std.ArrayList([]const u8).empty;
|
|
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
|
if (param_types.items.len > 0) {
|
|
try self.expect(.comma);
|
|
if (self.current.tag == .r_paren) break;
|
|
}
|
|
if (self.current.tag != .identifier and self.current.tag != .kw_Self) {
|
|
return self.fail("expected parameter name in '#jni_class' method");
|
|
}
|
|
const pname = self.tokenSlice(self.current);
|
|
self.advance();
|
|
try self.expect(.colon);
|
|
const ptype = try self.parseTypeExpr();
|
|
try param_names.append(self.allocator, pname);
|
|
try param_types.append(self.allocator, ptype);
|
|
}
|
|
try self.expect(.r_paren);
|
|
|
|
// Instance vs class method is determined by the first param's
|
|
// TYPE: `*Self` (pointer-to-Self) ⇒ instance method, anything
|
|
// else (including a method with no params at all) ⇒ class
|
|
// method. Keying on the type, not the param name, means the
|
|
// user can call the receiver whatever they like — `this`,
|
|
// `me`, etc. — without changing the dispatch shape.
|
|
const is_static = blk: {
|
|
if (param_types.items.len == 0) break :blk true;
|
|
const first = param_types.items[0];
|
|
if (first.data != .pointer_type_expr) break :blk true;
|
|
const pointee = first.data.pointer_type_expr.pointee_type;
|
|
if (pointee.data != .type_expr) break :blk true;
|
|
break :blk !std.mem.eql(u8, pointee.data.type_expr.name, "Self");
|
|
};
|
|
|
|
var return_type: ?*Node = null;
|
|
if (self.current.tag == .arrow) {
|
|
self.advance();
|
|
return_type = try self.parseTypeExpr();
|
|
}
|
|
|
|
// Optional `#jni_method_descriptor("(Sig)Ret")` — explicit JNI descriptor override.
|
|
var desc_override: ?[]const u8 = null;
|
|
if (self.current.tag == .hash_jni_method_descriptor) {
|
|
self.advance(); // skip `#jni_method_descriptor`
|
|
try self.expect(.l_paren);
|
|
if (self.current.tag != .string_literal) {
|
|
return self.fail("expected string literal JNI descriptor after '#jni_method_descriptor('");
|
|
}
|
|
const raw_desc = self.tokenSlice(self.current);
|
|
desc_override = raw_desc[1 .. raw_desc.len - 1];
|
|
self.advance();
|
|
try self.expect(.r_paren);
|
|
}
|
|
|
|
// Optional `#selector("explicit:string")` — explicit Obj-C selector override
|
|
// (Phase 3.2). Same slot as the JNI descriptor; they're not mutually
|
|
// exclusive at parse time though they belong to different runtimes.
|
|
var sel_override: ?[]const u8 = null;
|
|
if (self.current.tag == .hash_selector) {
|
|
self.advance(); // skip `#selector`
|
|
try self.expect(.l_paren);
|
|
if (self.current.tag != .string_literal) {
|
|
return self.fail("expected string literal selector after '#selector('");
|
|
}
|
|
const raw_sel = self.tokenSlice(self.current);
|
|
sel_override = raw_sel[1 .. raw_sel.len - 1];
|
|
self.advance();
|
|
try self.expect(.r_paren);
|
|
}
|
|
|
|
// Method body is optional: `;` → declaration (foreign or inherited
|
|
// method we just want to call); `{ ... }` → sx-side block body
|
|
// for sx-defined classes; `=> expr;` → expression-body form
|
|
// (M1.0), lowered as a single-statement block holding `expr`.
|
|
var body_node: ?*Node = null;
|
|
if (self.current.tag == .l_brace) {
|
|
body_node = try self.parseBlock();
|
|
} else if (self.current.tag == .fat_arrow) {
|
|
self.advance();
|
|
const expr = try self.parseExpr();
|
|
try self.expect(.semicolon);
|
|
const stmts = try self.allocator.alloc(*Node, 1);
|
|
stmts[0] = expr;
|
|
body_node = try self.createNode(expr.span.start, .{ .block = .{ .stmts = stmts } });
|
|
} else {
|
|
try self.expect(.semicolon);
|
|
}
|
|
|
|
try members.append(self.allocator, .{ .method = .{
|
|
.name = member_name,
|
|
.params = try param_types.toOwnedSlice(self.allocator),
|
|
.param_names = try param_names.toOwnedSlice(self.allocator),
|
|
.return_type = return_type,
|
|
.is_static = is_static,
|
|
.jni_descriptor_override = desc_override,
|
|
.selector_override = sel_override,
|
|
.body = body_node,
|
|
} });
|
|
}
|
|
try self.expect(.r_brace);
|
|
|
|
return try self.createNode(start_pos, .{ .foreign_class_decl = .{
|
|
.name = name,
|
|
.foreign_path = foreign_path,
|
|
.runtime = runtime,
|
|
.members = try members.toOwnedSlice(self.allocator),
|
|
.is_foreign = is_foreign,
|
|
.is_main = is_main,
|
|
} });
|
|
}
|
|
|
|
fn parseImplBlock(self: *Parser, start_pos: u32) anyerror!*Node {
|
|
self.advance(); // skip 'impl'
|
|
|
|
// Protocol name
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail("expected protocol name after 'impl'");
|
|
}
|
|
const protocol_name = self.tokenSlice(self.current);
|
|
self.advance();
|
|
|
|
// Optional protocol type args: impl Into(Block) for ...
|
|
var protocol_type_args = std.ArrayList(*Node).empty;
|
|
if (self.current.tag == .l_paren) {
|
|
self.advance(); // skip '('
|
|
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
|
if (protocol_type_args.items.len > 0) {
|
|
try self.expect(.comma);
|
|
if (self.current.tag == .r_paren) break;
|
|
}
|
|
try protocol_type_args.append(self.allocator, try self.parseTypeExpr());
|
|
}
|
|
try self.expect(.r_paren);
|
|
}
|
|
|
|
// 'for' — note: 'for' is a keyword (kw_for), not an identifier
|
|
if (self.current.tag != .kw_for) {
|
|
return self.fail("expected 'for' after protocol name in impl block");
|
|
}
|
|
self.advance();
|
|
|
|
// Source-type spelling. For parameterised protocols we accept any TypeExpr
|
|
// (`Closure(...) -> R`, `*T`, etc.). For nullary protocols we keep the
|
|
// legacy identifier-only path so existing `impl P for SomeStruct` keeps
|
|
// working unchanged (the parser doesn't try to over-parse trailing tokens).
|
|
var target_type: []const u8 = "";
|
|
var target_type_expr: ?*Node = null;
|
|
var target_type_params = std.ArrayList(ast.StructTypeParam).empty;
|
|
|
|
if (protocol_type_args.items.len > 0) {
|
|
// Parameterised protocol — source is a general TypeExpr.
|
|
target_type_expr = try self.parseTypeExpr();
|
|
// Synthesize a string view of the source for back-compat consumers
|
|
// (LSP hover, etc.). The semantic key for the impl map uses
|
|
// structural mangling, not this string.
|
|
if (target_type_expr.?.data == .type_expr) {
|
|
target_type = target_type_expr.?.data.type_expr.name;
|
|
}
|
|
} else {
|
|
// Legacy nullary-protocol path: single identifier source.
|
|
if (self.current.tag != .identifier and !self.current.tag.isTypeKeyword()) {
|
|
return self.fail("expected type name after 'for'");
|
|
}
|
|
target_type = self.tokenSlice(self.current);
|
|
self.advance();
|
|
|
|
// Optional type params: impl Protocol for List($T)
|
|
if (self.current.tag == .l_paren) {
|
|
self.advance(); // skip '('
|
|
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
|
if (target_type_params.items.len > 0) {
|
|
try self.expect(.comma);
|
|
if (self.current.tag == .r_paren) break;
|
|
}
|
|
try self.expect(.dollar);
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail("expected type parameter name after '$'");
|
|
}
|
|
const param_name = self.tokenSlice(self.current);
|
|
self.advance();
|
|
// Optional constraint — for now just use Type
|
|
const constraint = try self.createNode(self.current.loc.start, .{ .type_expr = .{ .name = "Type" } });
|
|
try target_type_params.append(self.allocator, .{ .name = param_name, .constraint = constraint });
|
|
}
|
|
try self.expect(.r_paren);
|
|
}
|
|
}
|
|
|
|
try self.expect(.l_brace);
|
|
|
|
// Set struct type params context so method params can reference T without $
|
|
var tp_names = std.ArrayList([]const u8).empty;
|
|
for (target_type_params.items) |tp| try tp_names.append(self.allocator, tp.name);
|
|
const saved_struct_type_params = self.struct_type_params;
|
|
self.struct_type_params = tp_names.items;
|
|
defer self.struct_type_params = saved_struct_type_params;
|
|
|
|
var methods = std.ArrayList(*Node).empty;
|
|
|
|
while (self.current.tag != .r_brace and self.current.tag != .eof) {
|
|
// Method: name :: (params) -> type { body }
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail("expected method name in impl block");
|
|
}
|
|
const method_start = self.current.loc.start;
|
|
const method_name = self.tokenSlice(self.current);
|
|
self.advance();
|
|
try self.expect(.colon_colon);
|
|
|
|
if (self.current.tag == .l_paren and self.isFunctionDef()) {
|
|
try methods.append(self.allocator, try self.parseFnDecl(method_name, method_start));
|
|
} else {
|
|
return self.fail("expected function declaration in impl block");
|
|
}
|
|
}
|
|
|
|
try self.expect(.r_brace);
|
|
|
|
return try self.createNode(start_pos, .{ .impl_block = .{
|
|
.protocol_name = protocol_name,
|
|
.target_type = target_type,
|
|
.target_type_params = try target_type_params.toOwnedSlice(self.allocator),
|
|
.methods = try methods.toOwnedSlice(self.allocator),
|
|
.protocol_type_args = try protocol_type_args.toOwnedSlice(self.allocator),
|
|
.target_type_expr = target_type_expr,
|
|
} });
|
|
}
|
|
|
|
fn parseStructLiteral(self: *Parser, struct_name: ?[]const u8, type_expr: ?*Node, start_pos: u32) anyerror!*Node {
|
|
try self.expect(.l_brace);
|
|
|
|
var field_inits = std.ArrayList(ast.StructFieldInit).empty;
|
|
|
|
while (self.current.tag != .r_brace and self.current.tag != .eof) {
|
|
if (field_inits.items.len > 0) {
|
|
try self.expect(.comma);
|
|
if (self.current.tag == .r_brace) break;
|
|
}
|
|
|
|
// Check if this is a named field: identifier followed by '='
|
|
if (self.current.tag == .identifier) {
|
|
const saved_lexer = self.lexer;
|
|
const saved_current = self.current;
|
|
const saved_prev_end = self.prev_end;
|
|
const fname = self.tokenSlice(self.current);
|
|
const ident_start = self.current.loc.start;
|
|
self.advance();
|
|
|
|
if (self.current.tag == .equal) {
|
|
// Named field: name = expr
|
|
self.advance(); // skip '='
|
|
const value = try self.parseExpr();
|
|
try field_inits.append(self.allocator, .{ .name = fname, .value = value });
|
|
continue;
|
|
} else if (self.current.tag == .comma or self.current.tag == .r_brace) {
|
|
// Shorthand: just an identifier (name = identifier with same name)
|
|
const ident_node = try self.createNode(ident_start, .{ .identifier = .{ .name = fname } });
|
|
try field_inits.append(self.allocator, .{ .name = fname, .value = ident_node });
|
|
continue;
|
|
}
|
|
|
|
// Not named — backtrack and parse as positional expression
|
|
self.lexer = saved_lexer;
|
|
self.current = saved_current;
|
|
self.prev_end = saved_prev_end;
|
|
}
|
|
|
|
// Positional field: just an expression
|
|
const value = try self.parseExpr();
|
|
try field_inits.append(self.allocator, .{ .name = null, .value = value });
|
|
}
|
|
try self.expect(.r_brace);
|
|
|
|
// Optional init block: T.{ fields } { stmts }
|
|
const init_block: ?*Node = if (self.current.tag == .l_brace)
|
|
try self.parseBlock()
|
|
else
|
|
null;
|
|
|
|
return try self.createNode(start_pos, .{ .struct_literal = .{
|
|
.struct_name = struct_name,
|
|
.type_expr = type_expr,
|
|
.field_inits = try field_inits.toOwnedSlice(self.allocator),
|
|
.init_block = init_block,
|
|
} });
|
|
}
|
|
|
|
fn reconstructQualifiedName(self: *Parser, node: *Node) ![]const u8 {
|
|
if (node.data == .identifier) return node.data.identifier.name;
|
|
if (node.data == .field_access) {
|
|
const obj_name = try self.reconstructQualifiedName(node.data.field_access.object);
|
|
return std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ obj_name, node.data.field_access.field });
|
|
}
|
|
return error.ParseError;
|
|
}
|
|
|
|
/// Parse a parenthesized parameter list: `(name: type, $T: Type, args: ..Any)`
|
|
/// Handles `$` generic params, `..` variadic marker, and comptime detection.
|
|
/// Expects opening `(` already NOT consumed — this function consumes `(` through `)`.
|
|
fn parseParams(self: *Parser) anyerror![]const ast.Param {
|
|
try self.expect(.l_paren);
|
|
var params = std.ArrayList(ast.Param).empty;
|
|
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
|
if (params.items.len > 0) {
|
|
try self.expect(.comma);
|
|
if (self.current.tag == .r_paren) break;
|
|
}
|
|
// Leading `..` marks a variadic param at the binding site
|
|
// (e.g., `..$args` heterogeneous pack, or future homogeneous
|
|
// `..args: []$T`). The old `args: ..T` form keeps its marker
|
|
// after the colon (handled below).
|
|
var is_variadic = false;
|
|
if (self.current.tag == .dot_dot) {
|
|
is_variadic = true;
|
|
self.advance();
|
|
}
|
|
var is_ct_param = false;
|
|
if (self.current.tag == .dollar) {
|
|
is_ct_param = true;
|
|
self.advance();
|
|
}
|
|
if (!self.isIdentLike()) {
|
|
return self.fail("expected parameter name");
|
|
}
|
|
const param_name = self.tokenSlice(self.current);
|
|
const param_name_span = ast.Span{ .start = self.current.loc.start, .end = self.current.loc.end };
|
|
self.advance();
|
|
// Optional type annotation: if no ':', infer type from context
|
|
if (self.current.tag != .colon) {
|
|
const inferred_node = try self.createNode(param_name_span.start, .{ .inferred_type = {} });
|
|
try params.append(self.allocator, .{ .name = param_name, .name_span = param_name_span, .type_expr = inferred_node, .is_variadic = is_variadic, .is_comptime = is_ct_param });
|
|
continue;
|
|
}
|
|
self.advance(); // consume ':'
|
|
const param_type = try self.parseTypeExpr();
|
|
var is_comptime_param = false;
|
|
if (is_ct_param and param_type.data == .type_expr) {
|
|
const constraint_name = param_type.data.type_expr.name;
|
|
if (std.mem.eql(u8, constraint_name, "Type")) {
|
|
// Parse optional protocol constraints: $T: Type/Eq/Hashable
|
|
var constraints = std.ArrayList([]const u8).empty;
|
|
while (self.current.tag == .slash) {
|
|
self.advance(); // skip '/'
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail("expected protocol name after '/'");
|
|
}
|
|
try constraints.append(self.allocator, self.tokenSlice(self.current));
|
|
self.advance();
|
|
}
|
|
const pc = try constraints.toOwnedSlice(self.allocator);
|
|
param_type.data = .{ .type_expr = .{ .name = param_name, .is_generic = true, .protocol_constraints = pc } };
|
|
} else {
|
|
is_comptime_param = true;
|
|
}
|
|
}
|
|
// Optional default value: `param: T = expr`. Stored on the Param
|
|
// node; lowering fills it in for callers that omit this positional arg.
|
|
var default_expr: ?*Node = null;
|
|
if (self.current.tag == .equal) {
|
|
self.advance(); // consume '='
|
|
default_expr = try self.parseExpr();
|
|
}
|
|
// Protocol-constrained variadic pack: `..xs: Protocol` — a bare
|
|
// type (not a slice/array) on a non-comptime variadic param. The
|
|
// trailing args each conform to the protocol with their own
|
|
// type-arg. Slice variadics (`..xs: []T`) keep `is_pack == false`.
|
|
const is_pack = is_variadic and !is_comptime_param and switch (param_type.data) {
|
|
.type_expr, .parameterized_type_expr => true,
|
|
else => false,
|
|
};
|
|
try params.append(self.allocator, .{ .name = param_name, .name_span = param_name_span, .type_expr = param_type, .is_variadic = is_variadic, .is_comptime = is_comptime_param, .is_pack = is_pack, .default_expr = default_expr });
|
|
}
|
|
for (params.items, 0..) |param, i| {
|
|
if (param.is_variadic and i != params.items.len - 1) {
|
|
return self.fail("variadic parameter must be the last parameter");
|
|
}
|
|
}
|
|
try self.expect(.r_paren);
|
|
return try params.toOwnedSlice(self.allocator);
|
|
}
|
|
|
|
/// Recursively find all generic type names ($T) in a type expression tree.
|
|
fn collectGenericNames(node: *Node, list: *std.ArrayList([]const u8), allocator: std.mem.Allocator) void {
|
|
switch (node.data) {
|
|
.type_expr => |te| {
|
|
if (te.is_generic) list.append(allocator, te.name) catch {};
|
|
},
|
|
.pointer_type_expr => |pte| collectGenericNames(pte.pointee_type, list, allocator),
|
|
.many_pointer_type_expr => |mpte| collectGenericNames(mpte.element_type, list, allocator),
|
|
.slice_type_expr => |ste| collectGenericNames(ste.element_type, list, allocator),
|
|
.array_type_expr => |ate| collectGenericNames(ate.element_type, list, allocator),
|
|
.parameterized_type_expr => |pte| {
|
|
for (pte.args) |arg| collectGenericNames(arg, list, allocator);
|
|
},
|
|
.closure_type_expr => |cte| {
|
|
for (cte.param_types) |pt| collectGenericNames(pt, list, allocator);
|
|
if (cte.return_type) |rt| collectGenericNames(rt, list, allocator);
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
/// Collect generic type params and comptime value params from parameter annotations.
|
|
fn collectTypeParams(self: *Parser, params: []const ast.Param) ![]const ast.StructTypeParam {
|
|
var type_params = std.ArrayList(ast.StructTypeParam).empty;
|
|
var seen = std.StringHashMap(void).init(self.allocator);
|
|
for (params) |param| {
|
|
if (param.is_comptime) {
|
|
if (!seen.contains(param.name)) {
|
|
try seen.put(param.name, {});
|
|
try type_params.append(self.allocator, .{ .name = param.name, .constraint = param.type_expr });
|
|
}
|
|
} else {
|
|
// Collect all generic type params found anywhere in the type expression
|
|
var generic_names = std.ArrayList([]const u8).empty;
|
|
collectGenericNames(param.type_expr, &generic_names, self.allocator);
|
|
for (generic_names.items) |gen_name| {
|
|
if (!seen.contains(gen_name)) {
|
|
try seen.put(gen_name, {});
|
|
// Propagate protocol constraints from the TypeExpr if present
|
|
const pc = if (param.type_expr.data == .type_expr)
|
|
param.type_expr.data.type_expr.protocol_constraints
|
|
else
|
|
&[_][]const u8{};
|
|
const type_constraint = self.createNode(param.type_expr.span.start, .{ .type_expr = .{ .name = "Type" } }) catch continue;
|
|
type_params.append(self.allocator, .{ .name = gen_name, .constraint = type_constraint, .protocol_constraints = pc }) catch {};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return try type_params.toOwnedSlice(self.allocator);
|
|
}
|
|
|
|
fn parseFnDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node {
|
|
const params = try self.parseParams();
|
|
|
|
// Optional return type
|
|
var return_type: ?*Node = null;
|
|
if (self.current.tag == .arrow) {
|
|
self.advance();
|
|
return_type = try self.parseTypeExpr();
|
|
}
|
|
|
|
// Optional calling convention: callconv(.c)
|
|
const call_conv = try self.parseOptionalCallConv();
|
|
|
|
// Body: block `{ ... }`, arrow `=> expr;`, #builtin, #compiler, or #foreign marker
|
|
var is_arrow = false;
|
|
const body = if (self.current.tag == .hash_builtin) blk: {
|
|
const bi_start = self.current.loc.start;
|
|
self.advance();
|
|
try self.expect(.semicolon);
|
|
break :blk try self.createNode(bi_start, .{ .builtin_expr = {} });
|
|
} else if (self.current.tag == .hash_compiler) blk: {
|
|
const ci_start = self.current.loc.start;
|
|
self.advance();
|
|
try self.expect(.semicolon);
|
|
break :blk try self.createNode(ci_start, .{ .compiler_expr = {} });
|
|
} else if (self.struct_default_compiler and self.current.tag == .semicolon) blk: {
|
|
// Inside `struct #compiler { ... }`: a bodyless method is
|
|
// implicitly a `#compiler` hook.
|
|
const ci_start = self.current.loc.start;
|
|
self.advance();
|
|
break :blk try self.createNode(ci_start, .{ .compiler_expr = {} });
|
|
} else if (self.current.tag == .hash_foreign) blk: {
|
|
const fi_start = self.current.loc.start;
|
|
self.advance();
|
|
// Required: library reference (identifier)
|
|
// Optional: library reference (identifier).
|
|
var lib_ref: ?[]const u8 = null;
|
|
if (self.current.tag == .identifier) {
|
|
lib_ref = self.tokenSlice(self.current);
|
|
self.advance();
|
|
}
|
|
// Optional: C symbol name (string literal)
|
|
var c_name: ?[]const u8 = null;
|
|
if (self.current.tag == .string_literal) {
|
|
const raw = self.tokenSlice(self.current);
|
|
c_name = raw[1 .. raw.len - 1];
|
|
self.advance();
|
|
}
|
|
try self.expect(.semicolon);
|
|
break :blk try self.createNode(fi_start, .{ .foreign_expr = .{
|
|
.library_ref = lib_ref,
|
|
.c_name = c_name,
|
|
} });
|
|
} else if (self.current.tag == .fat_arrow) blk: {
|
|
is_arrow = true;
|
|
self.advance();
|
|
const expr = try self.parseExpr();
|
|
try self.expect(.semicolon);
|
|
const stmts = try self.allocator.alloc(*Node, 1);
|
|
stmts[0] = expr;
|
|
const block_start = expr.span.start;
|
|
const block = try self.createNode(block_start, .{ .block = .{ .stmts = stmts } });
|
|
break :blk block;
|
|
} else try self.parseBlock();
|
|
|
|
const type_params = try self.collectTypeParams(params);
|
|
|
|
return try self.createNode(start_pos, .{ .fn_decl = .{
|
|
.name = name,
|
|
.params = params,
|
|
.return_type = return_type,
|
|
.body = body,
|
|
.type_params = type_params,
|
|
.is_arrow = is_arrow,
|
|
.call_conv = call_conv,
|
|
} });
|
|
}
|
|
|
|
fn parseBlock(self: *Parser) anyerror!*Node {
|
|
const start = self.current.loc.start;
|
|
try self.expect(.l_brace);
|
|
var stmts = std.ArrayList(*Node).empty;
|
|
while (self.current.tag != .r_brace and self.current.tag != .eof) {
|
|
const stmt = try self.parseStmt();
|
|
try stmts.append(self.allocator, stmt);
|
|
}
|
|
try self.expect(.r_brace);
|
|
return try self.createNode(start, .{ .block = .{ .stmts = try stmts.toOwnedSlice(self.allocator) } });
|
|
}
|
|
|
|
/// Block-form if/match/while/bare blocks don't require trailing semicolon.
|
|
fn expectSemicolonAfter(self: *Parser, expr: *Node) anyerror!void {
|
|
const needs_semi = switch (expr.data) {
|
|
.if_expr => |ie| ie.is_inline,
|
|
.match_expr => false,
|
|
.while_expr => false,
|
|
.for_expr => false,
|
|
.block => false,
|
|
.jni_env_block => false,
|
|
else => true,
|
|
};
|
|
if (needs_semi) {
|
|
try self.expect(.semicolon);
|
|
} else if (self.current.tag == .semicolon) {
|
|
self.advance(); // consume optional ;
|
|
}
|
|
}
|
|
|
|
pub fn parseStmt(self: *Parser) anyerror!*Node {
|
|
// Check if this is a declaration (IDENT followed by ::, :=, or : type)
|
|
if (self.isIdentLike()) {
|
|
const saved_lexer = self.lexer;
|
|
const saved_current = self.current;
|
|
const saved_prev_end = self.prev_end;
|
|
const start = self.current.loc.start;
|
|
const name = self.tokenSlice(self.current);
|
|
self.advance();
|
|
|
|
if (self.current.tag == .colon_colon) {
|
|
self.advance();
|
|
return self.parseConstBinding(name, start);
|
|
}
|
|
if (self.current.tag == .colon_equal) {
|
|
self.advance();
|
|
const value = try self.parseExpr();
|
|
try self.expectSemicolonAfter(value);
|
|
return try self.createNode(start, .{ .var_decl = .{ .name = name, .type_annotation = null, .value = value } });
|
|
}
|
|
if (self.current.tag == .colon) {
|
|
self.advance();
|
|
return self.parseTypedBinding(name, start);
|
|
}
|
|
|
|
// Multi-target assignment: ident, expr, ... = expr, expr, ...;
|
|
if (self.current.tag == .comma) {
|
|
const first_target = try self.createNode(start, .{ .identifier = .{ .name = name } });
|
|
return try self.parseMultiAssign(first_target, start);
|
|
}
|
|
|
|
// Check for assignment operators
|
|
if (self.isAssignOp()) {
|
|
const op = self.assignOp();
|
|
self.advance();
|
|
const value = try self.parseExpr();
|
|
try self.expect(.semicolon);
|
|
const target = try self.createNode(start, .{ .identifier = .{ .name = name } });
|
|
return try self.createNode(start, .{ .assignment = .{ .target = target, .op = op, .value = value } });
|
|
}
|
|
|
|
// Not a declaration or assignment — backtrack and parse as expression
|
|
self.lexer = saved_lexer;
|
|
self.current = saved_current;
|
|
self.prev_end = saved_prev_end;
|
|
}
|
|
|
|
// Return statement: return expr; or return;
|
|
if (self.current.tag == .kw_return) {
|
|
const start = self.current.loc.start;
|
|
self.advance();
|
|
if (self.current.tag == .semicolon) {
|
|
self.advance();
|
|
return try self.createNode(start, .{ .return_stmt = .{ .value = null } });
|
|
}
|
|
const value = try self.parseExpr();
|
|
try self.expect(.semicolon);
|
|
return try self.createNode(start, .{ .return_stmt = .{ .value = value } });
|
|
}
|
|
|
|
// Defer statement: defer <expr>;
|
|
if (self.current.tag == .kw_defer) {
|
|
const start = self.current.loc.start;
|
|
self.advance();
|
|
const saved_defer = self.in_defer_body;
|
|
self.in_defer_body = true;
|
|
defer self.in_defer_body = saved_defer;
|
|
const deferred = try self.parseExpr();
|
|
try self.expect(.semicolon);
|
|
return try self.createNode(start, .{ .defer_stmt = .{ .expr = deferred } });
|
|
}
|
|
|
|
// Raise statement: raise <expr>;
|
|
if (self.current.tag == .kw_raise) {
|
|
const start = self.current.loc.start;
|
|
if (self.in_onfail_body) {
|
|
return self.fail("`raise` is not allowed inside an `onfail` body — an error during cleanup has no propagation target");
|
|
}
|
|
if (self.in_defer_body) {
|
|
return self.fail("`raise` is not allowed inside a `defer` body — an error during cleanup has no propagation target");
|
|
}
|
|
self.advance();
|
|
const tag_expr = try self.parseExpr();
|
|
try self.expect(.semicolon);
|
|
return try self.createNode(start, .{ .raise_stmt = .{ .tag = tag_expr } });
|
|
}
|
|
|
|
// Onfail statement: onfail { body } | onfail e { body } | onfail <expr>;
|
|
// A binding is present only when an identifier is immediately followed
|
|
// by `{`; otherwise the text after `onfail` is the (no-binding) body.
|
|
if (self.current.tag == .kw_onfail) {
|
|
const start = self.current.loc.start;
|
|
self.advance();
|
|
var binding: ?[]const u8 = null;
|
|
if (self.current.tag == .identifier and self.peekNext() == .l_brace) {
|
|
binding = self.tokenSlice(self.current);
|
|
self.advance();
|
|
}
|
|
const saved_onfail = self.in_onfail_body;
|
|
self.in_onfail_body = true;
|
|
defer self.in_onfail_body = saved_onfail;
|
|
const body: *Node = if (self.current.tag == .l_brace)
|
|
try self.parseBlock()
|
|
else blk: {
|
|
const e = try self.parseExpr();
|
|
try self.expect(.semicolon);
|
|
break :blk e;
|
|
};
|
|
return try self.createNode(start, .{ .onfail_stmt = .{ .binding = binding, .body = body } });
|
|
}
|
|
|
|
// Break statement: break;
|
|
if (self.current.tag == .kw_break) {
|
|
const start = self.current.loc.start;
|
|
self.advance();
|
|
try self.expect(.semicolon);
|
|
return try self.createNode(start, .{ .break_expr = {} });
|
|
}
|
|
|
|
// Continue statement: continue;
|
|
if (self.current.tag == .kw_continue) {
|
|
const start = self.current.loc.start;
|
|
self.advance();
|
|
try self.expect(.semicolon);
|
|
return try self.createNode(start, .{ .continue_expr = {} });
|
|
}
|
|
|
|
// Insert directive: #insert <expr>;
|
|
if (self.current.tag == .hash_insert) {
|
|
const start = self.current.loc.start;
|
|
self.advance();
|
|
const inner = try self.parseExpr();
|
|
try self.expect(.semicolon);
|
|
return try self.createNode(start, .{ .insert_expr = .{ .expr = inner } });
|
|
}
|
|
|
|
// `#import "path";` / `#framework "Name";` inside a block body.
|
|
// Only meaningful inside an `inline if OS == ... { ... }` arm —
|
|
// the imports.zig flatten pass (issue-0042) surfaces those
|
|
// declarations to the top level before resolution. Anywhere else
|
|
// these nodes survive into lowering and produce a clear error.
|
|
if (self.current.tag == .hash_import) {
|
|
const start = self.current.loc.start;
|
|
self.advance();
|
|
if (self.current.tag != .string_literal) {
|
|
return self.fail("expected string path after '#import'");
|
|
}
|
|
const raw = self.tokenSlice(self.current);
|
|
const path = raw[1 .. raw.len - 1];
|
|
self.advance();
|
|
try self.expect(.semicolon);
|
|
return try self.createNode(start, .{ .import_decl = .{ .path = path, .name = null } });
|
|
}
|
|
if (self.current.tag == .hash_framework) {
|
|
const start = self.current.loc.start;
|
|
self.advance();
|
|
if (self.current.tag != .string_literal) {
|
|
return self.fail("expected string after '#framework'");
|
|
}
|
|
const raw = self.tokenSlice(self.current);
|
|
const fw_name = raw[1 .. raw.len - 1];
|
|
self.advance();
|
|
try self.expect(.semicolon);
|
|
return try self.createNode(start, .{ .framework_decl = .{ .name = fw_name } });
|
|
}
|
|
|
|
// inline if — compile-time conditional
|
|
if (self.current.tag == .kw_inline) {
|
|
if (self.peekNext() == .kw_if) {
|
|
self.advance(); // skip 'inline'
|
|
const expr = try self.parseIfExpr();
|
|
if (expr.data == .if_expr) {
|
|
expr.data.if_expr.is_comptime = true;
|
|
} else if (expr.data == .match_expr) {
|
|
expr.data.match_expr.is_comptime = true;
|
|
}
|
|
try self.expectSemicolonAfter(expr);
|
|
return expr;
|
|
}
|
|
if (self.peekNext() == .kw_for) {
|
|
self.advance(); // skip 'inline'
|
|
const expr = try self.parseForExpr();
|
|
expr.data.for_expr.is_inline = 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).
|
|
// Consumer-aware: the piped LHS goes into the RHS's HEAD call,
|
|
// looking THROUGH a `try` prefix / `catch` postfix / `or` fallback
|
|
// and leaving the wrapper intact:
|
|
// a |> try f(x) → try f(a, x)
|
|
// a |> f(x) catch e {...} → f(a, x) catch e {...}
|
|
// a |> f(x) or default → f(a, x) or default (only f gets a)
|
|
if (self.current.tag == .pipe_arrow and Prec.pipe >= min_prec) {
|
|
self.advance();
|
|
const rhs = try self.parseBinary(Prec.pipe + 1);
|
|
// Walk through error-handling wrappers to the head call node.
|
|
var head = rhs;
|
|
const head_call: ?*Node = while (true) {
|
|
switch (head.data) {
|
|
.call => break head,
|
|
.try_expr => head = head.data.try_expr.operand,
|
|
.catch_expr => head = head.data.catch_expr.operand,
|
|
.binary_op => |bo| {
|
|
if (bo.op == .or_op) head = bo.lhs else break null;
|
|
},
|
|
else => break null,
|
|
}
|
|
};
|
|
if (head_call) |cn| {
|
|
// a |> ...f(args)... → ...f(a, args)... (wrapper preserved;
|
|
// mutating the head call in place updates the wrapper).
|
|
var new_args = std.ArrayList(*Node).empty;
|
|
try new_args.append(self.allocator, lhs);
|
|
for (cn.data.call.args) |arg| try new_args.append(self.allocator, arg);
|
|
cn.data.call.args = try new_args.toOwnedSlice(self.allocator);
|
|
lhs = rhs;
|
|
} 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 } });
|
|
}
|
|
// `try X` — failable-attempt prefix. Joins the unary tier (binds
|
|
// tighter than any binary op incl. `or`); right-recursive so prefixes
|
|
// stack by adjacency (`xx try foo()` = `xx (try foo())`). Failability
|
|
// of the operand is a sema check (E1.4), not a parse-time restriction.
|
|
if (self.current.tag == .kw_try) {
|
|
const start = self.current.loc.start;
|
|
self.advance();
|
|
const operand = try self.parseUnary();
|
|
return try self.createNode(start, .{ .try_expr = .{ .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 and !self.suppress_call) {
|
|
// 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();
|
|
// Inside `[...]`, calls parse normally even within a range bound.
|
|
const saved_suppress_idx = self.suppress_call;
|
|
self.suppress_call = false;
|
|
defer self.suppress_call = saved_suppress_idx;
|
|
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 if (self.current.tag == .kw_catch) {
|
|
// `X catch [binding] BODY` — postfix failure handler.
|
|
// Four shapes, disambiguated by peeking after `catch`:
|
|
// catch { block } — no binding (braces required)
|
|
// catch e { block } — binding + block body
|
|
// catch e == { case ... } — binding + match body (sugar)
|
|
// catch e EXPR — binding + bare-expression body
|
|
self.advance(); // consume 'catch'
|
|
var binding: ?[]const u8 = null;
|
|
if (self.current.tag == .identifier) {
|
|
binding = self.tokenSlice(self.current);
|
|
self.advance();
|
|
}
|
|
var is_match_body = false;
|
|
const body: *Node = if (self.current.tag == .l_brace)
|
|
try self.parseBlock()
|
|
else if (binding != null and self.current.tag == .equal_equal) blk: {
|
|
const m_start = self.current.loc.start;
|
|
self.advance(); // consume '=='
|
|
is_match_body = true;
|
|
const subject = try self.createNode(m_start, .{ .identifier = .{ .name = binding.? } });
|
|
break :blk try self.parseMatchBody(subject, m_start);
|
|
} else if (binding != null)
|
|
try self.parseExpr()
|
|
else
|
|
return self.fail("`catch` without a binding requires a braced body: `catch { ... }`");
|
|
expr = try self.createNode(expr.span.start, .{ .catch_expr = .{
|
|
.operand = expr,
|
|
.binding = binding,
|
|
.body = body,
|
|
.is_match_body = is_match_body,
|
|
} });
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return expr;
|
|
}
|
|
|
|
fn parsePrimary(self: *Parser) anyerror!*Node {
|
|
const start = self.current.loc.start;
|
|
// Pack references in expression position:
|
|
// `$<pack_name>[<int_literal>]` → `pack_index_type_expr`
|
|
// (single Type value, step 3 shape)
|
|
// `$<pack_name>` → `comptime_pack_ref`
|
|
// (whole pack as []Type value, step 4 final slice)
|
|
// Lowering routes each through `pack_arg_types` to either
|
|
// a `const_type(TypeId)` or a `[]Type` aggregate of them.
|
|
if (self.current.tag == .dollar) {
|
|
self.advance();
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail("expected pack name after '$'");
|
|
}
|
|
const pname = self.tokenSlice(self.current);
|
|
self.advance();
|
|
if (self.current.tag == .l_bracket) {
|
|
self.advance(); // skip '['
|
|
if (self.current.tag != .int_literal) {
|
|
return self.fail("expected integer literal in pack index");
|
|
}
|
|
const idx_text = self.tokenSlice(self.current);
|
|
const idx_val = std.fmt.parseInt(i64, idx_text, 10) catch {
|
|
return self.fail("invalid integer literal in pack index");
|
|
};
|
|
if (idx_val < 0) return self.fail("pack index cannot be negative");
|
|
self.advance();
|
|
try self.expect(.r_bracket);
|
|
return try self.createNode(start, .{ .pack_index_type_expr = .{
|
|
.pack_name = pname,
|
|
.index = @intCast(idx_val),
|
|
} });
|
|
}
|
|
return try self.createNode(start, .{ .comptime_pack_ref = .{
|
|
.pack_name = pname,
|
|
} });
|
|
}
|
|
switch (self.current.tag) {
|
|
.int_literal => {
|
|
const text = self.tokenSlice(self.current);
|
|
const base: u8 = if (text.len > 2 and text[0] == '0' and (text[1] == 'x' or text[1] == 'X'))
|
|
16
|
|
else if (text.len > 2 and text[0] == '0' and (text[1] == 'b' or text[1] == 'B'))
|
|
2
|
|
else
|
|
10;
|
|
const digits = if (base != 10) text[2..] else text;
|
|
const value = std.fmt.parseInt(i64, digits, base) catch {
|
|
return self.fail("integer literal overflow");
|
|
};
|
|
self.advance();
|
|
return try self.createNode(start, .{ .int_literal = .{ .value = value } });
|
|
},
|
|
.float_literal => {
|
|
const text = self.tokenSlice(self.current);
|
|
const value = std.fmt.parseFloat(f64, text) catch {
|
|
return self.fail("float literal overflow");
|
|
};
|
|
self.advance();
|
|
return try self.createNode(start, .{ .float_literal = .{ .value = value } });
|
|
},
|
|
.string_literal => {
|
|
// raw includes quotes
|
|
const raw = self.tokenSlice(self.current);
|
|
self.advance();
|
|
return try self.createNode(start, .{ .string_literal = .{ .raw = raw[1 .. raw.len - 1] } });
|
|
},
|
|
.raw_string_literal => {
|
|
// #string heredoc — token span is content only, no stripping needed
|
|
const raw = self.tokenSlice(self.current);
|
|
self.advance();
|
|
return try self.createNode(start, .{ .string_literal = .{ .raw = raw, .is_raw = true } });
|
|
},
|
|
.kw_true => {
|
|
self.advance();
|
|
return try self.createNode(start, .{ .bool_literal = .{ .value = true } });
|
|
},
|
|
.kw_false => {
|
|
self.advance();
|
|
return try self.createNode(start, .{ .bool_literal = .{ .value = false } });
|
|
},
|
|
.kw_null => {
|
|
self.advance();
|
|
return try self.createNode(start, .{ .null_literal = {} });
|
|
},
|
|
.identifier => {
|
|
const name = self.tokenSlice(self.current);
|
|
// Check if this identifier is a type name (e.g. s32, u8, s128)
|
|
if (Type.fromName(name) != null) {
|
|
self.advance();
|
|
return try self.createNode(start, .{ .type_expr = .{ .name = name } });
|
|
}
|
|
self.advance();
|
|
return try self.createNode(start, .{ .identifier = .{ .name = name } });
|
|
},
|
|
.kw_closure, .kw_protocol, .kw_impl, .kw_ufcs => {
|
|
// Contextual keywords used as identifiers in expressions
|
|
const name = self.tokenSlice(self.current);
|
|
self.advance();
|
|
return try self.createNode(start, .{ .identifier = .{ .name = name } });
|
|
},
|
|
.dot => {
|
|
self.advance();
|
|
// Anonymous struct literal: .{ ... }
|
|
if (self.current.tag == .l_brace) {
|
|
return self.parseStructLiteral(null, null, start);
|
|
}
|
|
// Array literal: .[expr, expr, ...]
|
|
if (self.current.tag == .l_bracket) {
|
|
self.advance(); // skip '['
|
|
var elements = std.ArrayList(*Node).empty;
|
|
while (self.current.tag != .r_bracket and self.current.tag != .eof) {
|
|
if (elements.items.len > 0) {
|
|
try self.expect(.comma);
|
|
if (self.current.tag == .r_bracket) break;
|
|
}
|
|
const elem = try self.parseExpr();
|
|
try elements.append(self.allocator, elem);
|
|
}
|
|
try self.expect(.r_bracket);
|
|
return try self.createNode(start, .{ .array_literal = .{ .elements = try elements.toOwnedSlice(self.allocator) } });
|
|
}
|
|
// Enum literal: .variant_name
|
|
if (self.current.tag != .identifier) {
|
|
return self.fail("expected variant name, '{', or '[' after '.'");
|
|
}
|
|
const name = self.tokenSlice(self.current);
|
|
self.advance();
|
|
// Enum literal: .variant_name — parsePostfix handles optional (...) as a call
|
|
return try self.createNode(start, .{ .enum_literal = .{ .name = name } });
|
|
},
|
|
.l_paren => {
|
|
// Lambda: (params) => expr
|
|
if (self.isLambda()) {
|
|
return self.parseLambda();
|
|
}
|
|
// Function-type literal: (T1, T2) -> R (no body — isLambda would have caught a body)
|
|
if (self.isFunctionTypeExprAtLParen()) {
|
|
return try self.parseTypeExpr();
|
|
}
|
|
self.advance(); // skip '('
|
|
|
|
// A `(` here opens a grouping/tuple, not a `for` range bound, so
|
|
// calls inside it parse normally even within a range bound.
|
|
const saved_suppress_grp = self.suppress_call;
|
|
self.suppress_call = false;
|
|
defer self.suppress_call = saved_suppress_grp;
|
|
|
|
// 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();
|
|
},
|
|
// `error` in expression position is the head of a tag literal
|
|
// `error.X` (parsed as a field access); sema (E1) gives it meaning.
|
|
.kw_error => {
|
|
self.advance();
|
|
return try self.createNode(start, .{ .identifier = .{ .name = "error" } });
|
|
},
|
|
.kw_raise => return self.fail("`raise` is a statement and cannot appear in expression position"),
|
|
.kw_onfail => return self.fail("`onfail` is a statement and cannot appear in expression position"),
|
|
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();
|
|
|
|
// Range form: `for start..end (i)? { }`. The `..` only appears here for a
|
|
// range (slice ranges live inside `[]`), so it's unambiguous.
|
|
var range_end: ?*Node = null;
|
|
if (self.current.tag == .dot_dot) {
|
|
self.advance(); // skip '..'
|
|
const saved_suppress = self.suppress_call;
|
|
self.suppress_call = true;
|
|
range_end = try self.parseExpr();
|
|
self.suppress_call = saved_suppress;
|
|
}
|
|
|
|
var capture_name: []const u8 = "";
|
|
var index_name: ?[]const u8 = null;
|
|
var capture_by_ref = false;
|
|
|
|
if (range_end != null) {
|
|
// Optional cursor, introduced by `:` for symmetry with the
|
|
// collection form: `for 0..N: (i)` (or `for 0..N` with no cursor).
|
|
// The colon is required when a cursor is present.
|
|
if (self.current.tag == .colon) {
|
|
self.advance();
|
|
try self.expect(.l_paren);
|
|
if (self.current.tag != .identifier) return self.fail("expected cursor variable name");
|
|
capture_name = self.tokenSlice(self.current);
|
|
self.advance();
|
|
try self.expect(.r_paren);
|
|
}
|
|
} else {
|
|
// Collection form: `: (capture, index?)`. A leading `*` on the
|
|
// capture (`(*x)`) binds it by pointer into the collection.
|
|
try self.expect(.colon);
|
|
try self.expect(.l_paren);
|
|
if (self.current.tag == .star) {
|
|
capture_by_ref = true;
|
|
self.advance();
|
|
}
|
|
if (self.current.tag != .identifier) return self.fail("expected capture variable name");
|
|
capture_name = self.tokenSlice(self.current);
|
|
self.advance();
|
|
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,
|
|
.range_end = range_end,
|
|
.capture_by_ref = capture_by_ref,
|
|
} });
|
|
}
|
|
|
|
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)`. Disambiguated from a
|
|
// parenthesized / tuple arm-value expression (`(5)`, `(1, 1)`):
|
|
// a capture is exactly `( <identifier> )`; anything else is the
|
|
// arm body (an expression) and is left for the body parse below.
|
|
var capture: ?[]const u8 = null;
|
|
if (self.current.tag == .l_paren and self.isLoneIdentParen()) {
|
|
self.advance(); // '('
|
|
capture = self.tokenSlice(self.current);
|
|
self.advance(); // ident
|
|
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 == .bang 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.is_arrow);
|
|
// Arrow bodies are wrapped in a block; the expression is the sole stmt.
|
|
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 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];
|
|
// A named `::` arrow function is a fn_decl (carrying its own type params).
|
|
try std.testing.expect(decl.data == .fn_decl);
|
|
const fd = decl.data.fn_decl;
|
|
try std.testing.expectEqual(@as(usize, 1), fd.params.len);
|
|
try std.testing.expectEqualStrings("x", fd.params[0].name);
|
|
try std.testing.expectEqual(@as(usize, 1), fd.type_params.len);
|
|
try std.testing.expectEqualStrings("T", fd.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 == .fn_decl);
|
|
const fd = decl.data.fn_decl;
|
|
try std.testing.expect(fd.return_type != null);
|
|
try std.testing.expect(fd.return_type.?.data == .type_expr);
|
|
try std.testing.expectEqualStrings("s32", fd.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);
|
|
}
|
|
|
|
// ── ERR step E0.1 — `error { ... }` decls + `!` / `!Named` type exprs ──
|
|
|
|
test "parse error-set decl: tags collected" {
|
|
const source = "ParseErr :: error { BadDigit, Overflow, Empty }";
|
|
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 == .error_set_decl);
|
|
try std.testing.expectEqualStrings("ParseErr", decl.data.error_set_decl.name);
|
|
const tags = decl.data.error_set_decl.tag_names;
|
|
try std.testing.expectEqual(@as(usize, 3), tags.len);
|
|
try std.testing.expectEqualStrings("BadDigit", tags[0]);
|
|
try std.testing.expectEqualStrings("Overflow", tags[1]);
|
|
try std.testing.expectEqualStrings("Empty", tags[2]);
|
|
}
|
|
|
|
test "parse error-set decl: single tag, trailing comma, trailing semicolon" {
|
|
const source = "E :: error { Only, };";
|
|
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 == .error_set_decl);
|
|
const tags = decl.data.error_set_decl.tag_names;
|
|
try std.testing.expectEqual(@as(usize, 1), tags.len);
|
|
try std.testing.expectEqualStrings("Only", tags[0]);
|
|
}
|
|
|
|
test "parse bare failable return: inferred `!`" {
|
|
const source = "f :: () -> ! { 0; }";
|
|
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 rt = root.data.root.decls[0].data.fn_decl.return_type.?;
|
|
try std.testing.expect(rt.data == .error_type_expr);
|
|
try std.testing.expect(rt.data.error_type_expr.name == null);
|
|
}
|
|
|
|
test "parse bare failable return: named `!Foo`" {
|
|
const source = "f :: () -> !ParseErr { 0; }";
|
|
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 rt = root.data.root.decls[0].data.fn_decl.return_type.?;
|
|
try std.testing.expect(rt.data == .error_type_expr);
|
|
try std.testing.expectEqualStrings("ParseErr", rt.data.error_type_expr.name.?);
|
|
}
|
|
|
|
test "parse multi-return with inferred `!` as trailing element" {
|
|
const source = "f :: () -> (s32, !) { 0; }";
|
|
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 rt = root.data.root.decls[0].data.fn_decl.return_type.?;
|
|
try std.testing.expect(rt.data == .tuple_type_expr);
|
|
const fields = rt.data.tuple_type_expr.field_types;
|
|
try std.testing.expectEqual(@as(usize, 2), fields.len);
|
|
try std.testing.expect(fields[0].data == .type_expr);
|
|
try std.testing.expectEqualStrings("s32", fields[0].data.type_expr.name);
|
|
try std.testing.expect(fields[1].data == .error_type_expr);
|
|
try std.testing.expect(fields[1].data.error_type_expr.name == null);
|
|
}
|
|
|
|
test "parse multi-return with named `!Foo` as trailing element" {
|
|
const source = "f :: () -> (s32, s64, !ParseErr) { 0; }";
|
|
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 rt = root.data.root.decls[0].data.fn_decl.return_type.?;
|
|
try std.testing.expect(rt.data == .tuple_type_expr);
|
|
const fields = rt.data.tuple_type_expr.field_types;
|
|
try std.testing.expectEqual(@as(usize, 3), fields.len);
|
|
try std.testing.expect(fields[2].data == .error_type_expr);
|
|
try std.testing.expectEqualStrings("ParseErr", fields[2].data.error_type_expr.name.?);
|
|
}
|
|
|
|
test "parse error type rejected when not the trailing result element" {
|
|
const source = "f :: () -> (!, s32) { 0; }";
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), source);
|
|
try std.testing.expectError(error.ParseError, parser.parse());
|
|
}
|
|
|
|
test "parse error type rejected in the middle of a result list" {
|
|
const source = "f :: () -> (s32, !, s64) { 0; }";
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), source);
|
|
try std.testing.expectError(error.ParseError, parser.parse());
|
|
}
|
|
|
|
test "round-trip print: error-set decl" {
|
|
const source = "ParseErr :: error { BadDigit, Overflow, Empty }";
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), source);
|
|
const root = try parser.parse();
|
|
var aw = std.Io.Writer.Allocating.init(arena.allocator());
|
|
try print.printNode(root.data.root.decls[0], &aw.writer);
|
|
try std.testing.expectEqualStrings(source, aw.writer.toArrayList().items);
|
|
}
|
|
|
|
test "round-trip print: multi-return result list with pointer + named error" {
|
|
const source = "open :: () -> (*Handle, !IoErr) { 0; }";
|
|
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 rt = root.data.root.decls[0].data.fn_decl.return_type.?;
|
|
var aw = std.Io.Writer.Allocating.init(arena.allocator());
|
|
try print.printType(rt, &aw.writer);
|
|
try std.testing.expectEqualStrings("(*Handle, !IoErr)", aw.writer.toArrayList().items);
|
|
}
|
|
|
|
test "round-trip print: bare inferred and named error types" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
{
|
|
var parser = Parser.init(arena.allocator(), "f :: () -> ! { 0; }");
|
|
const root = try parser.parse();
|
|
const rt = root.data.root.decls[0].data.fn_decl.return_type.?;
|
|
var aw = std.Io.Writer.Allocating.init(arena.allocator());
|
|
try print.printType(rt, &aw.writer);
|
|
try std.testing.expectEqualStrings("!", aw.writer.toArrayList().items);
|
|
}
|
|
{
|
|
var parser = Parser.init(arena.allocator(), "f :: () -> !ParseErr { 0; }");
|
|
const root = try parser.parse();
|
|
const rt = root.data.root.decls[0].data.fn_decl.return_type.?;
|
|
var aw = std.Io.Writer.Allocating.init(arena.allocator());
|
|
try print.printType(rt, &aw.writer);
|
|
try std.testing.expectEqualStrings("!ParseErr", aw.writer.toArrayList().items);
|
|
}
|
|
}
|
|
|
|
// ── ERR step E0.2 — raise / try / catch / onfail + precedence + pipe ──
|
|
|
|
/// Parse `src` (a single `f :: () { ... }` decl) and return its body's first
|
|
/// statement node.
|
|
fn e02FirstStmt(alloc: std.mem.Allocator, src: [:0]const u8) anyerror!*Node {
|
|
var parser = Parser.init(alloc, src);
|
|
const root = try parser.parse();
|
|
return root.data.root.decls[0].data.fn_decl.body.data.block.stmts[0];
|
|
}
|
|
|
|
/// Parse `src` (a `f :: () { v := EXPR; }` decl) and return the EXPR node.
|
|
fn e02FirstValue(alloc: std.mem.Allocator, src: [:0]const u8) anyerror!*Node {
|
|
const stmt = try e02FirstStmt(alloc, src);
|
|
return stmt.data.var_decl.value.?;
|
|
}
|
|
|
|
fn e02ExpectPrints(alloc: std.mem.Allocator, node: *const Node, expected: []const u8) !void {
|
|
var aw = std.Io.Writer.Allocating.init(alloc);
|
|
try print.printNode(node, &aw.writer);
|
|
try std.testing.expectEqualStrings(expected, aw.writer.toArrayList().items);
|
|
}
|
|
|
|
test "E0.2 try binds tighter than or: try foo() or try boo()" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const v = try e02FirstValue(arena.allocator(), "f :: () { v := try foo() or try boo(); }");
|
|
try std.testing.expect(v.data == .binary_op);
|
|
try std.testing.expect(v.data.binary_op.op == .or_op);
|
|
try std.testing.expect(v.data.binary_op.lhs.data == .try_expr);
|
|
try std.testing.expect(v.data.binary_op.rhs.data == .try_expr);
|
|
}
|
|
|
|
test "E0.2 or is left-associative: a or b or c => (a or b) or c" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const v = try e02FirstValue(arena.allocator(), "f :: () { v := try a() or try b() or try c(); }");
|
|
try std.testing.expect(v.data == .binary_op and v.data.binary_op.op == .or_op);
|
|
// LHS is the nested (a or b); RHS is the final operand.
|
|
try std.testing.expect(v.data.binary_op.lhs.data == .binary_op);
|
|
try std.testing.expect(v.data.binary_op.lhs.data.binary_op.op == .or_op);
|
|
try std.testing.expect(v.data.binary_op.rhs.data == .try_expr);
|
|
}
|
|
|
|
test "E0.2 try prefix stacks under xx: xx try foo()" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const v = try e02FirstValue(arena.allocator(), "f :: () { v := xx try foo(); }");
|
|
try std.testing.expect(v.data == .unary_op and v.data.unary_op.op == .xx);
|
|
try std.testing.expect(v.data.unary_op.operand.data == .try_expr);
|
|
}
|
|
|
|
test "E0.2 catch no binding, braced body" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const v = try e02FirstValue(arena.allocator(), "f :: () { v := foo() catch { }; }");
|
|
try std.testing.expect(v.data == .catch_expr);
|
|
try std.testing.expect(v.data.catch_expr.binding == null);
|
|
try std.testing.expect(v.data.catch_expr.is_match_body == false);
|
|
try std.testing.expect(v.data.catch_expr.body.data == .block);
|
|
}
|
|
|
|
test "E0.2 catch with binding, block body" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const v = try e02FirstValue(arena.allocator(), "f :: () { v := foo() catch e { bar(); }; }");
|
|
try std.testing.expect(v.data == .catch_expr);
|
|
try std.testing.expectEqualStrings("e", v.data.catch_expr.binding.?);
|
|
try std.testing.expect(v.data.catch_expr.body.data == .block);
|
|
}
|
|
|
|
test "E0.2 catch with binding, bare-expression body" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const v = try e02FirstValue(arena.allocator(), "f :: () { v := foo() catch e bar(); }");
|
|
try std.testing.expect(v.data == .catch_expr);
|
|
try std.testing.expectEqualStrings("e", v.data.catch_expr.binding.?);
|
|
try std.testing.expect(v.data.catch_expr.is_match_body == false);
|
|
try std.testing.expect(v.data.catch_expr.body.data == .call);
|
|
}
|
|
|
|
test "E0.2 catch match-body desugars to match_expr over the binding" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const v = try e02FirstValue(arena.allocator(), "f :: () { v := foo() catch e == { case .Empty: 0; else: 1; }; }");
|
|
try std.testing.expect(v.data == .catch_expr);
|
|
try std.testing.expect(v.data.catch_expr.is_match_body);
|
|
try std.testing.expect(v.data.catch_expr.body.data == .match_expr);
|
|
try std.testing.expectEqual(@as(usize, 2), v.data.catch_expr.body.data.match_expr.arms.len);
|
|
// subject is the binding identifier
|
|
try std.testing.expect(v.data.catch_expr.body.data.match_expr.subject.data == .identifier);
|
|
try std.testing.expectEqualStrings("e", v.data.catch_expr.body.data.match_expr.subject.data.identifier.name);
|
|
}
|
|
|
|
test "E0.2 catch over a parenthesized or-chain" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const v = try e02FirstValue(arena.allocator(), "f :: () { v := (try foo() or try boo()) catch e { }; }");
|
|
try std.testing.expect(v.data == .catch_expr);
|
|
try std.testing.expect(v.data.catch_expr.operand.data == .binary_op);
|
|
try std.testing.expect(v.data.catch_expr.operand.data.binary_op.op == .or_op);
|
|
}
|
|
|
|
test "E0.2 catch without binding and unbraced body is rejected" {
|
|
// No binding (the token after `catch` is not an identifier) and no braces:
|
|
// the no-binding form requires a braced body.
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), "f :: () { v := foo() catch 42; }");
|
|
try std.testing.expectError(error.ParseError, parser.parse());
|
|
}
|
|
|
|
test "E0.2 raise error.X parses as raise_stmt over a field access" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const s = try e02FirstStmt(arena.allocator(), "f :: () { raise error.BadDigit; }");
|
|
try std.testing.expect(s.data == .raise_stmt);
|
|
try std.testing.expect(s.data.raise_stmt.tag.data == .field_access);
|
|
try std.testing.expectEqualStrings("BadDigit", s.data.raise_stmt.tag.data.field_access.field);
|
|
const obj = s.data.raise_stmt.tag.data.field_access.object;
|
|
try std.testing.expect(obj.data == .identifier);
|
|
try std.testing.expectEqualStrings("error", obj.data.identifier.name);
|
|
}
|
|
|
|
test "E0.2 raise variable form" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const s = try e02FirstStmt(arena.allocator(), "f :: () { raise e; }");
|
|
try std.testing.expect(s.data == .raise_stmt);
|
|
try std.testing.expect(s.data.raise_stmt.tag.data == .identifier);
|
|
}
|
|
|
|
test "E0.2 raise rejected in expression position" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), "f :: () { x := 1 + raise error.X; }");
|
|
try std.testing.expectError(error.ParseError, parser.parse());
|
|
}
|
|
|
|
test "E0.2 raise rejected inside an onfail body" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), "f :: () { onfail { raise error.X; } }");
|
|
try std.testing.expectError(error.ParseError, parser.parse());
|
|
}
|
|
|
|
test "E1.3 raise rejected inside a defer body" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var parser = Parser.init(arena.allocator(), "f :: () { defer { raise error.X; } }");
|
|
try std.testing.expectError(error.ParseError, parser.parse());
|
|
}
|
|
|
|
test "E0.2 onfail with binding and block body" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const s = try e02FirstStmt(arena.allocator(), "f :: () { onfail e { close(h); } }");
|
|
try std.testing.expect(s.data == .onfail_stmt);
|
|
try std.testing.expectEqualStrings("e", s.data.onfail_stmt.binding.?);
|
|
try std.testing.expect(s.data.onfail_stmt.body.data == .block);
|
|
}
|
|
|
|
test "E0.2 onfail no-binding block vs bare-expression body" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const block_body = try e02FirstStmt(arena.allocator(), "f :: () { onfail { close(h); } }");
|
|
try std.testing.expect(block_body.data == .onfail_stmt);
|
|
try std.testing.expect(block_body.data.onfail_stmt.binding == null);
|
|
try std.testing.expect(block_body.data.onfail_stmt.body.data == .block);
|
|
|
|
const expr_body = try e02FirstStmt(arena.allocator(), "f :: () { onfail close(h); }");
|
|
try std.testing.expect(expr_body.data == .onfail_stmt);
|
|
try std.testing.expect(expr_body.data.onfail_stmt.binding == null);
|
|
try std.testing.expect(expr_body.data.onfail_stmt.body.data == .call);
|
|
}
|
|
|
|
test "E0.2 consumer-aware pipe: x |> try f() inserts x into the head call" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const v = try e02FirstValue(arena.allocator(), "f :: () { v := x |> try f(); }");
|
|
try std.testing.expect(v.data == .try_expr);
|
|
const call = v.data.try_expr.operand;
|
|
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 == .identifier);
|
|
try std.testing.expectEqualStrings("x", call.data.call.args[0].data.identifier.name);
|
|
}
|
|
|
|
test "E0.2 consumer-aware pipe: x |> f() catch e { } preserves the catch" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const v = try e02FirstValue(arena.allocator(), "f :: () { v := x |> g() catch e { }; }");
|
|
try std.testing.expect(v.data == .catch_expr);
|
|
try std.testing.expectEqualStrings("e", v.data.catch_expr.binding.?);
|
|
try std.testing.expect(v.data.catch_expr.operand.data == .call);
|
|
try std.testing.expectEqual(@as(usize, 1), v.data.catch_expr.operand.data.call.args.len);
|
|
}
|
|
|
|
test "E0.2 consumer-aware pipe: x |> f() or d feeds only the head call" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const v = try e02FirstValue(arena.allocator(), "f :: () { v := x |> g() or d; }");
|
|
try std.testing.expect(v.data == .binary_op and v.data.binary_op.op == .or_op);
|
|
// LHS (head call g) receives x; RHS fallback `d` is untouched.
|
|
try std.testing.expect(v.data.binary_op.lhs.data == .call);
|
|
try std.testing.expectEqual(@as(usize, 1), v.data.binary_op.lhs.data.call.args.len);
|
|
try std.testing.expect(v.data.binary_op.rhs.data == .identifier);
|
|
try std.testing.expectEqualStrings("d", v.data.binary_op.rhs.data.identifier.name);
|
|
}
|
|
|
|
test "E0.2 plain pipe still works: x |> f(a) => f(x, a)" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const v = try e02FirstValue(arena.allocator(), "f :: () { v := x |> g(a); }");
|
|
try std.testing.expect(v.data == .call);
|
|
try std.testing.expectEqual(@as(usize, 2), v.data.call.args.len);
|
|
try std.testing.expectEqualStrings("x", v.data.call.args[0].data.identifier.name);
|
|
try std.testing.expectEqualStrings("a", v.data.call.args[1].data.identifier.name);
|
|
}
|
|
|
|
test "E0.2 round-trip print: try / or precedence / raise / catch / onfail" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const a = arena.allocator();
|
|
try e02ExpectPrints(a, try e02FirstValue(a, "f :: () { v := try foo(); }"), "try foo()");
|
|
try e02ExpectPrints(a, try e02FirstValue(a, "f :: () { v := try foo() or try boo(); }"), "try foo() or try boo()");
|
|
try e02ExpectPrints(a, try e02FirstStmt(a, "f :: () { raise error.BadDigit; }"), "raise error.BadDigit");
|
|
try e02ExpectPrints(a, try e02FirstStmt(a, "f :: () { raise e; }"), "raise e");
|
|
try e02ExpectPrints(a, try e02FirstValue(a, "f :: () { v := foo() catch e bar(); }"), "foo() catch e bar()");
|
|
try e02ExpectPrints(a, try e02FirstValue(a, "f :: () { v := foo() catch e { bar(); }; }"), "foo() catch e { bar(); }");
|
|
try e02ExpectPrints(a, try e02FirstValue(a, "f :: () { v := foo() catch { bar(); }; }"), "foo() catch { bar(); }");
|
|
try e02ExpectPrints(a, try e02FirstStmt(a, "f :: () { onfail close(h); }"), "onfail close(h)");
|
|
try e02ExpectPrints(a, try e02FirstStmt(a, "f :: () { onfail e { close(h); } }"), "onfail e { close(h); }");
|
|
}
|
|
|
|
test "E0.2 round-trip print: catch match-body form" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const a = arena.allocator();
|
|
const v = try e02FirstValue(a, "f :: () { v := foo() catch e == { case .Empty: 0; else: 1; }; }");
|
|
try e02ExpectPrints(a, v, "foo() catch e == { case .Empty: 0; else: 1; }");
|
|
}
|
|
|
|
// ── ERR step E0.3 — coverage consolidation (gaps + integration) ──
|
|
|
|
test "E0.3 try in statement position (propagate, discard value)" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const s = try e02FirstStmt(arena.allocator(), "f :: () { try must_init(); }");
|
|
try std.testing.expect(s.data == .try_expr);
|
|
try std.testing.expect(s.data.try_expr.operand.data == .call);
|
|
}
|
|
|
|
test "E0.3 try over a parenthesized or-chain: try (foo() or boo())" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const v = try e02FirstValue(arena.allocator(), "f :: () { v := try (foo() or boo()); }");
|
|
// Distinct from `try foo() or try boo()`: here `try` wraps the whole chain.
|
|
try std.testing.expect(v.data == .try_expr);
|
|
try std.testing.expect(v.data.try_expr.operand.data == .binary_op);
|
|
try std.testing.expect(v.data.try_expr.operand.data.binary_op.op == .or_op);
|
|
}
|
|
|
|
test "E0.3 or value-terminator: parse(s) or 0" {
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const v = try e02FirstValue(arena.allocator(), "f :: () { v := parse(s) or 0; }");
|
|
try std.testing.expect(v.data == .binary_op and v.data.binary_op.op == .or_op);
|
|
try std.testing.expect(v.data.binary_op.lhs.data == .call);
|
|
try std.testing.expect(v.data.binary_op.rhs.data == .int_literal);
|
|
}
|
|
|
|
test "E0.3 full failable function parses end-to-end (all E0 forms)" {
|
|
const source =
|
|
\\parse :: (s: string) -> (s32, !ParseErr) {
|
|
\\ onfail e { cleanup(s); }
|
|
\\ v := try inner(s) or 0;
|
|
\\ w := other(s) catch e2 { return 0; };
|
|
\\ if bad(s) { raise error.BadDigit; }
|
|
\\ return v;
|
|
\\}
|
|
;
|
|
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);
|
|
try std.testing.expectEqualStrings("parse", decl.data.fn_decl.name);
|
|
// return type is a multi-value result list ending in `!ParseErr`
|
|
const rt = decl.data.fn_decl.return_type.?;
|
|
try std.testing.expect(rt.data == .tuple_type_expr);
|
|
const fields = rt.data.tuple_type_expr.field_types;
|
|
try std.testing.expect(fields[fields.len - 1].data == .error_type_expr);
|
|
try std.testing.expectEqualStrings("ParseErr", fields[fields.len - 1].data.error_type_expr.name.?);
|
|
// body statement kinds
|
|
const stmts = decl.data.fn_decl.body.data.block.stmts;
|
|
try std.testing.expectEqual(@as(usize, 5), stmts.len);
|
|
try std.testing.expect(stmts[0].data == .onfail_stmt);
|
|
try std.testing.expect(stmts[1].data == .var_decl and stmts[1].data.var_decl.value.?.data == .binary_op);
|
|
try std.testing.expect(stmts[2].data == .var_decl and stmts[2].data.var_decl.value.?.data == .catch_expr);
|
|
try std.testing.expect(stmts[3].data == .if_expr);
|
|
try std.testing.expect(stmts[4].data == .return_stmt);
|
|
// the onfail flag was restored: the raise inside the (separate) if-block is allowed
|
|
const then_block = stmts[3].data.if_expr.then_branch;
|
|
try std.testing.expect(then_block.data.block.stmts[0].data == .raise_stmt);
|
|
}
|