Several restrictive limits removed.

This commit is contained in:
Scott Duensing 2026-07-02 00:08:08 -05:00
parent c11bf8481f
commit 6c644064dd
24 changed files with 1536 additions and 84 deletions

View file

@ -55,5 +55,11 @@ applies to its files; the full text ships alongside each engine's source.
- **Wren** — Copyright (c) 2013-2021 Robert Nystrom and Wren Contributors. MIT
License. `vendor/wren/LICENSE` (amalgamated into `vendor/wren/wren.c`).
> Note: `vendor/wren/wren.c` and `wren.h` carry one small calog patch adding a public
> C map-key iterator (`wrenGetMapCapacity` / `wrenGetMapEntry`), which upstream Wren
> lacks; it mirrors Wren's own internal `map_iterate`. The additions are bracketed by
> `--- calog patch ---` comments. Re-apply them if the amalgamation is regenerated from
> upstream (`util/generate_amalgamation.py`).
- **Berry** — Copyright (c) 2018-2026 Guan Wenliang and Berry contributors. MIT
License. `vendor/berry/` (see the upstream `LICENSE`).

View file

@ -36,8 +36,9 @@ Swap `&calogJsEngine` for `&calogLuaEngine`, `&calogSquirrelEngine`,
native inline on the script's thread when you want it.)
- **Fire-and-forget scripts.** Scripts run on their own context threads; the host stays
responsive and drives everything from its own loop.
- **Callbacks both ways.** A script can hand you a function value; you keep it and call
it later, and calog routes the call back to the engine that owns it.
- **Callbacks both ways.** A script can hand you a function value to keep and call later
(routed back to the engine that owns it); and you can hand a native to a script as a
callable value with `calogFnFromNative` (every engine but MY-BASIC).
- **Many runtimes.** Independent `CalogT` runtimes coexist in one process; one host
thread can drive several.
- **Load by filename.** `calogContextLoad(calog, "config")` finds `config.lua` /
@ -60,8 +61,11 @@ Swap `&calogJsEngine` for `&calogLuaEngine`, `&calogSquirrelEngine`,
| Berry | `calogBerryEngine` | `.be` | scalars + callbacks |
A native can return a keyed `CalogValueT` record and **every** engine reads its fields with
native syntax (`user.name`, `user["name"]`, `(user "name")`). Handing a list/map *back* to
C is complete on Lua/JS/Squirrel/MY-BASIC and a v1 limit on Scheme/Wren/Berry.
native syntax (`user.name`, `user["name"]`, `(user "name")`), and a script can hand a
list/map *back* to C on **every** engine. (Wren's C API can't enumerate a map's keys, so
calog carries a small documented patch to the vendored Wren that adds one.) A host function
value also crosses *into* a script on every engine but MY-BASIC (which has no first-class
callable values).
You can also bring your own: `CalogEngineT` is a public four-function vtable.
@ -267,6 +271,21 @@ calogValueFree(&arg);
calogFnRelease(savedCb);
```
It works the other way too: wrap one of your natives as a function value with
`calogFnFromNative` and return it from a native, and the script gets a callable it can
invoke (which routes back to your host thread). Every engine but MY-BASIC supports this.
```c
static int32_t getAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogFnT *fn;
(void)args; (void)argCount; (void)userData;
calogValueNil(result);
calogFnFromNative(&fn, calog, add, NULL); // 'add' is one of your natives
calogValueFn(result, fn); // the script can now call getAdder()(2, 3)
return calogOkE;
}
```
### Loading a script by filename
Register the engines you want searchable (order is priority), then load by base name:

View file

@ -839,10 +839,9 @@ marshals the return; `squirrelCallableRelease` `sq_release`s on the owner thread
Single-threaded `testSquirrel` covers export+invoke-from-C, a closure passed as an
argument and called back through the broker, and the not-found/type-error paths;
ASan-clean (no addref/release leak). Caveat (same as Lua): release exported
callables before `squirrelContextDestroy`. Remaining limit: the reverse direction
(a foreign `CallableT` pushed INTO Squirrel so a script can call it) returns
`brokerErrUnsupportedE` -- Squirrel has no clean callable-userdata-with-`__gc` like
Lua, so it needs a class instance with a `_call` metamethod + release hook.
callables before `squirrelContextDestroy`. (The reverse direction -- a foreign `CalogFnT`
pushed INTO Squirrel -- is now supported too: a native closure whose one free variable is
a release-hooked userdata holding the `CalogFnT`; see sec 18.)
`make test` runs all seven binaries (411 checks). `make tsan` covers the actor core
and the Lua engine path; `make tsansq` the Squirrel path.
@ -900,8 +899,9 @@ nil. The sharp edge: `be_pcall(vm, argc)` leaves the **result in the function's
(`be_modtab.c`). A subtlety for records: `be_newmap`/`be_newlist` push *raw* containers
that a script cannot subscript, so an aggregate crossing out is wrapped in its `map`/`list`
class instance (`map(raw)` via `be_getbuiltin` + `be_call`, then `be_moveto`/`be_pop` to
drop the raw and `init`'s nil return) -- then `user['name']` works. Reading a script's
list/map back in is a v1 limit.
drop the raw and `init`'s nil return) -- then `user['name']` works. Ingress reverses it:
a list/map instance holds its raw container in the hidden `.p` member, iterated with
`be_pushiter`/`be_iter_next` (see sec 18).
**s7 Scheme** (`.scm`) uses the *current* official s7 (an older mirror lacked `s7_free`,
which would leak a heap per context). Since s7 native functions carry no user data, all
@ -937,3 +937,70 @@ Berry/s7/Wren.
**Verified** across all seven engines: `make test` (531 checks, including a materialized-
record read per new engine), ASan/UBSan clean, a `tsan<engine>` target clean for each, and
gcc + clang strict on the core.
## 18. Closing the v1 marshalling limits (function-into-script, aggregate ingress)
The engines above shipped with two directional gaps in the value bridge: a foreign
function value could not be pushed *into* a script (only Lua did it), and a script could
not hand a keyed aggregate (map) *back* to C on the three newest engines. Both are now
closed everywhere they can be, with one honest exception each.
**New public API** `calogFnFromNative(out, calog, fn, userData)` -- wraps one of your
natives as a host-owned function value (owner id 0, runs on the host thread during
`calogPump`, like `calogRegister` but anonymous). Without it, function-into-script was
unusable from `calog.h` alone (`calogFnCreate` is internal), so a host could only forward
a *script*-derived callable, never one of its own natives. Return the result from a native
and the script gets a callable that routes back to the host.
**Function value -> script** (each `*FromValue` `calogFnE` case): an engine callable
object wraps the `CalogFnT*`, a trampoline marshals the script's args -> `calogFnInvoke`
-> marshals the result, and a finalizer runs `calogFnRelease`; `calogFnRetain` at push.
Per engine:
- **Lua** (pre-existing): userdata + `__call` + `__gc` metatable -- the reference for the rest.
- **JS**: a `JSClassDef` with both `.call` and `.finalizer`; the finalizer only receives
the runtime, so `JS_SetRuntimeOpaque` carries the context to it.
- **Squirrel**: a native closure whose single free variable is a release-hooked userdata
holding the `CalogFnT` (freeing the closure frees the userdata -> the hook releases).
- **s7**: an applicable c-object (`s7_make_c_type` + `s7_c_type_set_ref` for the call,
`s7_c_type_set_free` for release); the `ref` fn gets `(obj . args)`, so the object is
`s7_car`. Script calls `(f ...)`.
- **Berry**: no per-value finalizer exists, so the `CalogFnT`s pushed into a context are
*tracked on the context* and released together in `calogBerryDestroy`; the callable is a
native closure over `(context, CalogFnT)` comptr upvalues. (`berryFromValue` gained the
context parameter so it could record them.)
- **Wren**: a `foreign class CalogFn { construct new() {} foreign call(args) }` whose
`call` takes a *list* (Wren method arity is fixed, so a list absorbs any argument count);
finalize releases. Script calls `f.call([...])`. Gotcha: Wren requires newlines between
class members, so the preamble is multi-line.
- **MY-BASIC**: inherent gap -- BASIC has no first-class callable values to invoke.
**Aggregate ingress** (`*ToValue` map/list): Lua/JS/Squirrel/MY-BASIC already read both.
Added:
- **s7**: read a hash-table by `s7_make_iterator` + `s7_iterate` (each yields a
`(key . value)` cons; gotcha: at end `s7_iterate` returns a *non-pair* sentinel even
when the at-end flag was still false, so guard `if (!s7_is_pair(pair)) break`).
- **Berry**: a list/map instance's raw container is its hidden `.p` member; iterate with
`be_pushiter`/`be_iter_hasnext`/`be_iter_next` (which take the *container* index with the
iterator kept on top, pushing one value for a list and key+value for a map -- restore the
stack to `[container, iterator]` after each entry).
- **Wren**: a `List` reads back directly. A `Map` needs key enumeration, which upstream
Wren's C API lacks (`wrenGetMapValue` is by-key only) -- so calog adds a small patch to
the vendored `wren.c`/`wren.h` (`wrenGetMapCapacity` / `wrenGetMapEntry`, mirroring Wren's
own internal `map_iterate`), and the adapter walks the raw table skipping empty slots.
With the patch, Wren too reads maps back in. (Documented in LICENSE.md; re-apply if the
amalgamation is regenerated.)
**Deliberately not "fixed" (inherent to the engine's value model):** MY-BASIC 32-bit ints
(over 2^31 range-checked to an error, not silently truncated), MY-BASIC NUL-in-string
truncation and serialize-at-load, and JS/Wren int64 above 2^53 (IEEE doubles -- JS *could*
emit a BigInt but that breaks arithmetic mixing with Number, a worse trap than the
documented precision edge). `WREN_MAX_CALL_ARITY` (16) is pinned to Wren's own engine
limit (`MAX_PARAMETERS`) and can't be raised. `MB_BANK_SIZE` (the MY-BASIC native cap) was
32 -- the one hard cap a real app could hit, since MY-BASIC natives can't carry userdata
so each needs a hand-materialized slot trampoline; it is now **256** (the trampoline bank
and its `[MB_BANK_SIZE]` table are regenerated together, so a count mismatch fails to
compile). Berry's `BE_BYTES_MAX_SIZE` was likewise raised from 32 kb to 256 MB.
**Verified**: `make test` (539 checks) with a `testForeignFunction` per engine and a
`testMapIngress` on s7 and Berry (the Berry one nests a list to exercise list ingress);
ASan-clean on every engine (retain/release balanced); gcc strict; per-engine `tsan*`.

View file

@ -4,10 +4,12 @@
// (a comptr) and the native's name -- so the trampoline recovers its binding via
// be_getupval, marshals the Berry arguments to CalogValueT, and dispatches through
// calogCall (honoring the actor route hook). Marshalling covers scalars and binary-safe
// strings; a Berry function crossing out becomes a refcounted CalogFnT kept reachable by
// a uniquely-named hidden global (a GC root), dropped by setting that global to nil on
// release. An aggregate crossing out becomes a Berry list or map instance (a host record
// materializes as a map); reading a script's list/map back in is a v1 limit.
// strings; aggregates cross both ways (out as a Berry list/map instance -- a host record
// materializes as a map -- and a script's list/map is read back by iterating its raw
// container). Functions cross both ways too: a Berry function out becomes a refcounted
// CalogFnT kept reachable by a hidden global (a GC root); a foreign CalogFnT pushed in
// becomes a native closure the script calls directly (tracked on the context and released
// at destroy, since Berry has no per-value finalizer).
//
// Error model: on failure the trampoline raises a Berry exception (be_raise, which
// longjmps out of the VM) after releasing any CalogValueT it owns.
@ -23,14 +25,18 @@
#include <stdlib.h>
#include <string.h>
#define BERRY_ERR_CAP 256
#define BERRY_REF_CAP 32
#define BERRY_ERR_CAP 256
#define BERRY_REF_CAP 32
#define BERRY_FOREIGN_INITIAL 8
struct CalogBerryT {
bvm *vm;
CalogT *broker;
uint64_t ctxId;
int32_t nextRef;
bvm *vm;
CalogT *broker;
uint64_t ctxId;
int32_t nextRef;
CalogFnT **foreignFns; // foreign function values pushed into this VM
int32_t foreignCount;
int32_t foreignCap;
};
// Backs a CalogFnT exported from this VM: the owning context and the name of the hidden
@ -43,7 +49,9 @@ typedef struct BerryExportT {
static int32_t berryCallableInvoke(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void berryCallableRelease(CalogFnT *callable);
static int32_t berryExportValue(CalogBerryT *context, int index, CalogFnT **out);
static int32_t berryFromValue(bvm *vm, const CalogValueT *value, int32_t depth);
static int berryForeignCall(bvm *vm);
static int32_t berryFromValue(CalogBerryT *context, const CalogValueT *value, int32_t depth);
static int32_t berryTrackForeign(CalogBerryT *context, CalogFnT *callable);
static int32_t berryToValue(CalogBerryT *context, int index, CalogValueT *out, int32_t depth);
static int berryTrampoline(bvm *vm);
@ -86,7 +94,7 @@ static int32_t berryCallableInvoke(CalogValueT *args, int32_t argCount, CalogVal
return calogFail(result, calogErrDeadE, "berry callable no longer exists");
}
for (index = 0; index < argCount; index++) {
status = berryFromValue(vm, &args[index], 0);
status = berryFromValue(context, &args[index], 0);
if (status != calogOkE) {
be_pop(vm, be_top(vm) - base);
return calogFail(result, status, "failed to marshal argument into berry");
@ -121,9 +129,16 @@ static void berryCallableRelease(CalogFnT *callable) {
void calogBerryDestroy(CalogBerryT *context) {
int32_t index;
if (context == NULL) {
return;
}
// Release the foreign function values pushed into this VM (see berryTrackForeign).
for (index = 0; index < context->foreignCount; index++) {
calogFnRelease(context->foreignFns[index]);
}
free(context->foreignFns);
if (context->vm != NULL) {
be_vm_delete(context->vm);
}
@ -207,7 +222,65 @@ int32_t calogBerryExpose(CalogBerryT *context, const char *name) {
}
static int32_t berryFromValue(bvm *vm, const CalogValueT *value, int32_t depth) {
// Call trampoline for a foreign CalogFnT pushed into Berry as a native closure. Its two
// upvalues are the context and the CalogFnT (both comptrs), pushed above the arguments.
static int berryForeignCall(bvm *vm) {
CalogBerryT *context;
CalogFnT *callable;
CalogValueT *args;
CalogValueT result;
int argc;
int index;
int32_t status;
char message[BERRY_ERR_CAP];
argc = be_top(vm); // arguments occupy stack slots 1..argc
be_getupval(vm, 0, 0);
context = (CalogBerryT *)be_tocomptr(vm, -1);
be_getupval(vm, 0, 1);
callable = (CalogFnT *)be_tocomptr(vm, -1);
be_pop(vm, 2);
args = NULL;
if (argc > 0) {
args = (CalogValueT *)calloc((size_t)argc, sizeof(CalogValueT));
if (args == NULL) {
be_raise(vm, "memory_error", "out of memory marshalling function-value args");
return 0;
}
}
for (index = 0; index < argc; index++) {
status = berryToValue(context, index + 1, &args[index], 0);
if (status != calogOkE) {
int cleanup;
for (cleanup = 0; cleanup < index; cleanup++) {
calogValueFree(&args[cleanup]);
}
free(args);
be_raise(vm, "value_error", "failed to marshal a function-value argument");
return 0;
}
}
status = calogFnInvoke(callable, args, argc, &result);
for (index = 0; index < argc; index++) {
calogValueFree(&args[index]);
}
free(args);
if (status != calogOkE) {
snprintf(message, sizeof(message), "%s", (result.type == calogStringE) ? result.as.s.bytes : "function value failed");
calogValueFree(&result);
be_raise(vm, "calog_error", message);
return 0;
}
berryFromValue(context, &result, 0);
calogValueFree(&result);
be_return(vm);
}
static int32_t berryFromValue(CalogBerryT *context, const CalogValueT *value, int32_t depth) {
bvm *vm;
vm = context->vm;
if (depth > CALOG_MAX_DEPTH) {
be_pushnil(vm);
return calogErrDepthE;
@ -241,7 +314,7 @@ static int32_t berryFromValue(bvm *vm, const CalogValueT *value, int32_t depth)
be_newmap(vm);
for (index = 0; index < aggregate->arrayCount; index++) {
be_pushint(vm, (bint)index);
berryFromValue(vm, &aggregate->array[index], depth + 1);
berryFromValue(context, &aggregate->array[index], depth + 1);
be_setindex(vm, -3);
be_pop(vm, 2);
}
@ -255,7 +328,7 @@ static int32_t berryFromValue(bvm *vm, const CalogValueT *value, int32_t depth)
} else {
continue; // non-scalar key: no Berry equivalent, drop the pair
}
berryFromValue(vm, &aggregate->pairs[index].value, depth + 1);
berryFromValue(context, &aggregate->pairs[index].value, depth + 1);
be_setindex(vm, -3);
be_pop(vm, 2);
}
@ -263,7 +336,7 @@ static int32_t berryFromValue(bvm *vm, const CalogValueT *value, int32_t depth)
className = "list";
be_newlist(vm);
for (index = 0; index < aggregate->arrayCount; index++) {
berryFromValue(vm, &aggregate->array[index], depth + 1);
berryFromValue(context, &aggregate->array[index], depth + 1);
be_data_push(vm, -2);
be_pop(vm, 1);
}
@ -279,10 +352,24 @@ static int32_t berryFromValue(bvm *vm, const CalogValueT *value, int32_t depth)
be_pop(vm, 2);
return calogOkE;
}
case calogFnE:
// Pushing a foreign function value into Berry is a v1 limit.
be_pushnil(vm);
return calogErrUnsupportedE;
case calogFnE: {
// Berry has no per-value finalizer, so track the foreign function on the
// context (released at destroy) and wrap it in a native closure -- callable
// as f(x) -- whose upvalues are the context and the CalogFnT.
if (berryTrackForeign(context, value->as.fn) != calogOkE) {
be_pushnil(vm);
return calogErrOomE;
}
be_pushntvclosure(vm, berryForeignCall, 2);
be_pushcomptr(vm, context);
be_setupval(vm, -2, 0);
be_pop(vm, 1);
be_pushcomptr(vm, value->as.fn);
be_setupval(vm, -2, 1);
be_pop(vm, 1);
calogFnRetain(value->as.fn);
return calogOkE;
}
}
be_pushnil(vm);
return calogErrTypeE;
@ -352,11 +439,110 @@ static int32_t berryToValue(CalogBerryT *context, int index, CalogValueT *out, i
calogValueFn(out, callable);
return calogOkE;
}
// Reading a script's list/map into an aggregate is a v1 limit.
// A list/map instance holds its raw container in the hidden ".p" member. be_iter_*
// take the CONTAINER index with the iterator on top of the stack (be_iter_next pushes
// one value for a list, key+value for a map), so keep the container at -2 and restore
// the iterator to the top after reading each entry.
if (be_islistinstance(vm, index)) {
CalogAggT *aggregate;
int base;
int32_t status;
status = calogAggCreate(&aggregate, calogListE);
if (status != calogOkE) {
return status;
}
base = be_top(vm);
be_getmember(vm, index, ".p"); // raw list -> top
be_pushiter(vm, -1); // iterator on top; container at -2
while (be_iter_hasnext(vm, -2)) {
CalogValueT element;
be_iter_next(vm, -2); // value on top
status = berryToValue(context, -1, &element, depth + 1);
be_pop(vm, be_top(vm) - (base + 2)); // restore to [container, iterator]
if (status != calogOkE) {
be_pop(vm, 2);
calogAggFree(aggregate);
return status;
}
status = calogAggPush(aggregate, &element);
if (status != calogOkE) {
calogValueFree(&element);
be_pop(vm, 2);
calogAggFree(aggregate);
return status;
}
}
be_pop(vm, 2); // container + iterator
calogValueAgg(out, aggregate);
return calogOkE;
}
if (be_ismapinstance(vm, index)) {
CalogAggT *aggregate;
int base;
int32_t status;
status = calogAggCreate(&aggregate, calogMapE);
if (status != calogOkE) {
return status;
}
base = be_top(vm);
be_getmember(vm, index, ".p"); // raw map -> top
be_pushiter(vm, -1); // iterator on top; container at -2
while (be_iter_hasnext(vm, -2)) {
CalogValueT key;
CalogValueT value;
be_iter_next(vm, -2); // key at -2, value at -1 (above the iterator)
status = berryToValue(context, -2, &key, depth + 1);
if (status != calogOkE) {
be_pop(vm, be_top(vm) - base);
calogAggFree(aggregate);
return status;
}
status = berryToValue(context, -1, &value, depth + 1);
if (status != calogOkE) {
calogValueFree(&key);
be_pop(vm, be_top(vm) - base);
calogAggFree(aggregate);
return status;
}
be_pop(vm, be_top(vm) - (base + 2)); // restore to [container, iterator]
status = calogAggSet(aggregate, &key, &value);
if (status != calogOkE) {
calogValueFree(&key);
calogValueFree(&value);
be_pop(vm, 2);
calogAggFree(aggregate);
return status;
}
}
be_pop(vm, 2); // container + iterator
calogValueAgg(out, aggregate);
return calogOkE;
}
// Other reference types have no CalogValueT equivalent.
return calogErrUnsupportedE;
}
// Record a foreign function pushed into this VM so it is released at destroy (Berry has
// no per-value finalizer to release it when the wrapping closure is collected).
static int32_t berryTrackForeign(CalogBerryT *context, CalogFnT *callable) {
if (context->foreignCount == context->foreignCap) {
int32_t newCap;
CalogFnT **resized;
newCap = (context->foreignCap == 0) ? BERRY_FOREIGN_INITIAL : context->foreignCap * CALOG_GROWTH_FACTOR;
resized = (CalogFnT **)realloc(context->foreignFns, (size_t)newCap * sizeof(CalogFnT *));
if (resized == NULL) {
return calogErrOomE;
}
context->foreignFns = resized;
context->foreignCap = newCap;
}
context->foreignFns[context->foreignCount] = callable;
context->foreignCount++;
return calogOkE;
}
static int berryTrampoline(bvm *vm) {
CalogBerryT *context;
const char *name;
@ -408,7 +594,7 @@ static int berryTrampoline(bvm *vm) {
be_raise(vm, "calog_error", message);
return 0;
}
status = berryFromValue(vm, &result, 0);
status = berryFromValue(context, &result, 0);
calogValueFree(&result);
if (status != calogOkE) {
be_pop(vm, 1);

View file

@ -3,9 +3,9 @@
// Exposes broker-registered native functions into a Berry VM, marshals values between
// Berry and CalogValueT by value (binary-safe strings; scalars), and exports Berry
// functions as refcounted CalogFnT handles (kept alive by a GC-rooted hidden global,
// dropped on release). An aggregate crossing OUT to a script becomes a Berry list or map
// instance (so a host record is readable as user['name']); reading a script's list/map
// back IN is a v1 limit -> calogErrUnsupportedE.
// dropped on release). Aggregates cross both ways: OUT a host record is a Berry map
// instance (user['name']); IN a script's list/map is read by iterating its raw container.
// A foreign CalogFnT pushed IN becomes a native closure the script calls directly.
//
// Lifetime note (as with Lua/JS): release every CalogFnT obtained from calogBerryExport
// (and drop every function value marshalled out) BEFORE calogBerryDestroy, since the

View file

@ -166,6 +166,11 @@ int32_t calogAggPush(CalogAggT *aggregate, CalogValueT *value);
int32_t calogAggSet(CalogAggT *aggregate, CalogValueT *key, CalogValueT *value);
// ---- function values ----
// Wrap a host native as a host-owned function value (runs on the host thread during
// calogPump, like calogRegister, but anonymous). Hand the result to a script via a
// native's result to give the script a callable; release your reference with
// calogFnRelease once you no longer hold it.
int32_t calogFnFromNative(CalogFnT **out, CalogT *calog, CalogNativeFnT fn, void *userData);
int32_t calogFnInvoke(CalogFnT *fn, CalogValueT *args, int32_t argCount, CalogValueT *result);
void calogFnRetain(CalogFnT *fn);
void calogFnRelease(CalogFnT *fn);

View file

@ -37,6 +37,7 @@ struct CalogJsT {
JSContext *ctx;
CalogT *broker;
uint64_t ctxId;
JSClassID fnClassId; // class for a foreign CalogFnT pushed into JS (callable + finalized)
};
// Backs a CalogFnT exported from this context: the retained JS function value (kept
@ -49,6 +50,8 @@ typedef struct JsExportT {
static int32_t jsCallableInvoke(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void jsCallableRelease(CalogFnT *callable);
static int32_t jsExportValue(CalogJsT *context, JSValueConst fn, CalogFnT **out);
static JSValue jsForeignCall(JSContext *ctx, JSValueConst funcObj, JSValueConst thisVal, int argc, JSValueConst *argv, int flags);
static void jsForeignFinalize(JSRuntime *rt, JSValueConst val);
static JSValue jsFromValue(JSContext *ctx, const CalogValueT *value, int32_t depth);
static int32_t jsToValue(JSContext *ctx, JSValueConst val, CalogValueT *out, int32_t depth);
static JSValue jsTrampoline(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic, JSValueConst *func_data);
@ -76,6 +79,19 @@ int32_t calogJsCreate(CalogJsT **out, CalogT *broker, uint64_t ctxId) {
context->broker = broker;
context->ctxId = ctxId;
JS_SetContextOpaque(context->ctx, context);
// A callable, finalized class wraps a foreign CalogFnT pushed into JS. The runtime
// opaque lets the finalizer (which gets only the runtime) recover the context.
JS_SetRuntimeOpaque(context->rt, context);
JS_NewClassID(context->rt, &context->fnClassId);
{
JSClassDef def;
def.class_name = "CalogFn";
def.finalizer = jsForeignFinalize;
def.gc_mark = NULL;
def.call = jsForeignCall;
def.exotic = NULL;
JS_NewClass(context->rt, context->fnClassId, &def);
}
*out = context;
return calogOkE;
}
@ -238,6 +254,68 @@ int32_t calogJsExpose(CalogJsT *context, const char *name) {
}
// Call handler for a foreign CalogFnT wrapped as a JS object: marshal the JS arguments,
// invoke the function value (routed to its owning context), and marshal the result back.
static JSValue jsForeignCall(JSContext *ctx, JSValueConst funcObj, JSValueConst thisVal, int argc, JSValueConst *argv, int flags) {
CalogJsT *context;
CalogFnT *callable;
CalogValueT *cargs;
CalogValueT result;
JSValue jsResult;
int index;
int32_t status;
(void)thisVal;
(void)flags;
context = (CalogJsT *)JS_GetContextOpaque(ctx);
callable = (CalogFnT *)JS_GetOpaque(funcObj, context->fnClassId);
cargs = NULL;
if (argc > 0) {
cargs = (CalogValueT *)calloc((size_t)argc, sizeof(CalogValueT));
if (cargs == NULL) {
return JS_ThrowOutOfMemory(ctx);
}
}
for (index = 0; index < argc; index++) {
status = jsToValue(ctx, argv[index], &cargs[index], 0);
if (status != calogOkE) {
int cleanup;
for (cleanup = 0; cleanup < index; cleanup++) {
calogValueFree(&cargs[cleanup]);
}
free(cargs);
return JS_ThrowTypeError(ctx, "failed to marshal a function-value argument");
}
}
status = calogFnInvoke(callable, cargs, argc, &result);
for (index = 0; index < argc; index++) {
calogValueFree(&cargs[index]);
}
free(cargs);
if (status != calogOkE) {
jsResult = JS_ThrowTypeError(ctx, "%s", (result.type == calogStringE) ? result.as.s.bytes : "function value failed");
calogValueFree(&result);
return jsResult;
}
jsResult = jsFromValue(ctx, &result, 0);
calogValueFree(&result);
return jsResult;
}
// Finalizer for that wrapper: drop the reference the push took (see the calogFnE case).
static void jsForeignFinalize(JSRuntime *rt, JSValueConst val) {
CalogJsT *context;
CalogFnT *callable;
context = (CalogJsT *)JS_GetRuntimeOpaque(rt);
callable = (CalogFnT *)JS_GetOpaque(val, context->fnClassId);
if (callable != NULL) {
calogFnRelease(callable);
}
}
static JSValue jsFromValue(JSContext *ctx, const CalogValueT *value, int32_t depth) {
if (depth > CALOG_MAX_DEPTH) {
return JS_ThrowRangeError(ctx, "value nesting too deep");
@ -301,10 +379,20 @@ static JSValue jsFromValue(JSContext *ctx, const CalogValueT *value, int32_t dep
}
return container;
}
case calogFnE:
// Pushing a foreign function value INTO JavaScript is a v1 fidelity limit
// (export OUT is supported via calogJsExport).
return JS_ThrowTypeError(ctx, "pushing a foreign function into JavaScript is unsupported");
case calogFnE: {
// Wrap the foreign function in a callable, finalized JS object; retain the
// handle for the wrapper's lifetime (the finalizer releases it).
CalogJsT *context;
JSValue obj;
context = (CalogJsT *)JS_GetContextOpaque(ctx);
obj = JS_NewObjectClass(ctx, context->fnClassId);
if (JS_IsException(obj)) {
return obj;
}
JS_SetOpaque(obj, value->as.fn);
calogFnRetain(value->as.fn);
return obj;
}
}
return JS_ThrowTypeError(ctx, "unknown value type");
}

View file

@ -15,7 +15,7 @@
#include <stdlib.h>
#include <string.h>
#define MB_BANK_SIZE 32
#define MB_BANK_SIZE 256
#define MB_INITIAL_ARGS 8
#define MB_NAME_MAX 128
#define MB_NATIVE_ERROR SE_RN_FAILED_TO_OPERATE
@ -90,13 +90,265 @@ MB_TRAMPOLINE(28)
MB_TRAMPOLINE(29)
MB_TRAMPOLINE(30)
MB_TRAMPOLINE(31)
MB_TRAMPOLINE(32)
MB_TRAMPOLINE(33)
MB_TRAMPOLINE(34)
MB_TRAMPOLINE(35)
MB_TRAMPOLINE(36)
MB_TRAMPOLINE(37)
MB_TRAMPOLINE(38)
MB_TRAMPOLINE(39)
MB_TRAMPOLINE(40)
MB_TRAMPOLINE(41)
MB_TRAMPOLINE(42)
MB_TRAMPOLINE(43)
MB_TRAMPOLINE(44)
MB_TRAMPOLINE(45)
MB_TRAMPOLINE(46)
MB_TRAMPOLINE(47)
MB_TRAMPOLINE(48)
MB_TRAMPOLINE(49)
MB_TRAMPOLINE(50)
MB_TRAMPOLINE(51)
MB_TRAMPOLINE(52)
MB_TRAMPOLINE(53)
MB_TRAMPOLINE(54)
MB_TRAMPOLINE(55)
MB_TRAMPOLINE(56)
MB_TRAMPOLINE(57)
MB_TRAMPOLINE(58)
MB_TRAMPOLINE(59)
MB_TRAMPOLINE(60)
MB_TRAMPOLINE(61)
MB_TRAMPOLINE(62)
MB_TRAMPOLINE(63)
MB_TRAMPOLINE(64)
MB_TRAMPOLINE(65)
MB_TRAMPOLINE(66)
MB_TRAMPOLINE(67)
MB_TRAMPOLINE(68)
MB_TRAMPOLINE(69)
MB_TRAMPOLINE(70)
MB_TRAMPOLINE(71)
MB_TRAMPOLINE(72)
MB_TRAMPOLINE(73)
MB_TRAMPOLINE(74)
MB_TRAMPOLINE(75)
MB_TRAMPOLINE(76)
MB_TRAMPOLINE(77)
MB_TRAMPOLINE(78)
MB_TRAMPOLINE(79)
MB_TRAMPOLINE(80)
MB_TRAMPOLINE(81)
MB_TRAMPOLINE(82)
MB_TRAMPOLINE(83)
MB_TRAMPOLINE(84)
MB_TRAMPOLINE(85)
MB_TRAMPOLINE(86)
MB_TRAMPOLINE(87)
MB_TRAMPOLINE(88)
MB_TRAMPOLINE(89)
MB_TRAMPOLINE(90)
MB_TRAMPOLINE(91)
MB_TRAMPOLINE(92)
MB_TRAMPOLINE(93)
MB_TRAMPOLINE(94)
MB_TRAMPOLINE(95)
MB_TRAMPOLINE(96)
MB_TRAMPOLINE(97)
MB_TRAMPOLINE(98)
MB_TRAMPOLINE(99)
MB_TRAMPOLINE(100)
MB_TRAMPOLINE(101)
MB_TRAMPOLINE(102)
MB_TRAMPOLINE(103)
MB_TRAMPOLINE(104)
MB_TRAMPOLINE(105)
MB_TRAMPOLINE(106)
MB_TRAMPOLINE(107)
MB_TRAMPOLINE(108)
MB_TRAMPOLINE(109)
MB_TRAMPOLINE(110)
MB_TRAMPOLINE(111)
MB_TRAMPOLINE(112)
MB_TRAMPOLINE(113)
MB_TRAMPOLINE(114)
MB_TRAMPOLINE(115)
MB_TRAMPOLINE(116)
MB_TRAMPOLINE(117)
MB_TRAMPOLINE(118)
MB_TRAMPOLINE(119)
MB_TRAMPOLINE(120)
MB_TRAMPOLINE(121)
MB_TRAMPOLINE(122)
MB_TRAMPOLINE(123)
MB_TRAMPOLINE(124)
MB_TRAMPOLINE(125)
MB_TRAMPOLINE(126)
MB_TRAMPOLINE(127)
MB_TRAMPOLINE(128)
MB_TRAMPOLINE(129)
MB_TRAMPOLINE(130)
MB_TRAMPOLINE(131)
MB_TRAMPOLINE(132)
MB_TRAMPOLINE(133)
MB_TRAMPOLINE(134)
MB_TRAMPOLINE(135)
MB_TRAMPOLINE(136)
MB_TRAMPOLINE(137)
MB_TRAMPOLINE(138)
MB_TRAMPOLINE(139)
MB_TRAMPOLINE(140)
MB_TRAMPOLINE(141)
MB_TRAMPOLINE(142)
MB_TRAMPOLINE(143)
MB_TRAMPOLINE(144)
MB_TRAMPOLINE(145)
MB_TRAMPOLINE(146)
MB_TRAMPOLINE(147)
MB_TRAMPOLINE(148)
MB_TRAMPOLINE(149)
MB_TRAMPOLINE(150)
MB_TRAMPOLINE(151)
MB_TRAMPOLINE(152)
MB_TRAMPOLINE(153)
MB_TRAMPOLINE(154)
MB_TRAMPOLINE(155)
MB_TRAMPOLINE(156)
MB_TRAMPOLINE(157)
MB_TRAMPOLINE(158)
MB_TRAMPOLINE(159)
MB_TRAMPOLINE(160)
MB_TRAMPOLINE(161)
MB_TRAMPOLINE(162)
MB_TRAMPOLINE(163)
MB_TRAMPOLINE(164)
MB_TRAMPOLINE(165)
MB_TRAMPOLINE(166)
MB_TRAMPOLINE(167)
MB_TRAMPOLINE(168)
MB_TRAMPOLINE(169)
MB_TRAMPOLINE(170)
MB_TRAMPOLINE(171)
MB_TRAMPOLINE(172)
MB_TRAMPOLINE(173)
MB_TRAMPOLINE(174)
MB_TRAMPOLINE(175)
MB_TRAMPOLINE(176)
MB_TRAMPOLINE(177)
MB_TRAMPOLINE(178)
MB_TRAMPOLINE(179)
MB_TRAMPOLINE(180)
MB_TRAMPOLINE(181)
MB_TRAMPOLINE(182)
MB_TRAMPOLINE(183)
MB_TRAMPOLINE(184)
MB_TRAMPOLINE(185)
MB_TRAMPOLINE(186)
MB_TRAMPOLINE(187)
MB_TRAMPOLINE(188)
MB_TRAMPOLINE(189)
MB_TRAMPOLINE(190)
MB_TRAMPOLINE(191)
MB_TRAMPOLINE(192)
MB_TRAMPOLINE(193)
MB_TRAMPOLINE(194)
MB_TRAMPOLINE(195)
MB_TRAMPOLINE(196)
MB_TRAMPOLINE(197)
MB_TRAMPOLINE(198)
MB_TRAMPOLINE(199)
MB_TRAMPOLINE(200)
MB_TRAMPOLINE(201)
MB_TRAMPOLINE(202)
MB_TRAMPOLINE(203)
MB_TRAMPOLINE(204)
MB_TRAMPOLINE(205)
MB_TRAMPOLINE(206)
MB_TRAMPOLINE(207)
MB_TRAMPOLINE(208)
MB_TRAMPOLINE(209)
MB_TRAMPOLINE(210)
MB_TRAMPOLINE(211)
MB_TRAMPOLINE(212)
MB_TRAMPOLINE(213)
MB_TRAMPOLINE(214)
MB_TRAMPOLINE(215)
MB_TRAMPOLINE(216)
MB_TRAMPOLINE(217)
MB_TRAMPOLINE(218)
MB_TRAMPOLINE(219)
MB_TRAMPOLINE(220)
MB_TRAMPOLINE(221)
MB_TRAMPOLINE(222)
MB_TRAMPOLINE(223)
MB_TRAMPOLINE(224)
MB_TRAMPOLINE(225)
MB_TRAMPOLINE(226)
MB_TRAMPOLINE(227)
MB_TRAMPOLINE(228)
MB_TRAMPOLINE(229)
MB_TRAMPOLINE(230)
MB_TRAMPOLINE(231)
MB_TRAMPOLINE(232)
MB_TRAMPOLINE(233)
MB_TRAMPOLINE(234)
MB_TRAMPOLINE(235)
MB_TRAMPOLINE(236)
MB_TRAMPOLINE(237)
MB_TRAMPOLINE(238)
MB_TRAMPOLINE(239)
MB_TRAMPOLINE(240)
MB_TRAMPOLINE(241)
MB_TRAMPOLINE(242)
MB_TRAMPOLINE(243)
MB_TRAMPOLINE(244)
MB_TRAMPOLINE(245)
MB_TRAMPOLINE(246)
MB_TRAMPOLINE(247)
MB_TRAMPOLINE(248)
MB_TRAMPOLINE(249)
MB_TRAMPOLINE(250)
MB_TRAMPOLINE(251)
MB_TRAMPOLINE(252)
MB_TRAMPOLINE(253)
MB_TRAMPOLINE(254)
MB_TRAMPOLINE(255)
#undef MB_TRAMPOLINE
static const mb_func_t mbTrampTable[MB_BANK_SIZE] = {
mbTramp0, mbTramp1, mbTramp2, mbTramp3, mbTramp4, mbTramp5, mbTramp6, mbTramp7,
mbTramp8, mbTramp9, mbTramp10, mbTramp11, mbTramp12, mbTramp13, mbTramp14, mbTramp15,
mbTramp16, mbTramp17, mbTramp18, mbTramp19, mbTramp20, mbTramp21, mbTramp22, mbTramp23,
mbTramp24, mbTramp25, mbTramp26, mbTramp27, mbTramp28, mbTramp29, mbTramp30, mbTramp31
mbTramp0, mbTramp1, mbTramp2, mbTramp3, mbTramp4, mbTramp5, mbTramp6, mbTramp7,
mbTramp8, mbTramp9, mbTramp10, mbTramp11, mbTramp12, mbTramp13, mbTramp14, mbTramp15,
mbTramp16, mbTramp17, mbTramp18, mbTramp19, mbTramp20, mbTramp21, mbTramp22, mbTramp23,
mbTramp24, mbTramp25, mbTramp26, mbTramp27, mbTramp28, mbTramp29, mbTramp30, mbTramp31,
mbTramp32, mbTramp33, mbTramp34, mbTramp35, mbTramp36, mbTramp37, mbTramp38, mbTramp39,
mbTramp40, mbTramp41, mbTramp42, mbTramp43, mbTramp44, mbTramp45, mbTramp46, mbTramp47,
mbTramp48, mbTramp49, mbTramp50, mbTramp51, mbTramp52, mbTramp53, mbTramp54, mbTramp55,
mbTramp56, mbTramp57, mbTramp58, mbTramp59, mbTramp60, mbTramp61, mbTramp62, mbTramp63,
mbTramp64, mbTramp65, mbTramp66, mbTramp67, mbTramp68, mbTramp69, mbTramp70, mbTramp71,
mbTramp72, mbTramp73, mbTramp74, mbTramp75, mbTramp76, mbTramp77, mbTramp78, mbTramp79,
mbTramp80, mbTramp81, mbTramp82, mbTramp83, mbTramp84, mbTramp85, mbTramp86, mbTramp87,
mbTramp88, mbTramp89, mbTramp90, mbTramp91, mbTramp92, mbTramp93, mbTramp94, mbTramp95,
mbTramp96, mbTramp97, mbTramp98, mbTramp99, mbTramp100, mbTramp101, mbTramp102, mbTramp103,
mbTramp104, mbTramp105, mbTramp106, mbTramp107, mbTramp108, mbTramp109, mbTramp110, mbTramp111,
mbTramp112, mbTramp113, mbTramp114, mbTramp115, mbTramp116, mbTramp117, mbTramp118, mbTramp119,
mbTramp120, mbTramp121, mbTramp122, mbTramp123, mbTramp124, mbTramp125, mbTramp126, mbTramp127,
mbTramp128, mbTramp129, mbTramp130, mbTramp131, mbTramp132, mbTramp133, mbTramp134, mbTramp135,
mbTramp136, mbTramp137, mbTramp138, mbTramp139, mbTramp140, mbTramp141, mbTramp142, mbTramp143,
mbTramp144, mbTramp145, mbTramp146, mbTramp147, mbTramp148, mbTramp149, mbTramp150, mbTramp151,
mbTramp152, mbTramp153, mbTramp154, mbTramp155, mbTramp156, mbTramp157, mbTramp158, mbTramp159,
mbTramp160, mbTramp161, mbTramp162, mbTramp163, mbTramp164, mbTramp165, mbTramp166, mbTramp167,
mbTramp168, mbTramp169, mbTramp170, mbTramp171, mbTramp172, mbTramp173, mbTramp174, mbTramp175,
mbTramp176, mbTramp177, mbTramp178, mbTramp179, mbTramp180, mbTramp181, mbTramp182, mbTramp183,
mbTramp184, mbTramp185, mbTramp186, mbTramp187, mbTramp188, mbTramp189, mbTramp190, mbTramp191,
mbTramp192, mbTramp193, mbTramp194, mbTramp195, mbTramp196, mbTramp197, mbTramp198, mbTramp199,
mbTramp200, mbTramp201, mbTramp202, mbTramp203, mbTramp204, mbTramp205, mbTramp206, mbTramp207,
mbTramp208, mbTramp209, mbTramp210, mbTramp211, mbTramp212, mbTramp213, mbTramp214, mbTramp215,
mbTramp216, mbTramp217, mbTramp218, mbTramp219, mbTramp220, mbTramp221, mbTramp222, mbTramp223,
mbTramp224, mbTramp225, mbTramp226, mbTramp227, mbTramp228, mbTramp229, mbTramp230, mbTramp231,
mbTramp232, mbTramp233, mbTramp234, mbTramp235, mbTramp236, mbTramp237, mbTramp238, mbTramp239,
mbTramp240, mbTramp241, mbTramp242, mbTramp243, mbTramp244, mbTramp245, mbTramp246, mbTramp247,
mbTramp248, mbTramp249, mbTramp250, mbTramp251, mbTramp252, mbTramp253, mbTramp254, mbTramp255
};

View file

@ -5,10 +5,11 @@
// "report" a), so the dispatcher recovers the name from its first argument and the
// context from a hidden *calog-context* c-pointer, marshals the rest to CalogValueT, and
// dispatches through calogCall (honoring the actor route hook). Marshalling covers
// scalars, binary-safe strings, and aggregates crossing out (a Scheme list, or a
// hash-table when keyed, so a record reads as (user "name")); reading a script's keyed
// value back in is a v1 limit. A Scheme procedure crossing out becomes a refcounted
// CalogFnT kept alive by s7_gc_protect (released with s7_gc_unprotect_at).
// scalars, binary-safe strings, and aggregates both ways (out: a Scheme list, or a
// hash-table when keyed, so a record reads as (user "name"); in: a list or a hash-table
// iterated back into an aggregate). Functions cross both ways: a Scheme procedure out is
// a refcounted CalogFnT kept alive by s7_gc_protect; a foreign CalogFnT pushed in is an
// applicable c-object the script calls as (f ...).
//
// Error model: a native failure raises a Scheme error via s7_error. calogS7Run wraps the
// source in (catch #t (lambda () (eval-string ...)) handler) so both read and run errors
@ -34,6 +35,7 @@ struct CalogS7T {
s7_scheme *sc;
CalogT *broker;
uint64_t ctxId;
s7_int fnType; // c-object type for a foreign CalogFnT pushed into Scheme (applicable + freed)
};
// Backs a CalogFnT exported from this interpreter: the owning context, the protected
@ -49,6 +51,8 @@ static void s7CallableRelease(CalogFnT *callable);
static s7_pointer s7DispatchNative(s7_scheme *sc, s7_pointer args);
static char *s7EscapeSource(const char *source);
static int32_t s7ExportValue(CalogS7T *context, s7_pointer proc, CalogFnT **out);
static s7_pointer s7ForeignApply(s7_scheme *sc, s7_pointer args);
static void s7ForeignFree(void *value);
static s7_pointer s7FromValue(CalogS7T *context, const CalogValueT *value, int32_t depth);
static int32_t s7ToValue(CalogS7T *context, s7_pointer obj, CalogValueT *out, int32_t depth);
@ -72,6 +76,11 @@ int32_t calogS7Create(CalogS7T **out, CalogT *broker, uint64_t ctxId) {
// single generic native dispatcher.
s7_define_variable(context->sc, S7_CONTEXT_VAR, s7_make_c_pointer(context->sc, context));
s7_define_function(context->sc, "%calog-call", s7DispatchNative, 1, 0, true, "calog native dispatch");
// A c-object type for a foreign function pushed into Scheme: applicable via its ref
// hook (so (f 2 3) invokes it) and released by its free hook.
context->fnType = s7_make_c_type(context->sc, "calog-fn");
s7_c_type_set_ref(context->sc, context->fnType, s7ForeignApply);
s7_c_type_set_free(context->sc, context->fnType, s7ForeignFree);
*out = context;
return calogOkE;
}
@ -262,6 +271,72 @@ int32_t calogS7Expose(CalogS7T *context, const char *name) {
}
// Applied when a foreign-function c-object is called: args is (obj arg1 arg2 ...), so the
// first element is the object itself. Marshal the rest, invoke, and return the result.
static s7_pointer s7ForeignApply(s7_scheme *sc, s7_pointer args) {
CalogS7T *context;
CalogFnT *callable;
s7_pointer rest;
s7_pointer node;
CalogValueT *cargs;
CalogValueT result;
s7_pointer sresult;
int argCount;
int index;
int32_t status;
context = (CalogS7T *)s7_c_pointer(s7_name_to_value(sc, S7_CONTEXT_VAR));
callable = (CalogFnT *)s7_c_object_value(s7_car(args));
rest = s7_cdr(args);
argCount = s7_list_length(sc, rest);
cargs = NULL;
if (argCount > 0) {
cargs = (CalogValueT *)calloc((size_t)argCount, sizeof(CalogValueT));
if (cargs == NULL) {
return s7_error(sc, s7_make_symbol(sc, "memory-error"), s7_make_string(sc, "out of memory marshalling function-value args"));
}
}
node = rest;
for (index = 0; index < argCount; index++) {
status = s7ToValue(context, s7_car(node), &cargs[index], 0);
if (status != calogOkE) {
int cleanup;
for (cleanup = 0; cleanup < index; cleanup++) {
calogValueFree(&cargs[cleanup]);
}
free(cargs);
return s7_error(sc, s7_make_symbol(sc, "wrong-type-arg"), s7_make_string(sc, "failed to marshal a function-value argument"));
}
node = s7_cdr(node);
}
status = calogFnInvoke(callable, cargs, argCount, &result);
for (index = 0; index < argCount; index++) {
calogValueFree(&cargs[index]);
}
free(cargs);
if (status != calogOkE) {
s7_pointer message;
message = s7_make_string(sc, (result.type == calogStringE) ? result.as.s.bytes : "function value failed");
calogValueFree(&result);
return s7_error(sc, s7_make_symbol(sc, "calog-error"), message);
}
sresult = s7FromValue(context, &result, 0);
calogValueFree(&result);
return sresult;
}
// Free hook for that c-object: drop the reference the push took.
static void s7ForeignFree(void *value) {
CalogFnT *callable;
callable = (CalogFnT *)value;
if (callable != NULL) {
calogFnRelease(callable);
}
}
static s7_pointer s7FromValue(CalogS7T *context, const CalogValueT *value, int32_t depth) {
s7_scheme *sc;
@ -322,9 +397,14 @@ static s7_pointer s7FromValue(CalogS7T *context, const CalogValueT *value, int32
return list;
}
}
case calogFnE:
// Pushing a foreign function value into Scheme is a v1 limit.
return s7_unspecified(sc);
case calogFnE: {
// Wrap the foreign function in an applicable c-object; retain for its lifetime
// (the free hook releases it).
s7_pointer obj;
obj = s7_make_c_object(sc, context->fnType, value->as.fn);
calogFnRetain(value->as.fn);
return obj;
}
}
return s7_unspecified(sc);
}
@ -398,6 +478,49 @@ static int32_t s7ToValue(CalogS7T *context, s7_pointer obj, CalogValueT *out, in
calogValueFn(out, callable);
return calogOkE;
}
if (s7_is_hash_table(obj)) {
CalogAggT *aggregate;
s7_pointer iter;
unsigned int loc;
int32_t status;
status = calogAggCreate(&aggregate, calogMapE);
if (status != calogOkE) {
return status;
}
iter = s7_make_iterator(sc, obj); // each iterate yields a (key . value) pair
loc = s7_gc_protect(sc, iter);
while (!s7_iterator_is_at_end(sc, iter)) {
s7_pointer pair;
CalogValueT key;
CalogValueT value;
pair = s7_iterate(sc, iter); // a (key . value) cons, or a non-pair at end
if (!s7_is_pair(pair)) {
break;
}
status = s7ToValue(context, s7_car(pair), &key, depth + 1);
if (status != calogOkE) {
break;
}
status = s7ToValue(context, s7_cdr(pair), &value, depth + 1);
if (status != calogOkE) {
calogValueFree(&key);
break;
}
status = calogAggSet(aggregate, &key, &value);
if (status != calogOkE) {
calogValueFree(&key);
calogValueFree(&value);
break;
}
}
s7_gc_unprotect_at(sc, loc);
if (status != calogOkE) {
calogAggFree(aggregate);
return status;
}
calogValueAgg(out, aggregate);
return calogOkE;
}
if (s7_is_null(sc, obj)) {
CalogAggT *aggregate;
int32_t status;
@ -435,6 +558,6 @@ static int32_t s7ToValue(CalogS7T *context, s7_pointer obj, CalogValueT *out, in
calogValueAgg(out, aggregate);
return calogOkE;
}
// #<unspecified>, eof, symbols, hash-tables, etc. -> nil (v1).
// #<unspecified>, eof, symbols, etc. -> nil.
return calogOkE;
}

View file

@ -1,11 +1,11 @@
// s7Adapter.h -- s7 Scheme engine adapter for the broker.
//
// Exposes broker-registered native functions into an s7 interpreter, marshals values
// between Scheme and CalogValueT by value (binary-safe strings; scalars; aggregates
// cross OUT as a Scheme list, or a hash-table when keyed so a record reads as (user
// "name")), and exports Scheme procedures as refcounted CalogFnT handles (kept alive by
// s7_gc_protect, released with s7_gc_unprotect_at). Reading a script's keyed value (map)
// back IN is a v1 limit -> calogErrUnsupportedE.
// between Scheme and CalogValueT by value (binary-safe strings; scalars; aggregates both
// ways -- OUT as a Scheme list or a hash-table when keyed so a record reads as (user
// "name"), IN by iterating a list or hash-table back), and moves Scheme procedures out as
// refcounted CalogFnT handles (kept alive by s7_gc_protect). A foreign CalogFnT pushed IN
// becomes an applicable c-object the script calls as (f ...).
//
// Lifetime note (as with Lua/JS): release every CalogFnT obtained from calogS7Export
// (and drop every procedure value marshalled out) BEFORE calogS7Destroy, since the

View file

@ -52,6 +52,8 @@ static int32_t squirrelCallableInvoke(CalogValueT *args, int32_t argCount, Calo
static void squirrelCallableRelease(CalogFnT *callable);
static CalogSquirrelT *squirrelContextOf(HSQUIRRELVM v);
static int32_t squirrelExportAt(CalogSquirrelT *context, SQInteger idx, CalogFnT **out);
static SQInteger squirrelForeignCall(HSQUIRRELVM v);
static SQInteger squirrelForeignRelease(SQUserPointer p, SQInteger size);
static void squirrelPrint(HSQUIRRELVM v, const SQChar *format, ...);
static int32_t squirrelPushValueDepth(HSQUIRRELVM v, const CalogValueT *value, int32_t depth);
static int32_t squirrelToValueDepth(HSQUIRRELVM v, SQInteger idx, CalogValueT *out, int32_t depth);
@ -249,6 +251,78 @@ int32_t calogSquirrelExpose(CalogSquirrelT *context, const char *name) {
}
// Call trampoline for a foreign CalogFnT pushed into Squirrel as a native closure. Its
// single free variable is a release-hooked userdata holding the CalogFnT; the VM pushes
// that free variable after the arguments, so it sits at the top of the stack.
static SQInteger squirrelForeignCall(HSQUIRRELVM v) {
CalogFnT *callable;
SQUserPointer ptr;
CalogValueT *args;
CalogValueT result;
SQInteger top;
int32_t argCount;
int32_t index;
int32_t status;
top = sq_gettop(v);
sq_getuserdata(v, top, &ptr, NULL);
callable = *(CalogFnT **)ptr;
argCount = (int32_t)(top - 2); // 'this' at 1, args at 2..top-1, free variable at top
if (argCount < 0) {
argCount = 0;
}
args = NULL;
if (argCount > 0) {
args = (CalogValueT *)calloc((size_t)argCount, sizeof(CalogValueT));
if (args == NULL) {
return sq_throwerror(v, _SC("out of memory marshalling function-value args"));
}
}
for (index = 0; index < argCount; index++) {
status = squirrelToValueDepth(v, index + 2, &args[index], 0);
if (status != calogOkE) {
int32_t cleanup;
for (cleanup = 0; cleanup < index; cleanup++) {
calogValueFree(&args[cleanup]);
}
free(args);
return sq_throwerror(v, _SC("failed to marshal a function-value argument"));
}
}
status = calogFnInvoke(callable, args, argCount, &result);
for (index = 0; index < argCount; index++) {
calogValueFree(&args[index]);
}
free(args);
if (status != calogOkE) {
const SQChar *message;
message = (result.type == calogStringE) ? result.as.s.bytes : _SC("function value failed");
sq_throwerror(v, message);
calogValueFree(&result);
return SQ_ERROR;
}
status = squirrelPushValueDepth(v, &result, 0);
calogValueFree(&result);
if (status != calogOkE) {
return sq_throwerror(v, _SC("failed to marshal a function-value result"));
}
return 1;
}
// Release hook on that userdata: drop the reference the push took.
static SQInteger squirrelForeignRelease(SQUserPointer p, SQInteger size) {
CalogFnT *callable;
(void)size;
callable = *(CalogFnT **)p;
if (callable != NULL) {
calogFnRelease(callable);
}
return 1;
}
static void squirrelPrint(HSQUIRRELVM v, const SQChar *format, ...) {
va_list args;
@ -342,10 +416,21 @@ static int32_t squirrelPushValueDepth(HSQUIRRELVM v, const CalogValueT *value, i
}
return calogOkE;
}
case calogFnE:
// Pushing a foreign function value INTO Squirrel is the remaining v1
// limit (export OUT is supported via calogSquirrelExport); see the header.
return calogErrUnsupportedE;
case calogFnE: {
// Wrap the foreign function in a native closure whose one free variable is a
// release-hooked userdata holding the CalogFnT (released when the closure is
// collected). Retain for the wrapper's lifetime.
CalogFnT **slot;
slot = (CalogFnT **)sq_newuserdata(v, sizeof(CalogFnT *));
if (slot == NULL) {
return calogErrOomE;
}
*slot = value->as.fn;
sq_setreleasehook(v, -1, squirrelForeignRelease);
sq_newclosure(v, squirrelForeignCall, 1); // pops the userdata as the free variable
calogFnRetain(value->as.fn);
return calogOkE;
}
}
return calogErrTypeE;
}

View file

@ -8,13 +8,10 @@
// A Squirrel closure crossing the boundary becomes a refcounted CalogFnT over a
// pinned HSQOBJECT (sq_addref/sq_release, mirroring Lua's luaL_ref lifecycle):
// calogSquirrelExport fetches a named global closure, and a closure passed as a native
// argument is exported the same way during ingress marshalling. As with Lua,
// release every exported CalogFnT BEFORE calogSquirrelDestroy, since sq_release
// touches the VM.
//
// Remaining v1 fidelity limit: the reverse direction (a foreign CalogFnT
// marshalled INTO Squirrel so a script can call it) is not yet supported -- pushing
// such a value returns calogErrUnsupportedE.
// argument is exported the same way during ingress marshalling. A foreign CalogFnT going
// the other way (pushed INTO Squirrel) becomes a native closure whose one free variable
// is a release-hooked userdata, so a script can call it directly. As with Lua, release
// every exported CalogFnT BEFORE calogSquirrelDestroy, since sq_release touches the VM.
#ifndef SQUIRREL_ADAPTER_H
#define SQUIRREL_ADAPTER_H

View file

@ -196,6 +196,12 @@ int32_t calogFnCreate(CalogFnT **out, CalogT *runtime, CalogNativeFnT fn, void *
}
int32_t calogFnFromNative(CalogFnT **out, CalogT *calog, CalogNativeFnT fn, void *userData) {
// A host-owned callable (owner id 0 = host): no engine handle to release.
return calogFnCreate(out, calog, fn, userData, NULL, 0);
}
void calogFnFinalize(CalogFnT *callable) {
if (callable == NULL) {
return;

View file

@ -44,15 +44,27 @@ typedef struct WrenExportT {
static int32_t wrenCallableInvoke(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void wrenCallableRelease(CalogFnT *callable);
static WrenHandle *wrenCallHandle(CalogWrenT *context, int arity);
static WrenForeignClassMethods wrenBindClass(WrenVM *vm, const char *module, const char *className);
static WrenForeignMethodFn wrenBindMethod(WrenVM *vm, const char *module, const char *className, bool isStatic, const char *signature);
static void wrenDispatch(WrenVM *vm);
static void wrenError(WrenVM *vm, WrenErrorType type, const char *module, int line, const char *message);
static int32_t wrenExportSlot(CalogWrenT *context, int slot, CalogFnT **out);
static void wrenForeignAllocate(WrenVM *vm);
static void wrenForeignFinalize(void *data);
static void wrenForeignInvoke(WrenVM *vm);
static void wrenFromValue(CalogWrenT *context, const CalogValueT *value, int slot, int32_t depth);
static int32_t wrenToValue(CalogWrenT *context, int slot, CalogValueT *out, int32_t depth);
static void wrenWrite(WrenVM *vm, const char *text);
static const char WREN_PREAMBLE[] = "class Calog { foreign static call(name, args) }";
// A foreign function pushed into Wren becomes a CalogFn instance; scripts invoke it as
// f.call([args]) (a list, since Wren method arity is fixed). Calog.call(name, args)
// reaches natives (Wren has no bare function calls).
static const char WREN_PREAMBLE[] =
"class Calog { foreign static call(name, args) }\n"
"foreign class CalogFn {\n"
" construct new() {}\n"
" foreign call(args)\n"
"}";
int32_t calogWrenCreate(CalogWrenT **out, CalogT *broker, uint64_t ctxId) {
@ -69,6 +81,7 @@ int32_t calogWrenCreate(CalogWrenT **out, CalogT *broker, uint64_t ctxId) {
wrenInitConfiguration(&config);
config.userData = context;
config.bindForeignMethodFn = wrenBindMethod;
config.bindForeignClassFn = wrenBindClass;
config.errorFn = wrenError;
config.writeFn = wrenWrite;
context->vm = wrenNewVM(&config);
@ -174,12 +187,30 @@ void calogWrenDestroy(CalogWrenT *context) {
}
static WrenForeignClassMethods wrenBindClass(WrenVM *vm, const char *module, const char *className) {
WrenForeignClassMethods methods;
(void)vm;
(void)module;
methods.allocate = NULL;
methods.finalize = NULL;
if (strcmp(className, "CalogFn") == 0) {
methods.allocate = wrenForeignAllocate;
methods.finalize = wrenForeignFinalize;
}
return methods;
}
static WrenForeignMethodFn wrenBindMethod(WrenVM *vm, const char *module, const char *className, bool isStatic, const char *signature) {
(void)vm;
(void)module;
if (strcmp(className, "Calog") == 0 && isStatic && strcmp(signature, "call(_,_)") == 0) {
return wrenDispatch;
}
if (strcmp(className, "CalogFn") == 0 && !isStatic && strcmp(signature, "call(_)") == 0) {
return wrenForeignInvoke;
}
return NULL;
}
@ -302,6 +333,82 @@ int32_t calogWrenExpose(CalogWrenT *context, const char *name) {
}
// Allocate hook for CalogFn (only reached if a script does CalogFn.new(); the host
// creates instances directly with wrenSetSlotNewForeign). Start empty.
static void wrenForeignAllocate(WrenVM *vm) {
CalogFnT **data;
data = (CalogFnT **)wrenSetSlotNewForeign(vm, 0, 0, sizeof(CalogFnT *));
*data = NULL;
}
// Finalizer for a CalogFn instance: drop the reference the push took.
static void wrenForeignFinalize(void *data) {
CalogFnT *callable;
callable = *(CalogFnT **)data;
if (callable != NULL) {
calogFnRelease(callable);
}
}
// CalogFn.call(args): invoke the wrapped function value with the argument list.
static void wrenForeignInvoke(WrenVM *vm) {
CalogWrenT *context;
CalogFnT *callable;
CalogValueT *cargs;
CalogValueT result;
int argCount;
int index;
int32_t status;
char message[WREN_ERR_CAP];
context = (CalogWrenT *)wrenGetUserData(vm);
callable = *(CalogFnT **)wrenGetSlotForeign(vm, 0); // slot 0 is the CalogFn instance
argCount = wrenGetListCount(vm, 1); // slot 1 is the argument list
cargs = NULL;
if (argCount > 0) {
cargs = (CalogValueT *)calloc((size_t)argCount, sizeof(CalogValueT));
if (cargs == NULL) {
wrenSetSlotString(vm, 0, "out of memory marshalling function-value args");
wrenAbortFiber(vm, 0);
return;
}
}
wrenEnsureSlots(vm, 3); // slot 2 is a scratch slot for list elements
for (index = 0; index < argCount; index++) {
wrenGetListElement(vm, 1, index, 2);
status = wrenToValue(context, 2, &cargs[index], 0);
if (status != calogOkE) {
int cleanup;
for (cleanup = 0; cleanup < index; cleanup++) {
calogValueFree(&cargs[cleanup]);
}
free(cargs);
wrenSetSlotString(vm, 0, "failed to marshal a function-value argument");
wrenAbortFiber(vm, 0);
return;
}
}
status = calogFnInvoke(callable, cargs, argCount, &result);
for (index = 0; index < argCount; index++) {
calogValueFree(&cargs[index]);
}
free(cargs);
if (status != calogOkE) {
snprintf(message, sizeof(message), "%s", (result.type == calogStringE) ? result.as.s.bytes : "function value failed");
calogValueFree(&result);
wrenSetSlotString(vm, 0, message);
wrenAbortFiber(vm, 0);
return;
}
wrenFromValue(context, &result, 0, 0); // return value in slot 0
calogValueFree(&result);
}
static void wrenFromValue(CalogWrenT *context, const CalogValueT *value, int slot, int32_t depth) {
WrenVM *vm;
@ -365,10 +472,17 @@ static void wrenFromValue(CalogWrenT *context, const CalogValueT *value, int slo
}
return;
}
case calogFnE:
// Pushing a foreign function value into Wren is a v1 limit.
wrenSetSlotNull(vm, slot);
case calogFnE: {
// Wrap the foreign function in a CalogFn instance (script calls f.call([...]));
// retain for its lifetime -- the finalizer releases it.
CalogFnT **data;
wrenEnsureSlots(vm, slot + 2);
wrenGetVariable(vm, "main", "CalogFn", slot + 1);
data = (CalogFnT **)wrenSetSlotNewForeign(vm, slot, slot + 1, sizeof(CalogFnT *));
*data = value->as.fn;
calogFnRetain(value->as.fn);
return;
}
}
wrenSetSlotNull(vm, slot);
}
@ -451,8 +565,49 @@ static int32_t wrenToValue(CalogWrenT *context, int slot, CalogValueT *out, int3
calogValueAgg(out, aggregate);
return calogOkE;
}
case WREN_TYPE_MAP:
return calogErrUnsupportedE; // map read is a v1 limit (no key enumeration in the C API)
case WREN_TYPE_MAP: {
// Enumerate the map via the calog patch (wrenGetMapCapacity/wrenGetMapEntry):
// walk the raw table, skipping empty slots. slot+1 = key, slot+2 = value
// scratch; marshal the value before the key so a nested-aggregate key can't
// clobber the value slot.
CalogAggT *aggregate;
int capacity;
int index;
int32_t status;
status = calogAggCreate(&aggregate, calogMapE);
if (status != calogOkE) {
return status;
}
capacity = wrenGetMapCapacity(vm, slot);
wrenEnsureSlots(vm, slot + 3);
for (index = 0; index < capacity; index++) {
CalogValueT key;
CalogValueT value;
if (!wrenGetMapEntry(vm, slot, index, slot + 1, slot + 2)) {
continue;
}
status = wrenToValue(context, slot + 2, &value, depth + 1);
if (status != calogOkE) {
calogAggFree(aggregate);
return status;
}
status = wrenToValue(context, slot + 1, &key, depth + 1);
if (status != calogOkE) {
calogValueFree(&value);
calogAggFree(aggregate);
return status;
}
status = calogAggSet(aggregate, &key, &value);
if (status != calogOkE) {
calogValueFree(&key);
calogValueFree(&value);
calogAggFree(aggregate);
return status;
}
}
calogValueAgg(out, aggregate);
return calogOkE;
}
case WREN_TYPE_FOREIGN:
case WREN_TYPE_UNKNOWN: {
// A non-primitive (typically a Fn) is captured as a callable.

View file

@ -2,11 +2,12 @@
//
// Wren has no bare function calls, so natives are reached through one foreign method:
// scripts call Calog.call("name", [args]). The adapter marshals values between Wren and
// CalogValueT by value (binary-safe strings; scalars; an aggregate crosses OUT as a Wren
// List, or a Map when keyed so a record reads as user["name"]) and exports Wren functions
// as refcounted CalogFnT handles (a retained WrenHandle, released with wrenReleaseHandle).
// Numbers are IEEE doubles, so int64 magnitudes above 2^53 lose precision. Reading a
// script's list/map back IN is a v1 limit (Wren's C API cannot enumerate a Map's keys).
// CalogValueT by value (binary-safe strings; scalars; aggregates both ways -- OUT as a
// Wren List, or a Map when keyed so a record reads as user["name"]; IN a List or a Map,
// the Map via a small calog patch to wren.c that enumerates map keys, which upstream's C
// API cannot). Functions cross both ways: OUT as a refcounted CalogFnT over a retained
// WrenHandle; a foreign CalogFnT pushed IN becomes a CalogFn instance the script calls as
// f.call([...]). Numbers are IEEE doubles, so int64 magnitudes above 2^53 lose precision.
//
// Lifetime note (as with Lua/JS): release every CalogFnT obtained from calogWrenExport
// (and drop every function value marshalled out) BEFORE calogWrenDestroy, since the

View file

@ -32,9 +32,12 @@ static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeMapAge(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReportName(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
@ -43,7 +46,9 @@ static void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(void);
static void testConcurrentContexts(void);
static void testCrossThreadCallback(void);
static void testForeignFunction(void);
static void testHostAndInlineNatives(void);
static void testMapIngress(void);
static void testMaterializedRecord(void);
static void testScriptError(void);
@ -57,6 +62,17 @@ static void checkImpl(bool condition, const char *message, const char *file, int
}
static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 2 || args[0].type != calogIntE || args[1].type != calogIntE) {
return calogFail(result, calogErrArgE, "add expects two integers");
}
calogValueInt(result, args[0].as.i + args[1].as.i);
return calogOkE;
}
static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
@ -77,6 +93,24 @@ static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *resu
}
static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogFnT *callable;
int32_t status;
(void)args;
(void)argCount;
(void)userData;
calogValueNil(result);
// A host-owned function value handed to the script; calling it routes back here.
status = calogFnFromNative(&callable, calog, nativeAdd, NULL);
if (status != calogOkE) {
return calogFail(result, status, "getAdder could not allocate");
}
calogValueFn(result, callable);
return calogOkE;
}
static int32_t nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogAggT *user;
CalogValueT key;
@ -102,6 +136,26 @@ static int32_t nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT *
}
static int32_t nativeMapAge(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogValueT key;
CalogValueT *found;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogAggE) {
return calogFail(result, calogErrArgE, "mapAge expects a map");
}
// Read a field out of a map the script built and handed to C (aggregate ingress).
calogValueString(&key, "age", 3);
found = calogAggGet(args[0].as.agg, &key);
calogValueFree(&key);
if (found != NULL && found->type == calogIntE) {
atomic_store(&reportedValue, found->as.i);
}
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
@ -215,6 +269,39 @@ static void testMaterializedRecord(void) {
}
static void testMapIngress(void) {
CalogContextT *ctx;
ctx = calogContextOpen(calog, &calogBerryEngine);
atomic_store(&scriptDone, false);
atomic_store(&reportedValue, 0);
// The script builds a map (with a nested list, exercising list ingress too) and
// hands it to a native, which reads a field.
calogContextEval(ctx, "m = {'age': 9, 'tags': [1, 2]}\nmapAge(m)\ndone()");
pumpUntilDone();
CHECK(atomic_load(&reportedValue) == 9, "native read a field from a map the script built");
calogContextClose(ctx);
}
static void testForeignFunction(void) {
CalogContextT *ctx;
ctx = calogContextOpen(calog, &calogBerryEngine);
atomic_store(&scriptDone, false);
atomic_store(&reportedValue, 0);
// The script receives a host-owned function value and calls it.
calogContextEval(ctx, "u = getAdder()\nreport(u(2, 3))\ndone()");
pumpUntilDone();
CHECK(atomic_load(&reportedValue) == 5, "script called a foreign function value pushed in from the host");
calogContextClose(ctx);
}
static void testCrossThreadCallback(void) {
CalogContextT *ctx;
CalogFnT *callback;
@ -293,10 +380,14 @@ int main(void) {
calogRegister(calog, "bump", nativeBump, NULL);
calogRegister(calog, "makeUser", nativeMakeUser, NULL);
calogRegister(calog, "reportName", nativeReportName, NULL);
calogRegister(calog, "getAdder", nativeGetAdder, NULL);
calogRegister(calog, "mapAge", nativeMapAge, NULL);
calogRegisterInline(calog, "reportInline", nativeReportInline, NULL);
testHostAndInlineNatives();
testMaterializedRecord();
testMapIngress();
testForeignFunction();
testCrossThreadCallback();
testScriptError();
testConcurrentContexts();

View file

@ -30,8 +30,10 @@ static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
@ -39,6 +41,7 @@ static void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(void);
static void testConcurrentContexts(void);
static void testCrossThreadCallback(void);
static void testForeignFunction(void);
static void testHostAndInlineNatives(void);
static void testScriptError(void);
@ -52,6 +55,17 @@ static void checkImpl(bool condition, const char *message, const char *file, int
}
static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 2 || args[0].type != calogIntE || args[1].type != calogIntE) {
return calogFail(result, calogErrArgE, "add expects two integers");
}
calogValueInt(result, args[0].as.i + args[1].as.i);
return calogOkE;
}
static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
@ -72,6 +86,24 @@ static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *resu
}
static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogFnT *callable;
int32_t status;
(void)args;
(void)argCount;
(void)userData;
calogValueNil(result);
// A host-owned function value handed to the script; calling it routes back here.
status = calogFnFromNative(&callable, calog, nativeAdd, NULL);
if (status != calogOkE) {
return calogFail(result, status, "getAdder could not allocate");
}
calogValueFn(result, callable);
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
@ -176,6 +208,22 @@ static void testCrossThreadCallback(void) {
}
static void testForeignFunction(void) {
CalogContextT *ctx;
ctx = calogContextOpen(calog, &calogJsEngine);
atomic_store(&scriptDone, false);
atomic_store(&reportedValue, 0);
// The script receives a host-owned function value and calls it.
calogContextEval(ctx, "report(getAdder()(2, 3))\ndone()");
pumpUntilDone();
CHECK(atomic_load(&reportedValue) == 5, "script called a foreign function value pushed in from the host");
calogContextClose(ctx);
}
static void testScriptError(void) {
CalogContextT *ctx;
int32_t before;
@ -225,10 +273,12 @@ int main(void) {
calogRegister(calog, "setCb", nativeSetCb, NULL);
calogRegister(calog, "done", nativeDone, NULL);
calogRegister(calog, "bump", nativeBump, NULL);
calogRegister(calog, "getAdder", nativeGetAdder, NULL);
calogRegisterInline(calog, "reportInline", nativeReportInline, NULL);
testHostAndInlineNatives();
testCrossThreadCallback();
testForeignFunction();
testScriptError();
testConcurrentContexts();

View file

@ -33,9 +33,11 @@ static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeCheckRuntime(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
@ -44,6 +46,7 @@ static void pumpUntilDone(void);
static void testConcurrentContexts(void);
static void testContextLoad(void);
static void testCrossThreadCallback(void);
static void testForeignFunction(void);
static void testHostAndInlineNatives(void);
static void testScriptError(void);
static void testSingleThreadMultiRuntime(void);
@ -58,6 +61,17 @@ static void checkImpl(bool condition, const char *message, const char *file, int
}
static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 2 || args[0].type != calogIntE || args[1].type != calogIntE) {
return calogFail(result, calogErrArgE, "add expects two integers");
}
calogValueInt(result, args[0].as.i + args[1].as.i);
return calogOkE;
}
static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
@ -95,6 +109,24 @@ static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *resu
}
static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogFnT *callable;
int32_t status;
(void)args;
(void)argCount;
(void)userData;
calogValueNil(result);
// A host-owned function value handed to the script; calling it routes back here.
status = calogFnFromNative(&callable, calog, nativeAdd, NULL);
if (status != calogOkE) {
return calogFail(result, status, "getAdder could not allocate");
}
calogValueFn(result, callable);
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
@ -202,6 +234,22 @@ static void testCrossThreadCallback(void) {
}
static void testForeignFunction(void) {
CalogContextT *ctx;
ctx = calogContextOpen(calog, &calogLuaEngine);
atomic_store(&scriptDone, false);
atomic_store(&reportedValue, 0);
// The script receives a host-owned function value and calls it.
calogContextEval(ctx, "report(getAdder()(2, 3)); done()");
pumpUntilDone();
CHECK(atomic_load(&reportedValue) == 5, "script called a foreign function value pushed in from the host");
calogContextClose(ctx);
}
static void testScriptError(void) {
CalogContextT *ctx;
int32_t before;
@ -322,10 +370,12 @@ int main(void) {
calogRegister(calog, "setCb", nativeSetCb, NULL);
calogRegister(calog, "done", nativeDone, NULL);
calogRegister(calog, "bump", nativeBump, NULL);
calogRegister(calog, "getAdder", nativeGetAdder, NULL);
calogRegisterInline(calog, "reportInline", nativeReportInline, NULL);
testHostAndInlineNatives();
testCrossThreadCallback();
testForeignFunction();
testScriptError();
testConcurrentContexts();
testSingleThreadMultiRuntime();

View file

@ -41,9 +41,12 @@ static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeMapAge(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReportName(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
@ -52,7 +55,9 @@ static void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(void);
static void testConcurrentContexts(void);
static void testCrossThreadCallback(void);
static void testForeignFunction(void);
static void testHostAndInlineNatives(void);
static void testMapIngress(void);
static void testMaterializedRecord(void);
static void testScriptError(void);
@ -66,6 +71,17 @@ static void checkImpl(bool condition, const char *message, const char *file, int
}
static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 2 || args[0].type != calogIntE || args[1].type != calogIntE) {
return calogFail(result, calogErrArgE, "add expects two integers");
}
calogValueInt(result, args[0].as.i + args[1].as.i);
return calogOkE;
}
static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
@ -86,6 +102,24 @@ static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *resu
}
static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogFnT *callable;
int32_t status;
(void)args;
(void)argCount;
(void)userData;
calogValueNil(result);
// A host-owned function value handed to the script; calling it routes back here.
status = calogFnFromNative(&callable, calog, nativeAdd, NULL);
if (status != calogOkE) {
return calogFail(result, status, "getAdder could not allocate");
}
calogValueFn(result, callable);
return calogOkE;
}
static int32_t nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogAggT *user;
CalogValueT key;
@ -111,6 +145,26 @@ static int32_t nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT *
}
static int32_t nativeMapAge(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogValueT key;
CalogValueT *found;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogAggE) {
return calogFail(result, calogErrArgE, "mapAge expects a map");
}
// Read a field out of a map the script built and handed to C (aggregate ingress).
calogValueString(&key, "age", 3);
found = calogAggGet(args[0].as.agg, &key);
calogValueFree(&key);
if (found != NULL && found->type == calogIntE) {
atomic_store(&reportedValue, found->as.i);
}
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
@ -224,6 +278,22 @@ static void testMaterializedRecord(void) {
}
static void testMapIngress(void) {
CalogContextT *ctx;
ctx = calogContextOpen(calog, &calogS7Engine);
atomic_store(&scriptDone, false);
atomic_store(&reportedValue, 0);
// The script builds a hash-table and hands it to a native, which reads a field.
calogContextEval(ctx, "(begin (define m (make-hash-table)) (hash-table-set! m \"age\" 9) (mapAge m) (done))");
pumpUntilDone();
CHECK(atomic_load(&reportedValue) == 9, "native read a field from a map the script built");
calogContextClose(ctx);
}
static void testCrossThreadCallback(void) {
CalogContextT *ctx;
CalogFnT *callback;
@ -251,6 +321,22 @@ static void testCrossThreadCallback(void) {
}
static void testForeignFunction(void) {
CalogContextT *ctx;
ctx = calogContextOpen(calog, &calogS7Engine);
atomic_store(&scriptDone, false);
atomic_store(&reportedValue, 0);
// The script receives a host-owned function value and applies it.
calogContextEval(ctx, "(begin (report ((getAdder) 2 3)) (done))");
pumpUntilDone();
CHECK(atomic_load(&reportedValue) == 5, "script called a foreign function value pushed in from the host");
calogContextClose(ctx);
}
static void testScriptError(void) {
CalogContextT *ctx;
int32_t before;
@ -302,11 +388,15 @@ int main(void) {
calogRegister(calog, "bump", nativeBump, NULL);
calogRegister(calog, "makeUser", nativeMakeUser, NULL);
calogRegister(calog, "reportName", nativeReportName, NULL);
calogRegister(calog, "getAdder", nativeGetAdder, NULL);
calogRegister(calog, "mapAge", nativeMapAge, NULL);
calogRegisterInline(calog, "reportInline", nativeReportInline, NULL);
testHostAndInlineNatives();
testMaterializedRecord();
testMapIngress();
testCrossThreadCallback();
testForeignFunction();
testScriptError();
testConcurrentContexts();

View file

@ -30,8 +30,10 @@ static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
@ -39,6 +41,7 @@ static void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(void);
static void testConcurrentContexts(void);
static void testCrossThreadCallback(void);
static void testForeignFunction(void);
static void testHostAndInlineNatives(void);
static void testScriptError(void);
@ -52,6 +55,17 @@ static void checkImpl(bool condition, const char *message, const char *file, int
}
static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 2 || args[0].type != calogIntE || args[1].type != calogIntE) {
return calogFail(result, calogErrArgE, "add expects two integers");
}
calogValueInt(result, args[0].as.i + args[1].as.i);
return calogOkE;
}
static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
@ -72,6 +86,24 @@ static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *resu
}
static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogFnT *callable;
int32_t status;
(void)args;
(void)argCount;
(void)userData;
calogValueNil(result);
// A host-owned function value handed to the script; calling it routes back here.
status = calogFnFromNative(&callable, calog, nativeAdd, NULL);
if (status != calogOkE) {
return calogFail(result, status, "getAdder could not allocate");
}
calogValueFn(result, callable);
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
@ -176,6 +208,22 @@ static void testCrossThreadCallback(void) {
}
static void testForeignFunction(void) {
CalogContextT *ctx;
ctx = calogContextOpen(calog, &calogSquirrelEngine);
atomic_store(&scriptDone, false);
atomic_store(&reportedValue, 0);
// The script receives a host-owned function value and calls it.
calogContextEval(ctx, "report(getAdder()(2, 3))\ndone()");
pumpUntilDone();
CHECK(atomic_load(&reportedValue) == 5, "script called a foreign function value pushed in from the host");
calogContextClose(ctx);
}
static void testScriptError(void) {
CalogContextT *ctx;
int32_t before;
@ -225,10 +273,12 @@ int main(void) {
calogRegister(calog, "setCb", nativeSetCb, NULL);
calogRegister(calog, "done", nativeDone, NULL);
calogRegister(calog, "bump", nativeBump, NULL);
calogRegister(calog, "getAdder", nativeGetAdder, NULL);
calogRegisterInline(calog, "reportInline", nativeReportInline, NULL);
testHostAndInlineNatives();
testCrossThreadCallback();
testForeignFunction();
testScriptError();
testConcurrentContexts();

View file

@ -32,9 +32,12 @@ static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeMapAge(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReportName(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
@ -43,7 +46,9 @@ static void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(void);
static void testConcurrentContexts(void);
static void testCrossThreadCallback(void);
static void testForeignFunction(void);
static void testHostAndInlineNatives(void);
static void testMapIngress(void);
static void testMaterializedRecord(void);
static void testScriptError(void);
@ -57,6 +62,17 @@ static void checkImpl(bool condition, const char *message, const char *file, int
}
static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 2 || args[0].type != calogIntE || args[1].type != calogIntE) {
return calogFail(result, calogErrArgE, "add expects two integers");
}
calogValueInt(result, args[0].as.i + args[1].as.i);
return calogOkE;
}
static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
@ -77,6 +93,24 @@ static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *resu
}
static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogFnT *callable;
int32_t status;
(void)args;
(void)argCount;
(void)userData;
calogValueNil(result);
// A host-owned function value handed to the script; calling it routes back here.
status = calogFnFromNative(&callable, calog, nativeAdd, NULL);
if (status != calogOkE) {
return calogFail(result, status, "getAdder could not allocate");
}
calogValueFn(result, callable);
return calogOkE;
}
static int32_t nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogAggT *user;
CalogValueT key;
@ -102,6 +136,26 @@ static int32_t nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT *
}
static int32_t nativeMapAge(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogValueT key;
CalogValueT *found;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogAggE) {
return calogFail(result, calogErrArgE, "mapAge expects a map");
}
// Read a field out of a Wren map the script built and handed to C (map ingress).
calogValueString(&key, "age", 3);
found = calogAggGet(args[0].as.agg, &key);
calogValueFree(&key);
if (found != NULL && found->type == calogIntE) {
atomic_store(&reportedValue, found->as.i);
}
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
@ -215,6 +269,39 @@ static void testMaterializedRecord(void) {
}
static void testMapIngress(void) {
CalogContextT *ctx;
ctx = calogContextOpen(calog, &calogWrenEngine);
atomic_store(&scriptDone, false);
atomic_store(&reportedValue, 0);
// The script builds a Wren Map and hands it to a native, which reads a field
// (exercises the calog wren.c patch that enumerates map keys from C).
calogContextEval(ctx, "Calog.call(\"mapAge\", [{\"age\": 9}])\nCalog.call(\"done\", [])");
pumpUntilDone();
CHECK(atomic_load(&reportedValue) == 9, "native read a field from a Wren map the script built");
calogContextClose(ctx);
}
static void testForeignFunction(void) {
CalogContextT *ctx;
ctx = calogContextOpen(calog, &calogWrenEngine);
atomic_store(&scriptDone, false);
atomic_store(&reportedValue, 0);
// The script receives a host-owned function value and calls it as f.call([...]).
calogContextEval(ctx, "var f = Calog.call(\"getAdder\", [])\nCalog.call(\"report\", [f.call([2, 3])])\nCalog.call(\"done\", [])");
pumpUntilDone();
CHECK(atomic_load(&reportedValue) == 5, "script called a foreign function value pushed in from the host");
calogContextClose(ctx);
}
static void testCrossThreadCallback(void) {
CalogContextT *ctx;
CalogFnT *callback;
@ -293,10 +380,14 @@ int main(void) {
calogRegister(calog, "bump", nativeBump, NULL);
calogRegister(calog, "makeUser", nativeMakeUser, NULL);
calogRegister(calog, "reportName", nativeReportName, NULL);
calogRegister(calog, "getAdder", nativeGetAdder, NULL);
calogRegister(calog, "mapAge", nativeMapAge, NULL);
calogRegisterInline(calog, "reportInline", nativeReportInline, NULL);
testHostAndInlineNatives();
testMaterializedRecord();
testMapIngress();
testForeignFunction();
testCrossThreadCallback();
testScriptError();
testConcurrentContexts();

View file

@ -37,12 +37,13 @@
#define BE_USE_SINGLE_FLOAT 0
/* Macro: BE_BYTES_MAX_SIZE
* Maximum size in bytes of a `bytes()` object.
* Putting too much pressure on the memory allocator can do
* harm, so we limit the maximum size.
* Default: 32kb
* Maximum size in bytes of a `bytes()` object. This is a ceiling, not a
* preallocation -- a script only uses what it allocates -- so it is set
* generously here (calog raised it from Berry's 32 kb default). The hard
* ceiling is ~INT32_MAX because a bytes object's size/len fields are int32;
* a host can also raise vm->bytesmaxsize at runtime.
**/
#define BE_BYTES_MAX_SIZE (32*1024) /* 32 kb default value */
#define BE_BYTES_MAX_SIZE (256*1024*1024) /* 256 MB (calog) */
/* Macro: BE_USE_PRECOMPILED_OBJECT
* Use precompiled objects to avoid creating these objects at

28
vendor/wren/wren.c vendored
View file

@ -4652,6 +4652,34 @@ void wrenGetMapValue(WrenVM* vm, int mapSlot, int keySlot, int valueSlot)
vm->apiStack[valueSlot] = value;
}
// --- calog patch: map key enumeration (upstream Wren has no C map iterator) ---
int wrenGetMapCapacity(WrenVM* vm, int mapSlot)
{
validateApiSlot(vm, mapSlot);
ASSERT(IS_MAP(vm->apiStack[mapSlot]), "Slot must hold a map.");
return (int)AS_MAP(vm->apiStack[mapSlot])->capacity;
}
bool wrenGetMapEntry(WrenVM* vm, int mapSlot, int index, int keySlot, int valueSlot)
{
validateApiSlot(vm, mapSlot);
validateApiSlot(vm, keySlot);
validateApiSlot(vm, valueSlot);
ASSERT(IS_MAP(vm->apiStack[mapSlot]), "Slot must hold a map.");
ObjMap* map = AS_MAP(vm->apiStack[mapSlot]);
if (index < 0 || (uint32_t)index >= map->capacity) return false;
MapEntry* entry = &map->entries[index];
if (IS_UNDEFINED(entry->key)) return false;
vm->apiStack[keySlot] = entry->key;
vm->apiStack[valueSlot] = entry->value;
return true;
}
// --- end calog patch ---
void wrenSetMapValue(WrenVM* vm, int mapSlot, int keySlot, int valueSlot)
{
validateApiSlot(vm, mapSlot);

11
vendor/wren/wren.h vendored
View file

@ -528,6 +528,17 @@ WREN_API void wrenSetMapValue(WrenVM* vm, int mapSlot, int keySlot, int valueSlo
WREN_API void wrenRemoveMapValue(WrenVM* vm, int mapSlot, int keySlot,
int removedValueSlot);
// --- calog patch: map key enumeration (upstream Wren has no C map iterator) ---
// Returns the raw hash-table capacity of the map in [mapSlot] (>= wrenGetMapCount).
// Iterate indices 0..capacity-1 and call wrenGetMapEntry to visit each occupied slot.
WREN_API int wrenGetMapCapacity(WrenVM* vm, int mapSlot);
// Stores the key and value of the map entry at raw table [index] into [keySlot] and
// [valueSlot] and returns true; returns false if that slot is empty (skip it). Mirrors
// Wren's own internal map_iterate/keyIteratorValue.
WREN_API bool wrenGetMapEntry(WrenVM* vm, int mapSlot, int index, int keySlot, int valueSlot);
// --- end calog patch ---
// Looks up the top level variable with [name] in resolved [module] and stores
// it in [slot].
WREN_API void wrenGetVariable(WrenVM* vm, const char* module, const char* name,