From 39b674c1ecf5d90712d4a92c8d8fb1486fc6da33 Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 1 Jun 2026 16:41:21 +0200 Subject: [PATCH] chore: throwaway mode-migration tool for the Implicit cutover (#55, 0121 task 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `ail migrate-modes ` (a throwaway CLI subcommand) and the AST walker `ailang_core::ast::for_each_fn_type_mut` it drives: parse a .ail, map every bare/Implicit fn-type slot to Own, preserve explicit Own/Borrow, print back. Semantically invisible today (PartialEq treats Implicit == Own), but it materialises the modes so the post-cutover parser — which will reject bare slots — accepts the migrated corpus. Both are throwaway: removed in task 4 once ParamMode::Implicit is deleted (the closure references Implicit and would not compile). RED-first: migrate_modes.rs failed to compile (missing walker) before for_each_fn_type_mut was added. Additive; full workspace suite green. refs #55 --- crates/ail/src/main.rs | 20 ++++++++++++++++++++ crates/ail/tests/migrate_modes.rs | 27 +++++++++++++++++++++++++++ crates/ailang-core/src/ast.rs | 28 ++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 crates/ail/tests/migrate_modes.rs diff --git a/crates/ail/src/main.rs b/crates/ail/src/main.rs index db33386..b9ec576 100644 --- a/crates/ail/src/main.rs +++ b/crates/ail/src/main.rs @@ -76,6 +76,10 @@ enum Cmd { /// `render | parse` reproduces the input's canonical bytes /// (gated by `crates/ailang-surface/tests/round_trip.rs`). Render { path: PathBuf }, + /// THROWAWAY (spec 0062): rewrite every bare fn-type slot in a + /// `.ail` file as `(own …)`, preserving explicit `(own)`/`(borrow)`. + /// Removed in the Implicit-deletion cutover. + MigrateModes { path: PathBuf }, /// Prints a single definition as JSON or pretty text. Describe { path: PathBuf, @@ -440,6 +444,22 @@ fn main() -> Result<()> { let m = ailang_surface::load_module(&path)?; print!("{}", ailang_surface::print(&m)); } + Cmd::MigrateModes { path } => { + let src = std::fs::read_to_string(&path)?; + let mut m = ailang_surface::parse(&src)?; + ailang_core::ast::for_each_fn_type_mut(&mut m, &mut |n, pm, rm| { + pm.resize(n, ailang_core::ast::ParamMode::Own); + for x in pm.iter_mut() { + if matches!(x, ailang_core::ast::ParamMode::Implicit) { + *x = ailang_core::ast::ParamMode::Own; + } + } + if matches!(rm, ailang_core::ast::ParamMode::Implicit) { + *rm = ailang_core::ast::ParamMode::Own; + } + }); + std::fs::write(&path, ailang_surface::print(&m))?; + } Cmd::Prose { path } => { // load via `load_module` (single-module mode, // matching how `render` / `parse` work). Workspace-wide diff --git a/crates/ail/tests/migrate_modes.rs b/crates/ail/tests/migrate_modes.rs new file mode 100644 index 0000000..65ef6a3 --- /dev/null +++ b/crates/ail/tests/migrate_modes.rs @@ -0,0 +1,27 @@ +//! Throwaway migration-tool test (spec 0062). Asserts the +//! parse→Implicit↦Own→print pass materialises bare slots as `(own …)` +//! and leaves explicit `(borrow …)` untouched. Deleted in the cutover. +use ailang_surface::{parse, print}; +use ailang_core::ast::ParamMode; + +/// Re-implements the tool's core transform for the unit test so the +/// assertion does not depend on the CLI I/O wrapper. +fn migrate_text(src: &str) -> String { + let mut m = parse(src).expect("parse"); + ailang_core::ast::for_each_fn_type_mut(&mut m, &mut |params_len, pm, rm| { + pm.resize(params_len, ParamMode::Own); + for x in pm.iter_mut() { + if matches!(x, ParamMode::Implicit) { *x = ParamMode::Own; } + } + if matches!(rm, ParamMode::Implicit) { *rm = ParamMode::Own; } + }); + print(&m) +} + +#[test] +fn bare_slot_becomes_own_borrow_preserved() { + let src = "(module m\n (fn f\n (doc \"d\")\n (type (fn-type (params (con Int) (borrow (con Int))) (ret (con Int))))\n (params x y)\n (body x)))\n"; + let out = migrate_text(src); + assert!(out.contains("(params (own (con Int)) (borrow (con Int)))"), "got: {out}"); + assert!(out.contains("(ret (own (con Int)))"), "got: {out}"); +} diff --git a/crates/ailang-core/src/ast.rs b/crates/ailang-core/src/ast.rs index 5eabb5c..ab2c419 100644 --- a/crates/ailang-core/src/ast.rs +++ b/crates/ailang-core/src/ast.rs @@ -846,6 +846,34 @@ impl Type { } } +/// Visit every `Type::Fn` in `m`, letting `f` rewrite its modes. +/// `f(params_len, param_modes, ret_mode)`. Used by the throwaway +/// `migrate-modes` tool (spec 0062); has no other caller and is +/// removed if the migration machinery is retired. +pub fn for_each_fn_type_mut( + m: &mut Module, + f: &mut impl FnMut(usize, &mut Vec, &mut ParamMode), +) { + fn walk_ty(t: &mut Type, f: &mut impl FnMut(usize, &mut Vec, &mut ParamMode)) { + match t { + Type::Fn { params, param_modes, ret, ret_mode, .. } => { + let n = params.len(); + for p in params.iter_mut() { walk_ty(p, f); } + walk_ty(ret, f); + f(n, param_modes, ret_mode); + } + Type::Con { args, .. } => { for a in args.iter_mut() { walk_ty(a, f); } } + Type::Forall { body, .. } => walk_ty(body, f), + Type::Var { .. } => {} + } + } + for def in m.defs.iter_mut() { + if let Def::Fn(fd) = def { + walk_ty(&mut fd.ty, f); + } + } +} + /// Per-parameter / return mode marker on a [`Type::Fn`]. Full /// contract lives in `design/contracts/0008-memory-model.md`. ///