1
mod arithmetic;
2
mod comparison;
3
mod convert;
4
mod entity;
5
mod error_object;
6
mod host_fn;
7
mod io;
8
mod list;
9
mod report_node;
10
mod string;
11
mod structure;
12
mod typed_entity;
13
#[cfg(test)]
14
mod typed_entity_tests;
15

            
16
use super::context::CompileContext;
17
use super::emit::FunctionEmitter;
18
use crate::ast::{Expr, WasmType};
19
use crate::error::{Error, Result};
20
use crate::runtime::SymbolTable;
21

            
22
pub(in crate::compiler) use entity::compile_create_tag;
23
pub(super) use host_fn::{compile_host_fn_for_effect, compile_host_fn_for_stack};
24
pub(super) use io::compile_debug_effect;
25
pub(super) use io::{compile_newline_effect, compile_print_effect};
26
pub(in crate::compiler) use list::emit_pair_car_downcast;
27

            
28
/// Eval-time handler — folds constants, surfaces a `WasmRuntime` /
29
/// `WasmLocal` stand-in when arguments aren't fully known, validates
30
/// arity. Required for every native; the compile paths build on top.
31
pub(super) type EvalFn = fn(&mut SymbolTable, &[Expr]) -> Result<Expr>;
32

            
33
/// Stack-producing codegen — emits wasm that leaves the form's result
34
/// on the wasm operand stack and returns its `WasmType`. Required for
35
/// natives that compose as sub-expressions (CONS in `(+ x (car xs))`,
36
/// every arithmetic op, entity accessors). Natives whose result has
37
/// no single stack representation (CREATE-TAG, DELETE-ENTITY, MAP)
38
/// set this to `None` — the dispatcher refuses with a structured error.
39
pub(super) type StackFn =
40
    fn(&mut CompileContext, &mut FunctionEmitter, &mut SymbolTable, &[Expr]) -> Result<WasmType>;
41

            
42
/// Effect codegen — emits wasm at the top-level / for-effect position.
43
/// Most natives derive this from `stack` by emitting the stack form and
44
/// piping the result through the debug serializer; effect-only natives
45
/// (DEBUG side-effects, MAP folded at compile time) supply their own.
46
pub(super) type EffectFn =
47
    fn(&mut CompileContext, &mut FunctionEmitter, &mut SymbolTable, &[Expr]) -> Result<()>;
48

            
49
/// Canonical metadata for a built-in native fn. Every name appears
50
/// exactly once across the three dispatch paths — adding a new native
51
/// adds one row, and a missing handler is a compile-time error.
52
/// Replaces the three parallel hand-maintained match tables that
53
/// diverged silently before P3a 3a.3.
54
///
55
/// `effect: None` auto-derives the effect path as `stack + serialize`
56
/// — useful for the ~20 natives whose effect codegen is exactly
57
/// "produce the value on the stack, then debug-serialize". Natives
58
/// with custom effect logic (const-fold short-circuits that write
59
/// to output directly, structure forms, etc.) supply an explicit
60
/// EffectFn. `effect: None` requires `stack: Some(_)` — the
61
/// dispatcher refuses an `effect = None, stack = None` spec.
62
pub(super) struct NativeSpec {
63
    pub name: &'static str,
64
    pub eval: EvalFn,
65
    pub stack: Option<StackFn>,
66
    pub effect: Option<EffectFn>,
67
}
68

            
69
const DOMAINS: &[&[NativeSpec]] = &[
70
    arithmetic::NATIVES,
71
    comparison::NATIVES,
72
    convert::NATIVES,
73
    list::NATIVES,
74
    entity::NATIVES,
75
    typed_entity::NATIVES,
76
    report_node::NATIVES,
77
    structure::NATIVES,
78
    string::NATIVES,
79
    io::NATIVES,
80
    error_object::NATIVES,
81
];
82

            
83
425411
fn lookup(name: &str) -> Option<&'static NativeSpec> {
84
425411
    DOMAINS
85
425411
        .iter()
86
1278339
        .flat_map(|d| d.iter())
87
7828817
        .find(|s| s.name == name)
