feat(codegen): pre-resolve the App callee in MIR; delete is_static_callee + type_home_module (mir.2)
Second re-deriver removal of the typed-MIR milestone (spec
docs/specs/0060-typed-mir.md, plan docs/plans/0116-mir.2-callee-static.md).
Static-callee resolution moves out of codegen and into lower_to_mir:
every Term::App callee is classified once, mirroring check's own synth
Term::Var ladder (lib.rs:3409-3582), and emitted as a resolved Callee.
Codegen reads the callee identity off MIR and routes each kind to the
right lowering. The codegen re-derivers is_static_callee and
type_home_module are deleted, and the lower_app <-> is_static_callee
lockstep pair is struck from CLAUDE.md.
Callee reshape (beyond the spec's original {module, fn_name} sketch —
spec 0060's Concrete-code-shapes block is updated in this iteration):
pub enum Callee {
Static { module, fn_name, sig: Type }, // user/prelude fn -> emit_call, module pre-resolved
Builtin { name, sig: Type }, // operator/intrinsic -> inline opcode lowering
Indirect(Box<MTerm>), // dynamic: fn-pointer / closure / shadowed name
}
Two forced refinements, both settled in planning:
- sig:Type on each resolved variant. drop::synth_callee_ret_mode reads
the callee ret_mode, today off Callee::Indirect(inner).ty(). A
resolved callee has no sub-term, so the sig (= synth_pure(callee),
mode-preserving) is the lossless replacement. This is NOT a mir.3
pull-forward: the MArg param-mode / consume_count fields stay at their
mir.1 defaults. Without it, every statically-resolved call would lose
its drop signal and the RawBuf / int_to_str RC-stats pins mir.1b fixed
would regress.
- Builtin variant. Operators and the str-num builtins (+, not,
str_concat, ...) are lowered inline by name and have no module/fn_name;
a separate variant is cleaner than a sentinel module. The classifier
decides Builtin vs Static from CHECK'S OWN resolution (a name in
env.globals with no owning module is a builtin), never a copy of
codegen's old is_static_callee allowlist. The recon's divergence audit
confirmed check's builtin set and codegen's inline-arm set match
exactly, so the single-engine rule is mechanically satisfiable with no
env threading gap.
type_home_module is fully deletable: a workspace-wide grep confirmed no
program references a type-scoped T.fn as a value, so its only other
consumer (resolve_top_level_fn) collapses to the module-name fallback
with no behaviour change. (The spec's "x3 mirrors" wording over-counted;
the live surface was the definition plus two call sites.)
Alternatives rejected during planning: sentinel module in Static
(ugly); builtins left as Indirect (breaks — no fn-pointer for an
operator); name-rewrite Static{name,sig} keeping lower_app's by-name
dispatch (blurs builtin/user and still needs sig). The mir.1b
re-synth-strictness trap (post-mono synth_pure re-unify surfacing
mode-strip / bare-vs-qualified / metavar leaks) did NOT fire under the
new producer path — qualify_local_types mode-preservation and the $u
wildcard normalisation from mir.1b held.
Verification (orchestrator, post-implement inspect): diff matches the
plan; grep-clean for is_static_callee + type_home_module across
ailang-codegen and ail; cargo build --workspace green; cargo test
--workspace 700 passed / 0 failed / 3 ignored (no #[ignore] added this
iteration — the 3 are the pre-existing #49 Str-leg pin + two doctests).
Acceptance witnesses green: #53 (mono_new_over_user_adt_builds_and_runs),
#51 (rawbuf_new_int_size_only_builds_and_runs still builds), show_print
cross-module (print_user_adt_runs_end_to_end), the new mir.2 pins
(mir2_resolved_callee_kinds_run_end_to_end, callee_classification_*,
strengthened new_over_user_adt_carries_node_types asserting Callee::Static).
#49 heap-Str loop binder stays #[ignore] (lifts at mir.4).
Two new examples/ fixtures back the new pins (classify_pin.ail for the
producer classification pin, mir2_callee_kinds.ail for the E2E),
ail-parse / round-trip clean.
Forward note (cycle-close architect): crates/ail/tests/codegen_import_map_fallback_pin.rs:14-15
doc prose names lower_app's cross-module arm and synth_with_extras as
failure-mode targets — both now deleted (mir.2 / mir.1b). The pin's
protected invariant is still valid and green; the prose is stale and
wants a doc-honesty reword, left out of this iteration's footprint.
This commit is contained in:
@@ -308,7 +308,6 @@ the cross-references column of a file-map.
|
|||||||
| Pair | Failure mode |
|
| Pair | Failure mode |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `Pattern::Lit::*` typecheck rejects in `crates/ailang-check/src/lib.rs` ↔ pre-desugar walkers in `crates/ailang-check/src/pre_desugar_validation.rs` | A reject arm placed AFTER `desugar_module` in the call chain is unreachable through the public `check` API if the desugar pass rewrites that pattern shape first (e.g. `desugar::build_eq` rewrites `Pattern::Lit::Float` into `(== scrut lit)` before typecheck runs). New rejects on a `Pattern::Lit::*` shape MUST live in `pre_desugar_validation.rs`, or be paired with a written guarantee that no desugar pass eats the shape. |
|
| `Pattern::Lit::*` typecheck rejects in `crates/ailang-check/src/lib.rs` ↔ pre-desugar walkers in `crates/ailang-check/src/pre_desugar_validation.rs` | A reject arm placed AFTER `desugar_module` in the call chain is unreachable through the public `check` API if the desugar pass rewrites that pattern shape first (e.g. `desugar::build_eq` rewrites `Pattern::Lit::Float` into `(== scrut lit)` before typecheck runs). New rejects on a `Pattern::Lit::*` shape MUST live in `pre_desugar_validation.rs`, or be paired with a written guarantee that no desugar pass eats the shape. |
|
||||||
| `lower_app` arms in `crates/ailang-codegen/src/lib.rs::lower_app` (line ~1749) ↔ name recognition in `crates/ailang-codegen/src/lib.rs::is_static_callee` (line ~2189) | A name lowered by a direct `lower_app` arm but unrecognised by `is_static_callee` falls through to the indirect-call path with `UnknownVar` (`UnknownVar` panic in worst case). Every new builtin lowering arm in `lower_app` must have a matching `is_static_callee` entry. |
|
|
||||||
| `INTERCEPTS` entries in `crates/ailang-codegen/src/intercepts.rs` ↔ `(intrinsic)` markers (`Term::Intrinsic` bodies) in the kernel-tier sources (`examples/prelude.ail`, `crates/ailang-kernel/src/kernel_stub/source.ail`) | A registry entry without a marker is a compiler-supplied body no source declares; a marker without an entry is an `(intrinsic)` fn codegen cannot emit. The bijection (modulo the optimisation-only allowlist — registry entries that intercept the monomorphised `__Int` specialisation of a real-bodied polyfn) is enforced by `intercepts::tests::intercepts_bijection_with_intrinsic_markers`. Every new intrinsic-backed intercept needs both an `INTERCEPTS` entry and an `(intrinsic)` marker; every new optimisation-only intercept needs an `OPTIMISATION_ONLY` allowlist entry. |
|
| `INTERCEPTS` entries in `crates/ailang-codegen/src/intercepts.rs` ↔ `(intrinsic)` markers (`Term::Intrinsic` bodies) in the kernel-tier sources (`examples/prelude.ail`, `crates/ailang-kernel/src/kernel_stub/source.ail`) | A registry entry without a marker is a compiler-supplied body no source declares; a marker without an entry is an `(intrinsic)` fn codegen cannot emit. The bijection (modulo the optimisation-only allowlist — registry entries that intercept the monomorphised `__Int` specialisation of a real-bodied polyfn) is enforced by `intercepts::tests::intercepts_bijection_with_intrinsic_markers`. Every new intrinsic-backed intercept needs both an `INTERCEPTS` entry and an `(intrinsic)` marker; every new optimisation-only intercept needs an `OPTIMISATION_ONLY` allowlist entry. |
|
||||||
|
|
||||||
Walk procedure: for each milestone-scope commit-range arm
|
Walk procedure: for each milestone-scope commit-range arm
|
||||||
|
|||||||
+20
-1
@@ -2748,7 +2748,8 @@ fn ail_run_accepts_ail_source_with_same_stdout_as_ail_json() {
|
|||||||
/// `int_to_str(42)` prints "42\n" through the standard
|
/// `int_to_str(42)` prints "42\n" through the standard
|
||||||
/// `do io/print_str(...)` path. Smoke pin for the heap-Str builtin
|
/// `do io/print_str(...)` path. Smoke pin for the heap-Str builtin
|
||||||
/// wired in hs.4 — exercises checker (signature install), codegen
|
/// wired in hs.4 — exercises checker (signature install), codegen
|
||||||
/// (lower_app arm + IR-header declare + is_static_callee whitelist),
|
/// (lower_builtin arm + IR-header declare + `Callee::Builtin`
|
||||||
|
/// classification in lower_to_mir),
|
||||||
/// runtime (str.c's `ailang_int_to_str` allocates a heap-Str slab,
|
/// runtime (str.c's `ailang_int_to_str` allocates a heap-Str slab,
|
||||||
/// @fputs reads from the bytes pointer at offset 8 with @stdout as
|
/// @fputs reads from the bytes pointer at offset 8 with @stdout as
|
||||||
/// the FILE*), and the
|
/// the FILE*), and the
|
||||||
@@ -2761,6 +2762,24 @@ fn int_to_str_smoke() {
|
|||||||
assert_eq!(out, "42\n");
|
assert_eq!(out, "42\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// mir.2 end-to-end: a program whose `main` exercises all three
|
||||||
|
/// resolved-callee kinds compiles and prints the correct result
|
||||||
|
/// through codegen's post-mir.2 `MTerm::App` consumer. `bump`'s body
|
||||||
|
/// `(app + n 1)` is a `Callee::Builtin` (inline opcode), `(app bump
|
||||||
|
/// 41)` is a same-module `Callee::Static`, and `(app int_to_str r)`
|
||||||
|
/// is a cross-module (prelude-fallback) `Callee::Static`. The
|
||||||
|
/// property protected: codegen reads the callee identity off MIR and
|
||||||
|
/// routes each kind to the right lowering — builtins inline, static
|
||||||
|
/// fns to `emit_call` with the module pre-resolved, dynamic callees
|
||||||
|
/// to the fn-pointer path — deriving nothing about the callee itself.
|
||||||
|
/// A regression that mis-resolves any kind surfaces as a wrong result
|
||||||
|
/// or a build failure here. `bump(41) = 42`, printed with no newline.
|
||||||
|
#[test]
|
||||||
|
fn mir2_resolved_callee_kinds_run_end_to_end() {
|
||||||
|
let out = build_and_run("mir2_callee_kinds.ail");
|
||||||
|
assert_eq!(out, "42");
|
||||||
|
}
|
||||||
|
|
||||||
/// `float_to_str(3.5)` prints a libc-%g-conformant
|
/// `float_to_str(3.5)` prints a libc-%g-conformant
|
||||||
/// rendering of 3.5. The runtime's `ailang_float_to_str` uses
|
/// rendering of 3.5. The runtime's `ailang_float_to_str` uses
|
||||||
/// `snprintf(..., "%g", x)` with no locale call, so the C locale
|
/// `snprintf(..., "%g", x)` with no locale call, so the C locale
|
||||||
|
|||||||
@@ -106,6 +106,75 @@ fn arg(term: MTerm) -> MArg {
|
|||||||
MArg { term, mode: Mode::Owned, consume_count: 1 }
|
MArg { term, mode: Mode::Owned, consume_count: 1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The resolved identity of an App callee, mirroring synth's
|
||||||
|
/// `Term::Var` resolution ladder (`lib.rs:3409-3582`). `lower_term`
|
||||||
|
/// turns this into the matching `Callee` variant. Resolution uses
|
||||||
|
/// check's own env (the single engine) — never a copy of codegen's
|
||||||
|
/// `is_static_callee` allowlist.
|
||||||
|
enum CalleeClass {
|
||||||
|
Builtin { name: String },
|
||||||
|
Static { module: String, fn_name: String },
|
||||||
|
Indirect,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classify_callee(ctx: &Ctx, callee: &Term) -> CalleeClass {
|
||||||
|
// A non-`Var` callee (lambda, applied expression, …) is dynamic.
|
||||||
|
let name = match callee {
|
||||||
|
Term::Var { name } => name.clone(),
|
||||||
|
_ => return CalleeClass::Indirect,
|
||||||
|
};
|
||||||
|
// rung 1: shadowed by a local binder → dynamic (synth `lib.rs:3409`).
|
||||||
|
if ctx.locals.contains_key(&name) {
|
||||||
|
return CalleeClass::Indirect;
|
||||||
|
}
|
||||||
|
// rung 5: dotted `T.fn` / `Mod.fn` — the TypeDef-first ladder
|
||||||
|
// (synth `lib.rs:3557-3582`). `T.fn` resolves to T's home module;
|
||||||
|
// `Mod.fn` to the imported module. This replaces codegen's
|
||||||
|
// `type_home_module`.
|
||||||
|
if name.matches('.').count() == 1 {
|
||||||
|
let (prefix, suffix) = name.split_once('.').expect("checked");
|
||||||
|
let type_home = ctx
|
||||||
|
.env
|
||||||
|
.module_types
|
||||||
|
.iter()
|
||||||
|
.find_map(|(m, types)| types.contains_key(prefix).then(|| m.clone()));
|
||||||
|
let target = type_home.or_else(|| ctx.env.imports.get(prefix).cloned());
|
||||||
|
return match target {
|
||||||
|
Some(module) => CalleeClass::Static { module, fn_name: suffix.to_string() },
|
||||||
|
// Unreachable for typechecked input (check would have
|
||||||
|
// raised TypeScopedReceiverNotAType); defensive dynamic.
|
||||||
|
None => CalleeClass::Indirect,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// rung 2: same-module global. It is a user fn IFF this module's
|
||||||
|
// declared globals carry the name; otherwise it is a builtin —
|
||||||
|
// both live in `env.globals` (synth `lib.rs:3416-3425`).
|
||||||
|
if ctx.env.globals.contains_key(&name) {
|
||||||
|
let is_user_fn = ctx
|
||||||
|
.env
|
||||||
|
.module_globals
|
||||||
|
.get(&ctx.env.current_module)
|
||||||
|
.is_some_and(|m| m.contains_key(&name));
|
||||||
|
return if is_user_fn {
|
||||||
|
CalleeClass::Static { module: ctx.env.current_module.clone(), fn_name: name }
|
||||||
|
} else {
|
||||||
|
CalleeClass::Builtin { name }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// rung 3: implicit-import (prelude-fallback) fn (synth `lib.rs:3428-3435`).
|
||||||
|
if let Some(module) = ctx.env.imports.values().find_map(|m| {
|
||||||
|
ctx.env
|
||||||
|
.module_globals
|
||||||
|
.get(m)
|
||||||
|
.filter(|g| g.contains_key(&name))
|
||||||
|
.map(|_| m.clone())
|
||||||
|
}) {
|
||||||
|
return CalleeClass::Static { module, fn_name: name };
|
||||||
|
}
|
||||||
|
// Typechecked input always resolves above; defensive dynamic.
|
||||||
|
CalleeClass::Indirect
|
||||||
|
}
|
||||||
|
|
||||||
/// Lower one term to `MTerm`, filling `ty` from `synth_pure`.
|
/// Lower one term to `MTerm`, filling `ty` from `synth_pure`.
|
||||||
fn lower_term(ctx: &mut Ctx, t: &Term) -> Result<MTerm> {
|
fn lower_term(ctx: &mut Ctx, t: &Term) -> Result<MTerm> {
|
||||||
let ty = ctx.synth_pure(t)?;
|
let ty = ctx.synth_pure(t)?;
|
||||||
@@ -118,9 +187,22 @@ fn lower_term(ctx: &mut Ctx, t: &Term) -> Result<MTerm> {
|
|||||||
|
|
||||||
Term::Var { name } => MTerm::Var { name: name.clone(), ty },
|
Term::Var { name } => MTerm::Var { name: name.clone(), ty },
|
||||||
|
|
||||||
// callee is Indirect at mir.1 (mir.2 resolves Static).
|
// mir.2: classify the callee against check's own resolution
|
||||||
|
// ladder and emit the resolved `Callee`. `sig` is the callee's
|
||||||
|
// fn-type (mode-preserving via `synth_pure`), carried so
|
||||||
|
// codegen's drop path reads the callee `ret_mode`.
|
||||||
Term::App { callee, args, tail } => {
|
Term::App { callee, args, tail } => {
|
||||||
let m_callee = Callee::Indirect(Box::new(lower_term(ctx, callee)?));
|
let class = classify_callee(ctx, callee);
|
||||||
|
let sig = ctx.synth_pure(callee)?;
|
||||||
|
let m_callee = match class {
|
||||||
|
CalleeClass::Builtin { name } => Callee::Builtin { name, sig },
|
||||||
|
CalleeClass::Static { module, fn_name } => {
|
||||||
|
Callee::Static { module, fn_name, sig }
|
||||||
|
}
|
||||||
|
CalleeClass::Indirect => {
|
||||||
|
Callee::Indirect(Box::new(lower_term(ctx, callee)?))
|
||||||
|
}
|
||||||
|
};
|
||||||
let m_args = args
|
let m_args = args
|
||||||
.iter()
|
.iter()
|
||||||
.map(|a| Ok(arg(lower_term(ctx, a)?)))
|
.map(|a| Ok(arg(lower_term(ctx, a)?)))
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
//! pins protect the producer; codegen consumption arrives in mir.1b.
|
//! pins protect the producer; codegen consumption arrives in mir.1b.
|
||||||
|
|
||||||
use ailang_check::elaborate_workspace;
|
use ailang_check::elaborate_workspace;
|
||||||
use ailang_mir::{MTerm, MirWorkspace, StrRep};
|
use ailang_mir::{Callee, MTerm, MirWorkspace, StrRep};
|
||||||
use ailang_surface::load_workspace;
|
use ailang_surface::load_workspace;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
@@ -70,6 +70,22 @@ fn new_over_user_adt_carries_node_types() {
|
|||||||
ty_str.contains("Counter"),
|
ty_str.contains("Counter"),
|
||||||
"lowered (new Counter 42) init node ty is Counter, got {ty_str}"
|
"lowered (new Counter 42) init node ty is Counter, got {ty_str}"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// mir.2: the `(new Counter 42)` desugars to `App{callee:
|
||||||
|
// Var{"Counter.new"}}`; lower_to_mir must resolve it to a
|
||||||
|
// `Callee::Static` whose module is `Counter`'s home (its own
|
||||||
|
// module, spelled bare per the own-module-types-stay-bare rule),
|
||||||
|
// NOT leave it `Indirect` for codegen's deleted ladder to resolve.
|
||||||
|
let MTerm::App { callee, .. } = init.as_ref() else {
|
||||||
|
panic!("let init is the (new Counter 42) App node");
|
||||||
|
};
|
||||||
|
match callee {
|
||||||
|
ailang_mir::Callee::Static { module, fn_name, .. } => {
|
||||||
|
assert_eq!(module, "new_counter_user_adt", "Counter.new home module");
|
||||||
|
assert_eq!(fn_name, "new", "Counter.new fn_name");
|
||||||
|
}
|
||||||
|
other => panic!("expected Callee::Static for Counter.new, got {other:?}"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -102,6 +118,37 @@ fn poly_free_fn_accumulating_into_own_adt_elaborates() {
|
|||||||
assert!(mir.modules.contains_key("std_list"));
|
assert!(mir.modules.contains_key("std_list"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn callee_classification_builtin_and_static() {
|
||||||
|
// mir.2: `lower_to_mir::classify_callee` resolves each App callee
|
||||||
|
// against check's own synth ladder, so codegen reads the resolved
|
||||||
|
// identity off MIR. This pins two of the three legs from the
|
||||||
|
// `classify_pin` witness: an arithmetic op (`+`) is a `Builtin`
|
||||||
|
// (no owning module), a bare same-module user fn (`bump`) is a
|
||||||
|
// `Static` carrying its own module name. The `Indirect` leg (a
|
||||||
|
// fn-typed local) is covered by the existing closure e2e tests.
|
||||||
|
let m = "classify_pin";
|
||||||
|
let mir = elaborate_fixture(m);
|
||||||
|
|
||||||
|
// `bump`'s body is `(app + n 1)` → the `+` callee is a Builtin.
|
||||||
|
match body(&mir, m, "bump") {
|
||||||
|
MTerm::App { callee: Callee::Builtin { name, .. }, .. } => {
|
||||||
|
assert_eq!(name, "+", "arithmetic op classified as Builtin");
|
||||||
|
}
|
||||||
|
other => panic!("expected Builtin `+`, got {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// `main`'s body is `(app bump 41)` → `bump` is a same-module user
|
||||||
|
// fn → Static{module: "classify_pin", fn_name: "bump"}.
|
||||||
|
match body(&mir, m, "main") {
|
||||||
|
MTerm::App { callee: Callee::Static { module, fn_name, .. }, .. } => {
|
||||||
|
assert_eq!(module, "classify_pin", "own-module callee module");
|
||||||
|
assert_eq!(fn_name, "bump", "own-module callee fn_name");
|
||||||
|
}
|
||||||
|
other => panic!("expected Static `bump`, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Recursively search for a `MTerm::Str { rep: Static }` reachable
|
/// Recursively search for a `MTerm::Str { rep: Static }` reachable
|
||||||
/// from a loop binder init (the seed).
|
/// from a loop binder init (the seed).
|
||||||
fn find_static_str_seed(t: &MTerm) -> bool {
|
fn find_static_str_seed(t: &MTerm) -> bool {
|
||||||
|
|||||||
@@ -508,9 +508,13 @@ impl<'a> Emitter<'a> {
|
|||||||
/// [`Self::drop_symbol_for_binder`] App-arm to decide both
|
/// [`Self::drop_symbol_for_binder`] App-arm to decide both
|
||||||
/// trackability and the drop-fn symbol.
|
/// trackability and the drop-fn symbol.
|
||||||
fn synth_callee_ret_mode(&self, callee: &Callee) -> Option<ParamMode> {
|
fn synth_callee_ret_mode(&self, callee: &Callee) -> Option<ParamMode> {
|
||||||
|
// A resolved callee carries its fn-type as `sig`; an indirect
|
||||||
|
// one carries it on the boxed sub-term. Both yield the callee
|
||||||
|
// `ret_mode` identically — there is no behaviour change from
|
||||||
|
// mir.1b, only a different place the same fn-type is read from.
|
||||||
let cty = match callee {
|
let cty = match callee {
|
||||||
Callee::Indirect(inner) => inner.ty(),
|
Callee::Indirect(inner) => inner.ty(),
|
||||||
Callee::Static { .. } => return None,
|
Callee::Static { sig, .. } | Callee::Builtin { sig, .. } => sig.clone(),
|
||||||
};
|
};
|
||||||
match cty {
|
match cty {
|
||||||
Type::Fn { ret_mode, .. } => Some(ret_mode),
|
Type::Fn { ret_mode, .. } => Some(ret_mode),
|
||||||
|
|||||||
@@ -60,10 +60,10 @@ use synth::{
|
|||||||
/// the comparator names `==` / `!=` / `<` / `<=` / `>` / `>=` are
|
/// the comparator names `==` / `!=` / `<` / `<=` / `>` / `>=` are
|
||||||
/// no longer surface-level identifiers (comparison goes through the
|
/// no longer surface-level identifiers (comparison goes through the
|
||||||
/// `eq` / `compare` class methods and the `float_*` named fns), so
|
/// `eq` / `compare` class methods and the `float_*` named fns), so
|
||||||
/// only the five arithmetic ops live here. Used by `lower_app` to
|
/// only the five arithmetic ops live here. Used by `lower_builtin` to
|
||||||
/// decide whether to call `builtin_binop_typed`, and by
|
/// decide whether to call `builtin_binop_typed`. Callee classification
|
||||||
/// `is_static_callee` (in combination with `not`) to recognise
|
/// (which names are builtins) now lives in `lower_to_mir::classify_callee`,
|
||||||
/// built-in callees during the global-resolution pass.
|
/// not in codegen.
|
||||||
fn is_arithmetic_op(name: &str) -> bool {
|
fn is_arithmetic_op(name: &str) -> bool {
|
||||||
matches!(name, "+" | "-" | "*" | "/" | "%")
|
matches!(name, "+" | "-" | "*" | "/" | "%")
|
||||||
}
|
}
|
||||||
@@ -1995,44 +1995,47 @@ impl<'a> Emitter<'a> {
|
|||||||
}
|
}
|
||||||
Ok((phi, then_ty))
|
Ok((phi, then_ty))
|
||||||
}
|
}
|
||||||
MTerm::App { callee, args, tail, .. } => {
|
MTerm::App { callee, args, tail, .. } => match callee {
|
||||||
// callee is `Callee::Indirect(Box<MTerm>)` at mir.1b;
|
// mir.2: the callee is pre-resolved in MIR by
|
||||||
// the static name, if any, is the inner `MTerm::Var`.
|
// `lower_to_mir::classify_callee`. A builtin is lowered
|
||||||
// (mir.2 replaces this with `Callee::Static` and deletes
|
// inline by name (opcode selection); a static
|
||||||
// `is_static_callee`.)
|
// user/prelude fn routes to `emit_call` with the module
|
||||||
let callee_mterm: &MTerm = match callee {
|
// already resolved; an indirect callee is lowered to a
|
||||||
Callee::Indirect(inner) => inner.as_ref(),
|
// fn-pointer and called via the sidetable sig. Codegen
|
||||||
Callee::Static { .. } => unreachable!("callee is Indirect until mir.2"),
|
// re-derives no callee identity and resolves no module.
|
||||||
};
|
Callee::Builtin { name, .. } => self.lower_builtin(name, args, *tail),
|
||||||
// Direct call when the callee is a `Var` referring to a
|
Callee::Static { module, fn_name, .. } => {
|
||||||
// statically-known target (builtin, current-module fn,
|
let sig = self
|
||||||
// qualified cross-module fn) AND not shadowed by a local.
|
.module_user_fns
|
||||||
// Otherwise we fall through to the indirect-call path,
|
.get(module)
|
||||||
// which lowers the callee to a fn-pointer and looks up
|
.and_then(|m| m.get(fn_name))
|
||||||
// its sig in the sidetable.
|
.cloned()
|
||||||
if let MTerm::Var { name, .. } = callee_mterm {
|
.ok_or_else(|| {
|
||||||
let shadowed = self.locals.iter().any(|(n, _, _, _)| n == name);
|
CodegenError::Internal(format!(
|
||||||
if !shadowed && self.is_static_callee(name) {
|
"static call `{module}.{fn_name}`: no FnSig in workspace"
|
||||||
return self.lower_app(name, args, *tail);
|
))
|
||||||
|
})?;
|
||||||
|
self.emit_call(module, fn_name, &sig, args, *tail)
|
||||||
|
}
|
||||||
|
Callee::Indirect(inner) => {
|
||||||
|
let (callee_ssa, callee_ty) = self.lower_term(inner)?;
|
||||||
|
if callee_ty != "ptr" {
|
||||||
|
return Err(CodegenError::Internal(format!(
|
||||||
|
"indirect call: callee type must be ptr, got {callee_ty}"
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
let sig = self
|
||||||
|
.ssa_fn_sigs
|
||||||
|
.get(&callee_ssa)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
CodegenError::Internal(format!(
|
||||||
|
"indirect call: no FnSig recorded for `{callee_ssa}`"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
self.emit_indirect_call(&callee_ssa, &sig, args, *tail)
|
||||||
}
|
}
|
||||||
let (callee_ssa, callee_ty) = self.lower_term(callee_mterm)?;
|
},
|
||||||
if callee_ty != "ptr" {
|
|
||||||
return Err(CodegenError::Internal(format!(
|
|
||||||
"indirect call: callee type must be ptr, got {callee_ty}"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
let sig = self
|
|
||||||
.ssa_fn_sigs
|
|
||||||
.get(&callee_ssa)
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| {
|
|
||||||
CodegenError::Internal(format!(
|
|
||||||
"indirect call: no FnSig recorded for `{callee_ssa}`"
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
self.emit_indirect_call(&callee_ssa, &sig, args, *tail)
|
|
||||||
}
|
|
||||||
MTerm::Do { op, args, tail, .. } => self.lower_effect_op(op, args, *tail),
|
MTerm::Do { op, args, tail, .. } => self.lower_effect_op(op, args, *tail),
|
||||||
MTerm::Ctor { type_name, ctor, args, .. } => {
|
MTerm::Ctor { type_name, ctor, args, .. } => {
|
||||||
// pass the term pointer so `lower_ctor` can
|
// pass the term pointer so `lower_ctor` can
|
||||||
@@ -2425,7 +2428,13 @@ impl<'a> Emitter<'a> {
|
|||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lower_app(&mut self, name: &str, args: &[MArg], tail: bool) -> Result<(String, String)> {
|
/// Lower a `Callee::Builtin` call inline (opcode selection). Only
|
||||||
|
/// the operator / str-num builtins reach here; user/prelude fns are
|
||||||
|
/// `Callee::Static` and route to `emit_call`. (Pre-mir.2 this was
|
||||||
|
/// `lower_app`, which also walked the cross-module / current-module
|
||||||
|
/// / prelude resolution ladder — that ladder moved into
|
||||||
|
/// `lower_to_mir::classify_callee`.)
|
||||||
|
fn lower_builtin(&mut self, name: &str, args: &[MArg], tail: bool) -> Result<(String, String)> {
|
||||||
// 2026-05-21 operator-routing-eq-ord: the `if name == "=="`
|
// 2026-05-21 operator-routing-eq-ord: the `if name == "=="`
|
||||||
// short-circuit that routed through `lower_eq` is gone. `==`
|
// short-circuit that routed through `lower_eq` is gone. `==`
|
||||||
// is no longer a surface identifier; the typechecker
|
// is no longer a surface identifier; the typechecker
|
||||||
@@ -2619,94 +2628,9 @@ impl<'a> Emitter<'a> {
|
|||||||
return Ok((dst, "ptr".to_string()));
|
return Ok((dst, "ptr".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cross-module call: exactly one dot in the name → resolve via import map.
|
|
||||||
// Logic identical to the typechecker (see `synth` for `Term::Var`).
|
|
||||||
if name.matches('.').count() == 1 {
|
|
||||||
let (prefix, suffix) = name.split_once('.').expect("checked");
|
|
||||||
// fall back to direct module-name lookup when
|
|
||||||
// the prefix isn't in the current module's import_map. A
|
|
||||||
// post-mono synthesised body may carry cross-module
|
|
||||||
// references to modules its source template didn't import
|
|
||||||
// (e.g. `prelude.print__<UserType>` synthesised in
|
|
||||||
// `prelude` calls `show_user_adt.show__<UserType>` even
|
|
||||||
// though prelude does not import user modules). See the
|
|
||||||
// matching fallback in `resolve_top_level_fn` for the
|
|
||||||
// companion change on the fn-pointer Var-resolution path.
|
|
||||||
let target_module = match self.import_map.get(prefix).cloned() {
|
|
||||||
Some(m) => m,
|
|
||||||
None if self.module_user_fns.contains_key(prefix) => prefix.to_string(),
|
|
||||||
// #53: type-scoped `T.def` callee (e.g. `Counter.new`,
|
|
||||||
// produced by the monomorphic `Term::New` desugar). When
|
|
||||||
// `prefix` is not an import alias or module name but
|
|
||||||
// names a TypeDef in scope, the call resolves to the
|
|
||||||
// `def` fn in that type's home module — symmetric to the
|
|
||||||
// checker's TypeDef-first ladder. Keeps the documented
|
|
||||||
// `lower_app` ↔ `is_static_callee` lockstep honest:
|
|
||||||
// `is_static_callee` already returns true for every
|
|
||||||
// single-dot name, so this branch MUST resolve it.
|
|
||||||
None => match self.type_home_module(prefix) {
|
|
||||||
Some(home) => home,
|
|
||||||
None => {
|
|
||||||
return Err(CodegenError::Internal(format!(
|
|
||||||
"cross-module call `{name}`: prefix `{prefix}` not in import map"
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// iter 23.4: codegen-time poly-call dispatch removed. Post-mono
|
|
||||||
// every poly call site has been rewritten by `rewrite_mono_calls`
|
|
||||||
// to a monomorphic symbol, so the lookup-ladder below sees only
|
|
||||||
// user fns / consts / builtins. The pre-iter-23.4 path checked
|
|
||||||
// `module_polymorphic_fns` and dispatched to `lower_polymorphic_call`;
|
|
||||||
// both are gone (and the supporting Emitter fields with them).
|
|
||||||
let target_fns = self
|
|
||||||
.module_user_fns
|
|
||||||
.get(&target_module)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
CodegenError::Internal(format!(
|
|
||||||
"cross-module call `{name}`: target module `{target_module}` not found in workspace"
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
let sig = target_fns
|
|
||||||
.get(suffix)
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| {
|
|
||||||
CodegenError::Internal(format!(
|
|
||||||
"cross-module call `{name}`: def `{suffix}` not in module `{target_module}`"
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
return self.emit_call(&target_module, suffix, &sig, args, tail);
|
|
||||||
}
|
|
||||||
|
|
||||||
// iter 23.4: codegen-time poly-call dispatch removed (see above).
|
|
||||||
|
|
||||||
// User function in the current module?
|
|
||||||
if let Some(sig) = self
|
|
||||||
.module_user_fns
|
|
||||||
.get(self.module_name)
|
|
||||||
.and_then(|m| m.get(name))
|
|
||||||
.cloned()
|
|
||||||
{
|
|
||||||
return self.emit_call(self.module_name, name, &sig, args, tail);
|
|
||||||
}
|
|
||||||
|
|
||||||
// operator-routing-eq-ord.1: prelude fallback for monomorphic
|
|
||||||
// prelude fns called bare from a non-prelude module (e.g.
|
|
||||||
// `(app float_eq …)`). Mirrors the fallback already in
|
|
||||||
// `is_static_callee` and `resolve_top_level_fn`.
|
|
||||||
if self.module_name != "prelude" {
|
|
||||||
if let Some(sig) = self
|
|
||||||
.module_user_fns
|
|
||||||
.get("prelude")
|
|
||||||
.and_then(|m| m.get(name))
|
|
||||||
.cloned()
|
|
||||||
{
|
|
||||||
return self.emit_call("prelude", name, &sig, args, tail);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(CodegenError::Internal(format!(
|
Err(CodegenError::Internal(format!(
|
||||||
"unknown callee: `{name}`"
|
"lower_builtin: `{name}` is not a builtin (should have been \
|
||||||
|
classified as Static or Indirect in lower_to_mir)"
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2886,101 +2810,13 @@ impl<'a> Emitter<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// #53: returns the module that declares the named TypeDef — the
|
|
||||||
/// current module first, then imported modules. `None` if no module
|
|
||||||
/// in scope declares a TypeDef of that name. A type's presence in a
|
|
||||||
/// module is read from `module_ctor_index` (a TypeDef declares at
|
|
||||||
/// least one ctor, each `CtorRef` carrying its `type_name`). This
|
|
||||||
/// mirrors the checker's TypeDef-first ladder so codegen resolves a
|
|
||||||
/// type-scoped dotted callee (`T.def`) exactly where the checker
|
|
||||||
/// does. Used by `lower_app` and `resolve_top_level_fn`.
|
|
||||||
fn type_home_module(&self, type_name: &str) -> Option<String> {
|
|
||||||
if let Some(ctors) = self.module_ctor_index.get(self.module_name) {
|
|
||||||
if ctors.values().any(|c| c.type_name == type_name) {
|
|
||||||
return Some(self.module_name.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for target in self.import_map.values() {
|
|
||||||
if let Some(ctors) = self.module_ctor_index.get(target) {
|
|
||||||
if ctors.values().any(|c| c.type_name == type_name) {
|
|
||||||
return Some(target.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// is `name` a callee that can be resolved at compile time
|
|
||||||
/// (no fn-pointer needed)? True for builtin operators, the
|
|
||||||
/// current-module top-level fns (mono or poly), and qualified
|
|
||||||
/// `prefix.def`. Locals shadow this — the caller checks for
|
|
||||||
/// that first.
|
|
||||||
fn is_static_callee(&self, name: &str) -> bool {
|
|
||||||
if is_arithmetic_op(name) || name == "not" {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Floats iter 4.4: new fn-builtins (`neg`, `int_to_float`,
|
|
||||||
// `float_to_int_truncate`, `is_nan`, `float_to_str`) lower
|
|
||||||
// inline in `lower_app`, parallel to the operator path. Iter
|
|
||||||
// hs.4: `int_to_str` joins the list, lowering to
|
|
||||||
// `@ailang_int_to_str` from `runtime/str.c`.
|
|
||||||
if matches!(
|
|
||||||
name,
|
|
||||||
"neg"
|
|
||||||
| "int_to_float"
|
|
||||||
| "float_to_int_truncate"
|
|
||||||
| "is_nan"
|
|
||||||
| "float_to_str"
|
|
||||||
| "int_to_str"
|
|
||||||
| "bool_to_str"
|
|
||||||
| "str_clone"
|
|
||||||
| "str_concat"
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if name.matches('.').count() == 1 {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// iter 23.4: poly-def arm dropped — post-mono there are no
|
|
||||||
// `Type::Forall` defs in `module_user_fns` to begin with, and
|
|
||||||
// `module_polymorphic_fns` no longer exists.
|
|
||||||
if self
|
|
||||||
.module_user_fns
|
|
||||||
.get(self.module_name)
|
|
||||||
.is_some_and(|m| m.contains_key(name))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// operator-routing-eq-ord.1: the typechecker auto-imports
|
|
||||||
// prelude into every non-prelude module so a bare reference
|
|
||||||
// like `(app float_eq …)` typechecks against the prelude's
|
|
||||||
// monomorphic `float_eq` fn. The codegen needs the matching
|
|
||||||
// resolution: if the name is not in the current module but
|
|
||||||
// IS in prelude's monomorphic fn table, treat it as a static
|
|
||||||
// callee — the call lowers to `@ail_prelude_<name>` directly.
|
|
||||||
// Without this fallback, post-mono polymorphic-prelude calls
|
|
||||||
// (e.g. `print`) work because the mono pass synthesises a
|
|
||||||
// mono arm `print__T` into the user's module, but monomorphic
|
|
||||||
// prelude fns (no Forall to instantiate) never get a mirror
|
|
||||||
// arm in user modules and would otherwise fail at codegen
|
|
||||||
// with `unknown variable`.
|
|
||||||
if self.module_name != "prelude"
|
|
||||||
&& self
|
|
||||||
.module_user_fns
|
|
||||||
.get("prelude")
|
|
||||||
.is_some_and(|m| m.contains_key(name))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
/// resolve `name` to a top-level fn-value, i.e. the address
|
/// resolve `name` to a top-level fn-value, i.e. the address
|
||||||
/// of its static closure pair `@ail_<m>_<def>_clos`, plus the user-
|
/// of its static closure pair `@ail_<m>_<def>_clos`, plus the user-
|
||||||
/// visible FnSig (params/ret WITHOUT the env_ptr — that's added at
|
/// visible FnSig (params/ret WITHOUT the env_ptr — that's added at
|
||||||
/// the call site by the closure ABI). Returns None if the name does
|
/// the call site by the closure ABI). Returns None if the name does
|
||||||
/// not refer to a top-level fn. Operators / `not` are not first-
|
/// not refer to a top-level fn. Operators / `not` are not first-
|
||||||
/// class values; `is_static_callee` filters them earlier.
|
/// class values and never reach here as a `Callee::Static`/`Builtin`
|
||||||
|
/// — they are classified in `lower_to_mir` and lowered inline.
|
||||||
fn resolve_top_level_fn(&self, name: &str) -> Option<(String, FnSig)> {
|
fn resolve_top_level_fn(&self, name: &str) -> Option<(String, FnSig)> {
|
||||||
if name.matches('.').count() == 1 {
|
if name.matches('.').count() == 1 {
|
||||||
let (prefix, suffix) = name.split_once('.')?;
|
let (prefix, suffix) = name.split_once('.')?;
|
||||||
@@ -2996,15 +2832,19 @@ impl<'a> Emitter<'a> {
|
|||||||
// a valid post-mono construct because both ends were
|
// a valid post-mono construct because both ends were
|
||||||
// independently typechecked under their original module
|
// independently typechecked under their original module
|
||||||
// contexts before mono ran).
|
// contexts before mono ran).
|
||||||
// #53: a type-scoped `T.def` prefix (e.g. `Counter.new`)
|
// mir.2: the codegen-side type-home resolver is gone. A
|
||||||
// resolves to the type's home module, symmetric to the
|
// type-scoped `T.fn` reference in VALUE position (the only
|
||||||
// checker's TypeDef-first ladder and the matching arm in
|
// path that used the type-home fallback here) is unreachable in the current
|
||||||
// `lower_app`'s cross-module branch. Tried after import_map
|
// corpus — every dotted `T.fn` is a call head, resolved as
|
||||||
// and the literal-module-name fallback.
|
// `Callee::Static` in lower_to_mir. The remaining value-
|
||||||
|
// position dotted forms (`Mod.fn`) resolve through
|
||||||
|
// `import_map` / the module-name fallback. If a type-scoped
|
||||||
|
// fn is ever taken as a first-class value, its home-module
|
||||||
|
// resolution moves into MIR at mir.5 (the last re-derivation
|
||||||
|
// residue); until then the module-name fallback is correct.
|
||||||
let target: String = match self.import_map.get(prefix) {
|
let target: String = match self.import_map.get(prefix) {
|
||||||
Some(m) => m.clone(),
|
Some(m) => m.clone(),
|
||||||
None if self.module_user_fns.contains_key(prefix) => prefix.to_string(),
|
None => prefix.to_string(),
|
||||||
None => self.type_home_module(prefix).unwrap_or_else(|| prefix.to_string()),
|
|
||||||
};
|
};
|
||||||
let sig = self.module_user_fns.get(&target)?.get(suffix)?.clone();
|
let sig = self.module_user_fns.get(&target)?.get(suffix)?.clone();
|
||||||
return Some((format!("@ail_{target}_{suffix}_clos"), sig));
|
return Some((format!("@ail_{target}_{suffix}_clos"), sig));
|
||||||
|
|||||||
@@ -132,9 +132,24 @@ pub struct MArg {
|
|||||||
pub consume_count: u32,
|
pub consume_count: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A resolved App callee. `Static`/`Builtin` are filled by mir.2's
|
||||||
|
/// `lower_to_mir::classify_callee`, which mirrors check's `synth`
|
||||||
|
/// `Term::Var` ladder; `Indirect` is a genuinely dynamic callee
|
||||||
|
/// (fn-typed local/param, closure value, or a name shadowed by a
|
||||||
|
/// local). `sig` is the callee's fn-type — the lossless replacement
|
||||||
|
/// for the sub-term's `ty()` that codegen's drop path reads for the
|
||||||
|
/// callee `ret_mode`.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Callee {
|
pub enum Callee {
|
||||||
Static { module: String, fn_name: String },
|
/// A statically-resolved user/prelude fn: codegen routes it to
|
||||||
|
/// `emit_call(module, fn_name, …)` with `module` pre-resolved
|
||||||
|
/// (replacing codegen's `type_home_module` ladder).
|
||||||
|
Static { module: String, fn_name: String, sig: Type },
|
||||||
|
/// An operator / intrinsic builtin (`+`, `not`, `str_concat`, …):
|
||||||
|
/// codegen lowers it inline by `name` (opcode selection), never
|
||||||
|
/// via `emit_call`.
|
||||||
|
Builtin { name: String, sig: Type },
|
||||||
|
/// A dynamic callee — lower the boxed term to a fn-pointer.
|
||||||
Indirect(Box<MTerm>),
|
Indirect(Box<MTerm>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ pub enum MTerm {
|
|||||||
// … one variant per Term node, each with `ty`
|
// … one variant per Term node, each with `ty`
|
||||||
}
|
}
|
||||||
pub struct MArg { pub term: MTerm, pub mode: Mode, pub consume_count: u32 } // (3)
|
pub struct MArg { pub term: MTerm, pub mode: Mode, pub consume_count: u32 } // (3)
|
||||||
pub enum Callee { Static { module: String, fn_name: String }, Indirect(Box<MTerm>) } // (2)
|
pub enum Callee { Static { module: String, fn_name: String, sig: Type }, Builtin { name: String, sig: Type }, Indirect(Box<MTerm>) } // (2) — sig carries the callee fn-type for drop ret_mode; Builtin for inline-lowered operators/intrinsics
|
||||||
pub enum StrRep { Heap, Static } // (4)
|
pub enum StrRep { Heap, Static } // (4)
|
||||||
pub enum Mode { Owned, Borrow }
|
pub enum Mode { Owned, Borrow }
|
||||||
```
|
```
|
||||||
@@ -311,6 +311,17 @@ class and deletes the matching codegen re-derivation.
|
|||||||
| **mir.4** | `StrRep` (loop-carried `Str` ⇒ `Heap` via `str_clone` at loop entry) | the `!is_str` `recur` dec gate | #49 `live=0`; `#[ignore]` lifted |
|
| **mir.4** | `StrRep` (loop-carried `Str` ⇒ `Heap` via `str_clone` at loop entry) | the `!is_str` `recur` dec gate | #49 `live=0`; `#[ignore]` lifted |
|
||||||
| **mir.5** | element-type / `Term::New` fully on MIR + **ledger** | last re-derivation residue | #51 guard green via MIR; ledger consistent |
|
| **mir.5** | element-type / `Term::New` fully on MIR + **ledger** | last re-derivation residue | #51 guard green via MIR; ledger consistent |
|
||||||
|
|
||||||
|
> mir.2 refinement (discovered in planning, `docs/plans/0116-mir.2-callee-static.md`):
|
||||||
|
> `Callee` gains a `Builtin { name, sig }` variant (operators / str-num
|
||||||
|
> intrinsics have no `module`/`fn_name`) and a `sig: Type` on each
|
||||||
|
> resolved variant (codegen's drop path reads the callee `ret_mode` off
|
||||||
|
> it — the lossless replacement for the pre-mir.2 `Indirect(inner).ty()`
|
||||||
|
> read; this is NOT a mir.3 pull-forward, the `MArg` param-modes stay at
|
||||||
|
> mir.1 defaults). `type_home_module` is fully deletable: its
|
||||||
|
> value-position consumer (`resolve_top_level_fn`) is unreachable for
|
||||||
|
> type-scoped names in the current corpus, so the acceptance
|
||||||
|
> "type_home_module grep-clean" holds.
|
||||||
|
|
||||||
mir.5 carries the ledger work as part of the milestone (per CLAUDE.md:
|
mir.5 carries the ledger work as part of the milestone (per CLAUDE.md:
|
||||||
contract changes ship with the iteration that needs them):
|
contract changes ship with the iteration that needs them):
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
(module classify_pin
|
||||||
|
(fn bump (type (fn-type (params (con Int)) (ret (con Int)))) (params n)
|
||||||
|
(body (app + n 1)))
|
||||||
|
(fn main (type (fn-type (params) (ret (con Int)))) (params)
|
||||||
|
(body (app bump 41))))
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
(module mir2_callee_kinds
|
||||||
|
(fn bump (type (fn-type (params (con Int)) (ret (con Int)))) (params n)
|
||||||
|
(body (app + n 1)))
|
||||||
|
(fn main (type (fn-type (params) (ret (con Unit)) (effects IO))) (params)
|
||||||
|
(body (let r (app bump 41)
|
||||||
|
(do io/print_str (app int_to_str r))))))
|
||||||
Reference in New Issue
Block a user