Add call-fn dispatch for HO forms: handle both Lambda and native callable

HO forms (map, filter, reduce, etc.) now use call-fn which dispatches
Lambda → call-lambda, native callable → apply, else → clear EvalError.
Previously call-lambda crashed with AttributeError on native functions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 22:45:01 +00:00
parent d076fc1465
commit ef04beba00
3 changed files with 48 additions and 14 deletions

View File

@@ -632,18 +632,26 @@
;; 7. Higher-order forms
;; --------------------------------------------------------------------------
;; call-fn: unified caller for HO forms — handles both Lambda and native callable
(define call-fn
(fn (f args env)
(cond
(lambda? f) (trampoline (call-lambda f args env))
(callable? f) (apply f args)
:else (error (str "Not callable in HO form: " (inspect f))))))
(define ho-map
(fn (args env)
(let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env))))
(map (fn (item) (trampoline (call-lambda f (list item) env))) coll))))
(map (fn (item) (call-fn f (list item) env)) coll))))
(define ho-map-indexed
(fn (args env)
(let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env))))
(map-indexed
(fn (i item) (trampoline (call-lambda f (list i item) env)))
(fn (i item) (call-fn f (list i item) env))
coll))))
(define ho-filter
@@ -651,7 +659,7 @@
(let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env))))
(filter
(fn (item) (trampoline (call-lambda f (list item) env)))
(fn (item) (call-fn f (list item) env))
coll))))
(define ho-reduce
@@ -660,7 +668,7 @@
(init (trampoline (eval-expr (nth args 1) env)))
(coll (trampoline (eval-expr (nth args 2) env))))
(reduce
(fn (acc item) (trampoline (call-lambda f (list acc item) env)))
(fn (acc item) (call-fn f (list acc item) env))
init
coll))))
@@ -669,7 +677,7 @@
(let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env))))
(some
(fn (item) (trampoline (call-lambda f (list item) env)))
(fn (item) (call-fn f (list item) env))
coll))))
(define ho-every
@@ -677,7 +685,7 @@
(let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env))))
(every?
(fn (item) (trampoline (call-lambda f (list item) env)))
(fn (item) (call-fn f (list item) env))
coll))))
@@ -686,7 +694,7 @@
(let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env))))
(for-each
(fn (item) (trampoline (call-lambda f (list item) env)))
(fn (item) (call-fn f (list item) env))
coll))))

View File

@@ -968,26 +968,29 @@ sf_set_bang = lambda args, env: (lambda name: (lambda value: _sx_begin(_sx_dict_
# expand-macro
expand_macro = lambda mac, raw_args, env: (lambda local: _sx_begin(for_each(lambda pair: _sx_dict_set(local, first(pair), (nth(raw_args, nth(pair, 1)) if sx_truthy((nth(pair, 1) < len(raw_args))) else NIL)), map_indexed(lambda i, p: [p, i], macro_params(mac))), (_sx_dict_set(local, macro_rest_param(mac), slice(raw_args, len(macro_params(mac)))) if sx_truthy(macro_rest_param(mac)) else NIL), trampoline(eval_expr(macro_body(mac), local))))(env_merge(macro_closure(mac), env))
# call-fn
call_fn = lambda f, args, env: (trampoline(call_lambda(f, args, env)) if sx_truthy(is_lambda(f)) else (apply(f, args) if sx_truthy(is_callable(f)) else error(sx_str('Not callable in HO form: ', inspect(f)))))
# ho-map
ho_map = lambda args, env: (lambda f: (lambda coll: map(lambda item: trampoline(call_lambda(f, [item], env)), coll))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env)))
ho_map = lambda args, env: (lambda f: (lambda coll: map(lambda item: call_fn(f, [item], env), coll))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env)))
# ho-map-indexed
ho_map_indexed = lambda args, env: (lambda f: (lambda coll: map_indexed(lambda i, item: trampoline(call_lambda(f, [i, item], env)), coll))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env)))
ho_map_indexed = lambda args, env: (lambda f: (lambda coll: map_indexed(lambda i, item: call_fn(f, [i, item], env), coll))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env)))
# ho-filter
ho_filter = lambda args, env: (lambda f: (lambda coll: filter(lambda item: trampoline(call_lambda(f, [item], env)), coll))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env)))
ho_filter = lambda args, env: (lambda f: (lambda coll: filter(lambda item: call_fn(f, [item], env), coll))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env)))
# ho-reduce
ho_reduce = lambda args, env: (lambda f: (lambda init: (lambda coll: reduce(lambda acc, item: trampoline(call_lambda(f, [acc, item], env)), init, coll))(trampoline(eval_expr(nth(args, 2), env))))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env)))
ho_reduce = lambda args, env: (lambda f: (lambda init: (lambda coll: reduce(lambda acc, item: call_fn(f, [acc, item], env), init, coll))(trampoline(eval_expr(nth(args, 2), env))))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env)))
# ho-some
ho_some = lambda args, env: (lambda f: (lambda coll: some(lambda item: trampoline(call_lambda(f, [item], env)), coll))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env)))
ho_some = lambda args, env: (lambda f: (lambda coll: some(lambda item: call_fn(f, [item], env), coll))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env)))
# ho-every
ho_every = lambda args, env: (lambda f: (lambda coll: every_p(lambda item: trampoline(call_lambda(f, [item], env)), coll))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env)))
ho_every = lambda args, env: (lambda f: (lambda coll: every_p(lambda item: call_fn(f, [item], env), coll))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env)))
# ho-for-each
ho_for_each = lambda args, env: (lambda f: (lambda coll: for_each(lambda item: trampoline(call_lambda(f, [item], env)), coll))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env)))
ho_for_each = lambda args, env: (lambda f: (lambda coll: for_each(lambda item: call_fn(f, [item], env), coll))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env)))
# === Transpiled from forms (server definition forms) ===

View File

@@ -384,6 +384,29 @@ class TestDefhandler:
assert adef.params == ["title", "body"]
class TestHOWithNativeCallable:
def test_map_with_native_fn(self):
"""map should work with native callables (primitives), not just Lambda."""
result = ev('(map str (list 1 2 3))')
assert result == ["1", "2", "3"]
def test_filter_with_native_fn(self):
result = ev('(filter number? (list 1 "a" 2 "b" 3))')
assert result == [1, 2, 3]
def test_map_with_env_fn(self):
"""map should work with Python functions registered in env."""
env = {"double": lambda x: x * 2}
result = ev('(map double (list 1 2 3))', env)
assert result == [2, 4, 6]
def test_ho_non_callable_fails_fast(self):
"""Passing a non-callable to map should error clearly."""
import pytest
with pytest.raises(sx_ref.EvalError, match="Not callable"):
ev('(map 42 (list 1 2 3))')
class TestMacros:
def test_defmacro_and_expand(self):
env = {}