8ac8756682
Ships RawBuf end-to-end as a consumer of the raw-buf.3 scope-qualified
intrinsic mechanism, plus the general Term::New construction sugar.
Working subset committed; the drop-CALL ratification (no-leak) is
re-carved to raw-buf.5 (see b49f57d) — the flat drop FUNCTION ships here,
its call-insertion needs a codegen resolution mechanism.
What ships (all green):
- raw_buf kernel-tier submodule (crates/ailang-kernel/src/raw_buf/):
RawBuf TypeDef (param-in {Int,Float,Bool}, ctor B a) + four
(intrinsic) ops new/get/set/size. parse_raw_buf + workspace injection
(kernel-tier auto-imported); workspace count 4 -> 5.
- 12 scope-qualified INTERCEPTS entries RawBuf_{new,get,set,size}__{Int,
Float,Bool} + emit fns: new allocs an @ailang_rc_alloc slab
(8-byte i64 size header + n*width element bytes), get/set
getelementptr+load/store at offset 8 + i*width, size loads the header.
Mechanical on the raw-buf.3 naming + bijection machinery; bijection
green (4 markers -> 12 entries).
- Term::New desugar (crates/ailang-core/src/desugar.rs): (new T <types>
<values>) -> (app T.new <values>), runs before check so the
type-scoped callee flows through the raw-buf.3 scope threading to
RawBuf_new__T; drops the NewArg::Type (element type inferred from
use). Both codegen Term::New deferral arms removed (replaced with
unreachable!). Ratified by new_stubt_builds_and_runs.
- Flat intrinsic-storage drop FUNCTION @drop_raw_buf_RawBuf (single
@ailang_rc_dec on the slab), emitted for any TypeDef whose new op is
(intrinsic)-bodied — distinguishes RawBuf (intrinsic new) from StubT
(real-body new -> generic ADT drop).
- E2E: raw_buf_int (-> 60), raw_buf_float (-> 4.0), raw_buf_bool
(-> 42), raw_buf_param_in_reject (param-not-in-restricted-set).
In-scope additions beyond the literal plan (both sound, ratified):
- qualify_workspace_term now normalises a monomorphic cross-module
type-scoped callee (StubT.new) to <home>.f; without it
new_stubt_builds_and_runs cannot build (StubT.new is monomorphic, so
the raw-buf.3 poly-mono path mints no symbol). Same-module + polymorphic
callees carved out.
- diagnostic-behaviour: (new T ..) missing-new-op now surfaces
type-scoped-member-not-found (desugar runs before check, bypassing
synth's Term::New arm); new-arg-kind-mismatch obsoleted. Two unit
tests updated to the new behaviour. param-in reject unaffected.
The 5 .ll snapshots gained @drop_raw_buf_RawBuf (+ partial) — injecting
raw_buf emits its drop fn into every program; benign, regenerated to the
final IR. (The plan's "snapshots stay green" assumption was wrong.)
Verification (orchestrator, this session): cargo test --workspace 676
passed / 0 failed / 2 ignored. raw_buf_int_e2e prints 60; bijection,
round-trip, new_stubt, float/bool/reject all green. The raw_buf_no_leak
test is NOT in this commit — it moves to raw-buf.5 with the drop-call
resolution that makes it pass (the slab currently leaks at scope close;
tracked, fixed next iter; milestone not released until close).
981 lines
36 KiB
Rust
981 lines
36 KiB
Rust
//! Codegen intercept registry — one dispatch table for every
|
|
//! mono-symbol whose body the codegen supplies as LLVM IR
|
|
//! directly (replacing the .ail placeholder body that the
|
|
//! intercept's home module ships for round-trip stability).
|
|
//!
|
|
//! Each entry carries:
|
|
//! - `name` — the mono symbol the dispatch keys on
|
|
//! - `expected_params` — LLVM-IR param types as text
|
|
//! - `expected_ret` — LLVM-IR return type as text
|
|
//! - `wants_alwaysinline` — whether the dispatch site should
|
|
//! attach the `alwaysinline` attribute
|
|
//! to the emitted fn (was a separate
|
|
//! hardcoded name-list in the predicate
|
|
//! `intercept_emit_wants_alwaysinline`
|
|
//! before raw-buf.1)
|
|
//! - `emit` — the per-arm IR-emission body, run
|
|
//! against `&mut Emitter` after the
|
|
//! sig check
|
|
//!
|
|
//! Adding a new intercept = adding one row to `INTERCEPTS`. The
|
|
//! dispatch site (`try_emit_primitive_instance_body` in
|
|
//! `crates/ailang-codegen/src/lib.rs`) and the alwaysinline
|
|
//! predicate both consult this single table.
|
|
|
|
use crate::{CodegenError, Emitter, Result};
|
|
|
|
pub(crate) struct Intercept {
|
|
pub name: &'static str,
|
|
pub expected_params: &'static [&'static str],
|
|
pub expected_ret: &'static str,
|
|
pub wants_alwaysinline: bool,
|
|
pub emit: fn(&mut Emitter<'_>) -> Result<()>,
|
|
}
|
|
|
|
pub(crate) static INTERCEPTS: &[Intercept] = &[
|
|
Intercept {
|
|
name: "eq__Str",
|
|
expected_params: &["ptr", "ptr"],
|
|
expected_ret: "i1",
|
|
wants_alwaysinline: true,
|
|
emit: emit_eq_str,
|
|
},
|
|
Intercept {
|
|
name: "compare__Int",
|
|
expected_params: &["i64", "i64"],
|
|
expected_ret: "ptr",
|
|
wants_alwaysinline: true,
|
|
emit: emit_compare_int,
|
|
},
|
|
Intercept {
|
|
name: "compare__Bool",
|
|
expected_params: &["i1", "i1"],
|
|
expected_ret: "ptr",
|
|
wants_alwaysinline: true,
|
|
emit: emit_compare_bool,
|
|
},
|
|
Intercept {
|
|
name: "compare__Str",
|
|
expected_params: &["ptr", "ptr"],
|
|
expected_ret: "ptr",
|
|
wants_alwaysinline: true,
|
|
emit: emit_compare_str,
|
|
},
|
|
Intercept {
|
|
name: "eq__Int",
|
|
expected_params: &["i64", "i64"],
|
|
expected_ret: "i1",
|
|
wants_alwaysinline: true,
|
|
emit: emit_eq_int,
|
|
},
|
|
Intercept {
|
|
name: "eq__Bool",
|
|
expected_params: &["i1", "i1"],
|
|
expected_ret: "i1",
|
|
wants_alwaysinline: true,
|
|
emit: emit_eq_bool,
|
|
},
|
|
Intercept {
|
|
name: "eq__Unit",
|
|
// Unit lowers to `i8` per the codegen type-mapping (see lib.rs
|
|
// //! header). The legacy arm checked only `ret_ty != "i1"` and
|
|
// ignored params; this entry restores that semantics under the
|
|
// centralised `check_sig` by naming the actual param shape the
|
|
// call site delivers. The emit fn discards the locals.
|
|
expected_params: &["i8", "i8"],
|
|
expected_ret: "i1",
|
|
wants_alwaysinline: true,
|
|
emit: emit_eq_unit,
|
|
},
|
|
Intercept {
|
|
name: "float_eq",
|
|
expected_params: &["double", "double"],
|
|
expected_ret: "i1",
|
|
wants_alwaysinline: true,
|
|
emit: emit_float_eq,
|
|
},
|
|
Intercept {
|
|
name: "float_ne",
|
|
expected_params: &["double", "double"],
|
|
expected_ret: "i1",
|
|
wants_alwaysinline: true,
|
|
emit: emit_float_ne,
|
|
},
|
|
Intercept {
|
|
name: "float_lt",
|
|
expected_params: &["double", "double"],
|
|
expected_ret: "i1",
|
|
wants_alwaysinline: true,
|
|
emit: emit_float_lt,
|
|
},
|
|
Intercept {
|
|
name: "float_le",
|
|
expected_params: &["double", "double"],
|
|
expected_ret: "i1",
|
|
wants_alwaysinline: true,
|
|
emit: emit_float_le,
|
|
},
|
|
Intercept {
|
|
name: "float_gt",
|
|
expected_params: &["double", "double"],
|
|
expected_ret: "i1",
|
|
wants_alwaysinline: true,
|
|
emit: emit_float_gt,
|
|
},
|
|
Intercept {
|
|
name: "float_ge",
|
|
expected_params: &["double", "double"],
|
|
expected_ret: "i1",
|
|
wants_alwaysinline: true,
|
|
emit: emit_float_ge,
|
|
},
|
|
Intercept {
|
|
name: "lt__Int",
|
|
expected_params: &["i64", "i64"],
|
|
expected_ret: "i1",
|
|
wants_alwaysinline: true,
|
|
emit: emit_lt_int,
|
|
},
|
|
Intercept {
|
|
name: "le__Int",
|
|
expected_params: &["i64", "i64"],
|
|
expected_ret: "i1",
|
|
wants_alwaysinline: true,
|
|
emit: emit_le_int,
|
|
},
|
|
Intercept {
|
|
name: "gt__Int",
|
|
expected_params: &["i64", "i64"],
|
|
expected_ret: "i1",
|
|
wants_alwaysinline: true,
|
|
emit: emit_gt_int,
|
|
},
|
|
Intercept {
|
|
name: "ge__Int",
|
|
expected_params: &["i64", "i64"],
|
|
expected_ret: "i1",
|
|
wants_alwaysinline: true,
|
|
emit: emit_ge_int,
|
|
},
|
|
Intercept {
|
|
name: "ne__Int",
|
|
expected_params: &["i64", "i64"],
|
|
expected_ret: "i1",
|
|
wants_alwaysinline: true,
|
|
emit: emit_ne_int,
|
|
},
|
|
Intercept {
|
|
name: "answer",
|
|
expected_params: &[],
|
|
expected_ret: "i64",
|
|
wants_alwaysinline: false,
|
|
emit: emit_answer,
|
|
},
|
|
Intercept {
|
|
name: "StubT_peek__Int",
|
|
expected_params: &["ptr"], // borrow (con StubT Int) lowers to ptr
|
|
expected_ret: "i64",
|
|
wants_alwaysinline: false,
|
|
emit: emit_stubt_peek_int,
|
|
},
|
|
Intercept {
|
|
name: "StubT_peek__Float",
|
|
expected_params: &["ptr"],
|
|
expected_ret: "double",
|
|
wants_alwaysinline: false,
|
|
emit: emit_stubt_peek_float,
|
|
},
|
|
// raw-buf.4: the 12 scope-qualified RawBuf ops. Symbols are
|
|
// `RawBuf_{new,get,set,size}__{Int,Float,Bool}` (raw-buf.3
|
|
// scope-qualified mono mangling). Slab layout:
|
|
// `[ size:i64 @0 ][ elem_0 @8 ][ elem_1 @8+w ]…`; element widths
|
|
// Int/Float = 8, Bool = 1. `own`/`borrow (con RawBuf T)` both
|
|
// lower to `ptr`. The header is always i64, so `new`/`size` are
|
|
// element-type-independent; `get`/`set` carry the element type.
|
|
Intercept {
|
|
name: "RawBuf_new__Int",
|
|
expected_params: &["i64"],
|
|
expected_ret: "ptr",
|
|
wants_alwaysinline: false,
|
|
emit: emit_rawbuf_new_int,
|
|
},
|
|
Intercept {
|
|
name: "RawBuf_get__Int",
|
|
expected_params: &["ptr", "i64"],
|
|
expected_ret: "i64",
|
|
wants_alwaysinline: false,
|
|
emit: emit_rawbuf_get_int,
|
|
},
|
|
Intercept {
|
|
name: "RawBuf_set__Int",
|
|
expected_params: &["ptr", "i64", "i64"],
|
|
expected_ret: "ptr",
|
|
wants_alwaysinline: false,
|
|
emit: emit_rawbuf_set_int,
|
|
},
|
|
Intercept {
|
|
name: "RawBuf_size__Int",
|
|
expected_params: &["ptr"],
|
|
expected_ret: "i64",
|
|
wants_alwaysinline: false,
|
|
emit: emit_rawbuf_size_int,
|
|
},
|
|
Intercept {
|
|
name: "RawBuf_new__Float",
|
|
expected_params: &["i64"],
|
|
expected_ret: "ptr",
|
|
wants_alwaysinline: false,
|
|
emit: emit_rawbuf_new_float,
|
|
},
|
|
Intercept {
|
|
name: "RawBuf_get__Float",
|
|
expected_params: &["ptr", "i64"],
|
|
expected_ret: "double",
|
|
wants_alwaysinline: false,
|
|
emit: emit_rawbuf_get_float,
|
|
},
|
|
Intercept {
|
|
name: "RawBuf_set__Float",
|
|
expected_params: &["ptr", "i64", "double"],
|
|
expected_ret: "ptr",
|
|
wants_alwaysinline: false,
|
|
emit: emit_rawbuf_set_float,
|
|
},
|
|
Intercept {
|
|
name: "RawBuf_size__Float",
|
|
expected_params: &["ptr"],
|
|
expected_ret: "i64",
|
|
wants_alwaysinline: false,
|
|
emit: emit_rawbuf_size_float,
|
|
},
|
|
Intercept {
|
|
name: "RawBuf_new__Bool",
|
|
expected_params: &["i64"],
|
|
expected_ret: "ptr",
|
|
wants_alwaysinline: false,
|
|
emit: emit_rawbuf_new_bool,
|
|
},
|
|
Intercept {
|
|
name: "RawBuf_get__Bool",
|
|
expected_params: &["ptr", "i64"],
|
|
expected_ret: "i1",
|
|
wants_alwaysinline: false,
|
|
emit: emit_rawbuf_get_bool,
|
|
},
|
|
Intercept {
|
|
name: "RawBuf_set__Bool",
|
|
expected_params: &["ptr", "i64", "i1"],
|
|
expected_ret: "ptr",
|
|
wants_alwaysinline: false,
|
|
emit: emit_rawbuf_set_bool,
|
|
},
|
|
Intercept {
|
|
name: "RawBuf_size__Bool",
|
|
expected_params: &["ptr"],
|
|
expected_ret: "i64",
|
|
wants_alwaysinline: false,
|
|
emit: emit_rawbuf_size_bool,
|
|
},
|
|
];
|
|
|
|
pub(crate) fn lookup(name: &str) -> Option<&'static Intercept> {
|
|
INTERCEPTS.iter().find(|i| i.name == name)
|
|
}
|
|
|
|
pub(crate) fn check_sig(
|
|
intercept: &Intercept,
|
|
param_tys: &[String],
|
|
ret_ty: &str,
|
|
) -> Result<()> {
|
|
let params_match = param_tys.len() == intercept.expected_params.len()
|
|
&& param_tys
|
|
.iter()
|
|
.zip(intercept.expected_params.iter())
|
|
.all(|(have, want)| have == want);
|
|
if !params_match || ret_ty != intercept.expected_ret {
|
|
return Err(CodegenError::Internal(format!(
|
|
"{} body intercept: unexpected signature \
|
|
({:?}) -> {} (want {:?} -> {})",
|
|
intercept.name,
|
|
param_tys,
|
|
ret_ty,
|
|
intercept.expected_params,
|
|
intercept.expected_ret,
|
|
)));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Per-intercept emit fns. Bodies lifted verbatim from the legacy
|
|
// `try_emit_primitive_instance_body` match arms in `lib.rs` (raw-buf.1
|
|
// migration). Per-arm sig-check prologues are removed because the
|
|
// dispatch shim runs `check_sig` first; trailing `Ok(true)` is
|
|
// replaced by `Ok(())` because the shim wraps the bool.
|
|
// ---------------------------------------------------------------
|
|
|
|
pub(crate) fn emit_eq_str(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
// The two params are the two most recently pushed locals;
|
|
// `emit_fn` populated `self.locals` from `f.params` before
|
|
// dispatching here. Pull their SSA names without assuming
|
|
// a specific surface-level binder name.
|
|
let n = emitter.locals.len();
|
|
let a_ssa = emitter.locals[n - 2].1.clone();
|
|
let b_ssa = emitter.locals[n - 1].1.clone();
|
|
// IR-Str pointers now land on the
|
|
// `len`-field of the packed-struct slab; @ail_str_eq's
|
|
// strcmp-based body needs the bytes pointer 8 bytes
|
|
// further on.
|
|
let a_bytes = emitter.fresh_ssa();
|
|
let b_bytes = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(
|
|
" {a_bytes} = getelementptr inbounds i8, ptr {a_ssa}, i64 8\n"
|
|
));
|
|
emitter.body.push_str(&format!(
|
|
" {b_bytes} = getelementptr inbounds i8, ptr {b_ssa}, i64 8\n"
|
|
));
|
|
let dst = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(
|
|
" {dst} = call zeroext i1 @ail_str_eq(ptr {a_bytes}, ptr {b_bytes})\n"
|
|
));
|
|
emitter.body.push_str(&format!(" ret i1 {dst}\n"));
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn emit_compare_int(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
let n = emitter.locals.len();
|
|
let a_ssa = emitter.locals[n - 2].1.clone();
|
|
let b_ssa = emitter.locals[n - 1].1.clone();
|
|
emitter.emit_compare_ladder(
|
|
&format!("icmp slt i64 {a_ssa}, {b_ssa}"),
|
|
&format!("icmp eq i64 {a_ssa}, {b_ssa}"),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn emit_compare_bool(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
let n = emitter.locals.len();
|
|
let a_ssa = emitter.locals[n - 2].1.clone();
|
|
let b_ssa = emitter.locals[n - 1].1.clone();
|
|
emitter.emit_compare_ladder(
|
|
&format!("icmp ult i1 {a_ssa}, {b_ssa}"),
|
|
&format!("icmp eq i1 {a_ssa}, {b_ssa}"),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn emit_compare_str(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
let n = emitter.locals.len();
|
|
let a_ssa = emitter.locals[n - 2].1.clone();
|
|
let b_ssa = emitter.locals[n - 1].1.clone();
|
|
// IR-Str pointers now land on the
|
|
// `len`-field of the packed-struct slab; @ail_str_compare's
|
|
// strcmp-based body needs the bytes pointer 8 bytes
|
|
// further on.
|
|
let a_bytes = emitter.fresh_ssa();
|
|
let b_bytes = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(
|
|
" {a_bytes} = getelementptr inbounds i8, ptr {a_ssa}, i64 8\n"
|
|
));
|
|
emitter.body.push_str(&format!(
|
|
" {b_bytes} = getelementptr inbounds i8, ptr {b_ssa}, i64 8\n"
|
|
));
|
|
let cmp_res = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(
|
|
" {cmp_res} = call i32 @ail_str_compare(ptr {a_bytes}, ptr {b_bytes})\n"
|
|
));
|
|
emitter.emit_compare_ladder(
|
|
&format!("icmp slt i32 {cmp_res}, 0"),
|
|
&format!("icmp eq i32 {cmp_res}, 0"),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn emit_eq_int(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
let n = emitter.locals.len();
|
|
let a_ssa = emitter.locals[n - 2].1.clone();
|
|
let b_ssa = emitter.locals[n - 1].1.clone();
|
|
let r = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(
|
|
" {r} = icmp eq i64 {a_ssa}, {b_ssa}\n"
|
|
));
|
|
emitter.body.push_str(&format!(" ret i1 {r}\n"));
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn emit_eq_bool(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
let n = emitter.locals.len();
|
|
let a_ssa = emitter.locals[n - 2].1.clone();
|
|
let b_ssa = emitter.locals[n - 1].1.clone();
|
|
let r = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(
|
|
" {r} = icmp eq i1 {a_ssa}, {b_ssa}\n"
|
|
));
|
|
emitter.body.push_str(&format!(" ret i1 {r}\n"));
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn emit_eq_unit(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
// Unit is single-inhabitant: all values compare equal.
|
|
// The fn signature still carries the operand slots; we
|
|
// discard them and return true unconditionally.
|
|
emitter.body.push_str(" ret i1 1\n");
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn emit_float_eq(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
let n = emitter.locals.len();
|
|
let a_ssa = emitter.locals[n - 2].1.clone();
|
|
let b_ssa = emitter.locals[n - 1].1.clone();
|
|
let r = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(
|
|
" {r} = fcmp oeq double {a_ssa}, {b_ssa}\n"
|
|
));
|
|
emitter.body.push_str(&format!(" ret i1 {r}\n"));
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn emit_float_ne(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
// `fcmp une` ("unordered or not-equal"), not `one`,
|
|
// mirroring float-semantics.md: NaN != NaN must be true.
|
|
let n = emitter.locals.len();
|
|
let a_ssa = emitter.locals[n - 2].1.clone();
|
|
let b_ssa = emitter.locals[n - 1].1.clone();
|
|
let r = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(
|
|
" {r} = fcmp une double {a_ssa}, {b_ssa}\n"
|
|
));
|
|
emitter.body.push_str(&format!(" ret i1 {r}\n"));
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn emit_float_lt(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
let n = emitter.locals.len();
|
|
let a_ssa = emitter.locals[n - 2].1.clone();
|
|
let b_ssa = emitter.locals[n - 1].1.clone();
|
|
let r = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(
|
|
" {r} = fcmp olt double {a_ssa}, {b_ssa}\n"
|
|
));
|
|
emitter.body.push_str(&format!(" ret i1 {r}\n"));
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn emit_float_le(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
let n = emitter.locals.len();
|
|
let a_ssa = emitter.locals[n - 2].1.clone();
|
|
let b_ssa = emitter.locals[n - 1].1.clone();
|
|
let r = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(
|
|
" {r} = fcmp ole double {a_ssa}, {b_ssa}\n"
|
|
));
|
|
emitter.body.push_str(&format!(" ret i1 {r}\n"));
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn emit_float_gt(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
let n = emitter.locals.len();
|
|
let a_ssa = emitter.locals[n - 2].1.clone();
|
|
let b_ssa = emitter.locals[n - 1].1.clone();
|
|
let r = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(
|
|
" {r} = fcmp ogt double {a_ssa}, {b_ssa}\n"
|
|
));
|
|
emitter.body.push_str(&format!(" ret i1 {r}\n"));
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn emit_float_ge(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
let n = emitter.locals.len();
|
|
let a_ssa = emitter.locals[n - 2].1.clone();
|
|
let b_ssa = emitter.locals[n - 1].1.clone();
|
|
let r = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(
|
|
" {r} = fcmp oge double {a_ssa}, {b_ssa}\n"
|
|
));
|
|
emitter.body.push_str(&format!(" ret i1 {r}\n"));
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
// The five icmp-delegate emit fns wrap the existing
|
|
// `Emitter::emit_direct_int_icmp_intercept` helper, which returns
|
|
// `Result<bool>` (always `Ok(true)` on success); we discard the
|
|
// bool because the dispatch shim wraps it from `check_sig`.
|
|
|
|
pub(crate) fn emit_lt_int(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
emitter
|
|
.emit_direct_int_icmp_intercept("lt__Int", "icmp slt i64", &["i64".into(), "i64".into()], "i1")
|
|
.map(|_| ())
|
|
}
|
|
|
|
pub(crate) fn emit_le_int(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
emitter
|
|
.emit_direct_int_icmp_intercept("le__Int", "icmp sle i64", &["i64".into(), "i64".into()], "i1")
|
|
.map(|_| ())
|
|
}
|
|
|
|
pub(crate) fn emit_gt_int(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
emitter
|
|
.emit_direct_int_icmp_intercept("gt__Int", "icmp sgt i64", &["i64".into(), "i64".into()], "i1")
|
|
.map(|_| ())
|
|
}
|
|
|
|
pub(crate) fn emit_ge_int(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
emitter
|
|
.emit_direct_int_icmp_intercept("ge__Int", "icmp sge i64", &["i64".into(), "i64".into()], "i1")
|
|
.map(|_| ())
|
|
}
|
|
|
|
pub(crate) fn emit_ne_int(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
emitter
|
|
.emit_direct_int_icmp_intercept("ne__Int", "icmp ne i64", &["i64".into(), "i64".into()], "i1")
|
|
.map(|_| ())
|
|
}
|
|
|
|
/// Ratifier intrinsic for intrinsic-bodies.1: `answer : () -> Int`
|
|
/// returns the constant 42. Exercises the Term::Intrinsic → registry
|
|
/// route end-to-end. May be retired once a real kernel-tier intrinsic
|
|
/// (raw-buf) lands.
|
|
pub(crate) fn emit_answer(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
emitter.body.push_str(" ret i64 42\n");
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
/// raw-buf.3 ratifier emit. NOTE: never instantiated to codegen this
|
|
/// iteration (no Term::New to build a StubT; unit-test-ratified only).
|
|
/// The load offset/correctness is exercised end-to-end only by
|
|
/// raw-buf.4's RawBuf path; here the entry exists to satisfy the
|
|
/// bijection. Retires with the stub in raw-buf.5.
|
|
pub(crate) fn emit_stubt_peek_int(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
let n = emitter.locals.len();
|
|
let s = emitter.locals[n - 1].1.clone();
|
|
let v = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(" {v} = load i64, ptr {s}\n"));
|
|
emitter.body.push_str(&format!(" ret i64 {v}\n"));
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn emit_stubt_peek_float(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
let n = emitter.locals.len();
|
|
let s = emitter.locals[n - 1].1.clone();
|
|
let v = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(" {v} = load double, ptr {s}\n"));
|
|
emitter.body.push_str(&format!(" ret double {v}\n"));
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// raw-buf.4: RawBuf op emits over an `@ailang_rc_alloc` slab.
|
|
// Slab layout `[ size:i64 @0 ][ elem_0 @8 ][ elem_1 @8+w ]…`. The
|
|
// rc-header is auto-prepended by `@ailang_rc_alloc` (it returns the
|
|
// payload ptr); the i64 size header lives at payload offset 0, the
|
|
// elements follow at offset 8. Element widths: Int/Float = 8, Bool
|
|
// = 1. `new`/`size` are byte-identical across element types (the
|
|
// header is always i64); `get`/`set` carry the element load/store
|
|
// type and offset width. Intercepts run only under `--alloc=rc`, so
|
|
// `@ailang_rc_alloc` is hardcoded (not `alloc.fn_name()`).
|
|
// ---------------------------------------------------------------
|
|
|
|
// --- Int variants (element type i64, width 8) ---
|
|
|
|
pub(crate) fn emit_rawbuf_new_int(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
let n = emitter.locals.len();
|
|
let cap = emitter.locals[n - 1].1.clone(); // i64 capacity
|
|
let elems_bytes = emitter.fresh_ssa();
|
|
let total = emitter.fresh_ssa();
|
|
let slab = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(" {elems_bytes} = mul i64 {cap}, 8\n"));
|
|
emitter.body.push_str(&format!(" {total} = add i64 {elems_bytes}, 8\n"));
|
|
emitter
|
|
.body
|
|
.push_str(&format!(" {slab} = call ptr @ailang_rc_alloc(i64 {total})\n"));
|
|
emitter.body.push_str(&format!(" store i64 {cap}, ptr {slab}\n"));
|
|
emitter.body.push_str(&format!(" ret ptr {slab}\n"));
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn emit_rawbuf_get_int(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
let n = emitter.locals.len();
|
|
let b = emitter.locals[n - 2].1.clone();
|
|
let i = emitter.locals[n - 1].1.clone();
|
|
let off = emitter.fresh_ssa();
|
|
let byteoff = emitter.fresh_ssa();
|
|
let ptr = emitter.fresh_ssa();
|
|
let v = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(" {off} = mul i64 {i}, 8\n"));
|
|
emitter.body.push_str(&format!(" {byteoff} = add i64 {off}, 8\n"));
|
|
emitter.body.push_str(&format!(
|
|
" {ptr} = getelementptr inbounds i8, ptr {b}, i64 {byteoff}\n"
|
|
));
|
|
emitter.body.push_str(&format!(" {v} = load i64, ptr {ptr}\n"));
|
|
emitter.body.push_str(&format!(" ret i64 {v}\n"));
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn emit_rawbuf_set_int(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
let n = emitter.locals.len();
|
|
let b = emitter.locals[n - 3].1.clone();
|
|
let i = emitter.locals[n - 2].1.clone();
|
|
let v = emitter.locals[n - 1].1.clone();
|
|
let off = emitter.fresh_ssa();
|
|
let byteoff = emitter.fresh_ssa();
|
|
let ptr = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(" {off} = mul i64 {i}, 8\n"));
|
|
emitter.body.push_str(&format!(" {byteoff} = add i64 {off}, 8\n"));
|
|
emitter.body.push_str(&format!(
|
|
" {ptr} = getelementptr inbounds i8, ptr {b}, i64 {byteoff}\n"
|
|
));
|
|
emitter.body.push_str(&format!(" store i64 {v}, ptr {ptr}\n"));
|
|
emitter.body.push_str(&format!(" ret ptr {b}\n"));
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn emit_rawbuf_size_int(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
let n = emitter.locals.len();
|
|
let b = emitter.locals[n - 1].1.clone();
|
|
let sz = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(" {sz} = load i64, ptr {b}\n"));
|
|
emitter.body.push_str(&format!(" ret i64 {sz}\n"));
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
// --- Float variants (element type double, width 8) ---
|
|
|
|
pub(crate) fn emit_rawbuf_new_float(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
// Header always i64; element width 8 — byte-identical to Int's new.
|
|
emit_rawbuf_new_int(emitter)
|
|
}
|
|
|
|
pub(crate) fn emit_rawbuf_get_float(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
let n = emitter.locals.len();
|
|
let b = emitter.locals[n - 2].1.clone();
|
|
let i = emitter.locals[n - 1].1.clone();
|
|
let off = emitter.fresh_ssa();
|
|
let byteoff = emitter.fresh_ssa();
|
|
let ptr = emitter.fresh_ssa();
|
|
let v = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(" {off} = mul i64 {i}, 8\n"));
|
|
emitter.body.push_str(&format!(" {byteoff} = add i64 {off}, 8\n"));
|
|
emitter.body.push_str(&format!(
|
|
" {ptr} = getelementptr inbounds i8, ptr {b}, i64 {byteoff}\n"
|
|
));
|
|
emitter.body.push_str(&format!(" {v} = load double, ptr {ptr}\n"));
|
|
emitter.body.push_str(&format!(" ret double {v}\n"));
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn emit_rawbuf_set_float(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
let n = emitter.locals.len();
|
|
let b = emitter.locals[n - 3].1.clone();
|
|
let i = emitter.locals[n - 2].1.clone();
|
|
let v = emitter.locals[n - 1].1.clone();
|
|
let off = emitter.fresh_ssa();
|
|
let byteoff = emitter.fresh_ssa();
|
|
let ptr = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(" {off} = mul i64 {i}, 8\n"));
|
|
emitter.body.push_str(&format!(" {byteoff} = add i64 {off}, 8\n"));
|
|
emitter.body.push_str(&format!(
|
|
" {ptr} = getelementptr inbounds i8, ptr {b}, i64 {byteoff}\n"
|
|
));
|
|
emitter.body.push_str(&format!(" store double {v}, ptr {ptr}\n"));
|
|
emitter.body.push_str(&format!(" ret ptr {b}\n"));
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn emit_rawbuf_size_float(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
// Header always i64 — byte-identical to Int's size.
|
|
emit_rawbuf_size_int(emitter)
|
|
}
|
|
|
|
// --- Bool variants (element type i1, width 1) ---
|
|
|
|
pub(crate) fn emit_rawbuf_new_bool(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
let n = emitter.locals.len();
|
|
let cap = emitter.locals[n - 1].1.clone(); // i64 capacity
|
|
let elems_bytes = emitter.fresh_ssa();
|
|
let total = emitter.fresh_ssa();
|
|
let slab = emitter.fresh_ssa();
|
|
// element width 1 byte for Bool.
|
|
emitter.body.push_str(&format!(" {elems_bytes} = mul i64 {cap}, 1\n"));
|
|
emitter.body.push_str(&format!(" {total} = add i64 {elems_bytes}, 8\n"));
|
|
emitter
|
|
.body
|
|
.push_str(&format!(" {slab} = call ptr @ailang_rc_alloc(i64 {total})\n"));
|
|
emitter.body.push_str(&format!(" store i64 {cap}, ptr {slab}\n"));
|
|
emitter.body.push_str(&format!(" ret ptr {slab}\n"));
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn emit_rawbuf_get_bool(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
let n = emitter.locals.len();
|
|
let b = emitter.locals[n - 2].1.clone();
|
|
let i = emitter.locals[n - 1].1.clone();
|
|
let off = emitter.fresh_ssa();
|
|
let byteoff = emitter.fresh_ssa();
|
|
let ptr = emitter.fresh_ssa();
|
|
let v = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(" {off} = mul i64 {i}, 1\n"));
|
|
emitter.body.push_str(&format!(" {byteoff} = add i64 {off}, 8\n"));
|
|
emitter.body.push_str(&format!(
|
|
" {ptr} = getelementptr inbounds i8, ptr {b}, i64 {byteoff}\n"
|
|
));
|
|
emitter.body.push_str(&format!(" {v} = load i1, ptr {ptr}\n"));
|
|
emitter.body.push_str(&format!(" ret i1 {v}\n"));
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn emit_rawbuf_set_bool(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
let n = emitter.locals.len();
|
|
let b = emitter.locals[n - 3].1.clone();
|
|
let i = emitter.locals[n - 2].1.clone();
|
|
let v = emitter.locals[n - 1].1.clone();
|
|
let off = emitter.fresh_ssa();
|
|
let byteoff = emitter.fresh_ssa();
|
|
let ptr = emitter.fresh_ssa();
|
|
emitter.body.push_str(&format!(" {off} = mul i64 {i}, 1\n"));
|
|
emitter.body.push_str(&format!(" {byteoff} = add i64 {off}, 8\n"));
|
|
emitter.body.push_str(&format!(
|
|
" {ptr} = getelementptr inbounds i8, ptr {b}, i64 {byteoff}\n"
|
|
));
|
|
emitter.body.push_str(&format!(" store i1 {v}, ptr {ptr}\n"));
|
|
emitter.body.push_str(&format!(" ret ptr {b}\n"));
|
|
emitter.body.push_str("}\n\n");
|
|
emitter.block_terminated = true;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn emit_rawbuf_size_bool(emitter: &mut Emitter<'_>) -> Result<()> {
|
|
// Header always i64 — byte-identical to Int's size.
|
|
emit_rawbuf_size_int(emitter)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{lookup, INTERCEPTS};
|
|
use ailang_check::mono::{mono_symbol, mono_symbol_n};
|
|
use ailang_core::ast::{Def, FnDef, Module, Term, Type};
|
|
use std::collections::BTreeSet;
|
|
|
|
/// INTERCEPTS entries that intercept the monomorphised `__Int`
|
|
/// specialisation of a polymorphic free fn carrying a REAL body
|
|
/// (`ne = not (eq x y)`; `lt/le/gt/ge = match compare ...`). These
|
|
/// are an optimisation class, not a compiler-supplied body — they
|
|
/// legitimately have no `(intrinsic)` marker. Any change here is a
|
|
/// deliberate registry-policy decision, not drift.
|
|
const OPTIMISATION_ONLY: &[&str] =
|
|
&["lt__Int", "le__Int", "gt__Int", "ge__Int", "ne__Int"];
|
|
|
|
/// Collect the mangled name of every `(intrinsic)` marker reachable
|
|
/// in the kernel-tier source modules (prelude + kernel_stub — the
|
|
/// only modules where an intrinsic body is legal today).
|
|
fn workspace_intrinsic_markers() -> BTreeSet<String> {
|
|
let mut markers = BTreeSet::new();
|
|
for module in [
|
|
ailang_surface::parse_prelude(),
|
|
ailang_surface::parse_kernel_stub(),
|
|
ailang_surface::parse_raw_buf(),
|
|
] {
|
|
for def in &module.defs {
|
|
match def {
|
|
// raw-buf.3: a polymorphic top-level (intrinsic) op
|
|
// type-scoped to a same-module TypeDef T (its
|
|
// signature mentions `(con T …)`) expands to one
|
|
// marker per element type in T's `param-in` set —
|
|
// the same `T_f__<elem>` strings the mono pass mints
|
|
// (mono::scoped_base + mono_symbol_n). One marker → N
|
|
// entries.
|
|
Def::Fn(f)
|
|
if matches!(f.body, Term::Intrinsic)
|
|
&& matches!(f.ty, Type::Forall { .. }) =>
|
|
{
|
|
match scope_typedef_and_elems(f, &module) {
|
|
Some((tdef, elems)) => {
|
|
for elem in elems {
|
|
markers.insert(mono_symbol_n(
|
|
&format!("{tdef}_{}", f.name),
|
|
std::slice::from_ref(&elem),
|
|
));
|
|
}
|
|
}
|
|
// a Forall intrinsic not scoped to a
|
|
// param-in TypeDef keeps the bare name (no
|
|
// such case ships today; future-proofing).
|
|
None => {
|
|
markers.insert(f.name.clone());
|
|
}
|
|
}
|
|
}
|
|
// Top-level intrinsic fn: name is already the symbol
|
|
// (float_eq, answer, ...).
|
|
Def::Fn(f) if matches!(f.body, Term::Intrinsic) => {
|
|
markers.insert(f.name.clone());
|
|
}
|
|
// Instance method whose lambda body is intrinsic:
|
|
// the codegen symbol is mono_symbol(method, type).
|
|
Def::Instance(inst) => {
|
|
for m in &inst.methods {
|
|
if let Term::Lam { body, .. } = &m.body {
|
|
if matches!(**body, Term::Intrinsic) {
|
|
markers.insert(mono_symbol(&m.name, &inst.type_));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
markers
|
|
}
|
|
|
|
/// Bijection over the intrinsic-backed class:
|
|
/// (A) every workspace intrinsic marker resolves to an INTERCEPTS entry;
|
|
/// (B) every INTERCEPTS entry not on the optimisation-only allowlist
|
|
/// has a workspace intrinsic marker.
|
|
#[test]
|
|
fn intercepts_bijection_with_intrinsic_markers() {
|
|
let markers = workspace_intrinsic_markers();
|
|
let registry: BTreeSet<String> =
|
|
INTERCEPTS.iter().map(|i| i.name.to_string()).collect();
|
|
|
|
// (A) no intrinsic marker without a codegen intercept
|
|
let orphan_markers: Vec<&String> =
|
|
markers.iter().filter(|m| lookup(m).is_none()).collect();
|
|
assert!(
|
|
orphan_markers.is_empty(),
|
|
"intrinsic markers with no INTERCEPTS entry: {orphan_markers:?}"
|
|
);
|
|
|
|
// (B) no non-allowlisted intercept without an intrinsic marker
|
|
let orphan_entries: Vec<&String> = registry
|
|
.iter()
|
|
.filter(|n| !OPTIMISATION_ONLY.contains(&n.as_str()))
|
|
.filter(|n| !markers.contains(*n))
|
|
.collect();
|
|
assert!(
|
|
orphan_entries.is_empty(),
|
|
"INTERCEPTS entries with no intrinsic marker (and not optimisation-only): {orphan_entries:?}"
|
|
);
|
|
|
|
// Guard: the allowlist names must actually be in the registry —
|
|
// a stale allowlist entry (name removed from INTERCEPTS) is drift.
|
|
let stale_allow: Vec<&&str> = OPTIMISATION_ONLY
|
|
.iter()
|
|
.filter(|n| lookup(n).is_none())
|
|
.collect();
|
|
assert!(
|
|
stale_allow.is_empty(),
|
|
"optimisation-only allowlist names not in INTERCEPTS: {stale_allow:?}"
|
|
);
|
|
}
|
|
|
|
/// raw-buf.3: for a type-scoped polymorphic intrinsic fn `f`, return
|
|
/// `(TypeDef-name, element-types)` where the TypeDef is the unique
|
|
/// same-module `Def::Type` referenced via `(con T …)` in `f`'s
|
|
/// signature and the element types are its `param-in` set expressed as
|
|
/// `Type::Con` values (so `mono_symbol_n` produces the same suffix the
|
|
/// mono pass mints). `None` if `f` references zero or several
|
|
/// same-module TypeDefs.
|
|
fn scope_typedef_and_elems(f: &FnDef, module: &Module) -> Option<(String, Vec<Type>)> {
|
|
let typedef_names: BTreeSet<&str> = module
|
|
.defs
|
|
.iter()
|
|
.filter_map(|d| match d {
|
|
Def::Type(td) => Some(td.name.as_str()),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
// collect Con-heads referenced anywhere in f's type
|
|
let mut referenced: BTreeSet<String> = BTreeSet::new();
|
|
collect_con_heads(&f.ty, &mut referenced);
|
|
let scoped: Vec<&String> = referenced
|
|
.iter()
|
|
.filter(|n| typedef_names.contains(n.as_str()))
|
|
.collect();
|
|
let tdef = match scoped.as_slice() {
|
|
[one] => (*one).clone(),
|
|
_ => return None,
|
|
};
|
|
let td = module.defs.iter().find_map(|d| match d {
|
|
Def::Type(td) if td.name == tdef => Some(td),
|
|
_ => None,
|
|
})?;
|
|
// param-in is keyed by the type var; take its allowed surface
|
|
// names and build `Type::Con` element types. The element strings
|
|
// (`Int`, `Float`) flow through `mono_symbol_n`'s
|
|
// `primitive_surface_name` gate to the same `__Int` / `__Float`
|
|
// suffixes the mono pass mints.
|
|
let allowed = td.param_in.values().next()?; // single-var TypeDefs
|
|
let elems: Vec<Type> = allowed
|
|
.iter()
|
|
.map(|s| Type::Con { name: s.clone(), args: vec![] })
|
|
.collect();
|
|
Some((tdef, elems))
|
|
}
|
|
|
|
/// raw-buf.3: push every `Type::Con` head name reachable in `ty`
|
|
/// into `out`. `borrow` / `own` are `ParamMode` metadata on
|
|
/// `Type::Fn`, not wrapper `Type` variants, so the only recursive
|
|
/// shapes are `Forall.body`, `Fn.params`/`ret`, and `Con.args`.
|
|
fn collect_con_heads(ty: &Type, out: &mut BTreeSet<String>) {
|
|
match ty {
|
|
Type::Con { name, args } => {
|
|
out.insert(name.clone());
|
|
for a in args {
|
|
collect_con_heads(a, out);
|
|
}
|
|
}
|
|
Type::Fn { params, ret, .. } => {
|
|
for p in params {
|
|
collect_con_heads(p, out);
|
|
}
|
|
collect_con_heads(ret, out);
|
|
}
|
|
Type::Forall { body, .. } => collect_con_heads(body, out),
|
|
Type::Var { .. } => {}
|
|
}
|
|
}
|
|
}
|