Improved sharing of maps between languages.

This commit is contained in:
Scott Duensing 2026-07-01 19:59:53 -05:00
parent 58a9008458
commit c11bf8481f
12 changed files with 392 additions and 58 deletions

View file

@ -55,9 +55,13 @@ Swap `&calogJsEngine` for `&calogLuaEngine`, `&calogSquirrelEngine`,
| JavaScript | `calogJsEngine` | `.js` | QuickJS-ng (ES2023+, BigInt → int64) | | JavaScript | `calogJsEngine` | `.js` | QuickJS-ng (ES2023+, BigInt → int64) |
| Squirrel 3.2 | `calogSquirrelEngine` | `.nut` | C++ VM | | Squirrel 3.2 | `calogSquirrelEngine` | `.nut` | C++ VM |
| MY-BASIC | `calogMyBasicEngine` | `.bas` | interpreters serialize at load | | MY-BASIC | `calogMyBasicEngine` | `.bas` | interpreters serialize at load |
| Scheme | `calogS7Engine` | `.scm` | s7; 64-bit ints, lists | | Scheme | `calogS7Engine` | `.scm` | s7; 64-bit ints |
| Wren | `calogWrenEngine` | `.wren` | doubles only; call via `Calog.call_(…)` | | Wren | `calogWrenEngine` | `.wren` | doubles only; call via `Calog.call(…)` |
| Berry | `calogBerryEngine` | `.be` | scalars + callbacks (aggregates: v1) | | 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.
You can also bring your own: `CalogEngineT` is a public four-function vtable. You can also bring your own: `CalogEngineT` is a public four-function vtable.

View file

@ -897,8 +897,11 @@ under a uniquely-named hidden global (globals are GC roots) and dropped by setti
nil. The sharp edge: `be_pcall(vm, argc)` leaves the **result in the function's slot nil. The sharp edge: `be_pcall(vm, argc)` leaves the **result in the function's slot
(`base+1`)**, not at `-1` (which holds the last stale argument). Vendoring needs Berry's (`base+1`)**, not at `-1` (which holds the last stale argument). Vendoring needs Berry's
`coc` codegen prebuild plus its OS port (`be_port.c`) and module/class tables `coc` codegen prebuild plus its OS port (`be_port.c`) and module/class tables
(`be_modtab.c`). Aggregate marshalling is a v1 limit (scalars + strings + callbacks are (`be_modtab.c`). A subtlety for records: `be_newmap`/`be_newlist` push *raw* containers
complete). 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.
**s7 Scheme** (`.scm`) uses the *current* official s7 (an older mirror lacked `s7_free`, **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 which would leak a heap per context). Since s7 native functions carry no user data, all
@ -907,20 +910,30 @@ wrapper (`(define (report . a) (apply %calog-call "report" a))`); the context ri
`*calog-context*` c-pointer global. Callables are kept alive by `s7_gc_protect` and `*calog-context*` c-pointer global. Callables are kept alive by `s7_gc_protect` and
invoked with `s7_call`. Because `s7_eval_c_string` evaluates a single form, `calogS7Run` invoked with `s7_call`. Because `s7_eval_c_string` evaluates a single form, `calogS7Run`
wraps the (escaped) source in `(catch #t (lambda () (eval-string …)) handler)`, so both wraps the (escaped) source in `(catch #t (lambda () (eval-string …)) handler)`, so both
read and run errors surface as a value -- a marker pair the runner detects. int64 and read and run errors surface as a value -- a marker pair the runner detects. An aggregate
Scheme lists round-trip both ways; the keyed part (map) is a v1 limit. s7's intentional crossing out is a Scheme list, or an (applicable) hash-table when keyed, so a materialized
record reads as `(user "name")`; reading a script's keyed value back in is a v1 limit. s7's intentional
"permanent string" interning (which `s7_free` does not reclaim) is a small, bounded "permanent string" interning (which `s7_free` does not reclaim) is a small, bounded
allocation, suppressed with a documented, allocation-site-specific `__lsan` hook. s7 is allocation, suppressed with a documented, allocation-site-specific `__lsan` hook. s7 is
per-interpreter thread-safe -- no serialization needed (unlike my-basic). per-interpreter thread-safe -- no serialization needed (unlike my-basic).
**Wren** (`.wren`) is the outlier: Wren has **no bare function calls**, so every native is **Wren** (`.wren`) is the outlier: Wren has **no bare function calls**, so every native is
reached through a single foreign method. A preamble defines `class Calog { foreign static reached through a single foreign method. A preamble defines `class Calog { foreign static
call_(name, args) }`, and scripts call `Calog.call_("report", [42])`; the C dispatcher call(name, args) }`, and scripts call `Calog.call("report", [42])`; the C dispatcher
recovers the context from `wrenGetUserData`, marshals the argument list, and dispatches recovers the context from `wrenGetUserData`, marshals the argument list, and dispatches
through `calogCall`. A Wren function crossing out is a retained `WrenHandle`, invoked with through `calogCall`. A Wren function crossing out is a retained `WrenHandle`, invoked with
a cached per-arity `call(_)` handle. Wren numbers are IEEE **doubles**, so int64 above a cached per-arity `call(_)` handle. Wren numbers are IEEE **doubles**, so int64 above
2^53 loses precision (the same edge my-basic and old-JS have). Lists round-trip; map read 2^53 loses precision (the same edge my-basic and old-JS have). An aggregate crossing out
is a v1 limit. Wren keeps no process-global state, so contexts run in parallel. is a Wren `List`, or a `Map` when keyed (a record reads as `user["name"]`); reading a
script's list/map back in is a v1 limit -- Wren's C API cannot enumerate a `Map`'s keys.
Wren keeps no process-global state, so contexts run in parallel.
**Verified** across all seven engines: `make test` (525 checks), ASan/UBSan clean, a Aggregate egress is therefore uniform across all seven engines: a host native can return a
`tsan<engine>` target clean for each, and gcc + clang strict on the core. keyed `CalogValueT` record and every engine reads its fields with native syntax
(`user.name` / `user['name']` / `user["name"]` / `(user "name")`). The reverse -- a script
handing a list/map *back* to C -- is complete on Lua/JS/Squirrel/my-basic and a v1 limit on
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.

