Lines
64.56 %
Functions
15 %
Branches
100 %
//! Numeric-strata bridge natives (ADR-0028): the only sanctioned crossings
//! between Index and Scalar for RUNTIME values.
//!
//! - `(index->scalar n)` widens an Index (count) to a Scalar — the i32 is
//! sign-extended (negatives are real) and lifted to `n/1`.
//! - `(scalar->index r)` narrows a Scalar to an Index, truncating toward zero
//! (`7/2 → 3`, `-7/2 → -3`), matching `i32.div_s`.
//! A bare integer literal still coerces to Scalar implicitly where an operator
//! demands it; these natives exist for runtime values, which never coerce.
use super::NativeSpec;
use crate::ast::{Expr, Fraction, WasmType};
use crate::compiler::context::CompileContext;
use crate::compiler::emit::FunctionEmitter;
use crate::compiler::expr::{compile_for_effect, compile_for_stack_as, eval_value, format_expr};
use crate::error::{Error, Result};
use crate::runtime::SymbolTable;
pub(in crate::compiler::native) const NATIVES: &[NativeSpec] = &[
NativeSpec {
name: "INDEX->SCALAR",
eval: index_to_scalar_eval,
stack: Some(compile_index_to_scalar),
effect: None,
},
name: "SCALAR->INDEX",
eval: scalar_to_index_eval,
stack: Some(compile_scalar_to_index),
];
fn one_arg<'a>(args: &'a [Expr], name: &str) -> Result<&'a Expr> {
match args {
[arg] => Ok(arg),
_ => Err(Error::Arity {
name: name.to_string(),
expected: 1,
actual: args.len(),
}),
}
/// `index->scalar` always surfaces a runtime Scalar: a Scalar that equals an
/// integer can't be represented as a const `Number` (an integer `Number`
/// classifies as Index), so even a constant index is lifted at runtime. A
/// constant index must fit `i32` — that is what makes it an Index, and codegen
/// (`compile_for_stack_as(.., I32)`) range-checks it identically, so eval
/// rejects an out-of-range constant here rather than accept what codegen
/// refuses.
fn index_to_scalar_eval(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
let resolved = eval_value(symbols, one_arg(args, "INDEX->SCALAR")?)?;
match &resolved {
Expr::Number(n) if *n.denom() == 1 => {
i32::try_from(*n.numer())
.map_err(|_| Error::Compile("index->scalar: index out of i32 range".to_string()))?;
Ok(Expr::WasmRuntime(WasmType::Ratio))
Expr::Number(_) => Err(Error::Compile(
"index->scalar expects an integer index, not a fractional value".to_string(),
)),
_ if resolved.wasm_type() == Some(WasmType::I32) => Ok(Expr::WasmRuntime(WasmType::Ratio)),
other => Err(Error::Compile(format!(
"index->scalar expects an index (count), got {}",
format_expr(other)
))),
/// `scalar->index` truncates toward zero. A constant scalar folds to an integer
/// `Number` (which classifies as Index); a runtime Scalar surfaces a runtime
/// Index. A constant whose truncated value overflows `i32` is a compile error
/// (eval and codegen agree); a *runtime* scalar that overflows narrows modulo
/// 2^32 via `i32.wrap_i64` (like Rust `as i32`) — there is no constant to
/// disagree with, so no eval/codegen drift.
fn scalar_to_index_eval(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
let resolved = eval_value(symbols, one_arg(args, "SCALAR->INDEX")?)?;
Expr::Number(n) => {
let truncated = n.numer() / n.denom();
i32::try_from(truncated).map_err(|_| {
Error::Compile("scalar->index: result out of i32 range".to_string())
})?;
Ok(Expr::Number(Fraction::from_integer(truncated)))
_ if resolved.wasm_type() == Some(WasmType::Ratio) => Ok(Expr::WasmRuntime(WasmType::I32)),
"scalar->index expects a scalar (rational), got {}",
fn compile_index_to_scalar(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
args: &[Expr],
) -> Result<WasmType> {
compile_for_stack_as(
ctx,
emit,
symbols,
one_arg(args, "INDEX->SCALAR")?,
WasmType::I32,
)?;
// SIGNED extend: a count can be negative (e.g. an index delta), so a -1 must
// become -1/1, not 4294967295/1 (the old unsigned-bridge bug).
emit.i64_extend_i32_s();
emit.call(ctx.ids.ratio_from_i64);
Ok(WasmType::Ratio)
fn compile_scalar_to_index(
let arg = one_arg(args, "SCALAR->INDEX")?;
// A constant operand folds to a range-checked `i32.const` (probed on a
// clone so the live table is untouched). This keeps codegen in lockstep
// with `scalar_to_index_eval`: an out-of-range constant errors on both
// surfaces rather than silently wrapping via the runtime `i32.wrap_i64`,
// which only narrows genuine runtime scalars.
if let Expr::Number(n) = eval_value(&mut symbols.clone(), arg)? {
// A non-literal form that merely *resolves* to a constant (e.g.
// `(begin (debug …) 5)`) still has side effects to emit before the
// folded value — same contract as `compile_for_stack_ratio/index`.
if !matches!(arg, Expr::Number(_)) {
compile_for_effect(ctx, emit, symbols, arg)?;
let narrowed = i32::try_from(truncated)
.map_err(|_| Error::Compile("scalar->index: result out of i32 range".to_string()))?;
emit.i32_const(narrowed);
return Ok(WasmType::I32);
compile_for_stack_as(ctx, emit, symbols, arg, WasmType::Ratio)?;
emit.call(ctx.ids.ratio_to_i64);
emit.i32_wrap_i64();
Ok(WasmType::I32)