Skip to content

Add Elixir-JavaScript bridge API with MQuickJS integration - #1

Merged
Valian merged 16 commits into
mainfrom
elixir-js-bridge-api
Dec 29, 2025
Merged

Add Elixir-JavaScript bridge API with MQuickJS integration#1
Valian merged 16 commits into
mainfrom
elixir-js-bridge-api

Conversation

@Valian

@Valian Valian commented Dec 29, 2025

Copy link
Copy Markdown
Owner

Comprehensive Elixir-JavaScript embedding implementation via MQuickJS NIF. Adds bidirectional function calling, automatic type conversion, API module system, and comprehensive documentation. Includes GitHub Actions CI/CD pipeline, build tooling, and full test coverage.
🤖 Generated with Claude Code

Co-Authored-By: Claude Haiku 4.5 noreply@anthropic.com

Valian and others added 16 commits December 26, 2025 18:49
Add minimal NIF embedding of the MQuickJS JavaScript engine into Elixir.
This validates the build system and basic JS evaluation from Elixir.

Key components:
- Two-stage Makefile build (stdlib generator → headers → NIF)
- NIF with new/1 and eval/2 functions
- Elixir wrapper with MquickjsEx.new/eval/eval!
- Vendored MQuickJS source files

The intermittent parsing failure issue (passing Erlang binary pointers
directly to JS_Eval) was resolved by copying binary data to a malloc'd
buffer before evaluation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add bidirectional type conversion enabling JS values to round-trip through
Elixir. This is the foundation for Phase 3's trampoline pattern where JS
code can call Elixir functions.

Changes:
- JS → Elixir: integers, floats, strings, booleans, null/undefined, arrays, objects
- Elixir → JS: all primitives, lists, maps (atom keys → strings)
- New NIFs: nif_get/2, nif_set/3, nif_gc/1
- eval now returns actual expression values via JS_EVAL_RETVAL
- Object enumeration via js_object_keys() from mquickjs_priv.h
- GC protection with JS_PushGCRef/JS_PopGCRef for nested conversions
- Recursion depth limit (100) to prevent stack overflow
- Functions return {:error, :function_not_serializable}

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Enable JavaScript code to call back into Elixir functions using a
replay-based trampoline pattern. This is the core feature for
LLM-generated JavaScript sandboxes.

Key changes:

NIF layer (c_src/mquickjs_ex.c):
- Add nif_run/3 that accepts code and cached callback results
- Inject __call function that returns cached results or yields
- Detect yield via special exception marker "__yield__:<name>:<args>"
- Parse yield exception and return {:yield, name, args_json}

Elixir layer (lib/mquickjs_ex.ex):
- Add run/3 and run!/3 functions for callback-enabled execution
- Wrap user code with callback function definitions
- Implement run loop: execute → yield → callback → resume
- Use Jason for JSON serialization of callback arguments/results

Testing:
- 25 new tests covering basic, sequential, nested callbacks
- Tests for error handling and complex scenarios
- Total: 69 tests, all passing

Documentation:
- Update API.md with "Running JavaScript with Elixir Callbacks" section
- Mark Phase 3 complete in implementation-plan.md

Dependencies:
- Add jason ~> 1.4 for JSON encoding/decoding

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Introduce MquickjsEx.Context struct to wrap the NIF reference alongside
a callbacks map. This enables `set/3` to accept Elixir functions that
persist across multiple `eval/2` calls without explicitly passing them
to `run/3`.

Key changes:
- Context struct stores ref + callbacks, replacing raw NIF reference
- set/3 accepts functions (arity 1) and stores them in context
- eval/2 auto-invokes trampoline when context has callbacks
- run/3 merges context callbacks with explicit ones (explicit wins)
- set/3 for values now returns {:ok, ctx} for API consistency

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The run/3 and run!/3 functions were redundant since set/3 already
allows registering callbacks that persist in the context. This change
simplifies the API to a single pattern: set callbacks with set!/3,
then execute with eval/2.

Also adds comprehensive security tests verifying the JS sandbox is
safe for LLM use cases - checking that file system, network, process
access, and dangerous globals are properly restricted.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add project MIT license (Copyright Jakub Skalecki)
- Add vendored mquickjs LICENSE file (Bellard & Gordon)
- Update README with third-party code attribution

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Change callback signature from `fn [a, b] -> ... end` (arity-1 with list)
to `fn a, b -> ... end` (natural Elixir functions). Function arity must
match the number of arguments passed from JavaScript.

- Remove arity-1 restriction on set/3 and set!/3
- Use apply/2 instead of direct invocation for callbacks
- Update type specs to use function() instead of (list() -> any())

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Verify that built-in JS functions (console.log, Math.*, JSON.*, etc.)
can be replaced by user code, that replacements persist across eval
calls, and that contexts remain isolated from each other.

Also documents MQuickJS ES5 limitations: no rest parameters, no
Object.freeze/seal, limited Date constructor.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Two related improvements to the set/set! API:

1. Callbacks now receive arguments as a single list instead of using
   apply/3 with variable arity. This simplifies the implementation
   and makes the callback signature consistent:
   - Before: fn a, b -> a + b end
   - After:  fn [a, b] -> a + b end

2. Add support for setting nested values via path lists:
   - set!(ctx, [:debug, :enabled], true) creates debug.enabled in JS
   - Intermediate objects are created automatically if they don't exist
   - Errors if an intermediate value exists but is not an object
   - Replaces nif_set with unified nif_set_path (single key becomes [key])

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add a Lua.API-inspired module for defining JavaScript-callable functions:

- `defjs` macro for defining functions (with/without state access)
- Scope support via `scope: "namespace"` or `scope: [:namespace]`
- Variadic functions with `@variadic true` attribute
- Guards support for type dispatch
- Argument destructuring via pattern matching
- `install/3` callback for setup code when API is loaded
- `runtime_exception!/1` macro for contextual errors

Also introduces RuntimeException for all JS errors, replacing plain
RuntimeError for better error context and consistency.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add put_private/3, get_private/2, get_private!/2, and delete_private/2
for storing Elixir data associated with a context but not exposed to
JavaScript. Useful for API functions that need shared state.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Test against Elixir 1.16.3/OTP 26, 1.17.3/OTP 27, 1.18.3/OTP 27
- Run tests and format check
- Update elixir version constraint to ~> 1.16
- Apply formatter fixes to test files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add "Why MquickjsEx?" section explaining no Node.js/Bun dependency
- Highlight LLM tool calling use case with example
- Document type conversions, API modules, private storage
- Explain trampoline pattern for Elixir callbacks from JavaScript
- List MQuickJS JavaScript subset limitations
- Add badges (hex placeholder, Elixir version, MIT license)
- Integrate README content into main module's @moduledoc via MDOC markers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
load_api/3 was returning the context directly while load_api!/3 claimed
to "raise on error" but just delegated without any error handling. This
was inconsistent with other functions like set/3 which return tuples.

Changes:
- load_api/3 now returns {:ok, ctx} or {:error, reason}
- load_api!/3 now properly unwraps and raises via raise_js_error/1
- Added try/rescue to catch UndefinedFunctionError (invalid API module)
  and RuntimeException (install callback failures)
- Updated all documentation and examples across README, docs/API.md,
  and module docs
- Updated all 24 test cases to use new tuple pattern
- Added 2 new tests for error handling paths

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@Valian
Valian merged commit 13392ea into main Dec 29, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant