Files
AILang/crates/ailang-codegen/src/synth.rs
T
Brummel ac4d545570 source: scrub iter-code / Decision-N residue from inline comments
Follow-up to bcd4181: the remaining ~530 inline `//` and `///`
comments still carrying opaque shorthand are now reformulated to
their content phrases. The only surviving `iter-<code>` reference
in source is the literal filename
`docs/journals/2026-05-13-iter-mq.3.md` (a real journal file).

Sweep covered:
  - `// Iter X.Y: <text>` prefixes (Iter 13a / 14a / 14e / 15g-aux /
    16b.x / 16d / 16e / 18b / 18c.x / 18d.x / 18e / 18g.x / 19a /
    19a.1 / 19b / 20a / 20f / 22-floats.x / 22b.x / 22c / 23.x /
    24.1 / cli-diag-human / hs.x / str-concat / etc.) — fully
    removed; the descriptive text that followed each prefix stays.
  - `// (Decision N)` and `per Decision N` and `Decision N axis 3` —
    replaced with the content phrase plus the relevant contract
    file (`design/contracts/tail-calls.md` for Decision 8,
    `design/contracts/memory-model.md` for Decision 10,
    `design/contracts/typeclasses.md` and `design/models/typeclasses.md`
    for Decision 11, `design/contracts/authoring-surface.md` for
    Decision 6, "the transitional dual-allocator" for Decision 9,
    "Effect prose" for Decision 3).
  - `// mq.X / mq.X (Task N) / mq.X journal / mq.X invariant` ->
    "the canonical-class-form rule / Class-class repurpose /
    method-dispatch-refactor journal / canonical-class-form
    invariant".
  - `// ct.X / ct.X (canonical-type-names) / ct.1.5a + ctt.2 /
    ct.2 Task N` -> "the canonical-form rule for type references /
    the canonical-form normalisation step / canonical-type-lookup
    refactor".
  - `// eob.X` -> "heap-Str-ABI" / "the Str carve-out".
  - `// rpe.X` -> "the per-type-print-op retirement".
  - `// post-mq.X / Pre-ct.X / pre-mq.X` -> "post-canonical-class-form" /
    "Pre-canonical-type-form" etc.
  - `/// Iter X regression: / /// Iter X.Y: / /// Iter A arm-close /
    /// Iter ct.4 (...): / /// Iter rpe.1 ...` -> descriptive
    phrases.

The journal filename in `crates/ailang-core/src/workspace.rs:573`
stays verbatim because it points at an actual file under
`docs/journals/`.

Tests: full `cargo test --workspace` green (80/80 test-result blocks
clean, no FAILED line). design_index_pin 5/5 + docs_honesty_pin 5/5
gating tests pass.
2026-05-20 09:58:04 +02:00

