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:
agra
2026-06-25 17:53:57 +03:00
parent c882c6c63e
commit 989e18b760
124 changed files with 941 additions and 1236 deletions

View File

@@ -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);