From 41f373981d41de0b870a68eb23177dc6d59bcc83 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Tue, 17 Feb 2026 00:04:55 -0600 Subject: [PATCH 1/3] add docs to website nav --- website/data/docs_nav.yaml | 8 ++++++++ website/data/spec_sections.yaml | 3 +++ website/data/tools_sections.yaml | 8 ++++++++ 3 files changed, 19 insertions(+) diff --git a/website/data/docs_nav.yaml b/website/data/docs_nav.yaml index 97884ca7..3d49fb3d 100644 --- a/website/data/docs_nav.yaml +++ b/website/data/docs_nav.yaml @@ -41,6 +41,14 @@ sections: url: "/docs/shop/" - title: "CLI" url: "/docs/cli/" + - title: "Tools" + pages: + - title: "Testing" + url: "/docs/testing/" + - title: "Compiler Inspection" + url: "/docs/compiler-tools/" + - title: "Semantic Index" + url: "/docs/semantic-index/" - title: "C API" pages: - title: "C Modules" diff --git a/website/data/spec_sections.yaml b/website/data/spec_sections.yaml index f24e83f9..6bc83f93 100644 --- a/website/data/spec_sections.yaml +++ b/website/data/spec_sections.yaml @@ -35,3 +35,6 @@ sections: - title: "Wota Format" page: "/docs/wota/" id: "wota" + - title: "C Runtime" + page: "/docs/spec/c-runtime/" + id: "c-runtime" diff --git a/website/data/tools_sections.yaml b/website/data/tools_sections.yaml index fe855010..ddaa6440 100644 --- a/website/data/tools_sections.yaml +++ b/website/data/tools_sections.yaml @@ -15,3 +15,11 @@ sections: page: "/docs/testing/" id: "testing" group: "CLI" + - title: "Compiler Inspection" + page: "/docs/compiler-tools/" + id: "compiler-tools" + group: "Tools" + - title: "Semantic Index" + page: "/docs/semantic-index/" + id: "semantic-index" + group: "Tools" From 400c58e5f256e6f58b016e7c88305928325fe2ca Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Tue, 17 Feb 2026 01:04:42 -0600 Subject: [PATCH 2/3] fix build --- build.cm | 16 +++++++--- docs/c-modules.md | 79 ++++++++++++++++++++++++++++++++++++++++++++++- source/cell.h | 7 ++--- 3 files changed, 92 insertions(+), 10 deletions(-) diff --git a/build.cm b/build.cm index 0031c83e..333e1014 100644 --- a/build.cm +++ b/build.cm @@ -108,7 +108,8 @@ Build.compile_file = function(pkg, file, target, buildtype) { var core_dir = null if (!fd.is_file(src_path)) { - print('Source file not found: ' + src_path); disrupt + print('Source file not found: ' + src_path) + return null } // Get flags (with sigil replacement) @@ -177,9 +178,10 @@ Build.compile_file = function(pkg, file, target, buildtype) { log.console('Compiling ' + file) var ret = os.system(full_cmd) if (ret != 0) { - print('Compilation failed: ' + file); disrupt + print('Compilation failed: ' + file) + return null } - + return obj_path } @@ -234,6 +236,7 @@ Build.build_module_dylib = function(pkg, file, target, opts) { var _buildtype = _opts.buildtype || 'release' var _extra = _opts.extra_objects || [] var obj = Build.compile_file(pkg, file, _target, _buildtype) + if (!obj) return null var tc = toolchains[_target] var dylib_ext = tc.system == 'windows' ? '.dll' : (tc.system == 'darwin' ? '.dylib' : '.so') @@ -299,7 +302,8 @@ Build.build_module_dylib = function(pkg, file, target, opts) { log.console('Linking module ' + file + ' -> ' + fd.basename(dylib_path)) ret = os.system(cmd_str) if (ret != 0) { - print('Linking failed: ' + file); disrupt + print('Linking failed: ' + file) + return null } } @@ -340,7 +344,9 @@ Build.build_dynamic = function(pkg, target, buildtype) { arrfor(c_files, function(file) { var sym_name = shop.c_symbol_for_file(pkg, file) var dylib = Build.build_module_dylib(pkg, file, _target, {buildtype: _buildtype, extra_objects: support_objects}) - push(results, {file: file, symbol: sym_name, dylib: dylib}) + if (dylib) { + push(results, {file: file, symbol: sym_name, dylib: dylib}) + } }) return results diff --git a/docs/c-modules.md b/docs/c-modules.md index 5e1631ef..d43b69a6 100644 --- a/docs/c-modules.md +++ b/docs/c-modules.md @@ -187,9 +187,11 @@ JSC_CCALL(vector_normalize, double y = js2number(js, argv[1]); double len = sqrt(x*x + y*y); if (len > 0) { - JSValue result = JS_NewObject(js); + JS_FRAME(js); + JS_LOCAL(result, JS_NewObject(js)); JS_SetPropertyStr(js, result, "x", number2js(js, x/len)); JS_SetPropertyStr(js, result, "y", number2js(js, y/len)); + JS_RestoreFrame(_js_ctx, _js_gc_frame, _js_local_frame); ret = result; } ) @@ -258,6 +260,81 @@ mypackage/ The module file (`rtree.c`) includes the library header and uses `cell.h` as usual. The support files are plain C — they don't need any cell macros. +## GC Safety + +ƿit uses a Cheney copying garbage collector. Any JS allocation — `JS_NewObject`, `JS_NewString`, `JS_SetPropertyStr`, etc. — can trigger GC, which **moves** heap objects to new addresses. C locals holding JSValue become stale after any allocating call. + +### When you need rooting + +If a function creates **one** heap object and returns it immediately, no rooting is needed: + +```c +JSC_CCALL(mymod_name, + ret = JS_NewString(js, "hello"); +) +``` + +If a function holds a heap object across further allocating calls, you must root it: + +```c +JSC_CCALL(vector_normalize, + double x = js2number(js, argv[0]); + double y = js2number(js, argv[1]); + double len = sqrt(x*x + y*y); + if (len > 0) { + JS_FRAME(js); + JS_LOCAL(result, JS_NewObject(js)); + // result is rooted — GC can update it through these calls: + JS_SetPropertyStr(js, result, "x", number2js(js, x/len)); + JS_SetPropertyStr(js, result, "y", number2js(js, y/len)); + JS_RestoreFrame(_js_ctx, _js_gc_frame, _js_local_frame); + ret = result; + } +) +``` + +### Macros + +| Macro | Purpose | +|-------|---------| +| `JS_FRAME(js)` | Save the GC and local frames. Required before any `JS_LOCAL`. | +| `JS_LOCAL(name, init)` | Declare and root a JSValue. GC updates it through its address. | +| `JS_RETURN(val)` | Restore the frame and return a value. | +| `JS_RETURN_NULL()` | Restore the frame and return `JS_NULL`. | +| `JS_RETURN_EX()` | Restore the frame and return `JS_EXCEPTION`. | +| `JS_RestoreFrame(...)` | Manual frame restore (for `JSC_CCALL` bodies that use `ret =`). | + +### Rules of thumb + +1. **One allocation, immediate return** — no rooting needed. +2. **Object + property sets** — root the object with `JS_LOCAL`. +3. **Array + loop** — root the array; if loop body creates objects, root the loop variable too with a manual `JSLocalRef`. +4. **`CELL_USE_INIT` bodies** — always use `JS_FRAME` / `JS_LOCAL` / `JS_RETURN`. + +### Migrating from gc_mark + +The old mark-and-sweep GC had a `gc_mark` callback in `JSClassDef` for C structs that held JSValue fields. This no longer exists. The copying GC needs to know the **address** of every pointer to update it when objects move. + +If your C struct holds a JSValue that must survive across GC points, root it for the duration it's alive: + +```c +typedef struct { + JSValue callback; + JSLocalRef callback_lr; +} MyWidget; + +// When storing: +widget->callback = value; +widget->callback_lr.ptr = &widget->callback; +JS_PushLocalRef(js, &widget->callback_lr); + +// When done (before freeing the struct): +// The local ref is cleaned up when the frame is restored, +// or manage it manually. +``` + +In practice, most C wrappers hold only opaque C pointers (like `SDL_Window*`) and never store JSValues in the struct — these need no migration. + ## Static Declarations Keep internal functions and variables `static`: diff --git a/source/cell.h b/source/cell.h index 537d9ed3..80bb7b56 100644 --- a/source/cell.h +++ b/source/cell.h @@ -100,7 +100,6 @@ TYPE##_free(rt,n);}\ static JSClassDef js_##TYPE##_class = {\ .class_name = #TYPE,\ .finalizer = js_##TYPE##_finalizer,\ - .gc_mark = js_##TYPE##_mark,\ };\ TYPE *js2##TYPE (JSContext *js, JSValue val) { \ if (JS_GetClassID(val) != js_##TYPE##_id) return NULL; \ @@ -120,7 +119,6 @@ TYPE##_free(rt,n);}\ static JSClassDef js_##TYPE##_class = {\ .class_name = #TYPE,\ .finalizer = js_##TYPE##_finalizer,\ - .gc_mark = js_##TYPE##_mark,\ };\ extern JSClassID js_##TYPE##_id;\ TYPE *js2##TYPE (JSContext *js, JSValue val) { \ @@ -231,9 +229,10 @@ JSValue CELL_USE_NAME(JSContext *js) { do { c ; } while(0); } #define CELL_USE_FUNCS(FUNCS) \ JSValue CELL_USE_NAME(JSContext *js) { \ - JSValue mod = JS_NewObject(js); \ + JS_FRAME(js); \ + JS_LOCAL(mod, JS_NewObject(js)); \ JS_SetPropertyFunctionList(js, mod, FUNCS, countof(FUNCS)); \ - return mod; } + JS_RETURN(mod); } #define CELL_PROGRAM_INIT(c) \ JSValue CELL_USE_NAME(JSContext *js) { do { c ; } while(0); } From 3c28dc2c30e368288c5158e3f0db982f66fb6955 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Tue, 17 Feb 2026 01:19:43 -0600 Subject: [PATCH 3/3] fix toml issue / isobject --- cell.toml | 2 +- docs/functions.md | 2 +- docs/language.md | 7 ++++--- package.cm | 12 ------------ source/runtime.c | 7 ++----- 5 files changed, 8 insertions(+), 22 deletions(-) diff --git a/cell.toml b/cell.toml index 9cf8d5a7..fd89a789 100644 --- a/cell.toml +++ b/cell.toml @@ -3,4 +3,4 @@ cell-steam = "/Users/johnalanbrook/work/cell-steam" cell-sdl3 = "/Users/johnalanbrook/work/cell-sdl3" [compilation] [compilation.playdate] -CFLAGS = "-DMINIZ_NO_TIME -DTARGET_EXTENSION -DTARGET_PLAYDATE -I$LOCAL/PlaydateSDK/C_API" \ No newline at end of file +CFLAGS = "-DMINIZ_NO_TIME -DTARGET_EXTENSION -DTARGET_PLAYDATE -I$LOCAL/PlaydateSDK/C_API" diff --git a/docs/functions.md b/docs/functions.md index 4d4bcf55..f7e92830 100644 --- a/docs/functions.md +++ b/docs/functions.md @@ -248,7 +248,7 @@ is_integer(42) // true is_logical(true) // true is_null(null) // true is_number(3.14) // true -is_object({}) // true +is_object({}) // true (records only) is_text("hello") // true ``` diff --git a/docs/language.md b/docs/language.md index 66755117..c51d0c03 100644 --- a/docs/language.md +++ b/docs/language.md @@ -512,12 +512,13 @@ var a = nil?(null) ? "yes" : "no" // "yes" is_number(42) // true is_text("hi") // true is_logical(true) // true -is_object({}) // true +is_object({}) // true (records only) is_array([]) // true is_function(function(){}) // true is_null(null) // true -is_object([]) // false (array is not object) -is_array({}) // false (object is not array) +is_object([]) // false (arrays are not records) +is_object("hello") // false (text is not a record) +is_array({}) // false (records are not arrays) ``` ### Truthiness diff --git a/package.cm b/package.cm index 5e74f7ce..a4c7e0fa 100644 --- a/package.cm +++ b/package.cm @@ -1,15 +1,11 @@ var package = {} var fd = use('fd') var toml = use('toml') -var json = use('json') var runtime = use('runtime') var link = use('link') var global_shop_path = runtime.shop_path -// Cache for loaded configs to avoid toml re-parsing corruption -var config_cache = {} - // Convert package name to a safe directory name // For absolute paths (local packages), replace / with _ // For remote packages, keep slashes as they use nested directories @@ -48,10 +44,6 @@ package.load_config = function(name) { var config_path = get_path(name) + '/cell.toml' - // Return cached config if available - if (config_cache[config_path]) - return config_cache[config_path] - if (!fd.is_file(config_path)) { print(`${config_path} does not exist`); disrupt } @@ -65,10 +57,6 @@ package.load_config = function(name) return {} } - // Deep copy to avoid toml module's shared state bug and cache it - result = json.decode(json.encode(result)) - config_cache[config_path] = result - return result } diff --git a/source/runtime.c b/source/runtime.c index bf0f5f40..7769efe7 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -10872,13 +10872,10 @@ static JSValue js_cell_is_number (JSContext *ctx, JSValue this_val, int argc, JS return JS_NewBool (ctx, JS_IsNumber (argv[0])); } -/* is_object(val) - true for non-array, non-null objects */ +/* is_object(val) - true for records */ static JSValue js_cell_is_object (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; - JSValue val = argv[0]; - if (!mist_is_gc_object (val)) return JS_FALSE; - if (JS_IsArray (val)) return JS_FALSE; - return JS_TRUE; + return JS_NewBool (ctx, mist_is_record (argv[0])); } /* is_stone(val) - check if value is immutable */