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 string;
10
mod structure;
11
mod typed_entity;
12
#[cfg(test)]
13
mod typed_entity_tests;
14

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

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

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

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

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

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

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

            
81
443599
fn lookup(name: &str) -> Option<&'static NativeSpec> {
82
443599
    DOMAINS
83
443599
        .iter()
84
1322391
        .flat_map(|d| d.iter())
85
8155333
        .find(|s| s.name == name)
86
443599
}
87

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

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

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

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

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

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

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

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

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