88
425411
}
89

            
90
306720
pub(super) fn call(symbols: &mut SymbolTable, name: &str, args: &[Expr]) -> Result<Expr> {
91
306720
    match lookup(name) {
92
306720
        Some(spec) => (spec.eval)(symbols, args),
93
        None => Err(Error::Compile(format!(
94
            "native function '{name}' not yet implemented"
95
        ))),
96
    }
97
306720
}
98

            
99
219944
pub(super) fn compile_for_stack(
100
219944
    ctx: &mut CompileContext,
101
219944
    emit: &mut FunctionEmitter,
102
219944
    symbols: &mut SymbolTable,
103
219944
    name: &str,
104
219944
    args: &[Expr],
105
219944
) -> Result<WasmType> {
106
219944
    if ctx.lookup_host_fn(name).is_some() {
107
131785
        return compile_host_fn_for_stack(ctx, emit, symbols, name, args);
108
88159
    }
109
88159
    match lookup(name) {
110
88159
        Some(spec) => match spec.stack {
111
88159
            Some(f) => f(ctx, emit, symbols, args),
112
            None => Err(Error::Compile(format!(
113
                "native function '{name}' cannot produce stack value"
114
            ))),
115
        },
116
        None => Err(Error::Compile(format!(
117
            "native function '{name}' not yet implemented"
118
        ))),
119
    }
120
219944
}
121

            
122
30815
pub(super) fn compile(
123
30815
    ctx: &mut CompileContext,
124
30815
    emit: &mut FunctionEmitter,
125
30815
    symbols: &mut SymbolTable,
126
30815
    name: &str,
127
30815
    args: &[Expr],
128
30815
) -> Result<()> {
129
30815
    if ctx.lookup_host_fn(name).is_some() {
130
408
        return compile_host_fn_for_effect(ctx, emit, symbols, name, args);
131
30407
    }
132
30407
    match lookup(name) {
133
30407
        Some(spec) => match spec.effect {
134
19118
            Some(f) => f(ctx, emit, symbols, args),
135
11289
            None => derived_effect(ctx, emit, symbols, name, args, spec),
136
        },
137
        None => Err(Error::Compile(format!(
138
            "native function '{name}' not yet implemented"
139
        ))),
140
    }
141
30815
}
142

            
143
/// Default effect path for natives with `effect: None`: compile via
144
/// the stack handler, then debug-serialize the result. Same shape as
145
/// the ~20 wrapper fns this replaces. Refuses if the spec also lacks
146
/// a stack handler — that's a programmer error in the registry, and
147
/// the registry test enforces "effect None implies stack Some".
148
11289
fn derived_effect(
149
11289
    ctx: &mut CompileContext,
150
11289
    emit: &mut FunctionEmitter,
151
11289
    symbols: &mut SymbolTable,
152
11289
    name: &str,
153
11289
    args: &[Expr],
154
11289
    spec: &NativeSpec,
155
11289
) -> Result<()> {
156
11289
    let stack_fn = spec.stack.ok_or_else(|| {
157
        Error::Compile(format!(
158
            "native function '{name}' has neither effect nor stack handler"
159
        ))
160
    })?;
161
11289
    let ty = stack_fn(ctx, emit, symbols, args)?;
162
9656
    super::expr::serialize_stack_to_output(ctx, emit, ty)?;
163
9656
    Ok(())
164
11289
}
165

            
166
#[cfg(test)]
167
mod tests {
168
    use super::*;
169
    use std::collections::HashSet;
170

            
171
    #[test]
172
1
    fn registry_names_unique() {
173
1
        let mut seen = HashSet::new();
174
93
        for spec in DOMAINS.iter().flat_map(|d| d.iter()) {
175
93
            assert!(
176
93
                seen.insert(spec.name),
177
                "duplicate native registration: {}",
178
                spec.name
179
            );
180
        }
181
1
    }
182

            
183
    #[test]
184
1
    fn registry_lookup_covers_every_entry() {
185
        // Every registered name must be reachable via the lookup helper —
186
        // catches accidental shadowing if a domain reorders its slice.
187
93
        for spec in DOMAINS.iter().flat_map(|d| d.iter()) {
188
93
            assert!(
189
93
                lookup(spec.name).is_some(),
190
                "lookup({}) returned None",
191
                spec.name
192
            );
193
        }
194
1
    }
195

            
196
    #[test]
197
1
    fn registry_none_effect_implies_some_stack() {
198
        // Auto-derived effect path needs the stack handler — a spec with
199
        // both None is a registry programmer error that the dispatcher
200
        // can only surface at runtime. Catch at unit-test time.
201
93
        for spec in DOMAINS.iter().flat_map(|d| d.iter()) {
202
93
            if spec.effect.is_none() {
203
58
                assert!(
204
58
                    spec.stack.is_some(),
205
                    "native {} has neither effect nor stack handler",
206
                    spec.name
207
                );
208
35
            }
209
        }
210
1
    }
211

            
212
    /// Every native NAME the reader registers as a builtin (tangled from
213
    /// `builtin_reference.org` into `builtins_generated::NATIVES`) must have a
214
    /// codegen handler reachable via `lookup`. Without this guard a documented
215
    /// native can exist as a symbol the reader accepts yet codegen rejects with
216
    /// "native function '…' not yet implemented" — the "phantom native" class
217
    /// that hid EQUAL?/EQ?/LENGTH/APPEND/PAIR?/PRINT/DISPLAY/NEWLINE until a
218
    /// script happened to call one. The samples are parse-only, so only this
219
    /// test (and real compile coverage) catches the gap.
220
    #[test]
221
1
    fn every_builtin_native_name_has_a_handler() {
222
32
        for name in crate::runtime::registered_native_names() {
223
32
            assert!(
224
32
                lookup(name).is_some(),
225
                "builtin native '{name}' is registered as a symbol but has no \
226
                 codegen handler — add a NativeSpec or remove it from the registry"
227
            );
228
        }
229
1
    }
230
}