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::{
26
    chain_pair_element, compile_constant_list, emit_pair_car_downcast, literal_pair_element,
27
};
28

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

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

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

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

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

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

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

            
99
499830
pub(super) fn compile_for_stack(
100
499830
    ctx: &mut CompileContext,
101
499830
    emit: &mut FunctionEmitter,
102
499830
    symbols: &mut SymbolTable,
103
499830
    name: &str,
104
499830
    args: &[Expr],
105
499830
) -> Result<WasmType> {
106
499830
    if ctx.lookup_host_fn(name).is_some() {
107
366446
        return compile_host_fn_for_stack(ctx, emit, symbols, name, args);
108
133384
    }
109
133384
    match lookup(name) {
110
133384
        Some(spec) => match spec.stack {
111
133384
            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
499830
}
121

            
122
37600
pub(super) fn compile(
123
37600
    ctx: &mut CompileContext,
124
37600
    emit: &mut FunctionEmitter,
125
37600
    symbols: &mut SymbolTable,
126
37600
    name: &str,
127
37600
    args: &[Expr],
128
37600
) -> Result<()> {
129
37600
    if ctx.lookup_host_fn(name).is_some() {
130
498
        return compile_host_fn_for_effect(ctx, emit, symbols, name, args);
131
37102
    }
132
37102
    match lookup(name) {
133
37102
        Some(spec) => match spec.effect {
134
23323
            Some(f) => f(ctx, emit, symbols, args),
135
13779
            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
37600
}
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
13779
fn derived_effect(
149
13779
    ctx: &mut CompileContext,
150
13779
    emit: &mut FunctionEmitter,
151
13779
    symbols: &mut SymbolTable,
152
13779
    name: &str,
153
13779
    args: &[Expr],
154
13779
    spec: &NativeSpec,
155
13779
) -> Result<()> {
156
13779
    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
13779
    let ty = stack_fn(ctx, emit, symbols, args)?;
162
11786
    super::expr::serialize_stack_to_output(ctx, emit, ty)?;
163
11786
    Ok(())
164
13779
}
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
}