iter ct.1.4: dev-only ail migrate-canonical-types subcommand

This commit is contained in:
2026-05-11 01:16:55 +02:00
parent 219908ed54
commit d45977f9bb
2 changed files with 597 additions and 0 deletions
+514
View File
@@ -225,6 +225,18 @@ enum Cmd {
/// Edited `.prose.txt` (carries the human's intent).
edited: PathBuf,
},
/// ct.1 (dev-only): rewrite every `*.ail.json` under `<dir>` so
/// that bare cross-module `Type::Con` and `Term::Ctor.type_name`
/// references gain their owning module's qualifier. Idempotent.
/// Uses the first-match-in-imports-order disambiguation rule
/// (matches iter 23.1.3's imports-fallback) so the rewrite never
/// silently changes which type a fixture resolves to. Prelude is
/// treated as an implicit last-resort fallback.
MigrateCanonicalTypes {
/// Directory containing `*.ail.json` to migrate (typically
/// `examples/`).
dir: PathBuf,
},
}
/// Composes the prose-round-trip prompt described in
@@ -432,6 +444,85 @@ fn main() -> Result<()> {
.with_context(|| format!("reading {}", edited.display()))?;
print!("{}", compose_merge_prose_prompt(&original_form_a, &prose));
}
Cmd::MigrateCanonicalTypes { dir } => {
use ailang_core::ast::{Def, Module};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
// Pass 1: load every *.ail.json in `dir` into a map by
// module name (NOT via load_workspace — these fixtures
// may currently fail validation).
let mut modules: BTreeMap<String, (PathBuf, Module)> = BTreeMap::new();
for entry in fs::read_dir(&dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) != Some("json") {
continue;
}
let bytes = fs::read(&path)?;
let m: Module = match serde_json::from_slice(&bytes) {
Ok(m) => m,
Err(_) => continue, // not a Module (e.g. snapshot files)
};
modules.insert(m.name.clone(), (path, m));
}
// Include the embedded prelude (so the implicit fallback
// matches the loader's behaviour).
let prelude_json: &str = include_str!(
"../../../examples/prelude.ail.json"
);
let prelude: Module = serde_json::from_str(prelude_json)
.expect("embedded prelude must parse");
if !modules.contains_key("prelude") {
modules.insert(
"prelude".to_string(),
(PathBuf::new(), prelude),
);
}
// Pre-pass: per-module local-types index.
let mut local_types: BTreeMap<String, BTreeSet<String>> =
BTreeMap::new();
for (name, (_, m)) in &modules {
let mut s = BTreeSet::new();
for d in &m.defs {
if let Def::Type(t) = d {
s.insert(t.name.clone());
}
}
local_types.insert(name.clone(), s);
}
// Pass 2: rewrite each module's bare cross-module refs.
// Skip the synthetic prelude entry (no path to write to).
let mut rewritten = 0usize;
for (mod_name, (path, m)) in modules.iter_mut() {
if mod_name == "prelude" && path.as_os_str().is_empty() {
continue;
}
let import_names: Vec<String> =
m.imports.iter().map(|i| i.module.clone()).collect();
let owning = mod_name.clone();
let mut changed = false;
for def in &mut m.defs {
rewrite_def(
def,
&owning,
&local_types,
&import_names,
&mut changed,
);
}
if changed {
let bytes = serde_json::to_vec_pretty(m)?;
fs::write(&*path, bytes)?;
println!("migrated: {}", path.display());
rewritten += 1;
}
}
println!("done. {} file(s) rewritten.", rewritten);
}
Cmd::Describe { path, name, json, workspace } => {
if workspace {
let ws = ailang_core::load_workspace(&path)?;
@@ -2238,6 +2329,429 @@ fn build_to(
Ok(out_bin)
}
/// ct.1 migration helper: rewrite bare cross-module Type::Con names
/// and Term::Ctor.type_name to their qualified form. Sets
/// `*changed` to `true` if any rewrite happened. Uses the
/// first-match-in-imports-order disambiguation rule with prelude as
/// implicit last-resort fallback, matching iter 23.1.3's typecheck
/// imports-fallback so the rewrite never silently re-aims a fixture.
fn rewrite_def(
def: &mut ailang_core::ast::Def,
owning_module: &str,
local_types: &std::collections::BTreeMap<String, std::collections::BTreeSet<String>>,
import_names: &[String],
changed: &mut bool,
) {
use ailang_core::ast::{Def, Term, Type};
fn rewrite_name(
name: &mut String,
owning_module: &str,
local_types: &std::collections::BTreeMap<String, std::collections::BTreeSet<String>>,
import_names: &[String],
changed: &mut bool,
) {
if name.contains('.') {
return;
}
if matches!(
name.as_str(),
"Int" | "Bool" | "Str" | "Unit" | "Float"
) {
return;
}
if local_types
.get(owning_module)
.map(|s| s.contains(name))
.unwrap_or(false)
{
return;
}
// Bare cross-module — find owner via imports-in-order, with
// prelude as last-resort fallback.
let owner: Option<&str> = import_names
.iter()
.map(|s| s.as_str())
.find(|imp| {
local_types
.get(*imp)
.map(|s| s.contains(name))
.unwrap_or(false)
})
.or_else(|| {
if local_types
.get("prelude")
.map(|s| s.contains(name))
.unwrap_or(false)
{
Some("prelude")
} else {
None
}
});
if let Some(owner) = owner {
*name = format!("{owner}.{name}");
*changed = true;
}
// If no owner found, leave bare — validator will reject later.
}
fn rewrite_type(
t: &mut Type,
owning_module: &str,
local_types: &std::collections::BTreeMap<String, std::collections::BTreeSet<String>>,
import_names: &[String],
changed: &mut bool,
) {
match t {
Type::Con { name, args } => {
rewrite_name(
name,
owning_module,
local_types,
import_names,
changed,
);
for a in args {
rewrite_type(
a,
owning_module,
local_types,
import_names,
changed,
);
}
}
Type::Fn { params, ret, .. } => {
for p in params {
rewrite_type(
p,
owning_module,
local_types,
import_names,
changed,
);
}
rewrite_type(
ret,
owning_module,
local_types,
import_names,
changed,
);
}
Type::Forall { body, constraints, .. } => {
for c in constraints {
rewrite_type(
&mut c.type_,
owning_module,
local_types,
import_names,
changed,
);
}
rewrite_type(
body,
owning_module,
local_types,
import_names,
changed,
);
}
Type::Var { .. } => {}
}
}
fn rewrite_term(
t: &mut Term,
owning_module: &str,
local_types: &std::collections::BTreeMap<String, std::collections::BTreeSet<String>>,
import_names: &[String],
changed: &mut bool,
) {
match t {
Term::Lit { .. } | Term::Var { .. } => {}
Term::App { callee, args, .. } => {
rewrite_term(
callee,
owning_module,
local_types,
import_names,
changed,
);
for a in args {
rewrite_term(
a,
owning_module,
local_types,
import_names,
changed,
);
}
}
Term::Let { value, body, .. } => {
rewrite_term(
value,
owning_module,
local_types,
import_names,
changed,
);
rewrite_term(
body,
owning_module,
local_types,
import_names,
changed,
);
}
Term::LetRec { ty, body, in_term, .. } => {
rewrite_type(
ty,
owning_module,
local_types,
import_names,
changed,
);
rewrite_term(
body,
owning_module,
local_types,
import_names,
changed,
);
rewrite_term(
in_term,
owning_module,
local_types,
import_names,
changed,
);
}
Term::If { cond, then, else_ } => {
rewrite_term(
cond,
owning_module,
local_types,
import_names,
changed,
);
rewrite_term(
then,
owning_module,
local_types,
import_names,
changed,
);
rewrite_term(
else_,
owning_module,
local_types,
import_names,
changed,
);
}
Term::Do { args, .. } => {
for a in args {
rewrite_term(
a,
owning_module,
local_types,
import_names,
changed,
);
}
}
Term::Ctor { type_name, args, .. } => {
rewrite_name(
type_name,
owning_module,
local_types,
import_names,
changed,
);
for a in args {
rewrite_term(
a,
owning_module,
local_types,
import_names,
changed,
);
}
}
Term::Match { scrutinee, arms } => {
rewrite_term(
scrutinee,
owning_module,
local_types,
import_names,
changed,
);
for arm in arms {
rewrite_term(
&mut arm.body,
owning_module,
local_types,
import_names,
changed,
);
}
}
Term::Lam { param_tys, ret_ty, body, .. } => {
for pt in param_tys {
rewrite_type(
pt,
owning_module,
local_types,
import_names,
changed,
);
}
rewrite_type(
ret_ty,
owning_module,
local_types,
import_names,
changed,
);
rewrite_term(
body,
owning_module,
local_types,
import_names,
changed,
);
}
Term::Seq { lhs, rhs } => {
rewrite_term(
lhs,
owning_module,
local_types,
import_names,
changed,
);
rewrite_term(
rhs,
owning_module,
local_types,
import_names,
changed,
);
}
Term::Clone { value } => rewrite_term(
value,
owning_module,
local_types,
import_names,
changed,
),
Term::ReuseAs { source, body } => {
rewrite_term(
source,
owning_module,
local_types,
import_names,
changed,
);
rewrite_term(
body,
owning_module,
local_types,
import_names,
changed,
);
}
}
}
match def {
Def::Fn(fd) => {
rewrite_type(
&mut fd.ty,
owning_module,
local_types,
import_names,
changed,
);
rewrite_term(
&mut fd.body,
owning_module,
local_types,
import_names,
changed,
);
}
Def::Const(cd) => {
rewrite_type(
&mut cd.ty,
owning_module,
local_types,
import_names,
changed,
);
rewrite_term(
&mut cd.value,
owning_module,
local_types,
import_names,
changed,
);
}
Def::Type(td) => {
for c in &mut td.ctors {
for fty in &mut c.fields {
rewrite_type(
fty,
owning_module,
local_types,
import_names,
changed,
);
}
}
}
Def::Class(cd) => {
for cm in &mut cd.methods {
rewrite_type(
&mut cm.ty,
owning_module,
local_types,
import_names,
changed,
);
if let Some(body) = &mut cm.default {
rewrite_term(
body,
owning_module,
local_types,
import_names,
changed,
);
}
}
}
Def::Instance(id) => {
rewrite_type(
&mut id.type_,
owning_module,
local_types,
import_names,
changed,
);
for im in &mut id.methods {
rewrite_term(
&mut im.body,
owning_module,
local_types,
import_names,
changed,
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::compose_merge_prose_prompt;