895ba846e8
Atomic completion of spec iteration mir.1 (docs/specs/0060-typed-mir.md): codegen now consumes the typed MIR produced by `lower_to_mir` instead of re-deriving types from the bare `ast::Term`. Every codegen helper that took `&Term` (lower_term, lower_app, drop.rs, match_lower.rs) takes `&MTerm` and reads each node's checker-proved type off `MTerm::ty()`. The build path is `Workspace -> elaborate_workspace -> MirWorkspace -> lower_workspace`; the public `lower_workspace*` entry points and their 18 call sites thread `&MirWorkspace`. The codegen-side type re-derivers `synth_with_extras` + `synth_arg_type` (and the `builtin_ail_type` / `builtin_effect_op_ret` mirror tables) are deleted — grep-clean. The three re-derivers the spec keeps until mir.2/mir.3 (`type_home_module`, `is_static_callee`, the second `infer_module_with_cross`) stay. The mechanical Term->MTerm match-arm conversion was straightforward and compiler-enforced. The substance was a set of producer-side correctness gaps that only surface once codegen reads `MTerm::ty()` and once the build path re-synthesises the post-mono AST through the canonical `synth` (which, unlike the old codegen, fully re-unifies). Each was root-caused against a failing e2e fixture: 1. `qualify_local_types` stripped fn-type modes (rebuilt `Type::Fn` with empty `param_modes` / `Implicit` `ret_mode`), so a monomorphised polymorphic intrinsic (`RawBuf.set`) lost its `Own` ret-mode and the owned temporary leaked at the call site. Made mode-preserving, like its sister `qualify_workspace_types` and `Subst::apply` (449df13). 2. `lower_to_mir::synth_pure` synthesises each node in isolation, so a nullary polymorphic ctor (`Nil : List<a>`) left its element type an unbound `$m` metavar that the canonical synth never pins. Codegen's mono unifier (`unify_for_subst`) already has a wildcard for exactly this — `$u`, the spelling the now-deleted codegen synth used — so the typed-MIR boundary normalises every residual `$m` to `$u` once (`wildcard_residual_metavars`), rather than teaching each consumer to tolerate a raw metavar. 3. The class-method mono arm (`synthesise_mono_fn`) substituted the registry-canonical *qualified* instance type into a method appended to the instance's own module, minting a `show_user_adt.IntBox` param against a bare-`IntBox` body. Localised to bare before substitution, symmetric to the free-fn arm (600565d). 4. Monomorphisation synthesises *downward* class-dispatch references — prelude's `print__<IntBox>` names the instance module `show_user_adt` that prelude never imports. The post-mono re-synth in `lower_module` seeds every workspace module name as an identity import (excluding the current module, to keep own types bare) so the qualified-var path resolves these; the canonical `synth` used by `check_workspace` stays strict. 5. Post-mono, a cross-module callee can name the consumer's *own* ADT qualified (`show_user_adt.IntBox`) where the consumer synthesises it bare — a spelling split that cannot exist pre-mono (the param is polymorphic there). synth's App arm strips the current module's own qualifier from both sides before unifying (`strip_own_module_qual`), a no-op pre-mono. Acceptance: whole workspace suite green (698 tests); `e2e` 98/98 and `show_print_e2e` 3/3; `synth_with_extras`/`synth_arg_type` grep-clean in codegen; #51/#53 fixtures build and run; lower_to_mir_ty pins green. The #49 heap-Str loop-binder leak remains ignored (lifts at mir.4). Builds on the standalone producer fix (600565d, free-fn own-ADT localisation) and the two standalone mode-preservation fixes (449df13Subst::apply); those landed separately as they are independently correct and inert on the old codegen.
148 lines
6.3 KiB
Rust
148 lines
6.3 KiB
Rust
//! Pure type-synthesis and IR-shaping helpers.
|
|
//!
|
|
//! Free functions extracted from `lib.rs` during the 18g tidy split.
|
|
//! None of these touch the `Emitter` state — they map AILang `Type`s
|
|
//! to LLVM type strings, mangling descriptors, or built-in op
|
|
//! signatures. Submodule access to the parent module's private
|
|
//! `Result`, `CodegenError`, and `FnSig` works through normal Rust
|
|
//! visibility (a submodule sees its parent's private items).
|
|
|
|
use ailang_core::ast::*;
|
|
|
|
use super::{CodegenError, FnSig, Result};
|
|
|
|
pub(crate) fn llvm_type(t: &Type) -> Result<String> {
|
|
match t {
|
|
Type::Con { name, .. } => match name.as_str() {
|
|
"Int" => Ok("i64".into()),
|
|
"Bool" => Ok("i1".into()),
|
|
"Unit" => Ok("i8".into()),
|
|
"Str" => Ok("ptr".into()),
|
|
"Float" => Ok("double".into()),
|
|
// All other type names are treated as ADT (boxed).
|
|
// If the typechecker didn't reject this earlier, it's
|
|
// intentional — otherwise `ptr` would mask a wrong value.
|
|
_ => Ok("ptr".into()),
|
|
},
|
|
// Function values (Iter 7): all fn-pointers are opaque `ptr`
|
|
// at the LLVM level. The actual signature travels via the
|
|
// emitter's `ssa_fn_sigs` sidetable.
|
|
Type::Fn { .. } => Ok("ptr".into()),
|
|
// an unresolved rigid `Type::Var` reaching codegen is
|
|
// a substitution bug. Earlier this silently lowered as `ptr`
|
|
// (via the ADT fallback) and produced garbage IR; failing loudly
|
|
// here surfaces the bug in the test suite.
|
|
Type::Var { name } => Err(CodegenError::UnsupportedType(format!(
|
|
"unresolved type var `{name}` in codegen"
|
|
))),
|
|
other => Err(CodegenError::UnsupportedType(
|
|
ailang_core::pretty::type_to_string(other),
|
|
)),
|
|
}
|
|
}
|
|
|
|
/// Builds an `FnSig` (LLVM types only) from an AILang `Type::Fn`.
|
|
/// Returns `None` for non-function types or if any param/ret type fails
|
|
/// to lower (e.g. a residual `Type::Var` or `Forall` that the typechecker
|
|
/// would reject before us).
|
|
pub(crate) fn fn_sig_from_type(t: &Type) -> Option<FnSig> {
|
|
if let Type::Fn { params, ret, .. } = t {
|
|
let p: Result<Vec<String>> = params.iter().map(llvm_type).collect();
|
|
let r = llvm_type(ret);
|
|
if let (Ok(p), Ok(r)) = (p, r) {
|
|
return Some(FnSig { params: p, ret: r });
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
// mir.1b: `builtin_ail_type` (the codegen-side mirror of a builtin's
|
|
// AILang type) and `builtin_effect_op_ret` were read only by the
|
|
// codegen-side type re-derivation deleted in this iteration. Codegen
|
|
// now reads `MTerm::ty()` off the typed MIR, so the mirror table is
|
|
// gone — both definitions removed.
|
|
|
|
// iter 23.4: `type_descriptor` was the helper that mangled a `Type`
|
|
// into an identifier-safe suffix for the codegen-side mono mangling
|
|
// (`Int → I`, `Bool → B`, etc.). The unified mono pass produces
|
|
// surface-named mono symbols instead (`Int → "Int"`, parameterised
|
|
// via 8-hex hash) — see `ailang_check::mono::mono_symbol_n`.
|
|
|
|
/// Floats iter 4.2: arithmetic / comparison ops are type-dispatched
|
|
/// over `{Int, Float}`. Caller (`lower_app`) reads the arg type off
|
|
/// `MTerm::ty()` and passes it here. Returns the
|
|
/// `(instruction, operand_llvm_type, result_llvm_type)` triple to
|
|
/// emit. `%` stays Int-only.
|
|
///
|
|
/// The triple carries operand and result types separately because
|
|
/// comparison ops produce `i1` regardless of operand width
|
|
/// (e.g. `icmp slt i64 ..., ... -> i1`); arithmetic produces the
|
|
/// same type as its operands. Keeping both in the table localises
|
|
/// the comparison-vs-arithmetic distinction here — the caller no
|
|
/// longer has to second-guess which arm fired.
|
|
///
|
|
/// Comparison-op Int arms are kept here in iter 4.2 to preserve
|
|
/// `builtin_binop_typed` is now arithmetic-only. Comparators
|
|
/// (`==` / `!=` / `<` / `<=` / `>` / `>=`) were removed in iter
|
|
/// operator-routing-eq-ord.1 — surface comparison routes through
|
|
/// the prelude.Eq / Ord class-method dispatch, with primitive
|
|
/// instance bodies emitted via `try_emit_primitive_instance_body`
|
|
/// in lib.rs (Eq Int / Bool / Str / Unit → `icmp`; Ord Int / Bool
|
|
/// / Str → `icmp` + Ordering ctor; float_eq / float_ne / float_lt
|
|
/// / float_le / float_gt / float_ge → `fcmp`).
|
|
pub(crate) fn builtin_binop_typed(
|
|
name: &str,
|
|
arg_ty: &Type,
|
|
) -> Option<(&'static str, &'static str, &'static str)> {
|
|
let is_int = matches!(arg_ty, Type::Con { name, .. } if name == "Int");
|
|
let is_float = matches!(arg_ty, Type::Con { name, .. } if name == "Float");
|
|
match (name, is_int, is_float) {
|
|
("+", true, _) => Some(("add", "i64", "i64")),
|
|
("+", _, true) => Some(("fadd", "double", "double")),
|
|
("-", true, _) => Some(("sub", "i64", "i64")),
|
|
("-", _, true) => Some(("fsub", "double", "double")),
|
|
("*", true, _) => Some(("mul", "i64", "i64")),
|
|
("*", _, true) => Some(("fmul", "double", "double")),
|
|
("/", true, _) => Some(("sdiv", "i64", "i64")),
|
|
("/", _, true) => Some(("fdiv", "double", "double")),
|
|
("%", true, _) => Some(("srem", "i64", "i64")),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn c_byte_len(s: &str) -> usize {
|
|
s.len() + 1 // + NUL terminator
|
|
}
|
|
|
|
/// Escapes a string for LLVM IR `c"..."`. All bytes outside
|
|
/// 0x20..0x7E are escaped as `\HH`; `"` and `\` likewise. Ends with `\00`.
|
|
pub(crate) fn default_triple() -> &'static str {
|
|
// In the MVP we query the compile host. For cross-compilation this
|
|
// would need to be configurable — not needed now.
|
|
if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") {
|
|
"x86_64-pc-linux-gnu"
|
|
} else if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") {
|
|
"arm64-apple-darwin"
|
|
} else if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") {
|
|
"x86_64-apple-darwin"
|
|
} else if cfg!(target_arch = "aarch64") {
|
|
"aarch64-unknown-linux-gnu"
|
|
} else {
|
|
"x86_64-pc-linux-gnu"
|
|
}
|
|
}
|
|
|
|
pub(crate) fn ll_string_literal(s: &str) -> String {
|
|
let mut out = String::new();
|
|
for &b in s.as_bytes() {
|
|
match b {
|
|
b'"' => out.push_str("\\22"),
|
|
b'\\' => out.push_str("\\5C"),
|
|
0x20..=0x7E => out.push(b as char),
|
|
_ => out.push_str(&format!("\\{:02X}", b)),
|
|
}
|
|
}
|
|
out.push_str("\\00");
|
|
out
|
|
}
|