diff --git a/shared/sx/ref/eval.sx b/shared/sx/ref/eval.sx index ccc1a8d..7d3b5e9 100644 --- a/shared/sx/ref/eval.sx +++ b/shared/sx/ref/eval.sx @@ -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)))) diff --git a/shared/sx/ref/sx_ref.py b/shared/sx/ref/sx_ref.py index dfac72f..a11f98e 100644 --- a/shared/sx/ref/sx_ref.py +++ b/shared/sx/ref/sx_ref.py @@ -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) === diff --git a/shared/sx/tests/test_sx_ref.py b/shared/sx/tests/test_sx_ref.py index ae21015..8e0fa80 100644 --- a/shared/sx/tests/test_sx_ref.py +++ b/shared/sx/tests/test_sx_ref.py @@ -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 = {}