feat: tuple syntax cutover — Tuple(...) type + .(...) value
Replace the bare-paren tuple grammar with explicit, position-unambiguous
forms, mirroring how structs work:
type `(A, B)` -> `Tuple(A, B)` (named keeps `:`)
value `(a, b)` -> `.(a, b)` (named uses `=`)
typed (new) -> `Tuple(A, B).(a, b)` (like `Point.{...}`)
failable `-> (T, !)` -> `-> T !`
`-> (T1, T2, !)`-> `-> Tuple(T1, T2) !` (channel outside Tuple)
Bare `(...)` is now grouping only, everywhere; a comma in bare parens is a
hard error with a migration hint. Grouping, function types `(A, B) -> R`,
param lists, lambdas, and match bindings are unaffected.
`Tuple(...)` is strictly a TYPE in every position (including `size_of` /
`type_info` args); a tuple VALUE comes only from `.(...)` (anonymous) or
`Tuple(...).(...)` (explicitly typed). A bare `Tuple(1, 2)` is a tuple
type with non-type elements -> rejected.
The ~110 tuple-bearing corpus files were migrated with a one-shot
AST-aware migrator (the `sx migrate` tool from the prior commit, removed
here). New examples: 0130 (new syntax), 0131 (typed construction), 1060
(named-tuple failable return). 1116 golden updated for the new hint text.
This commit is contained in:
@@ -869,6 +869,10 @@ pub const TupleTypeExpr = struct {
|
||||
|
||||
pub const TupleLiteral = struct {
|
||||
elements: []const TupleElement,
|
||||
// Explicit tuple type for the `Tuple(...).( ... )` typed-construction form
|
||||
// (mirrors `StructLiteral.type_expr` for `Name.{ ... }`). null for the
|
||||
// anonymous, contextually-typed `.( ... )` form.
|
||||
type_expr: ?*Node = null,
|
||||
};
|
||||
|
||||
pub const TupleElement = struct {
|
||||
|
||||
@@ -361,6 +361,13 @@ pub const ExprTyper = struct {
|
||||
return self.l.target_type orelse .unresolved;
|
||||
},
|
||||
.tuple_literal => |tl| {
|
||||
// Explicitly-typed `Tuple(A, B).( ... )`: the literal's type is
|
||||
// the carried tuple type (preserves field names for the named
|
||||
// form), exactly like `Name.{ ... }` infers to `Name`.
|
||||
if (tl.type_expr) |te| {
|
||||
const tuple_ty = self.l.resolveTypeWithBindings(te);
|
||||
if (tuple_ty != .unresolved) return tuple_ty;
|
||||
}
|
||||
var field_types = std.ArrayList(TypeId).empty;
|
||||
defer field_types.deinit(self.l.alloc);
|
||||
for (tl.elements) |elem| {
|
||||
|
||||
@@ -950,6 +950,38 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
}
|
||||
// A `Tuple(...)` element must denote a TYPE; a VALUE-literal element —
|
||||
// e.g. the `1` in `Tuple(i32, 1)` — is a user error. Diagnose it loudly
|
||||
// here (the same message the `.( ... )`-in-type path emits) BEFORE
|
||||
// `resolveCompound` would intern a tuple carrying an `.unresolved`
|
||||
// field. Only the unambiguous value literals are rejected: an
|
||||
// `error_type_expr` element (`-> Tuple(A, B) !` desugaring), names,
|
||||
// and the structural type shapes are all legitimate tuple elements.
|
||||
if (node.data == .tuple_type_expr) {
|
||||
for (node.data.tuple_type_expr.field_types) |ft| {
|
||||
// A signed numeric literal (`Tuple(i32, -1)`) arrives as a
|
||||
// `negate` unary over an int/float literal — reject it as the
|
||||
// literal it wraps, not as a generic non-type.
|
||||
const probe = if (ft.data == .unary_op and ft.data.unary_op.op == .negate)
|
||||
ft.data.unary_op.operand
|
||||
else
|
||||
ft;
|
||||
switch (probe.data) {
|
||||
.int_literal,
|
||||
.float_literal,
|
||||
.string_literal,
|
||||
.bool_literal,
|
||||
.null_literal,
|
||||
=> {
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, ft.span, "tuple type element is not a type (found `{s}`); a tuple used as a type must list only types, e.g. `Tuple(i32, i32)`", .{@tagName(probe.data)});
|
||||
}
|
||||
return .unresolved;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
// Structural type shapes — `*T`, `[*]T`, `[]T`, `?T`, `[N]T`, functions,
|
||||
// PLAIN closures, and PLAIN tuples — are owned by
|
||||
// `TypeResolver.resolveCompound` (A2.3b). Element types recurse through
|
||||
|
||||
@@ -323,12 +323,20 @@ pub fn buildFailableTuple(self: *Lowering, ret_ty: TypeId, value_refs: []const R
|
||||
/// dropped) for a multi-value one. Callers must pass a value-carrying
|
||||
/// tuple — a pure `-> !`'s success type is `void`, handled separately.
|
||||
pub fn failableSuccessType(self: *Lowering, op_ty: TypeId) TypeId {
|
||||
const fields = self.module.types.get(op_ty).tuple.fields;
|
||||
const tup = self.module.types.get(op_ty).tuple;
|
||||
const fields = tup.fields;
|
||||
const n_vals = fields.len - 1;
|
||||
if (n_vals == 1) return fields[0];
|
||||
// Carry the value-field names through, dropping the trailing error-slot
|
||||
// name, so a named failable tuple `-> Tuple(x: A, y: B) !` yields a value
|
||||
// type `(x: A, y: B)` whose `.x`/`.y` fields stay addressable.
|
||||
const succ_names: ?[]const types.StringId = if (tup.names) |ns|
|
||||
self.alloc.dupe(types.StringId, ns[0..n_vals]) catch unreachable
|
||||
else
|
||||
null;
|
||||
return self.module.types.intern(.{ .tuple = .{
|
||||
.fields = self.alloc.dupe(TypeId, fields[0..n_vals]) catch unreachable,
|
||||
.names = null,
|
||||
.names = succ_names,
|
||||
} });
|
||||
}
|
||||
|
||||
|
||||
@@ -1932,6 +1932,26 @@ pub fn lowerTupleLiteral(self: *Lowering, tl: *const ast.TupleLiteral) Ref {
|
||||
if (elem.value.data == .spread_expr) has_spread = true;
|
||||
}
|
||||
|
||||
// Explicitly-typed construction `Tuple(A, B).( ... )`: the literal carries
|
||||
// its tuple type, exactly like `Name.{ ... }` for structs. Resolve it and
|
||||
// drive element lowering through it as the target tuple — the produced
|
||||
// value equals what the anonymous `.( ... )` form yields against that type.
|
||||
// An ambient contextual `target_type` (annotation / call slot), if present
|
||||
// and a tuple, is honored over the explicit one only when the explicit type
|
||||
// fails to resolve; otherwise the explicit type wins.
|
||||
const saved_explicit_target = self.target_type;
|
||||
var restore_explicit_target = false;
|
||||
if (tl.type_expr) |te| {
|
||||
const tuple_ty = self.resolveTypeWithBindings(te);
|
||||
if (tuple_ty != .unresolved) {
|
||||
self.target_type = tuple_ty;
|
||||
restore_explicit_target = true;
|
||||
}
|
||||
}
|
||||
defer if (restore_explicit_target) {
|
||||
self.target_type = saved_explicit_target;
|
||||
};
|
||||
|
||||
// Contextual target tuple field types. Without a spread we require
|
||||
// exact arity (existing behavior); with a spread we index positionally
|
||||
// by output position (so `(..sources)` into a `(VL(T0), …)` field coerces
|
||||
@@ -2771,10 +2791,14 @@ pub fn lowerExpr(self: *Lowering, node: *const Node) Ref {
|
||||
.optional_type_expr,
|
||||
.array_type_expr,
|
||||
.function_type_expr,
|
||||
.tuple_type_expr,
|
||||
=> blk: {
|
||||
const ty = self.resolveTypeWithBindings(node);
|
||||
// The resolver diagnosed any unresolved leaf; don't mint a Type
|
||||
// value around the failure sentinel.
|
||||
// value around the failure sentinel. For `Tuple(...)` this is also
|
||||
// where a standalone `Tuple(1, 2)` value-expression is rejected —
|
||||
// `resolveTupleTypeWithBindings` diagnoses the non-type element and
|
||||
// returns `.unresolved`, so no value is fabricated.
|
||||
if (ty == .unresolved) break :blk self.emitError("unknown_expr", node.span);
|
||||
break :blk self.builder.constType(ty);
|
||||
},
|
||||
|
||||
@@ -288,6 +288,7 @@ pub fn isStaticTypeArg(self: *Lowering, node: *const Node) bool {
|
||||
.optional_type_expr,
|
||||
.function_type_expr,
|
||||
.tuple_literal,
|
||||
.tuple_type_expr,
|
||||
.call,
|
||||
=> return true,
|
||||
else => return false,
|
||||
@@ -355,7 +356,7 @@ pub fn resolveTupleLiteralTypeArg(self: *Lowering, node: *const Node) TypeId {
|
||||
for (node.data.tuple_literal.elements) |el| {
|
||||
if (!type_bridge.isTypeShapedAstNode(el.value, &self.module.types)) {
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, el.value.span, "tuple type element is not a type (found `{s}`); a tuple used as a type must list only types, e.g. `(i32, i32)`", .{@tagName(el.value.data)});
|
||||
diags.addFmt(.err, el.value.span, "tuple type element is not a type (found `{s}`); a tuple used as a type must list only types, e.g. `Tuple(i32, i32)`", .{@tagName(el.value.data)});
|
||||
}
|
||||
return .unresolved;
|
||||
}
|
||||
@@ -468,6 +469,7 @@ pub fn resolveTypeArg(self: *Lowering, node: *const Node) TypeId {
|
||||
// `(COnly, i64)`) is rejected exactly as in a normal annotation, instead
|
||||
// of `type_bridge.resolveAstType`'s ungated global lookup (E4).
|
||||
.tuple_literal,
|
||||
.tuple_type_expr,
|
||||
.pointer_type_expr,
|
||||
.many_pointer_type_expr,
|
||||
.array_type_expr,
|
||||
|
||||
@@ -254,7 +254,18 @@ pub const TypeResolver = struct {
|
||||
for (tt.field_types) |ft| if (ft.data == .spread_expr) break :blk null;
|
||||
var field_ids = std.ArrayList(TypeId).empty;
|
||||
defer field_ids.deinit(table.alloc);
|
||||
for (tt.field_types) |ft| field_ids.append(table.alloc, inner.resolveInner(ft)) catch return .unresolved;
|
||||
for (tt.field_types) |ft| {
|
||||
const fid = inner.resolveInner(ft);
|
||||
// A non-type tuple element (e.g. the `1` in `Tuple(i32, 1)`)
|
||||
// resolves to `.unresolved`; never intern a tuple carrying it
|
||||
// — that bogus type would reach LLVM emission and panic. The
|
||||
// user-facing diagnostic is emitted by the literal-rejection
|
||||
// arm in `resolveTypeArg` (lower.zig, the `tuple_type_expr`
|
||||
// check); here we just refuse to fabricate the type,
|
||||
// propagating the sentinel up.
|
||||
if (fid == .unresolved) break :blk .unresolved;
|
||||
field_ids.append(table.alloc, fid) catch return .unresolved;
|
||||
}
|
||||
// Preserve field names for a named tuple `(x: T, y: U)` when the
|
||||
// name and field counts agree (so `t.x` resolves).
|
||||
var name_ids: ?[]const StringId = null;
|
||||
|
||||
74
src/main.zig
74
src/main.zig
@@ -23,13 +23,6 @@ pub fn main(init: std.process.Init) !void {
|
||||
return;
|
||||
}
|
||||
|
||||
// `migrate` has its own flag (`--dry-run`) the generic flag loop below would
|
||||
// reject, so dispatch it here before that loop runs.
|
||||
if (std.mem.eql(u8, command, "migrate")) {
|
||||
runMigrate(allocator, io, args[2..]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse flags and positional arguments
|
||||
var input_path: ?[]const u8 = null;
|
||||
var target_config = sx.target.TargetConfig{};
|
||||
@@ -414,7 +407,6 @@ fn printUsage() void {
|
||||
\\ ir Print LLVM IR to stdout
|
||||
\\ asm Emit assembly (.s) file
|
||||
\\ lsp Start language server (LSP)
|
||||
\\ migrate Rewrite old tuple syntax to new (`(a,b)`->`.(a,b)`, type `(A,B)`->`Tuple(A,B)`); `--dry-run` prints only the worklist, `--force` emits output despite unmigrated ambiguous sites
|
||||
\\
|
||||
\\Options:
|
||||
\\ --target <target> Target triple or shorthand: wasm, macos, linux, windows, ios, ios-sim (default: host)
|
||||
@@ -525,72 +517,6 @@ fn compilePipeline(allocator: std.mem.Allocator, io: std.Io, input_path: []const
|
||||
return comp;
|
||||
}
|
||||
|
||||
/// `sx migrate [--dry-run] [--force] <file.sx>` — tuple-syntax migration tool.
|
||||
///
|
||||
/// Without flags: parse-only, rewrite the old tuple syntax, print the migrated
|
||||
/// source to stdout and any ambiguous-site worklist entries to stderr. A
|
||||
/// NON-EMPTY worklist is a hard failure (exit 2) — the migration is incomplete,
|
||||
/// so we do NOT print the rewritten source (which could be redirected over the
|
||||
/// input, silently shipping half-migrated code) unless `--force` is passed.
|
||||
///
|
||||
/// With `--dry-run`: print ONLY the worklist (to stderr), no rewritten source —
|
||||
/// so ambiguous sites can be audited first. A non-empty worklist still exits 2.
|
||||
///
|
||||
/// With `--force`: print the rewritten source even when the worklist is
|
||||
/// non-empty (the ambiguous sites are left in the OLD syntax). Exit is still 2
|
||||
/// so a script can detect the partial migration.
|
||||
fn runMigrate(allocator: std.mem.Allocator, io: std.Io, sub_args: []const []const u8) void {
|
||||
var dry_run = false;
|
||||
var force = false;
|
||||
var input_path: ?[]const u8 = null;
|
||||
for (sub_args) |a| {
|
||||
if (std.mem.eql(u8, a, "--dry-run")) {
|
||||
dry_run = true;
|
||||
} else if (std.mem.eql(u8, a, "--force")) {
|
||||
force = true;
|
||||
} else if (std.mem.startsWith(u8, a, "-")) {
|
||||
std.debug.print("error: unknown flag '{s}' for migrate\n", .{a});
|
||||
std.process.exit(1);
|
||||
} else {
|
||||
input_path = a;
|
||||
}
|
||||
}
|
||||
const path = input_path orelse {
|
||||
std.debug.print("usage: sx migrate [--dry-run] [--force] <file.sx>\n", .{});
|
||||
std.process.exit(1);
|
||||
};
|
||||
|
||||
const source = readSource(allocator, io, path) catch |err| {
|
||||
std.debug.print("error: cannot read '{s}': {}\n", .{ path, err });
|
||||
std.process.exit(1);
|
||||
};
|
||||
const result = sx.migrate.migrateSource(allocator, io, path, source) catch |err| {
|
||||
std.debug.print("error: migrate failed for '{s}': {}\n", .{ path, err });
|
||||
std.process.exit(1);
|
||||
};
|
||||
|
||||
// Worklist (ambiguous sites) always goes to stderr.
|
||||
for (result.worklist) |w| {
|
||||
std.debug.print("{s}:{d}:{d}: {s}: {s}\n", .{ path, w.line, w.col, w.reason, w.text });
|
||||
}
|
||||
|
||||
const has_worklist = result.worklist.len > 0;
|
||||
|
||||
// Emit the rewritten source unless we'd be shipping a half-migrated file: a
|
||||
// non-empty worklist in non-dry-run mode suppresses output unless --force.
|
||||
if (!dry_run and (!has_worklist or force)) {
|
||||
_ = std.c.write(1, result.output.ptr, result.output.len);
|
||||
}
|
||||
|
||||
if (has_worklist) {
|
||||
std.debug.print(
|
||||
"{d} ambiguous site(s) unmigrated; resolve by hand or pass --force\n",
|
||||
.{result.worklist.len},
|
||||
);
|
||||
std.process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
fn dumpSxIR(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, stdlib_paths: []const []const u8) !void {
|
||||
const source = try readSource(allocator, io, input_path);
|
||||
var comp = sx.core.Compilation.init(allocator, io, input_path, source, .{}, stdlib_paths);
|
||||
|
||||
@@ -1,330 +0,0 @@
|
||||
// Tests for migrate.zig — the `sx migrate` tuple-syntax rewriter.
|
||||
//
|
||||
// Each case parses an in-memory snippet (full decls, so it parses standalone),
|
||||
// runs the AST-walk migrator, and asserts the rewritten text and/or worklist.
|
||||
// The compiler grammar is UNCHANGED here: the migrator READS the old tuple
|
||||
// syntax `(a, b)` / `(A, B)` and EMITS the new `.(a, b)` / `Tuple(A, B)` text.
|
||||
|
||||
const std = @import("std");
|
||||
const Parser = @import("parser.zig").Parser;
|
||||
const migrate = @import("migrate.zig");
|
||||
|
||||
/// Parse `src` (must be valid old-syntax sx decls), migrate, return the
|
||||
/// rewritten text. Asserts the worklist is empty (use `runWith` for ambiguous
|
||||
/// cases).
|
||||
fn run(alloc: std.mem.Allocator, src: [:0]const u8) ![]const u8 {
|
||||
const res = try runWith(alloc, src);
|
||||
try std.testing.expectEqual(@as(usize, 0), res.worklist.len);
|
||||
return res.output;
|
||||
}
|
||||
|
||||
fn runWith(alloc: std.mem.Allocator, src: [:0]const u8) !migrate.MigrationResult {
|
||||
var parser = Parser.init(alloc, src);
|
||||
const root = try parser.parse();
|
||||
return migrate.migrateRoot(alloc, src, root);
|
||||
}
|
||||
|
||||
/// Assert that `needle` appears in `haystack` (substring), with a helpful
|
||||
/// failure message that prints the full migrated text.
|
||||
fn expectContains(haystack: []const u8, needle: []const u8) !void {
|
||||
if (std.mem.indexOf(u8, haystack, needle) == null) {
|
||||
std.debug.print("\nexpected to find:\n {s}\nin migrated output:\n{s}\n", .{ needle, haystack });
|
||||
return error.NotFound;
|
||||
}
|
||||
}
|
||||
|
||||
fn expectNotContains(haystack: []const u8, needle: []const u8) !void {
|
||||
if (std.mem.indexOf(u8, haystack, needle) != null) {
|
||||
std.debug.print("\nexpected NOT to find:\n {s}\nin migrated output:\n{s}\n", .{ needle, haystack });
|
||||
return error.UnexpectedlyFound;
|
||||
}
|
||||
}
|
||||
|
||||
// ── VALUE tuples → .(...) ────────────────────────────────────────────────
|
||||
|
||||
test "migrate value: positional (40,2) -> .(40,2)" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "f :: () { x := (40, 2); }\n");
|
||||
try expectContains(out, ".(40, 2)");
|
||||
try expectNotContains(out, " (40, 2)"); // the old, un-dotted form is gone
|
||||
}
|
||||
|
||||
test "migrate value: named (x:1,y:2) -> .(x = 1, y = 2)" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "f :: () { x := (x: 1, y: 2); }\n");
|
||||
try expectContains(out, ".(x = 1, y = 2)");
|
||||
}
|
||||
|
||||
test "migrate value: 1-tuple (x,) -> .(x)" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "f :: () { y := 9; x := (y,); }\n");
|
||||
try expectContains(out, ".(y)");
|
||||
try expectNotContains(out, "(y,)");
|
||||
}
|
||||
|
||||
test "migrate value: spread (..xs) -> .(..xs)" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "f :: (xs: i32) { t := (..xs); }\n");
|
||||
try expectContains(out, ".(..xs)");
|
||||
}
|
||||
|
||||
test "migrate value: operator operands (1,2)==(1,2)" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "f :: () { b := (1, 2) == (1, 2); }\n");
|
||||
// Both operands rewritten.
|
||||
try expectContains(out, ".(1, 2) == .(1, 2)");
|
||||
}
|
||||
|
||||
test "migrate value+type: return body -> Tuple(i64,i64){ .(b,a) }" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(),
|
||||
\\swap :: (a: i64, b: i64) -> (i64, i64) { (b, a) }
|
||||
\\
|
||||
);
|
||||
try expectContains(out, "-> Tuple(i64, i64)");
|
||||
try expectContains(out, ".(b, a)");
|
||||
}
|
||||
|
||||
test "migrate value: empty () value -> .()" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
// `x := ()` — empty tuple value.
|
||||
const out = try run(arena.allocator(), "f :: () { x := (); }\n");
|
||||
try expectContains(out, ".()");
|
||||
}
|
||||
|
||||
// ── TYPE tuples → Tuple(...) ─────────────────────────────────────────────
|
||||
|
||||
test "migrate type: annotation a:(i32,string) -> a:Tuple(i32,string)" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "f :: () { a : (i32, string) = ---; }\n");
|
||||
try expectContains(out, "Tuple(i32, string)");
|
||||
}
|
||||
|
||||
test "migrate type: named (x:i32,y:string) -> Tuple(x: i32, y: string) keeps colon" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "f :: () { a : (x: i32, y: string) = ---; }\n");
|
||||
try expectContains(out, "Tuple(x: i32, y: string)");
|
||||
}
|
||||
|
||||
test "migrate type: struct field xs:(i32,i32) -> Tuple(i32,i32)" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "S :: struct { xs: (i32, i32); }\n");
|
||||
try expectContains(out, "Tuple(i32, i32)");
|
||||
}
|
||||
|
||||
test "migrate type: pack (..Ts) -> Tuple(..Ts)" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "S :: struct { xs: (..Ts); }\n");
|
||||
try expectContains(out, "Tuple(..Ts)");
|
||||
}
|
||||
|
||||
test "migrate type: 1-tuple (T,) -> Tuple(T) drops comma" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "S :: struct { xs: (i32,); }\n");
|
||||
try expectContains(out, "Tuple(i32)");
|
||||
try expectNotContains(out, "(i32,)");
|
||||
}
|
||||
|
||||
// ── Worklist: ambiguous value-vs-type call arg ──────────────────────────
|
||||
|
||||
test "migrate worklist: size_of((Box,i32)) is NOT rewritten, records worklist" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const res = try runWith(arena.allocator(),
|
||||
\\f :: () { n := size_of((Box, i32)); }
|
||||
\\
|
||||
);
|
||||
// Ambiguous inner tuple left untouched: no `.(` rewrite of `(Box, i32)`.
|
||||
try expectNotContains(res.output, ".(Box, i32)");
|
||||
try expectContains(res.output, "(Box, i32)");
|
||||
// One worklist entry recorded.
|
||||
try std.testing.expectEqual(@as(usize, 1), res.worklist.len);
|
||||
try expectContains(res.worklist[0].text, "(Box, i32)");
|
||||
}
|
||||
|
||||
test "migrate value: call arg with literal-only tuple IS rewritten" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
// `take((1, 2))` — all elements are concrete values → safe to rewrite.
|
||||
const res = try runWith(arena.allocator(), "f :: () { take((1, 2)); }\n");
|
||||
try expectContains(res.output, ".(1, 2)");
|
||||
try std.testing.expectEqual(@as(usize, 0), res.worklist.len);
|
||||
}
|
||||
|
||||
// ── Nested tuples (recursive rewrite, ONE edit per outermost tuple) ──────
|
||||
|
||||
test "migrate nested value: ((1,2),(3,4)) -> .(.(1, 2), .(3, 4))" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "f :: () { x := ((1, 2), (3, 4)); }\n");
|
||||
try expectContains(out, ".(.(1, 2), .(3, 4))");
|
||||
// No stray un-migrated inner tuple, no trailing junk paren.
|
||||
try expectNotContains(out, ".(1, 2), 3)");
|
||||
}
|
||||
|
||||
test "migrate nested value: ((1,2),3) -> .(.(1, 2), 3)" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "f :: () { x := ((1, 2), 3); }\n");
|
||||
try expectContains(out, ".(.(1, 2), 3)");
|
||||
try expectNotContains(out, "(1, 2), 3))"); // the broken old output
|
||||
}
|
||||
|
||||
test "migrate nested named value: (a:(p:1,q:2),b:3) -> .(a = .(p = 1, q = 2), b = 3)" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "f :: () { n := (a: (p: 1, q: 2), b: 3); }\n");
|
||||
try expectContains(out, ".(a = .(p = 1, q = 2), b = 3)");
|
||||
}
|
||||
|
||||
test "migrate nested type: ((i32,i32),i64) -> Tuple(Tuple(i32, i32), i64)" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "f :: () { a : ((i32, i32), i64) = ---; }\n");
|
||||
try expectContains(out, "Tuple(Tuple(i32, i32), i64)");
|
||||
}
|
||||
|
||||
// ── Failable multi-returns: `!` channel stays OUTSIDE Tuple(...) ─────────
|
||||
|
||||
test "migrate failable: -> (T, !) -> -> T !" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "f :: () -> (i32, !) { }\n");
|
||||
try expectContains(out, "-> i32 !");
|
||||
try expectNotContains(out, "Tuple(");
|
||||
try expectNotContains(out, ".(");
|
||||
}
|
||||
|
||||
test "migrate failable: -> (T, !Named) keeps the named set" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(),
|
||||
\\E :: error { Bad }
|
||||
\\f :: () -> (i32, !E) { }
|
||||
\\
|
||||
);
|
||||
try expectContains(out, "-> i32 !E");
|
||||
try expectNotContains(out, "Tuple(");
|
||||
}
|
||||
|
||||
test "migrate failable: -> (T1, T2, !) -> -> Tuple(T1, T2) !" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "f :: () -> (i32, i64, !) { }\n");
|
||||
try expectContains(out, "-> Tuple(i32, i64) !");
|
||||
}
|
||||
|
||||
test "migrate failable: bare -> ! unchanged" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "f :: () -> ! { }\n");
|
||||
try expectContains(out, "-> !");
|
||||
try expectNotContains(out, "Tuple");
|
||||
}
|
||||
|
||||
// ── Inverted call-arg classification (conservative) ─────────────────────
|
||||
|
||||
test "migrate worklist: empty () call arg is worklisted (unit type ambiguity)" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const res = try runWith(arena.allocator(), "f :: () { n := size_of(()); }\n");
|
||||
// NOT silently rewritten to `.()`.
|
||||
try expectNotContains(res.output, "size_of(.())");
|
||||
try expectContains(res.output, "size_of(())");
|
||||
try std.testing.expectEqual(@as(usize, 1), res.worklist.len);
|
||||
}
|
||||
|
||||
test "migrate worklist: Vec(3) call-arg element is worklisted" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const res = try runWith(arena.allocator(), "f :: () { n := size_of((Vec(3), i32)); }\n");
|
||||
try expectNotContains(res.output, ".(Vec(3), i32)");
|
||||
try expectContains(res.output, "(Vec(3), i32)");
|
||||
try std.testing.expectEqual(@as(usize, 1), res.worklist.len);
|
||||
}
|
||||
|
||||
test "migrate worklist: pkg.T qualified path call-arg element is worklisted" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const res = try runWith(arena.allocator(), "f :: () { n := size_of((pkg.T, i32)); }\n");
|
||||
try expectNotContains(res.output, ".(pkg.T, i32)");
|
||||
try expectContains(res.output, "(pkg.T, i32)");
|
||||
try std.testing.expectEqual(@as(usize, 1), res.worklist.len);
|
||||
}
|
||||
|
||||
// ── Negatives: distinct AST nodes must NOT be touched ────────────────────
|
||||
|
||||
test "migrate negative: function type (i32,i32)->i32 unchanged" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "f :: () { g : (i32, i32) -> i32 = ---; }\n");
|
||||
try expectContains(out, "(i32, i32) -> i32");
|
||||
try expectNotContains(out, "Tuple(i32, i32)");
|
||||
}
|
||||
|
||||
test "migrate negative: function param list (self:*T,x:i32) unchanged" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "S :: struct {}\nm :: (self: *S, x: i32) { }\n");
|
||||
try expectContains(out, "(self: *S, x: i32)");
|
||||
try expectNotContains(out, "Tuple(");
|
||||
try expectNotContains(out, ".(self");
|
||||
}
|
||||
|
||||
test "migrate negative: array literal .[1,2,3] unchanged" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "f :: () { a := .[1, 2, 3]; }\n");
|
||||
try expectContains(out, ".[1, 2, 3]");
|
||||
}
|
||||
|
||||
test "migrate negative: struct literal .{x=1} unchanged" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "f :: () { a := .{ x = 1 }; }\n");
|
||||
try expectContains(out, ".{ x = 1 }");
|
||||
}
|
||||
|
||||
test "migrate negative: Closure(i32)->i32 type unchanged" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "f :: () { c : Closure(i32) -> i32 = ---; }\n");
|
||||
try expectContains(out, "Closure(i32) -> i32");
|
||||
try expectNotContains(out, "Tuple(");
|
||||
}
|
||||
|
||||
test "migrate negative: grouping (a+b)*c unchanged" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(), "f :: (a: i32, b: i32, c: i32) { x := (a + b) * c; }\n");
|
||||
try expectContains(out, "(a + b) * c");
|
||||
try expectNotContains(out, ".(a + b)");
|
||||
}
|
||||
|
||||
test "migrate negative: match capture case .some: (val) unchanged" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try run(arena.allocator(),
|
||||
\\check :: (v: ?i32) -> i32 {
|
||||
\\ return if v == {
|
||||
\\ case .some: (val) { val }
|
||||
\\ case .none: { 0 }
|
||||
\\ };
|
||||
\\}
|
||||
\\
|
||||
);
|
||||
try expectContains(out, "case .some: (val)");
|
||||
try expectNotContains(out, ".(val)");
|
||||
}
|
||||
512
src/migrate.zig
512
src/migrate.zig
@@ -1,512 +0,0 @@
|
||||
//! Tuple-syntax migration tool (`sx migrate`).
|
||||
//!
|
||||
//! Reads OLD-syntax `.sx` source (tuple TYPES `(A, B)`, tuple VALUES `(a, b)`)
|
||||
//! and emits NEW-syntax text (`Tuple(A, B)` / `.(a, b)`). The compiler grammar
|
||||
//! is UNCHANGED — this tool only reads the old syntax and rewrites it as text.
|
||||
//!
|
||||
//! Strategy: parse-only (read -> Compilation -> parse), then walk the parsed
|
||||
//! AST with a comptime-reflection child walker that recurses into every
|
||||
//! `*Node`-bearing field of every node variant. Two node kinds drive a rewrite:
|
||||
//!
|
||||
//! * `tuple_type_expr` — produced by the parser in grammatically-forced TYPE
|
||||
//! positions (`-> (...)`, `: (...)` annotations, struct-field/param types).
|
||||
//! Rewritten to `Tuple(...)`. SPECIAL CASE: a failable multi-return whose
|
||||
//! last element is the error-channel marker `!` keeps the channel OUTSIDE
|
||||
//! the `Tuple(...)` (see `rewriteTupleType`).
|
||||
//!
|
||||
//! * `tuple_literal` — produced in VALUE positions. Rewritten to `.(...)`.
|
||||
//! In CALL-ARG position the value/type distinction is ambiguous, so we only
|
||||
//! auto-rewrite when EVERY element is a concrete value literal; anything
|
||||
//! else (bare identifier, `Vec(3)`, `pkg.T`, empty `()`, ...) is recorded on
|
||||
//! the worklist and left untouched — never guess (CLAUDE.md silent-fallback
|
||||
//! rule).
|
||||
//!
|
||||
//! Nesting: the rewrite is RECURSIVE but emits exactly ONE edit per OUTERMOST
|
||||
//! tuple. The replacement text for a tuple is built by recursively migrating its
|
||||
//! nested tuple elements (and any non-tuple subexpressions, e.g. calls) directly
|
||||
//! into that text. We never emit a separate, overlapping child edit for anything
|
||||
//! inside a tuple's span — `applyEdits` asserts non-overlap as a tripwire.
|
||||
//!
|
||||
//! Edits are collected against the ORIGINAL source byte offsets and applied
|
||||
//! DESCENDING by start offset so earlier offsets stay valid; comments and
|
||||
//! formatting outside the edited spans are preserved verbatim.
|
||||
|
||||
const std = @import("std");
|
||||
const ast = @import("ast.zig");
|
||||
const core = @import("core.zig");
|
||||
|
||||
const Node = ast.Node;
|
||||
|
||||
/// A single text replacement against the original source: `source[start..end]`
|
||||
/// becomes `replacement`.
|
||||
pub const Edit = struct {
|
||||
start: u32,
|
||||
end: u32,
|
||||
replacement: []const u8,
|
||||
};
|
||||
|
||||
/// An ambiguous site we refused to rewrite. `line`/`col` are 1-based.
|
||||
pub const Worklist = struct {
|
||||
line: u32,
|
||||
col: u32,
|
||||
text: []const u8,
|
||||
reason: []const u8,
|
||||
};
|
||||
|
||||
pub const MigrationResult = struct {
|
||||
/// The rewritten source (a fresh allocation owning its bytes).
|
||||
output: []const u8,
|
||||
/// Ambiguous sites left untouched, in source order.
|
||||
worklist: []const Worklist,
|
||||
};
|
||||
|
||||
/// Walk state: collects edits + worklist entries while recursing the AST.
|
||||
const Walker = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
source: []const u8,
|
||||
edits: std.ArrayList(Edit) = .empty,
|
||||
worklist: std.ArrayList(Worklist) = .empty,
|
||||
|
||||
/// Recurse into `node`. `is_call_arg` is true when this node is a DIRECT
|
||||
/// argument of a `call` / `ffi_intrinsic_call` — the only context in which a
|
||||
/// `tuple_literal` may be value-vs-type ambiguous.
|
||||
///
|
||||
/// On hitting an OUTERMOST tuple we compute its full replacement (recursively
|
||||
/// baking any nested tuples / subexprs into the text) and emit a SINGLE edit;
|
||||
/// we do NOT continue the edit-emitting walk into the tuple's span (that would
|
||||
/// produce overlapping edits). Worklist collection for ambiguous nested
|
||||
/// call-args still happens, inside the recursive text builder.
|
||||
fn walk(self: *Walker, node: *const Node, is_call_arg: bool) anyerror!void {
|
||||
switch (node.data) {
|
||||
.tuple_type_expr => |tt| {
|
||||
const replacement = try self.buildTupleTypeText(node, tt);
|
||||
if (replacement) |rep| {
|
||||
try self.edits.append(self.allocator, .{
|
||||
.start = node.span.start,
|
||||
.end = node.span.end,
|
||||
.replacement = rep,
|
||||
});
|
||||
}
|
||||
// Do NOT recurse into the tuple's element subtrees here — they
|
||||
// are already baked into `replacement`. (A `null` replacement
|
||||
// means "leave unchanged"; that only happens for `-> !`, which
|
||||
// has no value elements to rewrite anyway.)
|
||||
return;
|
||||
},
|
||||
.tuple_literal => |tl| {
|
||||
if (is_call_arg and !tupleIsAllConcreteValues(tl)) {
|
||||
// Ambiguous in call-arg position (could be a type argument,
|
||||
// a parameterized type, a qualified path, the unit type
|
||||
// `()`, ...). Refuse to guess — record + leave untouched, and
|
||||
// keep walking into elements so nested unambiguous tuples are
|
||||
// still migrated.
|
||||
try self.recordWorklist(node);
|
||||
for (tl.elements) |el| try self.walk(el.value, false);
|
||||
} else {
|
||||
const rep = try self.buildTupleValueText(node, tl);
|
||||
try self.edits.append(self.allocator, .{
|
||||
.start = node.span.start,
|
||||
.end = node.span.end,
|
||||
.replacement = rep,
|
||||
});
|
||||
}
|
||||
return;
|
||||
},
|
||||
// A `call`'s direct args get the call-arg flag; the callee does not.
|
||||
.call => |c| {
|
||||
try self.walk(c.callee, false);
|
||||
for (c.args) |a| try self.walk(a, true);
|
||||
return;
|
||||
},
|
||||
.ffi_intrinsic_call => |c| {
|
||||
try self.walk(c.return_type, false);
|
||||
for (c.args) |a| try self.walk(a, true);
|
||||
return;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
// Generic recursion for every other node: visit each child *Node found
|
||||
// by reflection over the active union payload. Call-arg context does NOT
|
||||
// propagate past a non-call node.
|
||||
try self.walkChildren(node);
|
||||
}
|
||||
|
||||
/// Reflect over the active payload of `node.data` and recurse into every
|
||||
/// `*Node` reachable through its fields (directly, through optionals,
|
||||
/// slices, and nested aggregate structs/unions).
|
||||
fn walkChildren(self: *Walker, node: *const Node) anyerror!void {
|
||||
switch (node.data) {
|
||||
inline else => |payload| {
|
||||
try self.walkValue(@TypeOf(payload), payload);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Recurse into any `*Node` reachable from `value` of type `T`.
|
||||
fn walkValue(self: *Walker, comptime T: type, value: T) anyerror!void {
|
||||
if (T == *Node or T == *const Node) {
|
||||
try self.walk(value, false);
|
||||
return;
|
||||
}
|
||||
switch (@typeInfo(T)) {
|
||||
.pointer => |ptr| {
|
||||
switch (ptr.size) {
|
||||
.slice => {
|
||||
if (comptime containsNode(ptr.child)) {
|
||||
for (value) |elem| try self.walkValue(ptr.child, elem);
|
||||
}
|
||||
},
|
||||
// Non-slice pointers other than *Node (handled above) carry
|
||||
// no AST children we rewrite.
|
||||
else => {},
|
||||
}
|
||||
},
|
||||
.optional => |opt| {
|
||||
if (comptime containsNode(opt.child)) {
|
||||
if (value) |inner| try self.walkValue(opt.child, inner);
|
||||
}
|
||||
},
|
||||
.@"struct" => |st| {
|
||||
inline for (st.fields) |f| {
|
||||
if (comptime containsNode(f.type)) {
|
||||
try self.walkValue(f.type, @field(value, f.name));
|
||||
}
|
||||
}
|
||||
},
|
||||
.@"union" => |un| {
|
||||
if (comptime unionContainsNode(un)) {
|
||||
switch (value) {
|
||||
inline else => |inner| try self.walkValue(@TypeOf(inner), inner),
|
||||
}
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the replacement text for a `tuple_type_expr`, baking nested tuples
|
||||
/// recursively. Returns `null` when the node should be left unchanged.
|
||||
///
|
||||
/// Failable multi-return handling — the error channel `!` (an
|
||||
/// `error_type_expr` element, always last) stays OUTSIDE the `Tuple(...)`:
|
||||
/// * `(!)` → unchanged (no value tuple).
|
||||
/// * `(T, !)` → `T !` (single value: drop the parens).
|
||||
/// * `(T1, T2, !)` → `Tuple(T1, T2) !`.
|
||||
fn buildTupleTypeText(self: *Walker, node: *const Node, tt: ast.TupleTypeExpr) !?[]const u8 {
|
||||
// Detect a trailing error-channel marker.
|
||||
const n = tt.field_types.len;
|
||||
const has_err = n > 0 and tt.field_types[n - 1].data == .error_type_expr;
|
||||
|
||||
if (has_err) {
|
||||
const err_node = tt.field_types[n - 1];
|
||||
// Raw text of the error marker, e.g. `!` or `!JsonError`.
|
||||
const err_text = self.source[err_node.span.start..err_node.span.end];
|
||||
const value_count = n - 1;
|
||||
if (value_count == 0) {
|
||||
// `-> !` (no value tuple) — leave unchanged.
|
||||
return null;
|
||||
}
|
||||
if (value_count == 1) {
|
||||
// `(T, !)` → `T !` — strip the parens, no Tuple wrapper.
|
||||
const t_text = try self.migratedTypeElement(tt.field_types[0]);
|
||||
return try std.fmt.allocPrint(self.allocator, "{s} {s}", .{ t_text, err_text });
|
||||
}
|
||||
// `(T1, T2, ..., !)` → `Tuple(T1, T2, ...) !`.
|
||||
const inner = try self.buildTypeInner(node, tt, value_count);
|
||||
return try std.fmt.allocPrint(self.allocator, "Tuple{s} {s}", .{ inner, err_text });
|
||||
}
|
||||
|
||||
// Ordinary type tuple: `Tuple(...)`, names keep `:`.
|
||||
const inner = try self.buildTypeInner(node, tt, n);
|
||||
return try std.fmt.allocPrint(self.allocator, "Tuple{s}", .{inner});
|
||||
}
|
||||
|
||||
/// Build the parenthesized inner `(...)` for a type tuple covering the first
|
||||
/// `count` field types (a failable return passes `count < field_types.len` to
|
||||
/// exclude the trailing `!`). Names keep their `:`. A 1-tuple drops its
|
||||
/// trailing comma.
|
||||
fn buildTypeInner(self: *Walker, node: *const Node, tt: ast.TupleTypeExpr, count: usize) ![]const u8 {
|
||||
var out = std.ArrayList(u8).empty;
|
||||
try out.append(self.allocator, '(');
|
||||
for (tt.field_types[0..count], 0..) |ft, i| {
|
||||
if (i != 0) try out.appendSlice(self.allocator, ", ");
|
||||
// Named type tuple keeps `name: ` verbatim.
|
||||
if (tt.field_names) |names| {
|
||||
// Synthetic `_<i>` names mark positional slots — emit nothing.
|
||||
if (!isSyntheticName(names[i], i)) {
|
||||
try out.appendSlice(self.allocator, names[i]);
|
||||
try out.appendSlice(self.allocator, ": ");
|
||||
}
|
||||
}
|
||||
const el_text = try self.migratedTypeElement(ft);
|
||||
try out.appendSlice(self.allocator, el_text);
|
||||
}
|
||||
try out.append(self.allocator, ')');
|
||||
_ = node;
|
||||
return out.toOwnedSlice(self.allocator);
|
||||
}
|
||||
|
||||
/// Migrate a single TYPE element subtree to text. A nested tuple type is
|
||||
/// baked recursively; everything else is copied verbatim from source but with
|
||||
/// any nested tuples inside it rewritten.
|
||||
fn migratedTypeElement(self: *Walker, ft: *const Node) anyerror![]const u8 {
|
||||
if (ft.data == .tuple_type_expr) {
|
||||
const rep = try self.buildTupleTypeText(ft, ft.data.tuple_type_expr);
|
||||
return rep orelse self.source[ft.span.start..ft.span.end];
|
||||
}
|
||||
return self.migratedSubtree(ft, false);
|
||||
}
|
||||
|
||||
/// Build the replacement text for a `tuple_literal`, baking nested tuples
|
||||
/// recursively. Names flip `:` → ` = `.
|
||||
fn buildTupleValueText(self: *Walker, node: *const Node, tl: ast.TupleLiteral) ![]const u8 {
|
||||
var out = std.ArrayList(u8).empty;
|
||||
try out.appendSlice(self.allocator, ".(");
|
||||
for (tl.elements, 0..) |el, i| {
|
||||
if (i != 0) try out.appendSlice(self.allocator, ", ");
|
||||
if (el.name) |name| {
|
||||
try out.appendSlice(self.allocator, name);
|
||||
try out.appendSlice(self.allocator, " = ");
|
||||
}
|
||||
// Spread element: `..xs` — the parser models it as a spread_expr
|
||||
// whose operand is the spread target; copy its source verbatim
|
||||
// (its own nested tuples, if any, get migrated by migratedSubtree).
|
||||
const el_text = try self.migratedValueElement(el.value);
|
||||
try out.appendSlice(self.allocator, el_text);
|
||||
}
|
||||
try out.append(self.allocator, ')');
|
||||
_ = node;
|
||||
return out.toOwnedSlice(self.allocator);
|
||||
}
|
||||
|
||||
/// Migrate a single VALUE element subtree to text. A nested tuple literal is
|
||||
/// baked recursively; everything else is copied verbatim with nested tuples
|
||||
/// inside rewritten.
|
||||
fn migratedValueElement(self: *Walker, value: *const Node) anyerror![]const u8 {
|
||||
if (value.data == .tuple_literal) {
|
||||
const tl = value.data.tuple_literal;
|
||||
// A nested tuple in a VALUE position is unambiguously a value (it is
|
||||
// never itself a direct call-arg), so always rewrite it.
|
||||
return self.buildTupleValueText(value, tl);
|
||||
}
|
||||
return self.migratedSubtree(value, false);
|
||||
}
|
||||
|
||||
/// Return the migrated text for an arbitrary subtree by collecting the edits
|
||||
/// its descendants produce (relative to `node.span`) and splicing them into
|
||||
/// the raw source slice. Worklist entries discovered inside are appended to
|
||||
/// the shared worklist. This is how a NON-tuple element of a tuple (e.g. a
|
||||
/// `call` with its own nested tuple args) gets its inner tuples migrated
|
||||
/// while preserving its surrounding formatting verbatim.
|
||||
fn migratedSubtree(self: *Walker, node: *const Node, is_call_arg: bool) ![]const u8 {
|
||||
// Sub-walk with a private edit list but the SHARED worklist.
|
||||
var sub = Walker{
|
||||
.allocator = self.allocator,
|
||||
.source = self.source,
|
||||
.worklist = self.worklist,
|
||||
};
|
||||
try sub.walk(node, is_call_arg);
|
||||
// Carry any worklist entries the sub-walk found back to the parent.
|
||||
self.worklist = sub.worklist;
|
||||
|
||||
const base = node.span.start;
|
||||
const raw = self.source[node.span.start..node.span.end];
|
||||
if (sub.edits.items.len == 0) return raw;
|
||||
// Splice sub-edits (offsets are absolute; rebase to the slice).
|
||||
return applyEditsRebased(self.allocator, raw, base, sub.edits.items);
|
||||
}
|
||||
|
||||
fn recordWorklist(self: *Walker, node: *const Node) !void {
|
||||
const lc = lineCol(self.source, node.span.start);
|
||||
try self.worklist.append(self.allocator, .{
|
||||
.line = lc.line,
|
||||
.col = lc.col,
|
||||
.text = self.source[node.span.start..node.span.end],
|
||||
.reason = "ambiguous value-vs-type call arg; resolve to `Tuple(...)` or `.(...)` by hand",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/// A synthetic positional name is exactly `_<i>` for slot `i` (the parser
|
||||
/// fills these in for positional slots of an otherwise-named tuple). Treat such
|
||||
/// a name as "no name" so a mixed tuple's positional slots stay positional.
|
||||
fn isSyntheticName(name: []const u8, i: usize) bool {
|
||||
if (name.len < 2 or name[0] != '_') return false;
|
||||
var buf: [24]u8 = undefined;
|
||||
const expect = std.fmt.bufPrint(&buf, "_{d}", .{i}) catch return false;
|
||||
return std.mem.eql(u8, name, expect);
|
||||
}
|
||||
|
||||
/// True when EVERY element of a call-arg `tuple_literal` is a concrete value
|
||||
/// literal (or an unambiguous value-operator expression over such). Only then is
|
||||
/// it safe to auto-rewrite the tuple to `.(...)` in call-arg position — anything
|
||||
/// else (bare identifier, parameterized type `Vec(3)`, qualified path `pkg.T`,
|
||||
/// empty `()`, ...) is ambiguous and goes to the worklist.
|
||||
fn tupleIsAllConcreteValues(tl: ast.TupleLiteral) bool {
|
||||
// An empty `()` in call-arg position is ambiguous (unit type vs empty value).
|
||||
if (tl.elements.len == 0) return false;
|
||||
for (tl.elements) |el| {
|
||||
if (!nodeIsConcreteValue(el.value)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// A node is a "concrete value" when it can only denote a runtime value — never
|
||||
/// a type. Conservative: int/float/string/bool/char literals, null/undef, enum
|
||||
/// literals, array/struct literals, and value-operator expressions (binary /
|
||||
/// unary ops, comparisons) whose operands are themselves concrete values. A
|
||||
/// nested tuple literal of concrete values is concrete too. Everything else
|
||||
/// (identifiers, calls, field access, parameterized/qualified type syntax, ...)
|
||||
/// is NOT — it could be or contain a type.
|
||||
fn nodeIsConcreteValue(node: *const Node) bool {
|
||||
return switch (node.data) {
|
||||
.int_literal,
|
||||
.float_literal,
|
||||
.bool_literal,
|
||||
.string_literal,
|
||||
.null_literal,
|
||||
.undef_literal,
|
||||
.enum_literal,
|
||||
.array_literal,
|
||||
.struct_literal,
|
||||
=> true,
|
||||
.binary_op => |b| nodeIsConcreteValue(b.lhs) and nodeIsConcreteValue(b.rhs),
|
||||
.chained_comparison => |c| blk: {
|
||||
for (c.operands) |o| {
|
||||
if (!nodeIsConcreteValue(o)) break :blk false;
|
||||
}
|
||||
break :blk true;
|
||||
},
|
||||
.unary_op => |u| nodeIsConcreteValue(u.operand),
|
||||
.tuple_literal => |t| tupleIsAllConcreteValues(t),
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Comptime: does type `T` (transitively) contain a `*Node` we'd recurse into?
|
||||
/// Prunes the reflection walk so we never descend into pure-scalar payloads.
|
||||
fn containsNode(comptime T: type) bool {
|
||||
if (T == *Node or T == *const Node or T == Node) return true;
|
||||
return switch (@typeInfo(T)) {
|
||||
.pointer => |ptr| switch (ptr.size) {
|
||||
.slice => containsNode(ptr.child),
|
||||
.one => ptr.child == Node, // *Node handled above; other *X: no
|
||||
else => false,
|
||||
},
|
||||
.optional => |opt| containsNode(opt.child),
|
||||
.array => |arr| containsNode(arr.child),
|
||||
.@"struct" => |st| blk: {
|
||||
inline for (st.fields) |f| {
|
||||
if (containsNode(f.type)) break :blk true;
|
||||
}
|
||||
break :blk false;
|
||||
},
|
||||
.@"union" => |un| unionContainsNode(un),
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn unionContainsNode(comptime un: std.builtin.Type.Union) bool {
|
||||
inline for (un.fields) |f| {
|
||||
if (containsNode(f.type)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const LineCol = struct { line: u32, col: u32 };
|
||||
|
||||
fn lineCol(source: []const u8, offset: u32) LineCol {
|
||||
var line: u32 = 1;
|
||||
var col: u32 = 1;
|
||||
var i: usize = 0;
|
||||
while (i < offset and i < source.len) : (i += 1) {
|
||||
if (source[i] == '\n') {
|
||||
line += 1;
|
||||
col = 1;
|
||||
} else {
|
||||
col += 1;
|
||||
}
|
||||
}
|
||||
return .{ .line = line, .col = col };
|
||||
}
|
||||
|
||||
/// Migrate a source string in memory. Parse-only; never resolves imports or
|
||||
/// lowers. Returns the rewritten text + any ambiguous worklist entries.
|
||||
///
|
||||
/// `file_path` is used only for diagnostics labeling.
|
||||
pub fn migrateSource(
|
||||
allocator: std.mem.Allocator,
|
||||
io: std.Io,
|
||||
file_path: []const u8,
|
||||
source: [:0]const u8,
|
||||
) !MigrationResult {
|
||||
var comp = core.Compilation.init(allocator, io, file_path, source, .{}, &.{});
|
||||
defer comp.deinit();
|
||||
comp.parse() catch {
|
||||
comp.renderErrors();
|
||||
return error.ParseFailed;
|
||||
};
|
||||
const root = comp.root orelse return error.ParseFailed;
|
||||
return migrateRoot(allocator, source, root);
|
||||
}
|
||||
|
||||
/// Migrate from an already-parsed `root`. Split from `migrateSource` so unit
|
||||
/// tests can parse in memory (via `Parser.init`) without an `std.Io`.
|
||||
pub fn migrateRoot(
|
||||
allocator: std.mem.Allocator,
|
||||
source: []const u8,
|
||||
root: *const Node,
|
||||
) !MigrationResult {
|
||||
var walker = Walker{ .allocator = allocator, .source = source };
|
||||
for (root.data.root.decls) |decl| {
|
||||
try walker.walk(decl, false);
|
||||
}
|
||||
const output = try applyEdits(allocator, source, walker.edits.items);
|
||||
return .{
|
||||
.output = output,
|
||||
.worklist = try walker.worklist.toOwnedSlice(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
/// Apply edits to a COPY of the original source. Edits are sorted DESCENDING by
|
||||
/// start so each splice leaves earlier offsets valid. Overlapping edits are a
|
||||
/// hard error — the recursive rewrite must emit exactly one edit per outermost
|
||||
/// tuple, so two edits sharing any byte is a bug.
|
||||
pub fn applyEdits(allocator: std.mem.Allocator, source: []const u8, edits_in: []const Edit) ![]const u8 {
|
||||
const edits = try allocator.dupe(Edit, edits_in);
|
||||
std.mem.sort(Edit, edits, {}, struct {
|
||||
fn lessThan(_: void, a: Edit, b: Edit) bool {
|
||||
return a.start > b.start; // descending
|
||||
}
|
||||
}.lessThan);
|
||||
|
||||
// Tripwire: after the descending sort, each edit's end must not exceed the
|
||||
// next (lower-start) edit's start. Any overlap means the recursive rewrite
|
||||
// double-emitted — refuse to produce corrupt output.
|
||||
var prev_start: ?u32 = null;
|
||||
for (edits) |e| {
|
||||
if (prev_start) |ps| {
|
||||
if (e.end > ps) return error.OverlappingEdits;
|
||||
}
|
||||
prev_start = e.start;
|
||||
}
|
||||
|
||||
var out = try std.ArrayList(u8).initCapacity(allocator, source.len);
|
||||
try out.appendSlice(allocator, source);
|
||||
for (edits) |e| {
|
||||
// Splice source[e.start..e.end] -> e.replacement.
|
||||
try out.replaceRange(allocator, e.start, e.end - e.start, e.replacement);
|
||||
}
|
||||
return out.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
/// Apply edits whose `start`/`end` are ABSOLUTE source offsets to a `slice` that
|
||||
/// begins at absolute offset `base`. Used by `migratedSubtree` to splice a
|
||||
/// non-tuple subtree's inner tuple rewrites into its raw slice.
|
||||
fn applyEditsRebased(allocator: std.mem.Allocator, slice: []const u8, base: u32, edits_in: []const Edit) ![]const u8 {
|
||||
var rebased = try allocator.alloc(Edit, edits_in.len);
|
||||
for (edits_in, 0..) |e, i| {
|
||||
rebased[i] = .{ .start = e.start - base, .end = e.end - base, .replacement = e.replacement };
|
||||
}
|
||||
return applyEdits(allocator, slice, rebased);
|
||||
}
|
||||
@@ -217,3 +217,202 @@ test "parser: plain struct leaves abi == .default, extern_lib == null" {
|
||||
try std.testing.expectEqual(ast.ABI.default, sd.abi);
|
||||
try std.testing.expect(sd.extern_lib == null);
|
||||
}
|
||||
|
||||
// ── New tuple syntax (additive; the inline `(a, b)` forms stay valid) ──
|
||||
|
||||
// `Tuple(A, B)` magic type id → positional tuple_type_expr, mirroring `(A, B)`.
|
||||
// Exercised in a genuine type position (a fn return type), since a `::` RHS is
|
||||
// an EXPRESSION position where `Tuple(...)` is an ordinary call.
|
||||
test "parser: Tuple(A, B) type parses to positional tuple_type_expr" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: () -> Tuple(i64, i32) { 0 }");
|
||||
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 t = rt.data.tuple_type_expr;
|
||||
try std.testing.expectEqual(@as(usize, 2), t.field_types.len);
|
||||
try std.testing.expect(t.field_names == null);
|
||||
}
|
||||
|
||||
// `Tuple(x: A, y: B)` keeps `:` and stores field names.
|
||||
test "parser: named Tuple(x: A, y: B) stores field names" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: () -> Tuple(x: i64, y: i32) { 0 }");
|
||||
const root = try parser.parse();
|
||||
const t = root.data.root.decls[0].data.fn_decl.return_type.?.data.tuple_type_expr;
|
||||
try std.testing.expectEqual(@as(usize, 2), t.field_types.len);
|
||||
try std.testing.expect(t.field_names != null);
|
||||
try std.testing.expectEqualStrings("x", t.field_names.?[0]);
|
||||
try std.testing.expectEqualStrings("y", t.field_names.?[1]);
|
||||
}
|
||||
|
||||
// 1-tuple `Tuple(T)` and empty `Tuple()`. A `Tuple(T)` stays a 1-tuple — unlike
|
||||
// the inline `(T)` which is a grouping; my block never unwraps.
|
||||
test "parser: Tuple(T) is a 1-tuple, Tuple() is empty" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var p1 = Parser.init(arena.allocator(), "f :: () -> Tuple(i64) { 0 }");
|
||||
const r1 = try p1.parse();
|
||||
const t1 = r1.data.root.decls[0].data.fn_decl.return_type.?.data.tuple_type_expr;
|
||||
try std.testing.expectEqual(@as(usize, 1), t1.field_types.len);
|
||||
|
||||
var p2 = Parser.init(arena.allocator(), "f :: () -> Tuple() { 0 }");
|
||||
const r2 = try p2.parse();
|
||||
const t2 = r2.data.root.decls[0].data.fn_decl.return_type.?.data.tuple_type_expr;
|
||||
try std.testing.expectEqual(@as(usize, 0), t2.field_types.len);
|
||||
}
|
||||
|
||||
// `Tuple(..Ts)` reuses the spread/pack machinery (spread_expr field). Checked
|
||||
// in a PARAM type position (the inline `(..Ts)` form parses there too — a pack
|
||||
// tuple in bare RETURN position is a separate pre-existing parser limitation
|
||||
// that affects `(..Ts)` and `Tuple(..Ts)` identically).
|
||||
test "parser: Tuple(..Ts) pack field is a spread_expr" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: (t: Tuple(..Ts)) { }");
|
||||
const root = try parser.parse();
|
||||
const t = root.data.root.decls[0].data.fn_decl.params[0].type_expr.data.tuple_type_expr;
|
||||
try std.testing.expectEqual(@as(usize, 1), t.field_types.len);
|
||||
try std.testing.expect(t.field_types[0].data == .spread_expr);
|
||||
}
|
||||
|
||||
// A trailing `->` after `Tuple(...)` is a hard error (no return type).
|
||||
test "parser: Tuple(A, B) -> C is rejected" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: () -> Tuple(i64, i64) -> i64 { 0 }");
|
||||
try std.testing.expectError(error.ParseError, parser.parse());
|
||||
}
|
||||
|
||||
// A bare `Tuple` not followed by `(` stays an ordinary identifier.
|
||||
test "parser: bare Tuple (no paren) is an identifier, not a tuple type" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: () -> i64 { Tuple := 1; Tuple }");
|
||||
const root = try parser.parse();
|
||||
// Parses without error; the body references `Tuple` as a value name.
|
||||
try std.testing.expect(root.data.root.decls[0].data == .fn_decl);
|
||||
}
|
||||
|
||||
// `.(a, b)` value literal → tuple_literal, same node as inline `(a, b)`.
|
||||
test "parser: .(a, b) parses to tuple_literal" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: () { x := .(1, 2); }");
|
||||
const root = try parser.parse();
|
||||
const body = root.data.root.decls[0].data.fn_decl.body;
|
||||
const stmt = body.data.block.stmts[0];
|
||||
const val = stmt.data.var_decl.value.?;
|
||||
try std.testing.expect(val.data == .tuple_literal);
|
||||
try std.testing.expectEqual(@as(usize, 2), val.data.tuple_literal.elements.len);
|
||||
try std.testing.expect(val.data.tuple_literal.elements[0].name == null);
|
||||
}
|
||||
|
||||
// Named `.(x = a, y = b)` uses `=` and binds names onto TupleElement.
|
||||
test "parser: named .(x = a, y = b) uses = and stores names" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: () { x := .(x = 1, y = 2); }");
|
||||
const root = try parser.parse();
|
||||
const val = root.data.root.decls[0].data.fn_decl.body.data.block.stmts[0].data.var_decl.value.?;
|
||||
try std.testing.expect(val.data == .tuple_literal);
|
||||
const els = val.data.tuple_literal.elements;
|
||||
try std.testing.expectEqual(@as(usize, 2), els.len);
|
||||
try std.testing.expectEqualStrings("x", els[0].name.?);
|
||||
try std.testing.expectEqualStrings("y", els[1].name.?);
|
||||
}
|
||||
|
||||
// 1-tuple `.(x)` and empty `.()`.
|
||||
test "parser: .(x) is a 1-tuple, .() is empty" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var p1 = Parser.init(arena.allocator(), "f :: () { x := .(7); }");
|
||||
const r1 = try p1.parse();
|
||||
const v1 = r1.data.root.decls[0].data.fn_decl.body.data.block.stmts[0].data.var_decl.value.?;
|
||||
try std.testing.expect(v1.data == .tuple_literal);
|
||||
try std.testing.expectEqual(@as(usize, 1), v1.data.tuple_literal.elements.len);
|
||||
|
||||
var p2 = Parser.init(arena.allocator(), "f :: () { x := .(); }");
|
||||
const r2 = try p2.parse();
|
||||
const v2 = r2.data.root.decls[0].data.fn_decl.body.data.block.stmts[0].data.var_decl.value.?;
|
||||
try std.testing.expect(v2.data == .tuple_literal);
|
||||
try std.testing.expectEqual(@as(usize, 0), v2.data.tuple_literal.elements.len);
|
||||
}
|
||||
|
||||
// `-> T !` folds to the same `(T, !)` representation: tuple_type_expr whose
|
||||
// last field is an error_type_expr.
|
||||
test "parser: -> T ! folds to (T, !) tuple_type_expr" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: () -> i64 ! { 0 }");
|
||||
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.expect(fields[1].data == .error_type_expr);
|
||||
try std.testing.expect(fields[1].data.error_type_expr.name == null);
|
||||
}
|
||||
|
||||
// `-> Tuple(T1, T2) !` flattens to (T1, T2, !).
|
||||
test "parser: -> Tuple(A, B) ! flattens to (A, B, !)" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: () -> Tuple(i64, i32) !ParseErr { 0 }");
|
||||
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[0].data == .type_expr);
|
||||
try std.testing.expect(fields[1].data == .type_expr);
|
||||
try std.testing.expect(fields[2].data == .error_type_expr);
|
||||
try std.testing.expectEqualStrings("ParseErr", fields[2].data.error_type_expr.name.?);
|
||||
}
|
||||
|
||||
// `-> !` (void + error) stays a bare error_type_expr — the trailing-`!` fold
|
||||
// must NOT double-wrap it.
|
||||
test "parser: -> ! stays a bare error_type_expr" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: () -> ! { }");
|
||||
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);
|
||||
}
|
||||
|
||||
// Old inline `-> (T, !)` failable form is gone — rejected with the new-form hint.
|
||||
test "parser: old inline -> (T, !) is rejected" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: () -> (i64, !) { 0 }");
|
||||
try std.testing.expectError(error.ParseError, parser.parse());
|
||||
}
|
||||
|
||||
// Bare-paren tuple TYPE `(A, B)` is gone — rejected (tuple types use `Tuple(...)`).
|
||||
test "parser: bare-paren tuple type (A, B) is rejected" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: (t: (i64, i32)) { }");
|
||||
try std.testing.expectError(error.ParseError, parser.parse());
|
||||
}
|
||||
|
||||
// Bare-paren tuple VALUE `(a, b)` is gone — rejected (tuple values use `.(...)`).
|
||||
test "parser: bare-paren tuple value (a, b) is rejected" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: () { x := (1, 2); }");
|
||||
try std.testing.expectError(error.ParseError, parser.parse());
|
||||
}
|
||||
|
||||
// Bare-paren grouping `(a + b)` still works — single inner, no top-level comma.
|
||||
test "parser: bare-paren grouping (a + b) still parses" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: () -> i64 { (1 + 2) }");
|
||||
const root = try parser.parse();
|
||||
try std.testing.expect(root.data.root.decls[0].data == .fn_decl);
|
||||
}
|
||||
|
||||
396
src/parser.zig
396
src/parser.zig
@@ -431,6 +431,68 @@ pub const Parser = struct {
|
||||
return self.fail("expected ':', '=', ';', or 'extern' after type annotation");
|
||||
}
|
||||
|
||||
/// Parse a function/method/lambda return type, folding a trailing `!`
|
||||
/// error channel that sits OUTSIDE the value type into the same
|
||||
/// representation the inline `(T, !)` form produces.
|
||||
///
|
||||
/// `-> T !` ⇒ tuple_type_expr { [T, error_type_expr] } (== `(T, !)`)
|
||||
/// `-> Tuple(A, B) !` ⇒ tuple_type_expr { [A, B, error_type_expr] } (== `(A, B, !)`)
|
||||
/// `-> !` ⇒ error_type_expr (bare; handled by parseTypeExpr directly)
|
||||
///
|
||||
/// The old inline `(T, !)` / `(T1, T2, !)` forms keep working unchanged —
|
||||
/// this only ADDS the trailing-`!`-after-the-type spelling.
|
||||
fn parseFnReturnType(self: *Parser) anyerror!*Node {
|
||||
const start = self.current.loc.start;
|
||||
const ty = try self.parseTypeExpr();
|
||||
|
||||
// A trailing `!` (optionally `!Named`) after the return TYPE denotes the
|
||||
// error channel sitting OUTSIDE the value type. A bare `-> !` is already
|
||||
// an error_type_expr (no value), so a `!` after one would be a doubled
|
||||
// error channel — leave it for the normal "unexpected token" path.
|
||||
if (self.current.tag != .bang or ty.data == .error_type_expr) return ty;
|
||||
|
||||
self.advance(); // skip '!'
|
||||
var set_name: ?[]const u8 = null;
|
||||
if (self.current.tag == .identifier) {
|
||||
set_name = self.tokenSlice(self.current);
|
||||
self.advance();
|
||||
}
|
||||
const err_node = try self.createNode(start, .{ .error_type_expr = .{ .name = set_name } });
|
||||
|
||||
// Build the value+error result list. If the value type is itself a
|
||||
// tuple — `Tuple(A, B)` (positional) or `Tuple(x: A, y: B)` (named) —
|
||||
// flatten its fields and append the error channel, so `-> Tuple(A,B) !`
|
||||
// is identical to `(A, B, !)` and `-> Tuple(x: A, y: B) !` keeps the
|
||||
// `x`/`y` names on the flattened value fields. The value-return lowering
|
||||
// inserts the value slots FLAT into the result tuple, so the result type
|
||||
// must list the value fields flat too — wrapping a named tuple as
|
||||
// `{ {A,B}, err }` would miscompile the flat 2-tuple insert. Otherwise
|
||||
// wrap the single value as `(T, !)`.
|
||||
var fields = std.ArrayList(*Node).empty;
|
||||
var names = std.ArrayList([]const u8).empty;
|
||||
var has_names = false;
|
||||
if (ty.data == .tuple_type_expr) {
|
||||
const tt = ty.data.tuple_type_expr;
|
||||
for (tt.field_types) |f| try fields.append(self.allocator, f);
|
||||
if (tt.field_names) |fn_names| {
|
||||
has_names = true;
|
||||
for (fn_names) |nm| try names.append(self.allocator, nm);
|
||||
// The trailing error channel needs a placeholder name so the
|
||||
// names slice stays 1:1 with field_types. It is identified by
|
||||
// position (last field), never by this name (see
|
||||
// lower/error.zig errorChannelOf).
|
||||
try names.append(self.allocator, "!");
|
||||
}
|
||||
} else {
|
||||
try fields.append(self.allocator, ty);
|
||||
}
|
||||
try fields.append(self.allocator, err_node);
|
||||
return try self.createNode(start, .{ .tuple_type_expr = .{
|
||||
.field_types = try fields.toOwnedSlice(self.allocator),
|
||||
.field_names = if (has_names) try names.toOwnedSlice(self.allocator) else null,
|
||||
} });
|
||||
}
|
||||
|
||||
fn parseTypeExpr(self: *Parser) anyerror!*Node {
|
||||
const start = self.current.loc.start;
|
||||
|
||||
@@ -597,9 +659,12 @@ pub const Parser = struct {
|
||||
}
|
||||
try self.expect(.r_paren);
|
||||
if (self.current.tag == .arrow) {
|
||||
// '->' present: function type
|
||||
// '->' present: function type. Accept a trailing `!`/`!Named`
|
||||
// error channel after the return type (`(i64) -> i64 !E`), folded
|
||||
// to the SAME `(T, !)` / `(A, B, !)` representation the inline form
|
||||
// produces — the old `-> (T, !)` spelling keeps working too.
|
||||
self.advance(); // skip '->'
|
||||
const return_type = try self.parseTypeExpr();
|
||||
const return_type = try self.parseFnReturnType();
|
||||
const abi = try self.parseOptionalAbi();
|
||||
return try self.createNode(start, .{ .function_type_expr = .{
|
||||
.param_types = try param_types.toOwnedSlice(self.allocator),
|
||||
@@ -608,33 +673,25 @@ pub const Parser = struct {
|
||||
.abi = abi,
|
||||
} });
|
||||
}
|
||||
// No '->': GROUPING vs tuple. Mirror value position (`(expr)` groups,
|
||||
// `(expr,)` is a 1-tuple): a single UNNAMED, non-spread element with
|
||||
// NO trailing comma is a grouping — resolve to the inner type. This
|
||||
// lets `(Closure(i64,i64) -> i64)`, `?(?i64)`, etc. parenthesize a
|
||||
// type for grouping/readability. A 1-tuple type now requires the
|
||||
// trailing comma `(T,)`; named `(x: T)` and spread `(..Ts)` stay
|
||||
// tuples.
|
||||
// No '->': bare `(...)` in type position is GROUPING ONLY. A single
|
||||
// UNNAMED, non-spread element with NO trailing comma resolves to the
|
||||
// inner type. This lets `(Closure(i64,i64) -> i64)`, `?(?i64)`, etc.
|
||||
// parenthesize a type for readability.
|
||||
if (param_types.items.len == 1 and !had_trailing_comma and !has_names and
|
||||
param_types.items[0].data != .spread_expr)
|
||||
{
|
||||
return param_types.items[0];
|
||||
}
|
||||
// Tuple type. 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);
|
||||
// Anything else (a top-level comma, a `(T,)` 1-tuple, names, a
|
||||
// spread) used to build a bare-paren `tuple_type_expr`. That grammar
|
||||
// is gone: tuple types are written `Tuple( … )`. If the group ends in
|
||||
// an error channel `!`, it is the old failable spelling `-> (T, !)`.
|
||||
const last_is_err = param_types.items.len > 0 and
|
||||
param_types.items[param_types.items.len - 1].data == .error_type_expr;
|
||||
if (last_is_err) {
|
||||
return self.fail("failable returns use `-> T !` or `-> Tuple(T1,T2) !`");
|
||||
}
|
||||
return try self.createNode(start, .{ .tuple_type_expr = .{
|
||||
.field_types = try param_types.toOwnedSlice(self.allocator),
|
||||
.field_names = field_names,
|
||||
} });
|
||||
return self.fail("tuple types use `Tuple( … )` (e.g. `Tuple(A, B)`)");
|
||||
}
|
||||
|
||||
if (self.current.tag.isTypeKeyword() or self.isIdentLike()) {
|
||||
@@ -668,6 +725,17 @@ pub const Parser = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Tuple type: `Tuple(A, B)` / `Tuple(T)` / `Tuple()` /
|
||||
// named `Tuple(x: A, y: B)` / pack `Tuple(..Ts)` / `Tuple(..F(Ts))`.
|
||||
// Magic contextual id — only a `Tuple` IMMEDIATELY followed by `(`
|
||||
// builds a tuple type; a bare `Tuple` stays an ordinary identifier
|
||||
// (mirrors `Closure`). Lowers to the SAME `tuple_type_expr` the
|
||||
// inline `(A, B)` / `(x: A, y: B)` / `(..Ts)` forms produce.
|
||||
// Unlike `Closure`, a trailing `->` is REJECTED (no return type).
|
||||
if (std.mem.eql(u8, name, "Tuple") and self.current.tag == .l_paren) {
|
||||
return self.parseTupleTypeBody(start);
|
||||
}
|
||||
|
||||
// Closure type: Closure(params...) -> R
|
||||
// Variadic-pack trailing form: `Closure(Prefix..., ..$pack) -> R`
|
||||
// binds `pack` to a heterogeneous comptime type list at impl
|
||||
@@ -724,7 +792,11 @@ pub const Parser = struct {
|
||||
var return_type: ?*Node = null;
|
||||
if (self.current.tag == .arrow) {
|
||||
self.advance();
|
||||
return_type = try self.parseTypeExpr();
|
||||
// Accept a trailing `!`/`!Named` error channel after the
|
||||
// closure return type (`Closure(i64) -> i64 !E`, `... -> T !`),
|
||||
// folded to the same `(T, !)` / `(A, B, !)` representation; the
|
||||
// old `-> (T, !)` form keeps working.
|
||||
return_type = try self.parseFnReturnType();
|
||||
}
|
||||
return try self.createNode(start, .{ .closure_type_expr = .{
|
||||
.param_types = try param_types.toOwnedSlice(self.allocator),
|
||||
@@ -1267,7 +1339,7 @@ pub const Parser = struct {
|
||||
var return_type: ?*Node = null;
|
||||
if (self.current.tag == .arrow) {
|
||||
self.advance();
|
||||
return_type = try self.parseTypeExpr();
|
||||
return_type = try self.parseFnReturnType();
|
||||
}
|
||||
|
||||
// Optional body (default method) or semicolon
|
||||
@@ -1565,7 +1637,7 @@ pub const Parser = struct {
|
||||
var return_type: ?*Node = null;
|
||||
if (self.current.tag == .arrow) {
|
||||
self.advance();
|
||||
return_type = try self.parseTypeExpr();
|
||||
return_type = try self.parseFnReturnType();
|
||||
}
|
||||
|
||||
// Optional `#jni_method_descriptor("(Sig)Ret")` — explicit JNI descriptor override.
|
||||
@@ -1974,7 +2046,7 @@ pub const Parser = struct {
|
||||
var return_type: ?*Node = null;
|
||||
if (self.current.tag == .arrow) {
|
||||
self.advance();
|
||||
return_type = try self.parseTypeExpr();
|
||||
return_type = try self.parseFnReturnType();
|
||||
}
|
||||
|
||||
// Optional `#get` / `#set` property-accessor marker:
|
||||
@@ -2601,7 +2673,18 @@ pub const Parser = struct {
|
||||
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) {
|
||||
if (self.current.tag == .l_paren and expr.data == .tuple_type_expr) {
|
||||
// Typed tuple-value construction: `Tuple(A, B).( v1, v2 )`
|
||||
// — the `Tuple(...)` type followed by a `.( ... )`
|
||||
// initializer, exactly like `Name.{ ... }` for structs.
|
||||
// Same element rules as the anonymous `.( ... )` form
|
||||
// (positional, named `=`, spread); the resulting
|
||||
// `tuple_literal` carries the explicit tuple type so it
|
||||
// lowers against that type instead of self-typing.
|
||||
const lit = try self.parseDotTupleLiteral(expr.span.start);
|
||||
lit.data.tuple_literal.type_expr = expr;
|
||||
expr = lit;
|
||||
} else if (self.current.tag == .l_brace) {
|
||||
// Struct literal: Type.{ ... }
|
||||
if (expr.data == .identifier) {
|
||||
// Simple name: Vec4.{ ... }
|
||||
@@ -2988,6 +3071,19 @@ pub const Parser = struct {
|
||||
.identifier => {
|
||||
const name = self.tokenSlice(self.current);
|
||||
const is_raw = self.current.is_raw;
|
||||
// `Tuple(...)` in expression position is a tuple TYPE node —
|
||||
// identical to the one the type parser produces — NOT a value.
|
||||
// It is first-class in `size_of` / `type_info` / generic type
|
||||
// args (a non-type element like `Tuple(i32, 1)` is rejected at
|
||||
// the type-demanding site), can stand alone as a `Type` value,
|
||||
// and a postfix `.( ... )` (handled in `parsePostfix`)
|
||||
// constructs a typed tuple VALUE of that type — exactly like
|
||||
// `Name.{ ... }` for structs. A bare `Tuple` not followed by `(`
|
||||
// (or a backtick-raw `` `Tuple ``) stays an ordinary identifier.
|
||||
if (!is_raw and std.mem.eql(u8, name, "Tuple") and self.peekNext() == .l_paren) {
|
||||
self.advance(); // skip `Tuple`; `current` is now `(`
|
||||
return self.parseTupleTypeBody(start);
|
||||
}
|
||||
// A backtick raw identifier (`` `i2 ``) is NEVER type-classified —
|
||||
// it is always a value identifier, bypassing the reserved-type-name
|
||||
// rule. Only a bare spelling is checked for a type name
|
||||
@@ -3027,11 +3123,20 @@ pub const Parser = struct {
|
||||
try self.expect(.r_bracket);
|
||||
return try self.createNode(start, .{ .array_literal = .{ .elements = try elements.toOwnedSlice(self.allocator) } });
|
||||
}
|
||||
// Tuple value literal: .( ... )
|
||||
// positional `.(a, b)` / 1-tuple `.(x)` / empty `.()` /
|
||||
// named `.(x = a, y = b)` (uses `=`, like struct-literal field
|
||||
// init) / spread `.(..xs)` / nested `.( .(a,b), c )`.
|
||||
// Produces the SAME `tuple_literal` node the inline `(a, b)`
|
||||
// form produces, so it self-types structurally with no target.
|
||||
if (self.current.tag == .l_paren) {
|
||||
return self.parseDotTupleLiteral(start);
|
||||
}
|
||||
// Enum literal: .variant_name. A reserved keyword is a valid
|
||||
// variant name here — the leading dot disambiguates (`.enum`,
|
||||
// `.struct`), so no backtick escape is needed.
|
||||
const name = self.dotMemberName() orelse
|
||||
return self.fail("expected variant name, '{', or '[' after '.'");
|
||||
return self.fail("expected variant name, '{', '[', or '(' after '.'");
|
||||
// Enum literal: .variant_name — parsePostfix handles optional (...) as a call
|
||||
return try self.createNode(start, .{ .enum_literal = .{ .name = name } });
|
||||
},
|
||||
@@ -3052,34 +3157,25 @@ pub const Parser = struct {
|
||||
self.in_for_header = false;
|
||||
defer self.in_for_header = saved_hdr_grp;
|
||||
|
||||
// Check for named tuple: (name: expr, ...)
|
||||
// Bare `(...)` is GROUPING ONLY. Tuple VALUES are written
|
||||
// `.( … )` / `Tuple(T..).( … )`. A named element, an empty group,
|
||||
// a leading spread, or a top-level comma all used to build a
|
||||
// bare-paren `tuple_literal`; that grammar is gone.
|
||||
if (self.current.tag == .identifier and self.peekNext() == .colon) {
|
||||
return self.parseTupleLiteralNamed(start);
|
||||
return self.fail("tuple values use `.( … )` (e.g. `.(a, b)`) or `Tuple(T..).( … )`");
|
||||
}
|
||||
|
||||
// Empty parens or first expression
|
||||
if (self.current.tag == .r_paren) {
|
||||
self.advance();
|
||||
// () — empty tuple
|
||||
return try self.createNode(start, .{ .tuple_literal = .{ .elements = &.{} } });
|
||||
return self.fail("tuple values use `.( … )` (e.g. `.(a, b)`) or `Tuple(T..).( … )`");
|
||||
}
|
||||
|
||||
// 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);
|
||||
return self.fail("tuple values use `.( … )` (e.g. `.(a, b)`) or `Tuple(T..).( … )`");
|
||||
}
|
||||
|
||||
const first = try self.parseExpr();
|
||||
|
||||
// Check for comma → tuple
|
||||
// A top-level comma was a tuple; now it is an error.
|
||||
if (self.current.tag == .comma) {
|
||||
return self.finishTupleAfterFirst(start, first);
|
||||
return self.fail("tuple values use `.( … )` (e.g. `.(a, b)`) or `Tuple(T..).( … )`");
|
||||
}
|
||||
|
||||
// No comma → grouping
|
||||
@@ -3581,45 +3677,156 @@ pub const Parser = struct {
|
||||
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;
|
||||
|
||||
/// Parse a `.( ... )` tuple value literal. `current` is the `(`.
|
||||
/// A token that can only begin a VALUE literal, never a type. Used to give
|
||||
/// `Tuple(...)` a precise lowering-time "element is not a type" diagnostic
|
||||
/// (instead of a generic parse error) when a literal is supplied as a tuple
|
||||
/// element.
|
||||
fn currentTokenIsValueLiteral(self: *Parser) bool {
|
||||
switch (self.current.tag) {
|
||||
.int_literal,
|
||||
.float_literal,
|
||||
.string_literal,
|
||||
.raw_string_literal,
|
||||
.kw_true,
|
||||
.kw_false,
|
||||
.kw_null,
|
||||
=> return true,
|
||||
// A signed numeric literal — `Tuple(i32, -1)` / `Tuple(i32, +2)` —
|
||||
// is value-shaped too, so the precise "tuple type element is not a
|
||||
// type" diagnostic fires instead of the generic "expected type
|
||||
// name" parse error. Only a leading sign DIRECTLY before a number
|
||||
// counts (not `-T`, which is never a valid type anyway).
|
||||
.minus, .plus => {
|
||||
const next = self.peekNext();
|
||||
return next == .int_literal or next == .float_literal;
|
||||
},
|
||||
else => return false,
|
||||
}
|
||||
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`.
|
||||
/// Parse a `Tuple(...)` tuple-TYPE body. On entry `current` is the `(`
|
||||
/// immediately after the `Tuple` contextual id (the caller has already
|
||||
/// consumed `Tuple`). Used in BOTH type position (the type parser) and
|
||||
/// expression position (`parsePrimary`'s `.identifier` arm) — `Tuple(...)`
|
||||
/// always denotes a TYPE, never a value. A postfix `.( ... )` after the
|
||||
/// returned `tuple_type_expr` constructs a typed tuple VALUE (handled in
|
||||
/// `parsePostfix`, mirroring `Name.{ ... }` typed struct literals).
|
||||
/// `Tuple(A, B)` / `Tuple(T)` / `Tuple()` / named `Tuple(x: A, y: B)` /
|
||||
/// pack `Tuple(..Ts)` / `Tuple(..F(Ts))`. Lowers to the SAME
|
||||
/// `tuple_type_expr` the inline `(A, B)` / `(x: A, y: B)` / `(..Ts)`
|
||||
/// forms produce. Unlike `Closure`, a trailing `->` is REJECTED.
|
||||
fn parseTupleTypeBody(self: *Parser, start: u32) anyerror!*Node {
|
||||
self.advance(); // skip '('
|
||||
var field_types = std.ArrayList(*Node).empty;
|
||||
var field_name_opt = std.ArrayList(?[]const u8).empty;
|
||||
var has_names = false;
|
||||
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
||||
if (field_types.items.len > 0) {
|
||||
try self.expect(.comma);
|
||||
if (self.current.tag == .r_paren) break; // trailing comma ok
|
||||
}
|
||||
// Pack-spread field: `Tuple(..Ts)` / `Tuple(..F(Ts))` /
|
||||
// `Tuple(..Ts.Arg)`. Reuses `spread_expr` (same machinery as
|
||||
// the inline tuple-type and Closure pack paths).
|
||||
if (self.current.tag == .dot_dot) {
|
||||
const spread_start = self.current.loc.start;
|
||||
const sp_start = self.current.loc.start;
|
||||
self.advance(); // skip '..'
|
||||
const operand = try self.parseTypeExpr();
|
||||
try field_name_opt.append(self.allocator, null);
|
||||
try field_types.append(self.allocator, try self.createNode(sp_start, .{ .spread_expr = .{ .operand = operand } }));
|
||||
continue;
|
||||
}
|
||||
// Named field: `name: Type` (keeps `:`).
|
||||
if (self.isIdentLike() and self.peekNext() == .colon) {
|
||||
const fname = self.tokenSlice(self.current);
|
||||
self.advance(); // skip name
|
||||
self.advance(); // skip ':'
|
||||
try field_name_opt.append(self.allocator, fname);
|
||||
has_names = true;
|
||||
} else {
|
||||
try field_name_opt.append(self.allocator, null);
|
||||
}
|
||||
// A literal element (`Tuple(i32, 1)`) is NOT a type. Parse it as a
|
||||
// value expression so the lowering type-arg check rejects it with
|
||||
// the precise "tuple type element is not a type" diagnostic, rather
|
||||
// than `parseTypeExpr` bailing here with a generic "expected type
|
||||
// name" parse error. Type-shaped elements still go through the type
|
||||
// parser (so `*T`, `[N]T`, `Tuple(...)`, names all parse).
|
||||
if (self.currentTokenIsValueLiteral()) {
|
||||
// A leading `+` on a signed literal (`Tuple(i32, +1)`) has no
|
||||
// unary-op parse; consume it so the number parses as a bare
|
||||
// value literal. `parseUnary` handles the `-` case and falls
|
||||
// through to `parsePrimary` for an unsigned literal.
|
||||
if (self.current.tag == .plus) self.advance();
|
||||
try field_types.append(self.allocator, try self.parseUnary());
|
||||
} else {
|
||||
try field_types.append(self.allocator, try self.parseTypeExpr());
|
||||
}
|
||||
}
|
||||
try self.expect(.r_paren);
|
||||
// A `Tuple(...)` has NO return type — reject `-> R` loudly rather
|
||||
// than silently swallowing it the way `Closure` consumes it.
|
||||
if (self.current.tag == .arrow) {
|
||||
return self.fail("`Tuple` has no return type — remove the `->`");
|
||||
}
|
||||
// Per-slot field names are non-optional in the AST; synthesize
|
||||
// `_<i>` for any unnamed slot (mirrors the inline named-tuple path).
|
||||
var field_names: ?[]const []const u8 = null;
|
||||
if (has_names) {
|
||||
var fns = std.ArrayList([]const u8).empty;
|
||||
for (field_name_opt.items, 0..) |fn_opt, i| {
|
||||
try fns.append(self.allocator, fn_opt 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 field_types.toOwnedSlice(self.allocator),
|
||||
.field_names = field_names,
|
||||
} });
|
||||
}
|
||||
|
||||
/// Builds the SAME `tuple_literal` node as the inline `(a, b)` form so the
|
||||
/// two are structurally identical (operators / splat / `.0` all work with no
|
||||
/// target type). Supports positional, 1-tuple, empty, named (`x = a`, using
|
||||
/// `=`), spread (`..xs` / `..xs.field`) and nesting.
|
||||
fn parseDotTupleLiteral(self: *Parser, start: u32) anyerror!*Node {
|
||||
self.advance(); // skip '('
|
||||
|
||||
// `.(` always opens a tuple/grouping, so calls inside parse normally even
|
||||
// within a for header (mirrors the bare `(` primary path).
|
||||
const saved_hdr_grp = self.in_for_header;
|
||||
self.in_for_header = false;
|
||||
defer self.in_for_header = saved_hdr_grp;
|
||||
|
||||
var elements = std.ArrayList(ast.TupleElement).empty;
|
||||
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
||||
if (elements.items.len > 0) {
|
||||
try self.expect(.comma);
|
||||
if (self.current.tag == .r_paren) break; // trailing comma ok
|
||||
}
|
||||
// Spread element: `.(..xs)` / `.(a, ..xs, b)` — reuses `spread_expr`,
|
||||
// whose operand carries any `xs.field` projection.
|
||||
if (self.current.tag == .dot_dot) {
|
||||
const sp_start = self.current.loc.start;
|
||||
self.advance(); // skip '..'
|
||||
const operand = try self.parseExpr();
|
||||
const spread = try self.createNode(spread_start, .{ .spread_expr = .{ .operand = operand } });
|
||||
const spread = try self.createNode(sp_start, .{ .spread_expr = .{ .operand = operand } });
|
||||
try elements.append(self.allocator, .{ .name = null, .value = spread });
|
||||
continue;
|
||||
}
|
||||
// Named field: `name = value` (uses `=`, distinct from the inline
|
||||
// named-tuple value form which uses `:`). The name is stored on the
|
||||
// TupleElement exactly like the `:` path.
|
||||
if (self.isIdentLike() and self.peekNext() == .equal) {
|
||||
const fname = self.tokenSlice(self.current);
|
||||
self.advance(); // skip name
|
||||
self.advance(); // skip '='
|
||||
const value = try self.parseExpr();
|
||||
try elements.append(self.allocator, .{ .name = fname, .value = value });
|
||||
continue;
|
||||
}
|
||||
const value = try self.parseExpr();
|
||||
try elements.append(self.allocator, .{ .name = null, .value = value });
|
||||
}
|
||||
@@ -3751,7 +3958,7 @@ pub const Parser = struct {
|
||||
var return_type: ?*Node = null;
|
||||
if (self.current.tag == .arrow) {
|
||||
self.advance();
|
||||
return_type = try self.parseTypeExpr();
|
||||
return_type = try self.parseFnReturnType();
|
||||
}
|
||||
|
||||
// Optional ABI annotation: abi(.c) / abi(.zig) / abi(.naked)
|
||||
@@ -4569,8 +4776,8 @@ test "parse comptime type-pack is NOT a protocol pack (..$args)" {
|
||||
// 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);";
|
||||
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);
|
||||
@@ -4585,8 +4792,8 @@ test "parse pack expansion: tuple value (..xs)" {
|
||||
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);";
|
||||
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);
|
||||
@@ -4601,8 +4808,8 @@ test "parse pack expansion: tuple value projection (..xs.value)" {
|
||||
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;";
|
||||
test "parse pack expansion: tuple type Tuple(..F(Ts))" {
|
||||
const source = "g :: (x: Tuple(..F(Ts))) => x;";
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), source);
|
||||
@@ -4707,8 +4914,8 @@ test "parse bare failable return: named `!Foo`" {
|
||||
try std.testing.expectEqualStrings("ParseErr", rt.data.error_type_expr.name.?);
|
||||
}
|
||||
|
||||
test "parse multi-return with inferred `!` as trailing element" {
|
||||
const source = "f :: () -> (i32, !) { 0; }";
|
||||
test "parse failable with inferred `!` (new `-> T !` form)" {
|
||||
const source = "f :: () -> i32 ! { 0; }";
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), source);
|
||||
@@ -4723,8 +4930,8 @@ test "parse multi-return with inferred `!` as trailing element" {
|
||||
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 :: () -> (i32, i64, !ParseErr) { 0; }";
|
||||
test "parse failable with named `!Foo` (new `-> Tuple(...) !` form)" {
|
||||
const source = "f :: () -> Tuple(i32, i64) !ParseErr { 0; }";
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), source);
|
||||
@@ -4737,7 +4944,7 @@ test "parse multi-return with named `!Foo` as trailing element" {
|
||||
try std.testing.expectEqualStrings("ParseErr", fields[2].data.error_type_expr.name.?);
|
||||
}
|
||||
|
||||
test "parse error type rejected when not the trailing result element" {
|
||||
test "parse old bare-paren failable `-> (!, i32)` is rejected" {
|
||||
const source = "f :: () -> (!, i32) { 0; }";
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
@@ -4745,7 +4952,7 @@ test "parse error type rejected when not the trailing result element" {
|
||||
try std.testing.expectError(error.ParseError, parser.parse());
|
||||
}
|
||||
|
||||
test "parse error type rejected in the middle of a result list" {
|
||||
test "parse old bare-paren failable `-> (i32, !, i64)` is rejected" {
|
||||
const source = "f :: () -> (i32, !, i64) { 0; }";
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
@@ -4764,8 +4971,11 @@ test "round-trip print: error-set decl" {
|
||||
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; }";
|
||||
test "print: failable result list with pointer + named error folds to tuple repr" {
|
||||
// New `-> T !` form: a single value + named error channel folds to the SAME
|
||||
// internal `tuple_type_expr` the old `(*Handle, !IoErr)` spelling produced,
|
||||
// so printType still renders the canonical tuple representation.
|
||||
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);
|
||||
@@ -5115,7 +5325,7 @@ test "E0.3 or value-terminator: parse(s) or 0" {
|
||||
|
||||
test "E0.3 full failable function parses end-to-end (all E0 forms)" {
|
||||
const source =
|
||||
\\parse :: (s: string) -> (i32, !ParseErr) {
|
||||
\\parse :: (s: string) -> i32 !ParseErr {
|
||||
\\ onfail (e) { cleanup(s); }
|
||||
\\ v := try inner(s) or 0;
|
||||
\\ w := other(s) catch (e2) { return 0; };
|
||||
|
||||
@@ -20,8 +20,6 @@ pub const core = @import("core.zig");
|
||||
pub const c_import = @import("c_import.zig");
|
||||
pub const c_import_tests = @import("c_import.test.zig");
|
||||
pub const corpus_run_tests = @import("corpus_run.test.zig");
|
||||
pub const migrate = @import("migrate.zig");
|
||||
pub const migrate_tests = @import("migrate.test.zig");
|
||||
pub const ir = @import("ir/ir.zig");
|
||||
|
||||
pub const lsp = struct {
|
||||
|
||||
Reference in New Issue
Block a user