1
//! Numeric-strata bridge natives (ADR-0028): the only sanctioned crossings
2
//! between Index and Scalar for RUNTIME values.
3
//!
4
//! - `(index->scalar n)` widens an Index (count) to a Scalar — the i32 is
5
//!   sign-extended (negatives are real) and lifted to `n/1`.
6
//! - `(scalar->index r)` narrows a Scalar to an Index, truncating toward zero
7
//!   (`7/2 → 3`, `-7/2 → -3`), matching `i32.div_s`.
8
//!
9
//! A bare integer literal still coerces to Scalar implicitly where an operator
10
//! demands it; these natives exist for runtime values, which never coerce.
11

            
12
use super::NativeSpec;
13
use crate::ast::{Expr, Fraction, WasmType};
14
use crate::compiler::context::CompileContext;
15
use crate::compiler::emit::FunctionEmitter;
16
use crate::compiler::expr::{compile_for_effect, compile_for_stack_as, eval_value, format_expr};
17
use crate::error::{Error, Result};
18
use crate::runtime::SymbolTable;
19

            
20
pub(in crate::compiler::native) const NATIVES: &[NativeSpec] = &[
21
    NativeSpec {
22
        name: "INDEX->SCALAR",
23
        eval: index_to_scalar_eval,
24
        stack: Some(compile_index_to_scalar),
25
        effect: None,
26
    },
27
    NativeSpec {
28
        name: "SCALAR->INDEX",
29
        eval: scalar_to_index_eval,
30
        stack: Some(compile_scalar_to_index),
31
        effect: None,
32
    },
33
];
34

            
35
1020
fn one_arg<'a>(args: &'a [Expr], name: &str) -> Result<&'a Expr> {
36
1020
    match args {
37
1020
        [arg] => Ok(arg),
38
        _ => Err(Error::Arity {
39
            name: name.to_string(),
40
            expected: 1,
41
            actual: args.len(),
42
        }),
43
    }
44
1020
}
45

            
46
/// `index->scalar` always surfaces a runtime Scalar: a Scalar that equals an
47
/// integer can't be represented as a const `Number` (an integer `Number`
48
/// classifies as Index), so even a constant index is lifted at runtime. A
49
/// constant index must fit `i32` — that is what makes it an Index, and codegen
50
/// (`compile_for_stack_as(.., I32)`) range-checks it identically, so eval
51
/// rejects an out-of-range constant here rather than accept what codegen
52
/// refuses.
53
340
fn index_to_scalar_eval(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
54
340
    let resolved = eval_value(symbols, one_arg(args, "INDEX->SCALAR")?)?;
55
    match &resolved {
56
        Expr::Number(n) if *n.denom() == 1 => {
57
            i32::try_from(*n.numer())
58
                .map_err(|_| Error::Compile("index->scalar: index out of i32 range".to_string()))?;
59
            Ok(Expr::WasmRuntime(WasmType::Ratio))
60
        }
61
        Expr::Number(_) => Err(Error::Compile(
62
            "index->scalar expects an integer index, not a fractional value".to_string(),
63
        )),
64
340
        _ if resolved.wasm_type() == Some(WasmType::I32) => Ok(Expr::WasmRuntime(WasmType::Ratio)),
65
        other => Err(Error::Compile(format!(
66
            "index->scalar expects an index (count), got {}",
67
            format_expr(other)
68
        ))),
69
    }
70
340
}
71

            
72
/// `scalar->index` truncates toward zero. A constant scalar folds to an integer
73
/// `Number` (which classifies as Index); a runtime Scalar surfaces a runtime
74
/// Index. A constant whose truncated value overflows `i32` is a compile error
75
/// (eval and codegen agree); a *runtime* scalar that overflows narrows modulo
76
/// 2^32 via `i32.wrap_i64` (like Rust `as i32`) — there is no constant to
77
/// disagree with, so no eval/codegen drift.
78
136
fn scalar_to_index_eval(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
79
136
    let resolved = eval_value(symbols, one_arg(args, "SCALAR->INDEX")?)?;
80
136
    match &resolved {
81
        Expr::Number(n) => {
82
            let truncated = n.numer() / n.denom();
83
            i32::try_from(truncated).map_err(|_| {
84
                Error::Compile("scalar->index: result out of i32 range".to_string())
85
            })?;
86
            Ok(Expr::Number(Fraction::from_integer(truncated)))
87
        }
88
136
        _ if resolved.wasm_type() == Some(WasmType::Ratio) => Ok(Expr::WasmRuntime(WasmType::I32)),
89
        other => Err(Error::Compile(format!(
90
            "scalar->index expects a scalar (rational), got {}",
91
            format_expr(other)
92
        ))),
93
    }
94
136
}
95

            
96
272
fn compile_index_to_scalar(
97
272
    ctx: &mut CompileContext,
98
272
    emit: &mut FunctionEmitter,
99
272
    symbols: &mut SymbolTable,
100
272
    args: &[Expr],
101
272
) -> Result<WasmType> {
102
272
    compile_for_stack_as(
103
272
        ctx,
104
272
        emit,
105
272
        symbols,
106
272
        one_arg(args, "INDEX->SCALAR")?,
107
272
        WasmType::I32,
108
136
    )?;
109
    // SIGNED extend: a count can be negative (e.g. an index delta), so a -1 must
110
    // become -1/1, not 4294967295/1 (the old unsigned-bridge bug).
111
136
    emit.i64_extend_i32_s();
112
136
    emit.call(ctx.ids.ratio_from_i64);
113
136
    Ok(WasmType::Ratio)
114
272
}
115

            
116
272
fn compile_scalar_to_index(
117
272
    ctx: &mut CompileContext,
118
272
    emit: &mut FunctionEmitter,
119
272
    symbols: &mut SymbolTable,
120
272
    args: &[Expr],
121
272
) -> Result<WasmType> {
122
272
    let arg = one_arg(args, "SCALAR->INDEX")?;
123
    // A constant operand folds to a range-checked `i32.const` (probed on a
124
    // clone so the live table is untouched). This keeps codegen in lockstep
125
    // with `scalar_to_index_eval`: an out-of-range constant errors on both
126
    // surfaces rather than silently wrapping via the runtime `i32.wrap_i64`,
127
    // which only narrows genuine runtime scalars.
128
272
    if let Expr::Number(n) = eval_value(&mut symbols.clone(), arg)? {
129
        // A non-literal form that merely *resolves* to a constant (e.g.
130
        // `(begin (debug …) 5)`) still has side effects to emit before the
131
        // folded value — same contract as `compile_for_stack_ratio/index`.
132
136
        if !matches!(arg, Expr::Number(_)) {
133
            compile_for_effect(ctx, emit, symbols, arg)?;
134
136
        }
135
136
        let truncated = n.numer() / n.denom();
136
136
        let narrowed = i32::try_from(truncated)
137
136
            .map_err(|_| Error::Compile("scalar->index: result out of i32 range".to_string()))?;
138
68
        emit.i32_const(narrowed);
139
68
        return Ok(WasmType::I32);
140
136
    }
141
136
    compile_for_stack_as(ctx, emit, symbols, arg, WasmType::Ratio)?;
142
136
    emit.call(ctx.ids.ratio_to_i64);
143
136
    emit.i32_wrap_i64();
144
136
    Ok(WasmType::I32)
145
272
}