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) |
| Squirrel 3.2 | `calogSquirrelEngine` | `.nut` | C++ VM |
| MY-BASIC | `calogMyBasicEngine` | `.bas` | interpreters serialize at load |
| Scheme | `calogS7Engine` | `.scm` | s7; 64-bit ints, lists |
| Wren | `calogWrenEngine` | `.wren` | doubles only; call via `Calog.call_(…)` |
| Berry | `calogBerryEngine` | `.be` | scalars + callbacks (aggregates: v1) |
| Scheme | `calogS7Engine` | `.scm` | s7; 64-bit ints |
| Wren | `calogWrenEngine` | `.wren` | doubles only; call via `Calog.call(…)` |
| 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.

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
(`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
(`be_modtab.c`). Aggregate marshalling is a v1 limit (scalars + strings + callbacks are
complete).
(`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.
**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
@ -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
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
read and run errors surface as a value -- a marker pair the runner detects. int64 and
Scheme lists round-trip both ways; the keyed part (map) is a v1 limit. s7's intentional
read and run errors surface as a value -- a marker pair the runner detects. An aggregate
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
allocation, suppressed with a documented, allocation-site-specific `__lsan` hook. s7 is
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
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
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
2^53 loses precision (the same edge my-basic and old-JS have). Lists round-trip; map read
is a v1 limit. Wren keeps no process-global state, so contexts run in parallel.
2^53 loses precision (the same edge my-basic and old-JS have). An aggregate crossing out
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
`tsan<engine>` target clean for each, and gcc + clang strict on the core.
Aggregate egress is therefore uniform across all seven engines: a host native can return a
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
// 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. 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
// 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 void berryCallableRelease(CalogFnT *callable);
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 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");
}
for (index = 0; index < argCount; index++) {
status = berryFromValue(vm, &args[index]);
status = berryFromValue(vm, &args[index], 0);
if (status != calogOkE) {
be_pop(vm, be_top(vm) - base);
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) {
case calogNilE:
be_pushnil(vm);
@ -223,10 +228,57 @@ static int32_t berryFromValue(bvm *vm, const CalogValueT *value) {
case calogStringE:
be_pushnstring(vm, value->as.s.bytes, (size_t)value->as.s.length);
return calogOkE;
case calogAggE:
// Aggregate marshalling into Berry is a v1 limit.
be_pushnil(vm);
return calogErrUnsupportedE;
case calogAggE: {
CalogAggT *aggregate;
const char *className;
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:
// Pushing a foreign function value into Berry is a v1 limit.
be_pushnil(vm);
@ -300,7 +352,7 @@ static int32_t berryToValue(CalogBerryT *context, int index, CalogValueT *out, i
calogValueFn(out, callable);
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;
}
@ -356,7 +408,7 @@ static int berryTrampoline(bvm *vm) {
be_raise(vm, "calog_error", message);
return 0;
}
status = berryFromValue(vm, &result);
status = berryFromValue(vm, &result, 0);
calogValueFree(&result);
if (status != calogOkE) {
be_pop(vm, 1);

View file

@ -3,8 +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). Aggregate (list/map) marshalling is a v1 limit -- passing a
// list/map across the boundary reports calogErrUnsupportedE.
// 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.
//
// 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

@ -5,9 +5,10 @@
// "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 Scheme lists (<-> aggregate list); the keyed part
// of an aggregate (map) 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 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).
//
// 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
@ -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);
case calogAggE: {
CalogAggT *aggregate;
s7_pointer list;
int64_t index;
aggregate = value->as.agg;
// The keyed part (map) is a v1 limit; the sequence becomes a Scheme 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);
// Anything keyed (or an explicit map) becomes a hash-table (applicable, so a
// script reads (user "name")) with the sequence part at integer keys; a pure
// sequence becomes a Scheme list.
if (aggregate->pairCount > 0 || aggregate->kind == calogMapE) {
s7_pointer table;
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:
// 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.
//
// Exposes broker-registered native functions into an s7 interpreter, marshals values
// between Scheme and CalogValueT by value (binary-safe strings; scalars; Scheme list
// <-> aggregate list), and exports Scheme procedures as refcounted CalogFnT handles
// (kept alive by s7_gc_protect, released with s7_gc_unprotect_at). The keyed part of an
// aggregate (map) is a v1 limit -> calogErrUnsupportedE.
// 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.
//
// 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

@ -2,7 +2,7 @@
//
// 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
// 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
// CalogValueT, and dispatches through calogCall (honoring the actor route hook). A Wren
// 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 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) {
@ -177,7 +177,7 @@ void calogWrenDestroy(CalogWrenT *context) {
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) {
if (strcmp(className, "Calog") == 0 && isStatic && strcmp(signature, "call(_,_)") == 0) {
return wrenDispatch;
}
return NULL;
@ -294,7 +294,7 @@ static int32_t wrenExportSlot(CalogWrenT *context, int slot, CalogFnT **out) {
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) {
return calogErrNotFoundE;
}
@ -331,11 +331,37 @@ static void wrenFromValue(CalogWrenT *context, const CalogValueT *value, int slo
CalogAggT *aggregate;
int64_t index;
aggregate = value->as.agg;
wrenSetSlotNewList(vm, slot); // the keyed part (map) is a v1 limit
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);
// Anything keyed (or an explicit map) becomes a Wren Map with the sequence
// part at numeric keys (a script reads user["name"]); a pure sequence
// becomes a Wren List. slot+1 = key scratch, slot+2 = value scratch.
if (aggregate->pairCount > 0 || aggregate->kind == calogMapE) {
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;
}
@ -426,7 +452,7 @@ static int32_t wrenToValue(CalogWrenT *context, int slot, CalogValueT *out, int3
return calogOkE;
}
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_UNKNOWN: {
// 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.
//
// 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; Wren list <-> aggregate list) 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. The keyed part of an aggregate (map) is a v1 limit.
// 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).
//
// 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

@ -26,20 +26,25 @@ static _Atomic bool scriptDone = false;
static _Atomic int32_t errorCount = 0;
static _Atomic uint64_t errorCtxId = 0;
static CalogFnT *_Atomic storedCb = NULL;
static char storedName[32] = { 0 };
static _Atomic int32_t nameLen = -1;
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 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 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 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 void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(void);
static void testConcurrentContexts(void);
static void testCrossThreadCallback(void);
static void testHostAndInlineNatives(void);
static void testMaterializedRecord(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) {
(void)userData;
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) {
(void)userData;
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) {
CalogContextT *ctx;
CalogFnT *callback;
@ -225,9 +291,12 @@ int main(void) {
calogRegister(calog, "setCb", nativeSetCb, NULL);
calogRegister(calog, "done", nativeDone, NULL);
calogRegister(calog, "bump", nativeBump, NULL);
calogRegister(calog, "makeUser", nativeMakeUser, NULL);
calogRegister(calog, "reportName", nativeReportName, NULL);
calogRegisterInline(calog, "reportInline", nativeReportInline, NULL);
testHostAndInlineNatives();
testMaterializedRecord();
testCrossThreadCallback();
testScriptError();
testConcurrentContexts();

View file

@ -35,20 +35,25 @@ static _Atomic bool scriptDone = false;
static _Atomic int32_t errorCount = 0;
static _Atomic uint64_t errorCtxId = 0;
static CalogFnT *_Atomic storedCb = NULL;
static char storedName[32] = { 0 };
static _Atomic int32_t nameLen = -1;
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 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 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 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 void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(void);
static void testConcurrentContexts(void);
static void testCrossThreadCallback(void);
static void testHostAndInlineNatives(void);
static void testMaterializedRecord(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) {
(void)userData;
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) {
(void)userData;
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) {
CalogContextT *ctx;
CalogFnT *callback;
@ -234,9 +300,12 @@ int main(void) {
calogRegister(calog, "setCb", nativeSetCb, NULL);
calogRegister(calog, "done", nativeDone, NULL);
calogRegister(calog, "bump", nativeBump, NULL);
calogRegister(calog, "makeUser", nativeMakeUser, NULL);
calogRegister(calog, "reportName", nativeReportName, NULL);
calogRegisterInline(calog, "reportInline", nativeReportInline, NULL);
testHostAndInlineNatives();
testMaterializedRecord();
testCrossThreadCallback();
testScriptError();
testConcurrentContexts();

View file

@ -26,20 +26,25 @@ static _Atomic bool scriptDone = false;
static _Atomic int32_t errorCount = 0;
static _Atomic uint64_t errorCtxId = 0;
static CalogFnT *_Atomic storedCb = NULL;
static char storedName[32] = { 0 };
static _Atomic int32_t nameLen = -1;
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 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 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 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 void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(void);
static void testConcurrentContexts(void);
static void testCrossThreadCallback(void);
static void testHostAndInlineNatives(void);
static void testMaterializedRecord(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) {
(void)userData;
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) {
(void)userData;
calogValueNil(result);
@ -138,7 +187,7 @@ static void testHostAndInlineNatives(void) {
CHECK(ctx != NULL, "opened a Wren context");
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();
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) {
CalogContextT *ctx;
CalogFnT *callback;
@ -159,7 +225,7 @@ static void testCrossThreadCallback(void) {
ctx = calogContextOpen(calog, &calogWrenEngine);
atomic_store(&scriptDone, false);
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();
callback = atomic_load(&storedCb);
@ -201,7 +267,7 @@ static void testConcurrentContexts(void) {
atomic_store(&bumpCount, 0);
for (i = 0; i < 3; i++) {
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++) {
calogPump(calog);
@ -225,9 +291,12 @@ int main(void) {
calogRegister(calog, "setCb", nativeSetCb, NULL);
calogRegister(calog, "done", nativeDone, NULL);
calogRegister(calog, "bump", nativeBump, NULL);
calogRegister(calog, "makeUser", nativeMakeUser, NULL);
calogRegister(calog, "reportName", nativeReportName, NULL);
calogRegisterInline(calog, "reportInline", nativeReportInline, NULL);
testHostAndInlineNatives();
testMaterializedRecord();
testCrossThreadCallback();
testScriptError();
testConcurrentContexts();

View file

@ -111,7 +111,7 @@ int main(void) {
CHECK(loadAndRun("clTest") == 15, "loaded and ran a .scm Scheme script by base name");
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");
remove("clTest.wren");