Lines
97.89 %
Functions
100 %
Branches
//! LAMBDA / FUNCTION / FUNCALL / APPLY codegen.
use super::common::{compile_and_validate, compile_expect_error, wrap_with_runtime_ratio};
#[test]
fn lambda_literal_compiles() {
compile_and_validate("(lambda (x) (+ x 1))");
}
fn lambda_with_rest_param() {
compile_and_validate("(lambda (x &rest more) x)");
fn lambda_with_optional_param() {
compile_and_validate("(lambda (x &optional (y 10)) (+ x y))");
fn function_form_quotes_symbol() {
compile_and_validate("(defun greet (x) x) (function greet)");
fn function_unknown_symbol_errors() {
let err = compile_expect_error("(function no-such-fn)");
assert!(
err.contains("function definition")
|| err.contains("no-such-fn")
|| err.contains("Undefined")
|| err.contains("NO-SUCH-FN"),
"got: {err}",
);
fn funcall_resolves_designator_and_calls() {
compile_and_validate("(defun double (x) (* x 2)) (funcall (function double) 5)");
fn funcall_with_runtime_arg() {
compile_and_validate(&wrap_with_runtime_ratio(
"(defun double (n) (* n 2)) (funcall (function double) X)",
));
fn apply_with_list() {
compile_and_validate("(defun sum3 (a b c) (+ a b c)) (apply (function sum3) (list 1 2 3))");
fn apply_with_runtime_arg() {
"(defun id (n) n) (apply (function id) (list X))",
fn funcall_empty_args_errors() {
let err = compile_expect_error("(funcall)");
err.contains("funcall") || err.contains("FUNCALL") || err.contains("function argument"),
"got: {err}"
fn apply_too_few_args_errors() {
let err = compile_expect_error("(apply)");
assert!(err.contains("apply") || err.contains("APPLY"), "got: {err}");
fn apply_one_arg_errors() {
let err = compile_expect_error("(apply (function +))");
fn apply_with_non_list_last_arg_errors() {
let err = compile_expect_error("(defun id (x) x) (apply (function id) 42)");
err.contains("apply") || err.contains("APPLY") || err.contains("quoted list"),
fn apply_with_quoted_non_list_errors() {
// `'42` is a quoted number, not a quoted list — apply must
// reject so the spread doesn't reinterpret an atom as args.
let err = compile_expect_error("(defun id (x) x) (apply (function id) '42)");
fn apply_with_empty_quoted_list_compiles() {
// `apply` with `'()` as the last arg should be a zero-arg call
// — the empty spread is a real use case for variadic helpers.
compile_and_validate("(defun zero () 0) (apply (function zero) '())");
fn function_with_lambda_compiles() {
// `(function (lambda ...))` route — exercises the inline lambda
// case in function_form, distinct from the bare symbol case.
compile_and_validate("(function (lambda (x) x))");
fn function_with_invalid_arg_errors() {
// Bare number isn't a symbol or lambda — function_form's
// fallback error path.
let err = compile_expect_error("(function 42)");
err.contains("symbol or lambda") || err.contains("FUNCTION"),
fn lambda_missing_body_errors() {
let err = compile_expect_error("(lambda)");
err.contains("LAMBDA") || err.contains("parameter list"),
fn lambda_only_params_errors() {
let err = compile_expect_error("(lambda (x))");
fn lambda_multi_form_body_wraps_in_begin() {
// Two body forms must be wrapped in BEGIN — exercises the
// `forms.push(Symbol("BEGIN"))` branch in `lambda()`.
compile_and_validate("(funcall (lambda (x) (+ x 1) (+ x 2)) 5)");
/// Tier 1.5 Gap A regression-lock: a FUNCALL whose return value is
/// consumed in arithmetic must drive the `stack` callback in
/// special::lambda::FORMS — without it, `compile_for_stack` errored
/// with "special form 'FUNCALL' cannot produce stack value".
fn funcall_at_stack_position_compiles() {
compile_and_validate("(defun double (x) (* x 2)) (+ (funcall (function double) 5) 1)");
/// Tier 1.5 Gap A regression-lock: same constraint for APPLY in
/// stack position.
fn apply_at_stack_position_compiles() {
compile_and_validate("(defun sum2 (a b) (+ a b)) (+ (apply (function sum2) (list 3 4)) 1)");
/// Tier 1.5 runtime-closure dispatch: a let-bound lambda used in
/// FUNCALL must lower through the `$closure` GC value + `call_ref`
/// path, not the inline lambda-call path. Locks the call-site
/// rewrite that promotes `Expr::Lambda` inits to `Expr::WasmLocal`
/// closures.
fn let_bound_lambda_funcall_compiles_via_call_ref() {
compile_and_validate("(let ((f (lambda (x) (* x 2)))) (funcall f 5))");
fn let_star_bound_lambda_funcall_compiles_via_call_ref() {
compile_and_validate("(let* ((f (lambda (x) (* x 2)))) (funcall f 5))");
/// Stack-position variant: the runtime-closure dispatch must serve
/// `compile_symbol_call_for_stack` too so the value flows into
/// surrounding arithmetic.
fn let_bound_lambda_funcall_at_stack_position_compiles() {
compile_and_validate("(let ((f (lambda (x) (* x 2)))) (+ (funcall f 5) 1))");
/// Arity mismatch on a runtime-closure call must be caught at compile
/// time by `compile_call_ref` — the `$closure_<sig>` carries the
/// expected param count and we can't dispatch through `call_ref` with
/// the wrong shape.
fn let_bound_lambda_funcall_arity_mismatch_errors() {
let err = compile_expect_error("(let ((f (lambda (x) (* x 2)))) (funcall f 5 6))");
err.contains("closure") || err.contains("arity") || err.contains("expected"),
/// Tier 1.5 Gap B closure: a recursive defun called with a runtime
/// arg lowers through the monomorph cache + runtime-call path
/// (`__defun_<name>_<sig>` wasm fn) instead of overflowing the inline
/// const-fold walk. Validation is enough — the wasm's recursive
/// `call $idx` survives the validator's stack-typing pass only if the
/// fixpoint return-type inference converged on the right `Ratio`
/// signature.
fn recursive_defun_with_runtime_arg_compiles_and_runs() {
let src = "(defun fact (n) (if (<= n 1) 1 (* n (fact (- n 1)))))
(let* ((X (transaction-post-date 0))) (fact X))";
compile_and_validate(src);
/// A recursive runtime-call defun whose signature includes a `#f` / `nil`
/// literal argument must compile. Bool/nil literals lower to `WasmType::Bool`
/// on the stack, so `infer_arg_type` must classify them as `Bool` too —
/// otherwise the monomorph signature says `I32` while `emit_runtime_call`
/// pushes `Bool` and the exact-type check rejects the call. Regression for the
/// WasmType::Bool / runtime-call inference mismatch.
fn recursive_defun_with_bool_literal_arg_compiles() {
let src = "(defun bounce (x flag) (if flag (bounce x #f) x))
(let* ((X (transaction-post-date 0))) (bounce X #t))";
/// A recursive runtime-arg defun nested as an ARGUMENT to a native (not in
/// head position) routes through the eval-side recursion guard
/// (`dispatch_symbol` re-entry → `recursive_runtime_call_type` placeholder)
/// rather than overflowing the const-fold walk, then the codegen monomorph
/// path emits the real call. `fact` has a base case so it's well-formed;
/// validation confirms the nested call lowered without a compiler overflow.
fn recursive_runtime_arg_call_nested_in_native_compiles() {
(let* ((X (transaction-post-date 0))) (+ (/ 1 1) (fact X)))";
/// A pass-through recursive runtime-arg call (`(loopx x)`) nested in a native
/// COMPILES: the eval-side re-entry guard hands it to the codegen monomorph
/// path (the compile-time walk terminates; the unbounded recursion becomes a
/// runtime trap, not a compiler overflow). Validation confirms the nested
/// `call $idx` lowered. The nms suite covers the runtime-trap behavior and the
/// depth-guard error path for the cases the monomorph path can't take over.
fn passthrough_recursive_runtime_arg_nested_in_native_compiles() {
compile_and_validate(
"(defun loopx (x) (loopx x)) \
(let* ((X (transaction-post-date 0))) (+ (/ 1 1) (loopx X)))",
/// Two distinct call sites of the same defun with the same arg-type
/// signature share a single emitted monomorph — the cache key is
/// `(name, params)`. Compile success + validation is the contract the
/// test pins; emitting two helpers for an identical signature would
/// not change the wire result but would bloat the wasm and is the
/// regression we're guarding against.
fn monomorph_cache_reuses_for_same_sig() {
let src = "(defun double (n) (* n 2))
(let* ((X (transaction-post-date 0))
(Y (transaction-post-date 1)))
(+ (double X) (double Y)))";
/// Same defun called with two different runtime arg types must emit
/// two distinct monomorphs because the cache is keyed on the param
/// signature, not the name. We exercise it with `Ratio` on one site
/// and a `Ratio`-bearing local on another — both routes hit the
/// runtime-call path.
fn monomorph_cache_separates_for_different_sigs() {
let src = "(defun id (n) n)
(IDX (entity-count)))
(+ (id X) (id X)))";
/// Regression (AFL, compile-level): a lambda body that is a too-short binder
/// form — `(map (lambda (s) (do)) (list 1))` — drove closure param-type
/// inference to slice `&elems[2..]` out of range and PANIC. `compile_expect_error`
/// fails on a panic, so this pins the public contract that the malformed shape
/// surfaces a structured compile error through the real `compile()` path, not a
/// SIGABRT. (The `param_infer` unit test covers the helper in isolation.)
fn lambda_body_short_binder_does_not_panic_compiler() {
for body in ["(do)", "(let)", "(dolist)", "(do*)"] {
let _ = compile_expect_error(&format!("(map (lambda (s) {body}) (list 1))"));