add sx migrate tuple-syntax migration tool
Temporary scaffolding for the tuple-syntax cutover. Parses old-grammar
.sx and rewrites tuple syntax to the new spelling:
- tuple TYPES `(A, B)` -> `Tuple(A, B)` (named keeps `:`)
- tuple VALUES `(a, b)` -> `.(a, b)` (named flips `:` -> `=`)
- 1-tuples / empty / spread -> `.(x)` / `.()` / `.(..xs)`, `Tuple(..Ts)`
- failable returns: the `!` channel stays OUTSIDE Tuple
`-> (T, !)` -> `-> T !`
`-> (T1, T2, !)` -> `-> Tuple(T1, T2) !`
AST-walk based: rewrites only `tuple_literal` / `tuple_type_expr` nodes
(function types, param lists, match bindings, arrays, struct literals,
Closure sigs, groupings are left untouched). Nested tuples rewrite
recursively as a single non-overlapping edit per outermost tuple.
Value-vs-type ambiguity (call-arg tuples whose elements could be types,
e.g. `size_of((Box, i32))`, empty `()`) is never guessed: such sites go
to a worklist. A non-empty worklist exits nonzero and suppresses the
"looks-done" stdout output unless `--force` is passed.
`sx migrate <f>` prints migrated source; `--dry-run` prints only the
worklist. Built against the old grammar; removed after the cutover.
This commit is contained in:
74
src/main.zig
74
src/main.zig
@@ -23,6 +23,13 @@ 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{};
|
||||
@@ -407,6 +414,7 @@ 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)
|
||||
@@ -517,6 +525,72 @@ 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);
|
||||
|
||||
Reference in New Issue
Block a user