diff --git a/README.md b/README.md index eafca2f..73dbe59 100644 --- a/README.md +++ b/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. diff --git a/design.md b/design.md index 2fbf4e7..85726e8 100644 --- a/design.md +++ b/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` 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` target clean for each, and +gcc + clang strict on the core. diff --git a/src/berry/berryAdapter.c b/src/berry/berryAdapter.c index f560fae..aba963d 100644 --- a/src/berry/berryAdapter.c +++ b/src/berry/berryAdapter.c @@ -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); diff --git a/src/berry/berryAdapter.h b/src/berry/berryAdapter.h index 2a84b16..424b64d 100644 --- a/src/berry/berryAdapter.h +++ b/src/berry/berryAdapter.h @@ -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 diff --git a/src/s7/s7Adapter.c b/src/s7/s7Adapter.c index cb0269f..3f6cb3c 100644 --- a/src/s7/s7Adapter.c +++ b/src/s7/s7Adapter.c @@ -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. diff --git a/src/s7/s7Adapter.h b/src/s7/s7Adapter.h index 77e640e..2ad7d27 100644 --- a/src/s7/s7Adapter.h +++ b/src/s7/s7Adapter.h @@ -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 diff --git a/src/wren/wrenAdapter.c b/src/wren/wrenAdapter.c index b4fadea..647e8c0 100644 --- a/src/wren/wrenAdapter.c +++ b/src/wren/wrenAdapter.c @@ -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. diff --git a/src/wren/wrenAdapter.h b/src/wren/wrenAdapter.h index 3b3d174..b78e7a8 100644 --- a/src/wren/wrenAdapter.h +++ b/src/wren/wrenAdapter.h @@ -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 diff --git a/tests/testEngineBerry.c b/tests/testEngineBerry.c index 8743197..d01e05d 100644 --- a/tests/testEngineBerry.c +++ b/tests/testEngineBerry.c @@ -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(); diff --git a/tests/testEngineS7.c b/tests/testEngineS7.c index 096128e..0df17a6 100644 --- a/tests/testEngineS7.c +++ b/tests/testEngineS7.c @@ -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(); diff --git a/tests/testEngineWren.c b/tests/testEngineWren.c index 4969433..8a8ac09 100644 --- a/tests/testEngineWren.c +++ b/tests/testEngineWren.c @@ -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(); diff --git a/tests/testLoad.c b/tests/testLoad.c index db23116..591e4de 100644 --- a/tests/testLoad.c +++ b/tests/testLoad.c @@ -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");