View file

@ -6,7 +6,8 @@
// calogCall (honoring the actor route hook). Marshalling covers scalars and binary-safe // 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 // 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 // a uniquely-named hidden global (a GC root), dropped by setting that global to nil on
// release. Aggregates (list/map) are a v1 limit -> calogErrUnsupportedE. // 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.
// //
// Error model: on failure the trampoline raises a Berry exception (be_raise, which // Error model: on failure the trampoline raises a Berry exception (be_raise, which
// longjmps out of the VM) after releasing any CalogValueT it owns. // longjmps out of the VM) after releasing any CalogValueT it owns.
@ -42,7 +43,7 @@ typedef struct BerryExportT {
static int32_t berryCallableInvoke(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t berryCallableInvoke(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void berryCallableRelease(CalogFnT *callable); static void berryCallableRelease(CalogFnT *callable);
static int32_t berryExportValue(CalogBerryT *context, int index, CalogFnT **out); static int32_t berryExportValue(CalogBerryT *context, int index, CalogFnT **out);
static int32_t berryFromValue(bvm *vm, const CalogValueT *value); static int32_t berryFromValue(bvm *vm, const CalogValueT *value, int32_t depth);
static int32_t berryToValue(CalogBerryT *context, int index, CalogValueT *out, int32_t depth); static int32_t berryToValue(CalogBerryT *context, int index, CalogValueT *out, int32_t depth);
static int berryTrampoline(bvm *vm); static int berryTrampoline(bvm *vm);
@ -85,7 +86,7 @@ static int32_t berryCallableInvoke(CalogValueT *args, int32_t argCount, CalogVal
return calogFail(result, calogErrDeadE, "berry callable no longer exists"); return calogFail(result, calogErrDeadE, "berry callable no longer exists");
} }
for (index = 0; index < argCount; index++) { for (index = 0; index < argCount; index++) {
status = berryFromValue(vm, &args[index]); status = berryFromValue(vm, &args[index], 0);
if (status != calogOkE) { if (status != calogOkE) {
be_pop(vm, be_top(vm) - base); be_pop(vm, be_top(vm) - base);
return calogFail(result, status, "failed to marshal argument into berry"); return calogFail(result, status, "failed to marshal argument into berry");
@ -206,7 +207,11 @@ int32_t calogBerryExpose(CalogBerryT *context, const char *name) {
} }
static int32_t berryFromValue(bvm *vm, const CalogValueT *value) { static int32_t berryFromValue(bvm *vm, const CalogValueT *value, int32_t depth) {
if (depth > CALOG_MAX_DEPTH) {
be_pushnil(vm);
return calogErrDepthE;
}
switch (value->type) { switch (value->type) {
case calogNilE: case calogNilE:
be_pushnil(vm); be_pushnil(vm);
@ -223,10 +228,57 @@ static int32_t berryFromValue(bvm *vm, const CalogValueT *value) {
case calogStringE: case calogStringE:
be_pushnstring(vm, value->as.s.bytes, (size_t)value->as.s.length); be_pushnstring(vm, value->as.s.bytes, (size_t)value->as.s.length);
return calogOkE; return calogOkE;
case calogAggE: case calogAggE: {
// Aggregate marshalling into Berry is a v1 limit. CalogAggT *aggregate;
be_pushnil(vm); const char *className;
return calogErrUnsupportedE; int64_t index;
aggregate = value->as.agg;
// Populate a raw container: anything keyed (or an explicit map) is a map with
// the sequence part at integer keys, else a list. be_setindex and be_data_push
// take the value on top and do not pop, so drop it after each.
if (aggregate->pairCount > 0 || aggregate->kind == calogMapE) {
className = "map";
be_newmap(vm);
for (index = 0; index < aggregate->arrayCount; index++) {
be_pushint(vm, (bint)index);
berryFromValue(vm, &aggregate->array[index], depth + 1);
be_setindex(vm, -3);
be_pop(vm, 2);
}
for (index = 0; index < aggregate->pairCount; index++) {
const CalogValueT *key;
key = &aggregate->pairs[index].key;
if (key->type == calogStringE) {
be_pushnstring(vm, key->as.s.bytes, (size_t)key->as.s.length);
} else if (key->type == calogIntE) {
be_pushint(vm, (bint)key->as.i);
} else {
continue; // non-scalar key: no Berry equivalent, drop the pair
}
berryFromValue(vm, &aggregate->pairs[index].value, depth + 1);
be_setindex(vm, -3);
be_pop(vm, 2);
}
} else {
className = "list";
be_newlist(vm);
for (index = 0; index < aggregate->arrayCount; index++) {
berryFromValue(vm, &aggregate->array[index], depth + 1);
be_data_push(vm, -2);
be_pop(vm, 1);
}
}
// A raw map/list is not subscriptable in a script; wrap it in its class
// instance (map(raw) / list(raw)). Calling a class leaves the instance and
// init's (nil) return above the raw, so move the instance down over the raw
// and drop the two spare slots.
be_getbuiltin(vm, className);
be_pushvalue(vm, -2);
be_call(vm, 1);
be_moveto(vm, -2, -3);
be_pop(vm, 2);
return calogOkE;
}
case calogFnE: case calogFnE:
// Pushing a foreign function value into Berry is a v1 limit. // Pushing a foreign function value into Berry is a v1 limit.
be_pushnil(vm); be_pushnil(vm);
@ -300,7 +352,7 @@ static int32_t berryToValue(CalogBerryT *context, int index, CalogValueT *out, i
calogValueFn(out, callable); calogValueFn(out, callable);
return calogOkE; return calogOkE;
} }
// list / map marshalling out of Berry is a v1 limit. // Reading a script's list/map into an aggregate is a v1 limit.
return calogErrUnsupportedE; return calogErrUnsupportedE;
} }
@ -356,7 +408,7 @@ static int berryTrampoline(bvm *vm) {
be_raise(vm, "calog_error", message); be_raise(vm, "calog_error", message);
return 0; return 0;
} }
status = berryFromValue(vm, &result); status = berryFromValue(vm, &result, 0);
calogValueFree(&result); calogValueFree(&result);
if (status != calogOkE) { if (status != calogOkE) {
be_pop(vm, 1); be_pop(vm, 1);

View file

@ -3,8 +3,9 @@
// Exposes broker-registered native functions into a Berry VM, marshals values between // Exposes broker-registered native functions into a Berry VM, marshals values between
// Berry and CalogValueT by value (binary-safe strings; scalars), and exports Berry // 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, // functions as refcounted CalogFnT handles (kept alive by a GC-rooted hidden global,
// dropped on release). Aggregate (list/map) marshalling is a v1 limit -- passing a // dropped on release). An aggregate crossing OUT to a script becomes a Berry list or map
// list/map across the boundary reports calogErrUnsupportedE. // instance (so a host record is readable as user['name']); reading a script's list/map
// back IN is a v1 limit -> calogErrUnsupportedE.
// //
// Lifetime note (as with Lua/JS): release every CalogFnT obtained from calogBerryExport // Lifetime note (as with Lua/JS): release every CalogFnT obtained from calogBerryExport
// (and drop every function value marshalled out) BEFORE calogBerryDestroy, since the // (and drop every function value marshalled out) BEFORE calogBerryDestroy, since the

View file

@ -5,9 +5,10 @@
// "report" a), so the dispatcher recovers the name from its first argument and the // "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 // context from a hidden *calog-context* c-pointer, marshals the rest to CalogValueT, and
// dispatches through calogCall (honoring the actor route hook). Marshalling covers // dispatches through calogCall (honoring the actor route hook). Marshalling covers
// scalars, binary-safe strings, and Scheme lists (<-> aggregate list); the keyed part // scalars, binary-safe strings, and aggregates crossing out (a Scheme list, or a
// of an aggregate (map) is a v1 limit. A Scheme procedure crossing out becomes a // hash-table when keyed, so a record reads as (user "name")); reading a script's keyed
// refcounted CalogFnT kept alive by s7_gc_protect (released with s7_gc_unprotect_at). // 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).
// //
// Error model: a native failure raises a Scheme error via s7_error. calogS7Run wraps the // 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 // source in (catch #t (lambda () (eval-string ...)) handler) so both read and run errors
@ -281,17 +282,45 @@ static s7_pointer s7FromValue(CalogS7T *context, const CalogValueT *value, int32
return s7_make_string_with_length(sc, value->as.s.bytes, (s7_int)value->as.s.length); return s7_make_string_with_length(sc, value->as.s.bytes, (s7_int)value->as.s.length);
case calogAggE: { case calogAggE: {
CalogAggT *aggregate; CalogAggT *aggregate;
s7_pointer list;
int64_t index; int64_t index;
aggregate = value->as.agg; aggregate = value->as.agg;
// The keyed part (map) is a v1 limit; the sequence becomes a Scheme list. // Anything keyed (or an explicit map) becomes a hash-table (applicable, so a
list = s7_nil(sc); // script reads (user "name")) with the sequence part at integer keys; a pure
for (index = aggregate->arrayCount - 1; index >= 0; index--) { // sequence becomes a Scheme list.
s7_pointer element; if (aggregate->pairCount > 0 || aggregate->kind == calogMapE) {
element = s7FromValue(context, &aggregate->array[index], depth + 1); s7_pointer table;
list = s7_cons(sc, element, list); s7_int size;
size = (s7_int)(aggregate->arrayCount + aggregate->pairCount);
table = s7_make_hash_table(sc, size > 0 ? size : 1);
for (index = 0; index < aggregate->arrayCount; index++) {
s7_hash_table_set(sc, table, s7_make_integer(sc, (s7_int)index),
s7FromValue(context, &aggregate->array[index], depth + 1));
}
for (index = 0; index < aggregate->pairCount; index++) {
const CalogValueT *key;
s7_pointer keyObj;
key = &aggregate->pairs[index].key;
if (key->type == calogStringE) {
keyObj = s7_make_string_with_length(sc, key->as.s.bytes, (s7_int)key->as.s.length);
} else if (key->type == calogIntE) {
keyObj = s7_make_integer(sc, (s7_int)key->as.i);
} else {
continue; // non-scalar key: drop the pair
}
s7_hash_table_set(sc, table, keyObj,
s7FromValue(context, &aggregate->pairs[index].value, depth + 1));
}
return table;
} else {
s7_pointer list;
list = s7_nil(sc);
for (index = aggregate->arrayCount - 1; index >= 0; index--) {
s7_pointer element;
element = s7FromValue(context, &aggregate->array[index], depth + 1);
list = s7_cons(sc, element, list);
}
return list;
} }
return list;
} }
case calogFnE: case calogFnE:
// Pushing a foreign function value into Scheme is a v1 limit. // Pushing a foreign function value into Scheme is a v1 limit.

View file

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

View file

@ -2,7 +2,7 @@
// //
// Wren is class/method-oriented with no bare function calls, so all natives are reached // Wren is class/method-oriented with no bare function calls, so all natives are reached
// through a single foreign method: a preamble defines `class Calog { foreign static // through a single foreign method: a preamble defines `class Calog { foreign static
// call_(name, args) }`, and a script calls Calog.call_("report", [42]). The bound C // call(name, args) }`, and a script calls Calog.call("report", [42]). The bound C
// dispatcher recovers the context from wrenGetUserData, marshals the argument list to // dispatcher recovers the context from wrenGetUserData, marshals the argument list to
// CalogValueT, and dispatches through calogCall (honoring the actor route hook). A Wren // CalogValueT, and dispatches through calogCall (honoring the actor route hook). A Wren
// function crossing out becomes a refcounted CalogFnT over a retained WrenHandle, invoked // function crossing out becomes a refcounted CalogFnT over a retained WrenHandle, invoked
@ -52,7 +52,7 @@ static void wrenFromValue(CalogWrenT *context, const CalogValueT
static int32_t wrenToValue(CalogWrenT *context, int slot, CalogValueT *out, 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 void wrenWrite(WrenVM *vm, const char *text);
static const char WREN_PREAMBLE[] = "class Calog { foreign static call_(name, args) }"; static const char WREN_PREAMBLE[] = "class Calog { foreign static call(name, args) }";
int32_t calogWrenCreate(CalogWrenT **out, CalogT *broker, uint64_t ctxId) { int32_t calogWrenCreate(CalogWrenT **out, CalogT *broker, uint64_t ctxId) {
@ -177,7 +177,7 @@ void calogWrenDestroy(CalogWrenT *context) {
static WrenForeignMethodFn wrenBindMethod(WrenVM *vm, const char *module, const char *className, bool isStatic, const char *signature) { static WrenForeignMethodFn wrenBindMethod(WrenVM *vm, const char *module, const char *className, bool isStatic, const char *signature) {
(void)vm; (void)vm;
(void)module; (void)module;
if (strcmp(className, "Calog") == 0 && isStatic && strcmp(signature, "call_(_,_)") == 0) { if (strcmp(className, "Calog") == 0 && isStatic && strcmp(signature, "call(_,_)") == 0) {
return wrenDispatch; return wrenDispatch;
} }
return NULL; return NULL;
@ -294,7 +294,7 @@ static int32_t wrenExportSlot(CalogWrenT *context, int slot, CalogFnT **out) {
int32_t calogWrenExpose(CalogWrenT *context, const char *name) { int32_t calogWrenExpose(CalogWrenT *context, const char *name) {
// Every native is reachable through Calog.call_(name, args); nothing per-name to do. // Every native is reachable through Calog.call(name, args); nothing per-name to do.
if (calogLookup(context->broker, name) == NULL) { if (calogLookup(context->broker, name) == NULL) {
return calogErrNotFoundE; return calogErrNotFoundE;
} }
@ -331,11 +331,37 @@ static void wrenFromValue(CalogWrenT *context, const CalogValueT *value, int slo
CalogAggT *aggregate; CalogAggT *aggregate;
int64_t index; int64_t index;
aggregate = value->as.agg; aggregate = value->as.agg;
wrenSetSlotNewList(vm, slot); // the keyed part (map) is a v1 limit // Anything keyed (or an explicit map) becomes a Wren Map with the sequence
wrenEnsureSlots(vm, slot + 2); // part at numeric keys (a script reads user["name"]); a pure sequence
for (index = 0; index < aggregate->arrayCount; index++) { // becomes a Wren List. slot+1 = key scratch, slot+2 = value scratch.
wrenFromValue(context, &aggregate->array[index], slot + 1, depth + 1); if (aggregate->pairCount > 0 || aggregate->kind == calogMapE) {
wrenInsertInList(vm, slot, -1, slot + 1); wrenSetSlotNewMap(vm, slot);
wrenEnsureSlots(vm, slot + 3);
for (index = 0; index < aggregate->arrayCount; index++) {
wrenSetSlotDouble(vm, slot + 1, (double)index);
wrenFromValue(context, &aggregate->array[index], slot + 2, depth + 1);
wrenSetMapValue(vm, slot, slot + 1, slot + 2);
}
for (index = 0; index < aggregate->pairCount; index++) {
const CalogValueT *key;
key = &aggregate->pairs[index].key;
if (key->type == calogStringE) {
wrenSetSlotBytes(vm, slot + 1, key->as.s.bytes, (size_t)key->as.s.length);
} else if (key->type == calogIntE) {
wrenSetSlotDouble(vm, slot + 1, (double)key->as.i);
} else {
continue; // non-scalar key: drop the pair
}
wrenFromValue(context, &aggregate->pairs[index].value, slot + 2, depth + 1);
wrenSetMapValue(vm, slot, slot + 1, slot + 2);
}
} else {
wrenSetSlotNewList(vm, slot);
wrenEnsureSlots(vm, slot + 2);
for (index = 0; index < aggregate->arrayCount; index++) {
wrenFromValue(context, &aggregate->array[index], slot + 1, depth + 1);
wrenInsertInList(vm, slot, -1, slot + 1);
}
} }
return; return;
} }
@ -426,7 +452,7 @@ static int32_t wrenToValue(CalogWrenT *context, int slot, CalogValueT *out, int3
return calogOkE; return calogOkE;
} }
case WREN_TYPE_MAP: case WREN_TYPE_MAP:
return calogErrUnsupportedE; // map read is a v1 limit return calogErrUnsupportedE; // map read is a v1 limit (no key enumeration in the C API)
case WREN_TYPE_FOREIGN: case WREN_TYPE_FOREIGN:
case WREN_TYPE_UNKNOWN: { case WREN_TYPE_UNKNOWN: {
// A non-primitive (typically a Fn) is captured as a callable. // A non-primitive (typically a Fn) is captured as a callable.

View file

@ -1,11 +1,12 @@
// wrenAdapter.h -- Wren engine adapter for the broker. // wrenAdapter.h -- Wren engine adapter for the broker.
// //
// Wren has no bare function calls, so natives are reached through one foreign method: // 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 // scripts call Calog.call("name", [args]). The adapter marshals values between Wren and
// CalogValueT by value (binary-safe strings; scalars; Wren list <-> aggregate list) and // CalogValueT by value (binary-safe strings; scalars; an aggregate crosses OUT as a Wren
// exports Wren functions as refcounted CalogFnT handles (a retained WrenHandle, released // List, or a Map when keyed so a record reads as user["name"]) and exports Wren functions
// with wrenReleaseHandle). Numbers are IEEE doubles, so int64 magnitudes above 2^53 lose // as refcounted CalogFnT handles (a retained WrenHandle, released with wrenReleaseHandle).
// precision. The keyed part of an aggregate (map) is a v1 limit. // 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).
// //
// Lifetime note (as with Lua/JS): release every CalogFnT obtained from calogWrenExport // Lifetime note (as with Lua/JS): release every CalogFnT obtained from calogWrenExport
// (and drop every function value marshalled out) BEFORE calogWrenDestroy, since the // (and drop every function value marshalled out) BEFORE calogWrenDestroy, since the

View file

@ -26,20 +26,25 @@ static _Atomic bool scriptDone = false;
static _Atomic int32_t errorCount = 0; static _Atomic int32_t errorCount = 0;
static _Atomic uint64_t errorCtxId = 0; static _Atomic uint64_t errorCtxId = 0;
static CalogFnT *_Atomic storedCb = NULL; static CalogFnT *_Atomic storedCb = NULL;
static char storedName[32] = { 0 };
static _Atomic int32_t nameLen = -1;
static int32_t testsRun = 0; static int32_t testsRun = 0;
static int32_t testsFailed = 0; static int32_t testsFailed = 0;
static void checkImpl(bool condition, const char *message, const char *file, int32_t line); static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeBump(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 nativeDone(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 nativeReport(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 nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReportName(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void onError(uint64_t contextId, const char *message, void *userData); static void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(void); static void pumpUntilDone(void);
static void testConcurrentContexts(void); static void testConcurrentContexts(void);
static void testCrossThreadCallback(void); static void testCrossThreadCallback(void);
static void testHostAndInlineNatives(void); static void testHostAndInlineNatives(void);
static void testMaterializedRecord(void);
static void testScriptError(void); static void testScriptError(void);
@ -72,6 +77,31 @@ static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *resu
} }
static int32_t nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogAggT *user;
CalogValueT key;
CalogValueT val;
int32_t status;
(void)args;
(void)argCount;
(void)userData;
calogValueNil(result);
status = calogAggCreate(&user, calogMapE);
if (status != calogOkE) {
return calogFail(result, status, "makeUser could not allocate");
}
calogValueString(&key, "name", 4);
calogValueString(&val, "ada", 3);
calogAggSet(user, &key, &val);
calogValueString(&key, "age", 3);
calogValueInt(&val, 36);
calogAggSet(user, &key, &val);
calogValueAgg(result, user);
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData; (void)userData;
calogValueNil(result); calogValueNil(result);
@ -94,6 +124,25 @@ static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValu
} }
static int32_t nativeReportName(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
size_t length;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "reportName expects one string");
}
length = (size_t)args[0].as.s.length;
if (length >= sizeof(storedName)) {
length = sizeof(storedName) - 1;
}
memcpy(storedName, args[0].as.s.bytes, length);
storedName[length] = '\0';
atomic_store(&nameLen, (int32_t)length);
return calogOkE;
}
static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData; (void)userData;
calogValueNil(result); calogValueNil(result);
@ -149,6 +198,23 @@ static void testHostAndInlineNatives(void) {
} }
static void testMaterializedRecord(void) {
CalogContextT *ctx;
ctx = calogContextOpen(calog, &calogBerryEngine);
atomic_store(&scriptDone, false);
atomic_store(&nameLen, -1);
atomic_store(&reportedValue, 0);
calogContextEval(ctx, "u = makeUser()\nreport(u['age'])\nreportName(u['name'])\ndone()");
pumpUntilDone();
CHECK(atomic_load(&reportedValue) == 36, "read an int field from a materialized record map");
CHECK(atomic_load(&nameLen) == 3 && memcmp(storedName, "ada", 3) == 0, "read a string field from a materialized record map");
calogContextClose(ctx);
}
static void testCrossThreadCallback(void) { static void testCrossThreadCallback(void) {
CalogContextT *ctx; CalogContextT *ctx;
CalogFnT *callback; CalogFnT *callback;
@ -225,9 +291,12 @@ int main(void) {
calogRegister(calog, "setCb", nativeSetCb, NULL); calogRegister(calog, "setCb", nativeSetCb, NULL);
calogRegister(calog, "done", nativeDone, NULL); calogRegister(calog, "done", nativeDone, NULL);
calogRegister(calog, "bump", nativeBump, NULL); calogRegister(calog, "bump", nativeBump, NULL);
calogRegister(calog, "makeUser", nativeMakeUser, NULL);
calogRegister(calog, "reportName", nativeReportName, NULL);
calogRegisterInline(calog, "reportInline", nativeReportInline, NULL); calogRegisterInline(calog, "reportInline", nativeReportInline, NULL);
testHostAndInlineNatives(); testHostAndInlineNatives();
testMaterializedRecord();
testCrossThreadCallback(); testCrossThreadCallback();
testScriptError(); testScriptError();
testConcurrentContexts(); testConcurrentContexts();

View file

@ -35,20 +35,25 @@ static _Atomic bool scriptDone = false;
static _Atomic int32_t errorCount = 0; static _Atomic int32_t errorCount = 0;
static _Atomic uint64_t errorCtxId = 0; static _Atomic uint64_t errorCtxId = 0;
static CalogFnT *_Atomic storedCb = NULL; static CalogFnT *_Atomic storedCb = NULL;
static char storedName[32] = { 0 };
static _Atomic int32_t nameLen = -1;
static int32_t testsRun = 0; static int32_t testsRun = 0;
static int32_t testsFailed = 0; static int32_t testsFailed = 0;
static void checkImpl(bool condition, const char *message, const char *file, int32_t line); static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeBump(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 nativeDone(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 nativeReport(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 nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReportName(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void onError(uint64_t contextId, const char *message, void *userData); static void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(void); static void pumpUntilDone(void);
static void testConcurrentContexts(void); static void testConcurrentContexts(void);
static void testCrossThreadCallback(void); static void testCrossThreadCallback(void);
static void testHostAndInlineNatives(void); static void testHostAndInlineNatives(void);
static void testMaterializedRecord(void);
static void testScriptError(void); static void testScriptError(void);
@ -81,6 +86,31 @@ static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *resu
} }
static int32_t nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogAggT *user;
CalogValueT key;
CalogValueT val;
int32_t status;
(void)args;
(void)argCount;
(void)userData;
calogValueNil(result);
status = calogAggCreate(&user, calogMapE);
if (status != calogOkE) {
return calogFail(result, status, "makeUser could not allocate");
}
calogValueString(&key, "name", 4);
calogValueString(&val, "ada", 3);
calogAggSet(user, &key, &val);
calogValueString(&key, "age", 3);
calogValueInt(&val, 36);
calogAggSet(user, &key, &val);
calogValueAgg(result, user);
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData; (void)userData;
calogValueNil(result); calogValueNil(result);
@ -103,6 +133,25 @@ static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValu
} }
static int32_t nativeReportName(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
size_t length;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "reportName expects one string");
}
length = (size_t)args[0].as.s.length;
if (length >= sizeof(storedName)) {
length = sizeof(storedName) - 1;
}
memcpy(storedName, args[0].as.s.bytes, length);
storedName[length] = '\0';
atomic_store(&nameLen, (int32_t)length);
return calogOkE;
}
static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData; (void)userData;
calogValueNil(result); calogValueNil(result);
@ -158,6 +207,23 @@ static void testHostAndInlineNatives(void) {
} }
static void testMaterializedRecord(void) {
CalogContextT *ctx;
ctx = calogContextOpen(calog, &calogS7Engine);
atomic_store(&scriptDone, false);
atomic_store(&nameLen, -1);
atomic_store(&reportedValue, 0);
calogContextEval(ctx, "(begin (define u (makeUser)) (report (u \"age\")) (reportName (u \"name\")) (done))");
pumpUntilDone();
CHECK(atomic_load(&reportedValue) == 36, "read an int field from a materialized record map");
CHECK(atomic_load(&nameLen) == 3 && memcmp(storedName, "ada", 3) == 0, "read a string field from a materialized record map");
calogContextClose(ctx);
}
static void testCrossThreadCallback(void) { static void testCrossThreadCallback(void) {
CalogContextT *ctx; CalogContextT *ctx;
CalogFnT *callback; CalogFnT *callback;
@ -234,9 +300,12 @@ int main(void) {
calogRegister(calog, "setCb", nativeSetCb, NULL); calogRegister(calog, "setCb", nativeSetCb, NULL);
calogRegister(calog, "done", nativeDone, NULL); calogRegister(calog, "done", nativeDone, NULL);
calogRegister(calog, "bump", nativeBump, NULL); calogRegister(calog, "bump", nativeBump, NULL);
calogRegister(calog, "makeUser", nativeMakeUser, NULL);
calogRegister(calog, "reportName", nativeReportName, NULL);
calogRegisterInline(calog, "reportInline", nativeReportInline, NULL); calogRegisterInline(calog, "reportInline", nativeReportInline, NULL);
testHostAndInlineNatives(); testHostAndInlineNatives();
testMaterializedRecord();
testCrossThreadCallback(); testCrossThreadCallback();
testScriptError(); testScriptError();
testConcurrentContexts(); testConcurrentContexts();

View file

@ -26,20 +26,25 @@ static _Atomic bool scriptDone = false;
static _Atomic int32_t errorCount = 0; static _Atomic int32_t errorCount = 0;
static _Atomic uint64_t errorCtxId = 0; static _Atomic uint64_t errorCtxId = 0;
static CalogFnT *_Atomic storedCb = NULL; static CalogFnT *_Atomic storedCb = NULL;
static char storedName[32] = { 0 };
static _Atomic int32_t nameLen = -1;
static int32_t testsRun = 0; static int32_t testsRun = 0;
static int32_t testsFailed = 0; static int32_t testsFailed = 0;
static void checkImpl(bool condition, const char *message, const char *file, int32_t line); static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeBump(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 nativeDone(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 nativeReport(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 nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReportName(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void onError(uint64_t contextId, const char *message, void *userData); static void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(void); static void pumpUntilDone(void);
static void testConcurrentContexts(void); static void testConcurrentContexts(void);
static void testCrossThreadCallback(void); static void testCrossThreadCallback(void);
static void testHostAndInlineNatives(void); static void testHostAndInlineNatives(void);
static void testMaterializedRecord(void);
static void testScriptError(void); static void testScriptError(void);
@ -72,6 +77,31 @@ static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *resu
} }
static int32_t nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogAggT *user;
CalogValueT key;
CalogValueT val;
int32_t status;
(void)args;
(void)argCount;
(void)userData;
calogValueNil(result);
status = calogAggCreate(&user, calogMapE);
if (status != calogOkE) {
return calogFail(result, status, "makeUser could not allocate");
}
calogValueString(&key, "name", 4);
calogValueString(&val, "ada", 3);
calogAggSet(user, &key, &val);
calogValueString(&key, "age", 3);
calogValueInt(&val, 36);
calogAggSet(user, &key, &val);
calogValueAgg(result, user);
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData; (void)userData;
calogValueNil(result); calogValueNil(result);
@ -94,6 +124,25 @@ static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValu
} }
static int32_t nativeReportName(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
size_t length;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "reportName expects one string");
}
length = (size_t)args[0].as.s.length;
if (length >= sizeof(storedName)) {
length = sizeof(storedName) - 1;
}
memcpy(storedName, args[0].as.s.bytes, length);
storedName[length] = '\0';
atomic_store(&nameLen, (int32_t)length);
return calogOkE;
}
static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData; (void)userData;
calogValueNil(result); calogValueNil(result);
@ -138,7 +187,7 @@ static void testHostAndInlineNatives(void) {
CHECK(ctx != NULL, "opened a Wren context"); CHECK(ctx != NULL, "opened a Wren context");
atomic_store(&scriptDone, false); atomic_store(&scriptDone, false);
calogContextEval(ctx, "Calog.call_(\"report\", [42])\nCalog.call_(\"reportInline\", [1])\nCalog.call_(\"done\", [])"); calogContextEval(ctx, "Calog.call(\"report\", [42])\nCalog.call(\"reportInline\", [1])\nCalog.call(\"done\", [])");
pumpUntilDone(); pumpUntilDone();
CHECK(atomic_load(&reportedValue) == 42, "host native received the argument"); CHECK(atomic_load(&reportedValue) == 42, "host native received the argument");
@ -149,6 +198,23 @@ static void testHostAndInlineNatives(void) {
} }
static void testMaterializedRecord(void) {
CalogContextT *ctx;
ctx = calogContextOpen(calog, &calogWrenEngine);
atomic_store(&scriptDone, false);
atomic_store(&nameLen, -1);
atomic_store(&reportedValue, 0);
calogContextEval(ctx, "var u = Calog.call(\"makeUser\", [])\nCalog.call(\"report\", [u[\"age\"]])\nCalog.call(\"reportName\", [u[\"name\"]])\nCalog.call(\"done\", [])");
pumpUntilDone();
CHECK(atomic_load(&reportedValue) == 36, "read an int field from a materialized record map");
CHECK(atomic_load(&nameLen) == 3 && memcmp(storedName, "ada", 3) == 0, "read a string field from a materialized record map");
calogContextClose(ctx);
}
static void testCrossThreadCallback(void) { static void testCrossThreadCallback(void) {
CalogContextT *ctx; CalogContextT *ctx;
CalogFnT *callback; CalogFnT *callback;
@ -159,7 +225,7 @@ static void testCrossThreadCallback(void) {
ctx = calogContextOpen(calog, &calogWrenEngine); ctx = calogContextOpen(calog, &calogWrenEngine);
atomic_store(&scriptDone, false); atomic_store(&scriptDone, false);
atomic_store(&storedCb, NULL); atomic_store(&storedCb, NULL);
calogContextEval(ctx, "Calog.call_(\"setCb\", [Fn.new {|x| x + 100 }])\nCalog.call_(\"done\", [])"); calogContextEval(ctx, "Calog.call(\"setCb\", [Fn.new {|x| x + 100 }])\nCalog.call(\"done\", [])");
pumpUntilDone(); pumpUntilDone();
callback = atomic_load(&storedCb); callback = atomic_load(&storedCb);
@ -201,7 +267,7 @@ static void testConcurrentContexts(void) {
atomic_store(&bumpCount, 0); atomic_store(&bumpCount, 0);
for (i = 0; i < 3; i++) { for (i = 0; i < 3; i++) {
ctxs[i] = calogContextOpen(calog, &calogWrenEngine); ctxs[i] = calogContextOpen(calog, &calogWrenEngine);
calogContextEval(ctxs[i], "Calog.call_(\"bump\", [])\nCalog.call_(\"bump\", [])\nCalog.call_(\"bump\", [])"); calogContextEval(ctxs[i], "Calog.call(\"bump\", [])\nCalog.call(\"bump\", [])\nCalog.call(\"bump\", [])");
} }
for (i = 0; i < PUMP_LIMIT && atomic_load(&bumpCount) < 9; i++) { for (i = 0; i < PUMP_LIMIT && atomic_load(&bumpCount) < 9; i++) {
calogPump(calog); calogPump(calog);
@ -225,9 +291,12 @@ int main(void) {
calogRegister(calog, "setCb", nativeSetCb, NULL); calogRegister(calog, "setCb", nativeSetCb, NULL);
calogRegister(calog, "done", nativeDone, NULL); calogRegister(calog, "done", nativeDone, NULL);
calogRegister(calog, "bump", nativeBump, NULL); calogRegister(calog, "bump", nativeBump, NULL);
calogRegister(calog, "makeUser", nativeMakeUser, NULL);
calogRegister(calog, "reportName", nativeReportName, NULL);
calogRegisterInline(calog, "reportInline", nativeReportInline, NULL); calogRegisterInline(calog, "reportInline", nativeReportInline, NULL);
testHostAndInlineNatives(); testHostAndInlineNatives();
testMaterializedRecord();
testCrossThreadCallback(); testCrossThreadCallback();
testScriptError(); testScriptError();
testConcurrentContexts(); testConcurrentContexts();

View file

@ -111,7 +111,7 @@ int main(void) {
CHECK(loadAndRun("clTest") == 15, "loaded and ran a .scm Scheme script by base name"); CHECK(loadAndRun("clTest") == 15, "loaded and ran a .scm Scheme script by base name");
remove("clTest.scm"); remove("clTest.scm");
writeScript("clTest.wren", "Calog.call_(\"hit\", [16])"); writeScript("clTest.wren", "Calog.call(\"hit\", [16])");
CHECK(loadAndRun("clTest") == 16, "loaded and ran a .wren script by base name"); CHECK(loadAndRun("clTest") == 16, "loaded and ran a .wren script by base name");
remove("clTest.wren"); remove("clTest.wren");