typelisp 0.3.0

A statically typed Lisp with an interpreter and a compiler to native executables (LLVM)
//! Names and ids the compile boundary agrees on with the island.
//!
//! Everything here is a *convention*, not a translation: how a user-defined
//! function's LLVM symbol is spelled, how an `rt_llvm_call` operation is
//! numbered, which builtin methods bypass the generic `assoc` path, and the
//! two trait-object id tables a translation is handed. `compiler.rs`'s own
//! Lisp source mangles and re-derives these same strings and numbers, so a
//! change on either side has to be matched on the other — which is exactly
//! why they live in one small module of their own rather than inside the
//! translator that happens to use them most.
//!
//! Extracted from `ast_bridge` when that translator was deleted (Phase 2
//! Stage C): none of this ever touched the typed AST, so none of it went with
//! it.

use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};

use crate::types::Path;
use typelisp_mem::ConcreteNames;

/// The two trait-object id tables a translation reads, interned by `Interp`
/// *before* translation starts: both are baked into the emitted IR as
/// constants (`(dyn-new id ...)`, `(dyn-upcast trait-id ...)`), so there is
/// nothing to resolve lazily mid-translation, exactly like [`Ctx::globals`].
///
/// One parameter rather than two because every translation entry point and
/// every nested-scope hand-off needs both, and they are interned together
/// (`Interp::register_dyn_box`).
#[derive(Clone, Copy)]
pub struct DynTables<'a> {
    /// `(concrete type key, trait path)` -> vtable id, for every trait
    /// object this translation boxes (`dyn-new`).
    pub vtables: &'a HashMap<(String, Path), u32>,
    /// Trait path -> trait id, for every supertrait this translation upcasts
    /// to (`dyn-upcast`). The upcast's *target vtable* is not here: it
    /// depends on the value's concrete type and is looked up at run time
    /// (`rt_dyn_upcast`).
    pub trait_ids: &'a HashMap<Path, u32>,
}

/// A process-wide counter for synthesizing unique LLVM symbol names for
/// anonymous functions — every `lambda`/`fnref`-forwarding-wrapper/
/// immediately-invoked-lambda gets one (labels/closures Stage 4), since none
/// of those have a name from user source the way a `labels` def or `defun`
/// does. Process-wide (not per-translation-pass or per-outer-function) so
/// two lambdas compiled into the same shared AOT module — even from two
/// *different* `defun`s — can never collide, with no mangling scheme to get
/// right.
static LAMBDA_COUNTER: AtomicU64 = AtomicU64::new(0);

pub fn fresh_lambda_name(prefix: &str) -> String {
    let n = LAMBDA_COUNTER.fetch_add(1, Ordering::Relaxed);
    format!("{}${}", prefix, n)
}

/// The LLVM symbol a call to `path` must name: the `typelisp-rt` shim, when
/// `path` is a free builtin with no typelisp body
/// ([`crate::compile::externs::builtin_shim`]), and otherwise the mangled
/// name of the compiled function.
///
/// The one place that choice is made. `core_bridge` writes the answer into
/// the node it hands the island, which looks the name up and calls it —
/// there is no second derivation on the island side to disagree with this
/// one.
pub fn callee_symbol_name(path: &crate::Path) -> String {
    match crate::compile::externs::builtin_shim(path) {
        Some(shim) => shim.to_string(),
        None => ConcreteNames::symbol(&path.to_string()),
    }
}

/// The LLVM symbol `target`'s compiled body is defined under — the same
/// string a *call site* emits for it (`core_bridge::translate_call`'s
/// [`callee_symbol_name`], `translate_assoc`'s [`ConcreteNames::method_symbol`]),
/// so a body and the calls to it can't be declared under two different names.
///
/// One function for every place that builds or loads a precompiled library —
/// the generators in [`crate::compile::bootstrap`] and
/// [`crate::compile::prelude_bootstrap`], the JIT and `compile-file` — so they
/// cannot come to spell a definition two ways.
pub fn target_symbol_name(target: &crate::CompileTarget) -> String {
    match target {
        crate::CompileTarget::Fn(path) => ConcreteNames::symbol(&path.to_string()),
        crate::CompileTarget::Method(type_path, method) => ConcreteNames::method_symbol(type_path, method),
    }
}


/// `Vector<T>`'s builtin methods that have no compiled `defmethod` body and so
/// must be lowered to a dedicated `vector-op` node (`translate_vector_method`)
/// rather than routed through the generic `assoc` path. Deliberately *not*
/// `iter`: that one is a real prelude `defmethod` (`vector-iter::new`), a
/// normal compiled method, so it stays on the `assoc` path. `HashTable<K,V>`
/// shares the `new`/`get`/`set` names but a different `type_name`, so the
/// [`path_is_builtin`] guard at the call site keeps them apart.
pub const VECTOR_BUILTIN_METHODS: [&str; 6] = ["new", "get", "set", "len", "push", "pop"];

/// `HashTable<K,V>`'s builtin methods lowered to a `hashtable-op` node
/// (`translate_hashtable_method` -> the `rt_hashtable_*` family) rather than
/// the generic `assoc` path — every one except `iter`, `get`, `set`,
/// `remove`, `maphash` and `size`, which are genuine prelude `defmethod`s.
///
/// `get`/`set`/`remove` were here once, as three whole-lookup shims. They
/// could not stay: looking a key up means hashing it and comparing it, and
/// both of those are the key type's own `sxhash`/`equals` — typelisp
/// methods a Rust shim cannot call. The bucket primitives take the hash the
/// prelude computed and an index it found, which is the part this layer can
/// answer on its own.
pub const HASHTABLE_BUILTIN_METHODS: [&str; 11] = [
    "new", "count", "clear", "keys", "values", "entries",
    "bucket-count", "bucket-key", "bucket-value", "bucket-put", "bucket-delete",
];

/// The stable operation id compiled code passes as `rt_llvm_call`'s first
/// argument: FNV-1a over `"type-key::method"`, folded into a tagged-`Sexpr`
/// integer's 61-bit signed payload. A *hash*, not a table index, so the id
/// embedded in a committed compiler-island bitcode artifact (interp-closure
/// removal Stage 3) survives methods being added to or removed from the
/// table in a later build — an index would silently shift. Collisions are
/// checked once at table construction (`interp`'s `llvm_op_table`), which
/// panics rather than dispatching two methods through one id.
///
/// The `<< 3 >> 3` fold is load-bearing, not cosmetic: this id is baked into
/// the `llvm-op` node as a `Sexpr` `Int`, and the *compiled* island reads
/// `Sexpr` ints back in the 3-bit-tagged representation (`raw >> 3`), which
/// would silently drop the top 3 bits of a full-width hash — so a
/// natively-compiled `rt_llvm_call` would pass a truncated id the table (keyed
/// on the full hash) has no entry for, aborting at runtime. Folding here means
/// the id already fits 61 bits, so it round-trips identically through the
/// tagged compiled path and the interpreter's full-width `Value::Int`, and the
/// table (built from this same function) keys on exactly what compiled code
/// passes. (A change to this fold requires regenerating `compiler_island.bc`,
/// whose own native-scope ops carry ids baked by it — the freshness test
/// catches a stale artifact.)
pub fn llvm_op_id(type_key: &str, method: &str) -> i64 {
    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
    for b in type_key.as_bytes().iter().chain(b"::").chain(method.as_bytes()) {
        h ^= u64::from(*b);
        h = h.wrapping_mul(0x0000_0100_0000_01b3);
    }
    ((h as i64) << 3) >> 3
}