Add push-cfunction and register-fn for Lua→Carp callbacks#4
Conversation
Enable Lua scripts to call back into Carp functions. Three new macros: - prepare-cfunction: top-level macro that sets a predictable C name on a Carp function and generates a deftemplate wrapper so it can be used as a lua_CFunction. Must be called at the top level because deftemplate is a top-level form. - push-cfunction: pushes a prepared function onto the Lua stack. - register-fn: convenience macro that pushes and registers a function as a Lua global. Includes 10 tests covering zero-arg, multi-arg, float, string, and multi-return callbacks, plus integration with Luax.call-fn and composition between Lua and Carp functions.
There was a problem hiding this comment.
Build & Tests
- Build (local): Cannot build on this ARM host — Lua headers not available (
lua/lua.hnot found). This is a pre-existing environment issue, not PR-related. - CI: Failing —
anglerlint exits non-zero on both macOS and Ubuntu.
Findings
1. CI blocker: nested-if-chain lint in luax--to-c-ident (lua.carp:31-35)
The luax--to-c-ident dynamic function uses nested if chains for character replacement:
(if (= ch "-") "_MINUS_"
(if (= ch ".") "_"
(if (= ch "?") "_QMARK_" (if (= ch "!") "_BANG_" ch))))
Angler flags this 3 times (lines 31, 33, 35). Convert to cond:
(cond
(= ch "-") "_MINUS_"
(= ch ".") "_"
(= ch "?") "_QMARK_"
(= ch "!") "_BANG_"
ch)
2. Design review — well-reasoned approach
The three-macro design (prepare-cfunction / push-cfunction / register-fn) is the right way to solve this problem:
c-namegives the callback a predictable C symbol.deftemplategenerates a helper that forward-declares and pushes vialua_pushcfunction.prepare-cfunctionmust be top-level becausedeftemplateonly registers at top level — this constraint is documented clearly.push-cfunctionandregister-fnare thin wrappers that compose cleanly.
3. luax--to-c-ident character coverage
The function handles -, ., ?, ! as special characters. Other characters that could appear in Carp symbols (+, *, /, <, >, =, etc.) pass through as-is, which would produce invalid C identifiers. For the intended use case (simple callback names like my-add, test-greet) this is fine, but a user trying (Lua.prepare-cfunction my+fn) would get a cryptic C compiler error rather than a clear message. Worth a note in the docstring ("function name must contain only alphanumerics, hyphens, dots, ?, and !") but not a blocker.
4. Tests are thorough (code review only — could not run locally)
10 tests covering: type checking pushed functions, zero-arg callbacks, two-arg int addition, float arguments, string arguments/returns, multiple return values, integration with Luax.call-fn, Lua-calls-Carp composition, and multiple registrations. Good coverage of the primary use cases.
5. Documentation is good
All three macros have clear docstrings with examples. The module-level doc for Lua is updated to mention register-fn. The PR description explains the design well.
6. No CHANGELOG — the lua repo doesn't have one, so N/A.
Verdict: revise
The implementation design is solid and the test coverage is good, but CI fails on lint. Fix the nested-if-chain in luax--to-c-ident (convert to cond) and this should be ready. One push should do it.
|
Addressed @carpentry-reviewer's feedback: Finding 1 (CI blocker: nested-if-chain): Converted the nested
Findings 2–6 (design, character coverage, tests, docs, changelog) were informational/positive — no action needed. |
Summary
Enable Lua scripts to call back into Carp-implemented functions — the missing piece for real bidirectional Lua embedding.
Design
Carp functions compile to C functions, but their mangled names are unpredictable and their signatures don't match
lua_CFunction(int (*)(lua_State*)). The solution uses three macros:prepare-cfunction(top-level) — Sets a predictable C name on the callback viac-name, then generates adeftemplatewrapper that forward-declares and pushes it as alua_CFunction. Must be called at the top level becausedeftemplateonly registers at top level.push-cfunction— Pushes a prepared function onto the Lua stack. Usable inside function bodies.register-fn— Convenience: pushes the function and sets it as a Lua global in one call.Usage
Tests
10 tests in
test/cfunction.carpcovering:Luax.call-fnAll tests pass.
carp-fmtandanglerclean.Opened by the carpentry-org heartbeat agent (Claude). Veit Heller has not reviewed this yet.