316 lines
12 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
}
/// AILang type of a builtin operator. Used by
/// `synth_arg_type` for arg-type inference at polymorphic call sites.
/// Mirrors what the typechecker installs in its env via `builtins`.
pub(crate) fn builtin_ail_type(name: &str) -> Option<Type> {
// same widening as `crates/ailang-check/src/
// builtins.rs` — `+`/`-`/`*`/`/` and `!=`/`<`/`<=`/`>`/`>=` are
// polymorphic. `%` stays monomorphic-Int. Codegen lowering for
// these ops still goes through `builtin_binop` (Int-only) in
// iter 3; iter 4 converts that to type-dispatched.
let poly_a_a_to_a = || Type::Forall {
vars: vec!["a".into()],
constraints: vec![],
body: Box::new(Type::Fn {
params: vec![
Type::Var { name: "a".into() },
Type::Var { name: "a".into() },
],
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
}),
};
let poly_a_a_to_bool = || Type::Forall {
vars: vec!["a".into()],
constraints: vec![],
body: Box::new(Type::Fn {
params: vec![
Type::Var { name: "a".into() },
Type::Var { name: "a".into() },
],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
}),
};
let int_int_int = || Type::Fn {
params: vec![Type::int(), Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
};
Some(match name {
"+" | "-" | "*" | "/" => poly_a_a_to_a(),
"%" => int_int_int(),
"!=" | "<" | "<=" | ">" | ">=" => poly_a_a_to_bool(),
// `==` is polymorphic — `forall a. (a, a) -> Bool`.
// The mono pipeline asks `synth_arg_type` for the actual arg
// types at the call site; `lower_app` then dispatches to the
// right LLVM instruction (icmp eq i64 / i1, @strcmp, or
// constant i1 1) on those resolved types.
"==" => Type::Forall {
vars: vec!["a".into()],
constraints: vec![],
body: Box::new(Type::Fn {
params: vec![
Type::Var { name: "a".into() },
Type::Var { name: "a".into() },
],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
}),
},
"not" => Type::Fn {
params: vec![Type::bool_()],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
// `__unreachable__` is the polymorphic bottom value
// (`forall a. a`). Mirrors the typechecker's `builtins::install`.
"__unreachable__" => Type::Forall {
vars: vec!["a".into()],
constraints: vec![],
body: Box::new(Type::Var { name: "a".into() }),
},
// Float-conversion and inspection builtins.
// Same lockstep with `crates/ailang-check/src/builtins.rs`.
"neg" => Type::Forall {
vars: vec!["a".into()],
constraints: vec![],
body: Box::new(Type::Fn {
params: vec![Type::Var { name: "a".into() }],
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
}),
},
"int_to_float" => Type::Fn {
params: vec![Type::int()],
ret: Box::new(Type::float()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
"float_to_int_truncate" => Type::Fn {
params: vec![Type::float()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
"float_to_str" => Type::Fn {
params: vec![Type::float()],
ret: Box::new(Type::str_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Own,
},
"int_to_str" => Type::Fn {
params: vec![Type::int()],
ret: Box::new(Type::str_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Own,
},
"bool_to_str" => Type::Fn {
params: vec![Type::bool_()],
ret: Box::new(Type::str_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Own,
},
"str_clone" => Type::Fn {
params: vec![Type::str_()],
ret: Box::new(Type::str_()),
effects: vec![],
param_modes: vec![ParamMode::Borrow],
ret_mode: ParamMode::Own,
},
"is_nan" => Type::Fn {
params: vec![Type::float()],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
// Float bit-pattern constants. Bare values,
// not fns — referenced via `Term::Var`. Mirrors the
// typechecker's `builtins::install`. Codegen emits LLVM hex-
// float literals at the use site in iter 4.
"nan" | "inf" | "neg_inf" => Type::float(),
_ => return None,
})
}
/// AILang return type of a built-in effect op. The op's
/// param signature is irrelevant here since we only consume the ret.
pub(crate) fn builtin_effect_op_ret(op: &str) -> Option<Type> {
Some(match op {
"io/print_str" => Type::unit(),
_ => return None,
})
}
// 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`) resolves the arg type
/// via `synth_arg_type` 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
/// the no-regression invariant — pre-iter-4 codegen routed
/// `<`/`<=`/`>`/`>=`/`!=` through the same Int-only `builtin_binop`
/// table. Iter 4.3 adds the Float arms (`("fcmp olt", "double", "i1")`
/// and friends).
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")),
// Comparison ops: operand types differ, result is always `i1`.
// Int-arm comparisons preserved from pre-iter-4 `builtin_binop`;
// Float arms land in iter 4.3.
("!=", true, _) => Some(("icmp ne", "i64", "i1")),
("<", true, _) => Some(("icmp slt", "i64", "i1")),
("<=", true, _) => Some(("icmp sle", "i64", "i1")),
(">", true, _) => Some(("icmp sgt", "i64", "i1")),
(">=", true, _) => Some(("icmp sge", "i64", "i1")),
// Float comparison arms (Floats iter 4.3). Note: `!=` Float
// uses `fcmp une` ("unordered or not equal") — NOT `one`
// ("ordered and not equal"), which would return `false` for
// `nan != nan` and violate IEEE / spec A5.
("!=", _, true) => Some(("fcmp une", "double", "i1")),
("<", _, true) => Some(("fcmp olt", "double", "i1")),
("<=", _, true) => Some(("fcmp ole", "double", "i1")),
(">", _, true) => Some(("fcmp ogt", "double", "i1")),
(">=", _, true) => Some(("fcmp oge", "double", "i1")),
_ => 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
}