6fdb45d2f2
Single iter shipping the post-milestone-24 follow-up named in
docs/specs/2026-05-14-retire-per-type-print-effects.md. After this
iter the only surviving direct-output effect-op is `io/print_str`;
all per-type print primitives are replaced by the polymorphic
`print` helper (prelude, iter 24.3).
Components:
- 92 examples/*.ail fixtures migrated (do io/print_<T> x) →
(app print x); 6 .prose.txt snapshots regenerated via `ail prose`.
- Four-site lockstep compiler deletion: crates/ailang-check/src/builtins.rs
(3 effect_ops.insert blocks + 3 list() rows + the
install_io_print_float_signature test + module + EffectOpSig
doc-comments); crates/ailang-codegen/src/lib.rs lower_app
(3 arms + lowers_io_print_float test); crates/ailang-codegen/src/synth.rs
builtin_effect_op_ret match-arm pattern. Dead `intern_string`
helper removed as a follow-up.
- Five incidental test-body migrations (ailang-check x2, ailang-core
spec_drift + design_schema_drift, ailang-surface/src/lex.rs,
ailang-prose/src/lib.rs round-trip test).
- Cat B test-harness patch: six IR-shape tests in
crates/ail/tests/e2e.rs gained a monomorphise_workspace call
before lower_workspace_with_alloc so they follow the same
pipeline as `ail build` (the home-rolled desugar+lift loop
stayed because mono's precondition is "already lifted"; mono
inserts after lift).
- Six doc-comment touch-ups (lex.rs module doc, parse.rs
diagnostic example, ail/src/main.rs x2, runtime/str.c %g anchor,
crates/ailang-core/specs/form_a.md surface-spec example).
- DESIGN.md seven-site sweep (Decision 11 example, Polymorphic
print past-tense, Heap-Str output sentence, effect-op
invocation comment, Float NaN paragraph re-anchored on
float_to_str, two "What is supported" lists).
- Three E2E test-comment polish + four IR-snapshot refresh + one
canonical-hash pin update (plan-unanticipated downstream
consequences of the corpus migration).
- bench/{check,compile_check,cross_lang}.py: all exit 0; no
ratification needed.
- Roadmap entry struck through; per-iter journal at
docs/journals/2026-05-14-iter-rpe.1.md.
Tests 564/0/3. cargo clippy and cargo doc: zero warnings.
Two upstream codegen bugs surfaced during the first BLOCKED
attempt and were fixed in separate iters before this retry:
- 1fb225e bugfix: mono cursor misalignment at poly-free-fn Var
with class-constrained Forall.
- feb9413 bugfix: print leak — propagate ret_mode through rigid
substitution + prelude Show.show ret_mode.
Known debt (carried forward for next /audit):
- Emitter.strings field is functionally dead post-iter (orphan
after intern_string removal); cycles over empty map harmlessly.
316 lines
13 KiB
Rust
316 lines
13 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()),
|
|
// Iter 13b: 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
|
|
}
|
|
|
|
/// Iter 12b: 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> {
|
|
// Iter 22-floats.3: 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(),
|
|
// Iter 16e: `==` 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,
|
|
},
|
|
// Iter 16d: `__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() }),
|
|
},
|
|
// Iter 22-floats.3: 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,
|
|
},
|
|
// Iter 22-floats.3: 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,
|
|
})
|
|
}
|
|
|
|
/// Iter 12b: 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
|
|
}
|