Improved sharing of maps between languages.
This commit is contained in:
parent
58a9008458
commit
c11bf8481f
12 changed files with 392 additions and 58 deletions
10
README.md
10
README.md
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
31
design.md
31
design.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue