Compare commits
366 Commits
mcode2
...
async_http
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e45695399d | ||
|
|
52b55cb1ad | ||
|
|
fa9d2609b1 | ||
|
|
e38c2f07bf | ||
|
|
ecc1777b24 | ||
|
|
1cfd5b8133 | ||
|
|
9c1141f408 | ||
|
|
696cca530b | ||
|
|
c92a4087a6 | ||
|
|
01637c49b0 | ||
|
|
f9e660ebaa | ||
|
|
4f8fada57d | ||
|
|
adcaa92bea | ||
|
|
fc36707b39 | ||
|
|
7cb8ce7945 | ||
|
|
bb7997a751 | ||
|
|
327b990442 | ||
|
|
51c0a0b306 | ||
|
|
8ac82016dd | ||
|
|
d0bf757d91 | ||
|
|
c77f1f8639 | ||
|
|
2b877e6b0c | ||
|
|
3d4c0ec3d3 | ||
|
|
33d9013409 | ||
|
|
c2f57d1dae | ||
|
|
7bd17c6476 | ||
|
|
5ac1620b48 | ||
|
|
124c9536b4 | ||
|
|
940807c37a | ||
|
|
70f560550f | ||
|
|
060a494f47 | ||
|
|
6812d3edbc | ||
|
|
4da15d2a3e | ||
|
|
a34566a0c1 | ||
|
|
9f7d861932 | ||
|
|
c5536697ff | ||
|
|
d066ab03cd | ||
|
|
9c1cb43c7d | ||
|
|
99fb575c9c | ||
|
|
193991c532 | ||
|
|
76552c6854 | ||
|
|
f26b6e853d | ||
|
|
94c28f0e17 | ||
|
|
a18584afd3 | ||
|
|
3f6cfad7ef | ||
|
|
b03edb0d90 | ||
|
|
62440d3ed6 | ||
|
|
4edc4b7cc5 | ||
|
|
012b507415 | ||
|
|
ee6398ada9 | ||
|
|
173438e8bc | ||
|
|
7ac5ac63d2 | ||
|
|
a05f180356 | ||
|
|
d88692cd30 | ||
|
|
b0ac5de7e2 | ||
|
|
1d4fc11772 | ||
|
|
7372b80e07 | ||
|
|
d27047dd82 | ||
|
|
8e96379377 | ||
|
|
8f415fea80 | ||
|
|
cec0b99207 | ||
|
|
2d4645da9c | ||
|
|
017b63ba80 | ||
|
|
99fa86a09c | ||
|
|
6d6b53009f | ||
|
|
cc7fc6b667 | ||
|
|
bbeb757e40 | ||
|
|
2ac446f7cf | ||
|
|
517bd64275 | ||
|
|
7d0c96f328 | ||
|
|
f7e3c0803c | ||
|
|
e7ed6bd8b2 | ||
|
|
700b640cf1 | ||
|
|
071aa33153 | ||
|
|
d041c49972 | ||
|
|
74e0923629 | ||
|
|
eadad194be | ||
|
|
fea76ecac5 | ||
|
|
81c88f9439 | ||
|
|
20c2576fa7 | ||
|
|
ac707bc399 | ||
|
|
03b5fc1a5e | ||
|
|
5caa5d1288 | ||
|
|
3ebd98fc00 | ||
|
|
ede033f52e | ||
|
|
f4ad851a3f | ||
|
|
50a2d67c90 | ||
|
|
5ea0de9fbb | ||
|
|
34cb19c357 | ||
|
|
26f63bccee | ||
|
|
93aaaa43a1 | ||
|
|
1e606095a2 | ||
|
|
652e8a19f0 | ||
|
|
bcc9bdac83 | ||
|
|
f46784d884 | ||
|
|
fca1041e52 | ||
|
|
e73152bc36 | ||
|
|
6f932cd3c3 | ||
|
|
55fa250f12 | ||
|
|
374069ae99 | ||
|
|
3e4f3dff11 | ||
|
|
6cac591dc9 | ||
|
|
f6dab3c081 | ||
|
|
a82c13170f | ||
|
|
3e7b3e9994 | ||
|
|
68ae10bed0 | ||
|
|
cf3c2c9c5f | ||
|
|
bb8536f6c9 | ||
|
|
0e86d6f5a1 | ||
|
|
d8df467eae | ||
|
|
995d900e6e | ||
|
|
eeb2f038a1 | ||
|
|
ce2e11429f | ||
|
|
55fae8b5d0 | ||
|
|
c722b9d648 | ||
|
|
680b257a44 | ||
|
|
611d538e9f | ||
|
|
844ca0b8d5 | ||
|
|
ec404205ca | ||
|
|
9ebe6efe2b | ||
|
|
e455159b2d | ||
|
|
5af76bce9b | ||
|
|
5e413e8d81 | ||
|
|
8fe14f9a42 | ||
|
|
148cf12787 | ||
|
|
9c35e77f3f | ||
|
|
882a3ae8cb | ||
|
|
35d0890242 | ||
|
|
98515e9218 | ||
|
|
11fb213a74 | ||
|
|
4ac92c8a87 | ||
|
|
ed69d53573 | ||
|
|
e4588e43f2 | ||
|
|
2f41f58521 | ||
|
|
06866bcc0a | ||
|
|
f20fbedeea | ||
|
|
285395807b | ||
|
|
601a78b3c7 | ||
|
|
ebfc89e072 | ||
|
|
f0c2486a5c | ||
|
|
c5ad4f0a99 | ||
|
|
e6d05abd03 | ||
|
|
c0aff9e9bf | ||
|
|
8d449e6fc6 | ||
|
|
54e5be0773 | ||
|
|
38f368c6d6 | ||
|
|
06ad466b1a | ||
|
|
65fa37cc03 | ||
|
|
bab4d50b2a | ||
|
|
e7fec94e38 | ||
|
|
ab43ab0d2c | ||
|
|
a15844af58 | ||
|
|
85ef711229 | ||
|
|
ddfb0b1345 | ||
|
|
3f206d80dd | ||
|
|
3e0dc14318 | ||
|
|
19132c1517 | ||
|
|
e59bfe19f7 | ||
|
|
e004b2c472 | ||
|
|
27ca008f18 | ||
|
|
a05d0e2525 | ||
|
|
777474ab4f | ||
|
|
621da78de9 | ||
|
|
e2c26737f4 | ||
|
|
02eb58772c | ||
|
|
14a94aff12 | ||
|
|
6bc9dd53a7 | ||
|
|
f7499c4f60 | ||
|
|
fa5c0416fb | ||
|
|
dc70a15981 | ||
|
|
94fe47b472 | ||
|
|
34521e44f1 | ||
|
|
81561d426b | ||
|
|
7a4c72025f | ||
|
|
303f894a70 | ||
|
|
c33c35de87 | ||
|
|
417eec2419 | ||
|
|
c0cd6a61a6 | ||
|
|
42dc7243f3 | ||
|
|
469b7ac478 | ||
|
|
4872c62704 | ||
|
|
91b73f923a | ||
|
|
4868a50085 | ||
|
|
6f8cad9bb2 | ||
|
|
a1d1e721b6 | ||
|
|
dc7f933424 | ||
|
|
36f054d99d | ||
|
|
037fdbfd2c | ||
|
|
28f5a108d8 | ||
|
|
d8422ae69b | ||
|
|
187d7e9832 | ||
|
|
4aafb3c5e9 | ||
|
|
4b635228f9 | ||
|
|
42f7c270e1 | ||
|
|
bd7f9f34ec | ||
|
|
22c0b421d2 | ||
|
|
8be5936c10 | ||
|
|
bd53089578 | ||
|
|
76c482b84e | ||
|
|
b16fa75706 | ||
|
|
ad419797b4 | ||
|
|
5ee51198a7 | ||
|
|
2df45b2acb | ||
|
|
933c63caf8 | ||
|
|
dc422932d3 | ||
|
|
b25285f2e1 | ||
|
|
b3573dbf26 | ||
|
|
8a24a69120 | ||
|
|
56ac53637b | ||
|
|
5415726e33 | ||
|
|
56cb1fb4c6 | ||
|
|
278d685c8f | ||
|
|
51815b66d8 | ||
|
|
78051e24f3 | ||
|
|
c02fbbd9e0 | ||
|
|
ad26e71ad1 | ||
|
|
2e78e7e0b8 | ||
|
|
8f9eb0aaa9 | ||
|
|
0965aed0ef | ||
|
|
1b00fd1f0a | ||
|
|
3bf63780fd | ||
|
|
f7f26a1f00 | ||
|
|
4c9db198db | ||
|
|
eff3548c50 | ||
|
|
4fc48fd6f2 | ||
|
|
3f6388ff4e | ||
|
|
2be2b15a61 | ||
|
|
12b6c3544e | ||
|
|
570f0cdc83 | ||
|
|
cc82fcb7d9 | ||
|
|
5ef3381fff | ||
|
|
5fcf765c8d | ||
|
|
027c1549fc | ||
|
|
8c408a4b81 | ||
|
|
2d054fcf21 | ||
|
|
857f099a68 | ||
|
|
e0b6c69bfe | ||
|
|
2cef766b0a | ||
|
|
a3ecb0ad05 | ||
|
|
2a38292ff7 | ||
|
|
9e42a28d55 | ||
|
|
4d4d50a905 | ||
|
|
08515389d2 | ||
|
|
fbdfbc1200 | ||
|
|
d975214ba6 | ||
|
|
3c28dc2c30 | ||
|
|
2633fb986f | ||
|
|
400c58e5f2 | ||
|
|
bd4714a732 | ||
|
|
0ac575db85 | ||
|
|
41f373981d | ||
|
|
c9dad91ea1 | ||
|
|
63955e45ff | ||
|
|
4b7cde9400 | ||
|
|
8a19cffe9f | ||
|
|
6315574a45 | ||
|
|
2051677679 | ||
|
|
d398ab8db0 | ||
|
|
e7b599e3ac | ||
|
|
1f3e53587d | ||
|
|
dce0b5cc89 | ||
|
|
ce387d18d5 | ||
|
|
c02945e236 | ||
|
|
8e198d9822 | ||
|
|
17e35f023f | ||
|
|
5a7169654a | ||
|
|
a1ee7dd458 | ||
|
|
9dbe699033 | ||
|
|
f809cb05f0 | ||
|
|
788ea98651 | ||
|
|
433ce8a86e | ||
|
|
cd6e357b6e | ||
|
|
f4f56ed470 | ||
|
|
ff61ab1f50 | ||
|
|
46c345d34e | ||
|
|
dc440587ff | ||
|
|
8f92870141 | ||
|
|
7fc4a205f6 | ||
|
|
23b201bdd7 | ||
|
|
913ec9afb1 | ||
|
|
56de0ce803 | ||
|
|
96bbb9e4c8 | ||
|
|
ebd624b772 | ||
|
|
7de20b39da | ||
|
|
ee646db394 | ||
|
|
ff80e0d30d | ||
|
|
d9f41db891 | ||
|
|
860632e0fa | ||
|
|
dcc9659e6b | ||
|
|
2f7f2233b8 | ||
|
|
eee06009b9 | ||
|
|
a765872017 | ||
|
|
a93218e1ff | ||
|
|
f2c4fa2f2b | ||
|
|
5fe05c60d3 | ||
|
|
e75596ce30 | ||
|
|
86609c27f8 | ||
|
|
356c51bde3 | ||
|
|
89421e11a4 | ||
|
|
e5fc04fecd | ||
|
|
8ec56e85fa | ||
|
|
f49ca530bb | ||
|
|
83263379bd | ||
|
|
e80e615634 | ||
|
|
c1430fd59b | ||
|
|
db73eb4eeb | ||
|
|
f2556c5622 | ||
|
|
291304f75d | ||
|
|
3795533554 | ||
|
|
d26a96bc62 | ||
|
|
0acaabd5fa | ||
|
|
1ba060668e | ||
|
|
77fa058135 | ||
|
|
f7e2ff13b5 | ||
|
|
36fd0a35f9 | ||
|
|
77c02bf9bf | ||
|
|
f251691146 | ||
|
|
e9ea6ec299 | ||
|
|
bf5fdbc688 | ||
|
|
b960d03eeb | ||
|
|
b4d42fb83d | ||
|
|
0a680a0cd3 | ||
|
|
9f0fd84f4f | ||
|
|
cb9d6e0c0e | ||
|
|
4f18a0b524 | ||
|
|
f296a0c10d | ||
|
|
1df6553577 | ||
|
|
30a9cfee79 | ||
|
|
6fff96d9d9 | ||
|
|
4a50d0587d | ||
|
|
e346348eb5 | ||
|
|
ff560973f3 | ||
|
|
de4b3079d4 | ||
|
|
29227e655b | ||
|
|
588e88373e | ||
|
|
9aca365771 | ||
|
|
c56d4d5c3c | ||
|
|
c1e101b24f | ||
|
|
9f0dfbc6a2 | ||
|
|
5c9403a43b | ||
|
|
89e34ba71d | ||
|
|
73bfa8d7b1 | ||
|
|
4aedb8b0c5 | ||
|
|
ec072f3b63 | ||
|
|
65755d9c0c | ||
|
|
19524b3a53 | ||
|
|
f901332c5b | ||
|
|
add136c140 | ||
|
|
c1a99dfd4c | ||
|
|
7b46c6e947 | ||
|
|
1efb0b1bc9 | ||
|
|
0ba2783b48 | ||
|
|
6de542f0d0 | ||
|
|
6ba4727119 | ||
|
|
900db912a5 | ||
|
|
b771b2b5d8 | ||
|
|
68fb440502 | ||
|
|
e7a2f16004 | ||
|
|
3a8a17ab60 | ||
|
|
8a84be65e1 | ||
|
|
c1910ee1db | ||
|
|
7036cdf2d1 | ||
|
|
fbeec17ce5 | ||
|
|
2c55ae8cb2 | ||
|
|
259bc139fc | ||
|
|
a252412eca |
38
.gitea/workflows/ci.yml
Normal file
38
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,38 @@
|
||||
name: CI
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: gitea.pockle.world/john/cell-ci:latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build
|
||||
run: |
|
||||
meson setup build -Dbuildtype=release
|
||||
meson compile -C build
|
||||
- name: Prepare tests
|
||||
run: |
|
||||
cp build/libcell_runtime.so .
|
||||
cp build/cell .
|
||||
mkdir -p ~/.cell/packages
|
||||
ln -s "$PWD" ~/.cell/packages/core
|
||||
- name: VM tests
|
||||
run: ./cell --dev vm_suite
|
||||
- name: Language tests
|
||||
run: ./cell --dev test suite
|
||||
- name: Fuzz
|
||||
run: ./cell --dev fuzz
|
||||
|
||||
build-windows:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: gitea.pockle.world/john/cell-ci:latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build
|
||||
run: |
|
||||
meson setup build_win --cross-file cross/mingw64.ini -Dbuildtype=release
|
||||
meson compile -C build_win
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,7 +1,12 @@
|
||||
.git/
|
||||
.obj/
|
||||
website/public/
|
||||
website/site/
|
||||
website/.hugo_build.lock
|
||||
.cache
|
||||
.cell
|
||||
cell
|
||||
libcell_runtime*
|
||||
bin/
|
||||
build/
|
||||
*.zip
|
||||
|
||||
122
CLAUDE.md
122
CLAUDE.md
@@ -2,8 +2,8 @@
|
||||
|
||||
## Building
|
||||
|
||||
Recompile after changes: `make`
|
||||
Bootstrap from scratch (first time): `make bootstrap`
|
||||
Build (or rebuild after changes): `make`
|
||||
Install to system: `make install`
|
||||
Run `cell --help` to see all CLI flags.
|
||||
|
||||
## Code Style
|
||||
@@ -20,8 +20,9 @@ All code uses 2 spaces for indentation. K&R style for C and Javascript.
|
||||
- `==` and `!=` are strict (no `===` or `!==`)
|
||||
- No `undefined` — only `null`
|
||||
- No classes — only objects and prototypes (`meme()`, `proto()`, `isa()`)
|
||||
- No `switch`/`case` — use record dispatch (a record keyed by case, values are functions or results) instead of if/else chains
|
||||
- No `for...in`, `for...of`, spread (`...`), rest params, or default params
|
||||
- No named function declarations — use `var fn = function() {}` or arrow functions
|
||||
- Functions have a maximum of 4 parameters — use a record for more
|
||||
- Variables must be declared at function body level only (not in if/while/for/blocks)
|
||||
- All variables must be initialized at declaration (`var x` alone is an error; use `var x = null`)
|
||||
- No `try`/`catch`/`throw` — use `disrupt`/`disruption`
|
||||
@@ -56,7 +57,7 @@ The creator functions are **polymorphic** — behavior depends on argument types
|
||||
- `record(record, another)` — merge
|
||||
- `record(array_of_keys)` — create record from keys
|
||||
|
||||
Other key intrinsics: `length()`, `stone()`, `is_stone()`, `print()`, `filter()`, `find()`, `reduce()`, `sort()`, `reverse()`, `some()`, `every()`, `starts_with()`, `ends_with()`, `meme()`, `proto()`, `isa()`, `splat()`, `apply()`, `extract()`, `replace()`, `search()`, `format()`, `lower()`, `upper()`, `trim()`
|
||||
Other key intrinsics: `object()`, `length()`, `stone()`, `is_stone()`, `filter()`, `find()`, `reduce()`, `sort()`, `reverse()`, `some()`, `every()`, `starts_with()`, `ends_with()`, `meme()`, `proto()`, `isa()`, `splat()`, `apply()`, `extract()`, `replace()`, `search()`, `format()`, `lower()`, `upper()`, `trim()`
|
||||
|
||||
Sensory functions: `is_array()`, `is_text()`, `is_number()`, `is_object()`, `is_function()`, `is_null()`, `is_logical()`, `is_integer()`, `is_stone()`, etc.
|
||||
|
||||
@@ -103,6 +104,70 @@ var v = a[] // pop: v is 3, a is [1, 2]
|
||||
- Most files don't have headers; files in a package are not shared between packages
|
||||
- No undefined in C API: use `JS_IsNull` and `JS_NULL` only
|
||||
- A C file with correct macros (`CELL_USE_FUNCS` etc) is loaded as a module by its name (e.g., `png.c` in a package → `use('<package>/png')`)
|
||||
- C symbol naming: `js_<pkg>_<file>_use` (e.g., `js_core_math_radians_use` for `core/math/radians`)
|
||||
- Core is the `core` package — its symbols follow the same `js_core_<name>_use` pattern as all other packages
|
||||
- Package directories should contain only source files (no `.mach`/`.mcode` alongside source)
|
||||
- Build cache files in `build/` are bare hashes (no extensions)
|
||||
|
||||
### MANDATORY: GC Rooting for C Functions
|
||||
|
||||
This project uses a **copying garbage collector**. ANY JS allocation (`JS_NewObject`, `JS_NewString`, `JS_NewArray`, `JS_NewInt32`, `JS_SetPropertyStr`, `js_new_blob_stoned_copy`, etc.) can trigger GC, which **invalidates all unrooted JSValue locals**. This is not theoretical — it causes real crashes.
|
||||
|
||||
**Before writing or modifying ANY C function**, apply this checklist:
|
||||
|
||||
1. Count the number of `JS_New*`, `JS_SetProperty*`, and `js_new_blob*` calls in the function
|
||||
2. If there are 2 or more, the function MUST use `JS_FRAME`/`JS_ROOT`/`JS_RETURN`
|
||||
3. Every JSValue that is held across an allocating call must be rooted
|
||||
|
||||
**Pattern — object with properties:**
|
||||
```c
|
||||
JS_FRAME(js);
|
||||
JS_ROOT(obj, JS_NewObject(js));
|
||||
JS_SetPropertyStr(js, obj.val, "x", JS_NewInt32(js, 42));
|
||||
JSValue name = JS_NewString(js, "hello");
|
||||
JS_SetPropertyStr(js, obj.val, "name", name);
|
||||
JS_RETURN(obj.val);
|
||||
```
|
||||
|
||||
**Pattern — array with loop (declare root BEFORE the loop):**
|
||||
```c
|
||||
JS_FRAME(js);
|
||||
JS_ROOT(arr, JS_NewArray(js));
|
||||
JSGCRef item = { .val = JS_NULL, .prev = NULL };
|
||||
JS_PushGCRef(js, &item);
|
||||
for (int i = 0; i < count; i++) {
|
||||
item.val = JS_NewObject(js);
|
||||
JS_SetPropertyStr(js, item.val, "v", JS_NewInt32(js, i));
|
||||
JS_SetPropertyNumber(js, arr.val, i, item.val);
|
||||
}
|
||||
JS_RETURN(arr.val);
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Access rooted values via `.val` (e.g., `obj.val`, not `obj`)
|
||||
- NEVER put `JS_ROOT` inside a loop — it pushes the same stack address twice, corrupting the GC chain
|
||||
- Error returns before `JS_FRAME` use plain `return`
|
||||
- Error returns after `JS_FRAME` must use `JS_RETURN_EX()` or `JS_RETURN_NULL()`
|
||||
|
||||
**CRITICAL — C argument evaluation order bug:**
|
||||
|
||||
Allocating functions (`JS_NewString`, `JS_NewFloat64`, `js_new_blob_stoned_copy`, etc.) used as arguments to `JS_SetPropertyStr` can crash because C evaluates arguments in unspecified order. The compiler may read `obj.val` BEFORE the allocating call, then GC moves the object, leaving a stale pointer.
|
||||
|
||||
```c
|
||||
// UNSAFE — intermittent crash:
|
||||
JS_SetPropertyStr(js, obj.val, "format", JS_NewString(js, "rgba32"));
|
||||
JS_SetPropertyStr(js, obj.val, "pixels", js_new_blob_stoned_copy(js, data, len));
|
||||
|
||||
// SAFE — separate the allocation:
|
||||
JSValue fmt = JS_NewString(js, "rgba32");
|
||||
JS_SetPropertyStr(js, obj.val, "format", fmt);
|
||||
JSValue pixels = js_new_blob_stoned_copy(js, data, len);
|
||||
JS_SetPropertyStr(js, obj.val, "pixels", pixels);
|
||||
```
|
||||
|
||||
`JS_NewInt32`, `JS_NewUint32`, and `JS_NewBool` do NOT allocate and are safe inline.
|
||||
|
||||
See `docs/c-modules.md` for the full GC safety reference.
|
||||
|
||||
## Project Layout
|
||||
|
||||
@@ -113,6 +178,55 @@ var v = a[] // pop: v is 3, a is [1, 2]
|
||||
- `packages/` — core packages
|
||||
- `Makefile` — build system (`make` to rebuild, `make bootstrap` for first build)
|
||||
|
||||
## Package Management (Shop CLI)
|
||||
|
||||
**Two shops:** `cell <cmd>` uses the global shop at `~/.cell/packages/`. `cell --dev <cmd>` uses the local shop at `.cell/packages/`. Linked packages (via `cell link`) are symlinked into the shop — edit the source directory directly.
|
||||
|
||||
```
|
||||
cell add <path> # add a package (local path or remote)
|
||||
cell remove <path> # remove a package (cleans lock, symlink, dylibs)
|
||||
cell build <path> # build C modules for a package
|
||||
cell build <path> --force # force rebuild (ignore stat cache)
|
||||
cell test package <path> # run tests for a package
|
||||
cell list # list installed packages
|
||||
cell link # list linked packages
|
||||
```
|
||||
|
||||
The build step compiles C files to content-addressed dylibs in `~/.cell/build/<hash>` and writes a per-package manifest so the runtime can find them. C files in `src/` are support files linked into module dylibs, not standalone modules.
|
||||
|
||||
## Debugging Compiler Issues
|
||||
|
||||
When investigating bugs in compiled output (wrong values, missing operations, incorrect comparisons), **start from the optimizer down, not the VM up**. The compiler inspection tools will usually identify the problem faster than adding C-level tracing:
|
||||
|
||||
```
|
||||
./cell --dev streamline --types <file> # show inferred slot types — look for wrong types
|
||||
./cell --dev ir_report --events <file> # show every optimization applied and why
|
||||
./cell --dev ir_report --types <file> # show type inference results per function
|
||||
./cell --dev mcode --pretty <file> # show raw IR before optimization
|
||||
./cell --dev streamline --ir <file> # show human-readable optimized IR
|
||||
```
|
||||
|
||||
**Triage order:**
|
||||
1. `streamline --types` — are slot types correct? Wrong type inference causes wrong optimizations.
|
||||
2. `ir_report --events` — are type checks being incorrectly eliminated? Look for `known_type_eliminates_guard` on slots that shouldn't have known types.
|
||||
3. `mcode --pretty` — is the raw IR correct before optimization? If so, the bug is in streamline.
|
||||
4. Only dig into `source/mach.c` if the IR looks correct at all levels.
|
||||
|
||||
See `docs/compiler-tools.md` for the full tool reference and `docs/spec/streamline.md` for pass details.
|
||||
|
||||
## Testing
|
||||
|
||||
After any C runtime changes, run all three test suites before considering the work done:
|
||||
|
||||
```
|
||||
make # rebuild
|
||||
./cell --dev vm_suite # VM-level tests (641 tests)
|
||||
./cell --dev test suite # language-level tests (493 tests)
|
||||
./cell --dev fuzz # fuzzer (100 iterations)
|
||||
```
|
||||
|
||||
All three must pass with 0 failures.
|
||||
|
||||
## Documentation
|
||||
|
||||
The `docs/` folder is the single source of truth. The website at `website/` mounts it via Hugo. Key files:
|
||||
|
||||
123
Makefile
123
Makefile
@@ -1,98 +1,47 @@
|
||||
# Development build: creates libcell_runtime.dylib + thin main wrapper
|
||||
# This is the default target for working on cell itself
|
||||
#
|
||||
# If cell doesn't exist yet, use 'make bootstrap' first (requires meson)
|
||||
# or manually build with meson once.
|
||||
#
|
||||
# The cell shop is at ~/.cell and core scripts are installed to ~/.cell/core
|
||||
#
|
||||
# See BUILDING.md for details on the bootstrap process and .mach files.
|
||||
BUILD = build
|
||||
BUILD_DBG = build_debug
|
||||
INSTALL_BIN = /opt/homebrew/bin
|
||||
INSTALL_LIB = /opt/homebrew/lib
|
||||
INSTALL_INC = /opt/homebrew/include
|
||||
CELL_SHOP = $(HOME)/.cell
|
||||
|
||||
CELL_SHOP = $(HOME)/.cell
|
||||
CELL_CORE_PACKAGE = $(CELL_SHOP)/packages/core
|
||||
all: $(BUILD)/build.ninja
|
||||
meson compile -C $(BUILD)
|
||||
cp $(BUILD)/libcell_runtime.dylib .
|
||||
cp $(BUILD)/cell .
|
||||
|
||||
# .cm sources that compile to .mach bytecode
|
||||
MACH_SOURCES = tokenize.cm parse.cm fold.cm mcode.cm \
|
||||
internal/bootstrap.cm internal/engine.cm
|
||||
$(BUILD)/build.ninja:
|
||||
meson setup $(BUILD) -Dbuildtype=release
|
||||
|
||||
maker: install
|
||||
debug: $(BUILD_DBG)/build.ninja
|
||||
meson compile -C $(BUILD_DBG)
|
||||
cp $(BUILD_DBG)/libcell_runtime.dylib .
|
||||
cp $(BUILD_DBG)/cell .
|
||||
|
||||
makecell:
|
||||
cell pack core -o cell
|
||||
cp cell /opt/homebrew/bin/
|
||||
$(BUILD_DBG)/build.ninja:
|
||||
meson setup $(BUILD_DBG) -Dbuildtype=debug -Db_sanitize=address
|
||||
|
||||
# Install core: symlink this directory to ~/.cell/core
|
||||
install: bootstrap .mach.stamp $(CELL_SHOP)
|
||||
@echo "Linking cell core to $(CELL_CORE_PACKAGE)"
|
||||
rm -rf $(CELL_CORE_PACKAGE)
|
||||
ln -s $(PWD) $(CELL_CORE_PACKAGE)
|
||||
cp cell /opt/homebrew/bin/
|
||||
cp libcell_runtime.dylib /opt/homebrew/lib/
|
||||
@echo "Core installed."
|
||||
install: all $(CELL_SHOP)
|
||||
cp cell $(INSTALL_BIN)/cell
|
||||
cp libcell_runtime.dylib $(INSTALL_LIB)/
|
||||
cp source/cell.h $(INSTALL_INC)/
|
||||
rm -rf $(CELL_SHOP)/packages/core
|
||||
ln -s $(CURDIR) $(CELL_SHOP)/packages/core
|
||||
@echo "Installed cell to $(INSTALL_BIN) and $(INSTALL_LIB)"
|
||||
|
||||
cell: libcell_runtime.dylib cell_main
|
||||
cp cell_main cell
|
||||
chmod +x cell
|
||||
cp cell /opt/homebrew/bin/cell
|
||||
cp libcell_runtime.dylib /opt/homebrew/lib/
|
||||
install_debug: debug $(CELL_SHOP)
|
||||
cp cell $(INSTALL_BIN)/cell
|
||||
cp libcell_runtime.dylib $(INSTALL_LIB)/
|
||||
cp source/cell.h $(INSTALL_INC)/
|
||||
rm -rf $(CELL_SHOP)/packages/core
|
||||
ln -s $(CURDIR) $(CELL_SHOP)/packages/core
|
||||
@echo "Installed cell (debug+asan) to $(INSTALL_BIN) and $(INSTALL_LIB)"
|
||||
|
||||
# Build the shared runtime library (everything except main.c)
|
||||
# Uses existing cell to run build -d
|
||||
libcell_runtime.dylib: $(CELL_SHOP)/build/dynamic
|
||||
cell build -d
|
||||
cp $(CELL_SHOP)/build/dynamic/libcell_runtime.dylib .
|
||||
|
||||
# Build the thin main wrapper that links to libcell_runtime
|
||||
cell_main: source/main.c libcell_runtime.dylib
|
||||
cc -o cell_main source/main.c -L. -lcell_runtime -Wl,-rpath,@loader_path -Wl,-rpath,/opt/homebrew/lib
|
||||
|
||||
# Regenerate .mach bytecode when any .cm source changes
|
||||
.mach.stamp: $(MACH_SOURCES)
|
||||
./cell --core . regen.cm
|
||||
@touch $@
|
||||
|
||||
# Force-regenerate all .mach bytecode files
|
||||
regen:
|
||||
./cell --core . regen.cm
|
||||
@touch .mach.stamp
|
||||
|
||||
# Create the cell shop directories
|
||||
$(CELL_SHOP):
|
||||
mkdir -p $(CELL_SHOP)
|
||||
mkdir -p $(CELL_SHOP)/packages
|
||||
mkdir -p $(CELL_SHOP)/cache
|
||||
mkdir -p $(CELL_SHOP)/build
|
||||
mkdir -p $(CELL_SHOP)/packages $(CELL_SHOP)/cache $(CELL_SHOP)/build
|
||||
|
||||
$(CELL_CORE):
|
||||
ln -s $(PWD) $(CELL_CORE)
|
||||
|
||||
# Static build: creates a fully static cell binary (for distribution)
|
||||
static:
|
||||
cell build
|
||||
cp $(CELL_SHOP)/build/static/cell .
|
||||
|
||||
# Bootstrap: build cell from scratch using meson (only needed once)
|
||||
# Also installs core scripts to ~/.cell/core
|
||||
bootstrap:
|
||||
meson setup build_bootstrap -Dbuildtype=debugoptimized
|
||||
meson compile -C build_bootstrap
|
||||
cp build_bootstrap/cell .
|
||||
cp build_bootstrap/libcell_runtime.dylib .
|
||||
@echo "Bootstrap complete. Cell shop initialized at $(CELL_SHOP)"
|
||||
@echo "Now run 'make' to rebuild with cell itself."
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
rm -rf $(CELL_SHOP)/build build_bootstrap
|
||||
rm -f cell cell_main libcell_runtime.dylib .mach.stamp
|
||||
rm -rf $(BUILD) $(BUILD_DBG)
|
||||
rm -f cell libcell_runtime.dylib
|
||||
|
||||
# Ensure dynamic build directory exists
|
||||
$(CELL_SHOP)/build/dynamic: $(CELL_SHOP)
|
||||
mkdir -p $(CELL_SHOP)/build/dynamic
|
||||
|
||||
# Legacy meson target
|
||||
meson:
|
||||
meson setup build_dbg -Dbuildtype=debugoptimized
|
||||
meson install -C build_dbg
|
||||
|
||||
.PHONY: cell static bootstrap clean meson install regen
|
||||
.PHONY: all install debug install_debug clean
|
||||
|
||||
192
add.ce
192
add.ce
@@ -3,101 +3,133 @@
|
||||
// Usage:
|
||||
// cell add <locator> Add a dependency using default alias
|
||||
// cell add <locator> <alias> Add a dependency with custom alias
|
||||
// cell add -r <directory> Recursively find and add all packages in directory
|
||||
//
|
||||
// This adds the dependency to cell.toml and installs it to the shop.
|
||||
|
||||
var shop = use('internal/shop')
|
||||
var pkg = use('package')
|
||||
var build = use('build')
|
||||
var fd = use('fd')
|
||||
|
||||
var locator = null
|
||||
var alias = null
|
||||
var recursive = false
|
||||
var cwd = fd.realpath('.')
|
||||
var parts = null
|
||||
var locators = null
|
||||
var added = 0
|
||||
var failed = 0
|
||||
var _add_dep = null
|
||||
var _install = null
|
||||
var i = 0
|
||||
|
||||
array(args, function(arg) {
|
||||
if (arg == '--help' || arg == '-h') {
|
||||
log.console("Usage: cell add <locator> [alias]")
|
||||
log.console("")
|
||||
log.console("Add a dependency to the current package.")
|
||||
log.console("")
|
||||
log.console("Examples:")
|
||||
log.console(" cell add gitea.pockle.world/john/prosperon")
|
||||
log.console(" cell add gitea.pockle.world/john/cell-image image")
|
||||
log.console(" cell add ../local-package")
|
||||
$stop()
|
||||
} else if (!starts_with(arg, '-')) {
|
||||
if (!locator) {
|
||||
locator = arg
|
||||
} else if (!alias) {
|
||||
alias = arg
|
||||
var run = function() {
|
||||
for (i = 0; i < length(args); i++) {
|
||||
if (args[i] == '--help' || args[i] == '-h') {
|
||||
log.console("Usage: cell add <locator> [alias]")
|
||||
log.console("")
|
||||
log.console("Add a dependency to the current package.")
|
||||
log.console("")
|
||||
log.console("Examples:")
|
||||
log.console(" cell add gitea.pockle.world/john/prosperon")
|
||||
log.console(" cell add gitea.pockle.world/john/cell-image image")
|
||||
log.console(" cell add ../local-package")
|
||||
log.console(" cell add -r ../packages")
|
||||
return
|
||||
} else if (args[i] == '-r') {
|
||||
recursive = true
|
||||
} else if (!starts_with(args[i], '-')) {
|
||||
if (!locator) {
|
||||
locator = args[i]
|
||||
} else if (!alias) {
|
||||
alias = args[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!locator) {
|
||||
log.console("Usage: cell add <locator> [alias]")
|
||||
$stop()
|
||||
}
|
||||
|
||||
// Resolve relative paths to absolute paths
|
||||
if (locator == '.' || starts_with(locator, './') || starts_with(locator, '../') || fd.is_dir(locator)) {
|
||||
var resolved = fd.realpath(locator)
|
||||
if (resolved) {
|
||||
locator = resolved
|
||||
}
|
||||
}
|
||||
|
||||
// Generate default alias from locator
|
||||
if (!alias) {
|
||||
// Use the last component of the locator as alias
|
||||
var parts = array(locator, '/')
|
||||
alias = parts[length(parts) - 1]
|
||||
// Remove any version suffix
|
||||
if (search(alias, '@') != null) {
|
||||
alias = array(alias, '@')[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Check we're in a package directory
|
||||
var cwd = fd.realpath('.')
|
||||
if (!fd.is_file(cwd + '/cell.toml')) {
|
||||
log.error("Not in a package directory (no cell.toml found)")
|
||||
$stop()
|
||||
}
|
||||
|
||||
log.console("Adding " + locator + " as '" + alias + "'...")
|
||||
|
||||
// Add to local project's cell.toml
|
||||
try {
|
||||
pkg.add_dependency(null, locator, alias)
|
||||
log.console(" Added to cell.toml")
|
||||
} catch (e) {
|
||||
log.error("Failed to update cell.toml: " + e)
|
||||
$stop()
|
||||
}
|
||||
|
||||
// Install to shop
|
||||
try {
|
||||
shop.get(locator)
|
||||
shop.extract(locator)
|
||||
|
||||
// Build scripts
|
||||
shop.build_package_scripts(locator)
|
||||
|
||||
// Build C code if any
|
||||
try {
|
||||
var target = build.detect_host_target()
|
||||
build.build_dynamic(locator, target, 'release')
|
||||
} catch (e) {
|
||||
// Not all packages have C code
|
||||
if (!locator && !recursive) {
|
||||
log.console("Usage: cell add <locator> [alias]")
|
||||
return
|
||||
}
|
||||
|
||||
log.console(" Installed to shop")
|
||||
} catch (e) {
|
||||
log.error("Failed to install: " + e)
|
||||
$stop()
|
||||
}
|
||||
if (locator)
|
||||
locator = shop.resolve_locator(locator)
|
||||
|
||||
log.console("Added " + alias + " (" + locator + ")")
|
||||
// Generate default alias from locator
|
||||
if (!alias && locator) {
|
||||
parts = array(locator, '/')
|
||||
alias = parts[length(parts) - 1]
|
||||
if (search(alias, '@') != null)
|
||||
alias = array(alias, '@')[0]
|
||||
}
|
||||
|
||||
// Check we're in a package directory
|
||||
if (!fd.is_file(cwd + '/cell.toml')) {
|
||||
log.error("Not in a package directory (no cell.toml found)")
|
||||
return
|
||||
}
|
||||
|
||||
// Recursive mode
|
||||
if (recursive) {
|
||||
if (!locator) locator = '.'
|
||||
locator = shop.resolve_locator(locator)
|
||||
if (!fd.is_dir(locator)) {
|
||||
log.error(`${locator} is not a directory`)
|
||||
return
|
||||
}
|
||||
locators = filter(pkg.find_packages(locator), function(p) {
|
||||
return p != cwd
|
||||
})
|
||||
if (length(locators) == 0) {
|
||||
log.console("No packages found in " + locator)
|
||||
return
|
||||
}
|
||||
log.console(`Found ${text(length(locators))} package(s) in ${locator}`)
|
||||
|
||||
added = 0
|
||||
failed = 0
|
||||
arrfor(locators, function(loc) {
|
||||
var loc_parts = array(loc, '/')
|
||||
var loc_alias = loc_parts[length(loc_parts) - 1]
|
||||
log.console(" Adding " + loc + " as '" + loc_alias + "'...")
|
||||
var _add = function() {
|
||||
pkg.add_dependency(null, loc, loc_alias)
|
||||
shop.sync(loc)
|
||||
added = added + 1
|
||||
} disruption {
|
||||
log.console(` Warning: Failed to add ${loc}`)
|
||||
failed = failed + 1
|
||||
}
|
||||
_add()
|
||||
})
|
||||
|
||||
log.console("Added " + text(added) + " package(s)." + (failed > 0 ? " Failed: " + text(failed) + "." : ""))
|
||||
return
|
||||
}
|
||||
|
||||
// Single package add
|
||||
log.console("Adding " + locator + " as '" + alias + "'...")
|
||||
|
||||
_add_dep = function() {
|
||||
pkg.add_dependency(null, locator, alias)
|
||||
log.console(" Added to cell.toml")
|
||||
} disruption {
|
||||
log.error("Failed to update cell.toml")
|
||||
return
|
||||
}
|
||||
_add_dep()
|
||||
|
||||
_install = function() {
|
||||
shop.sync_with_deps(locator)
|
||||
log.console(" Installed to shop")
|
||||
} disruption {
|
||||
log.error("Failed to install")
|
||||
return
|
||||
}
|
||||
_install()
|
||||
|
||||
log.console("Added " + alias + " (" + locator + ")")
|
||||
}
|
||||
run()
|
||||
|
||||
$stop()
|
||||
|
||||
144
analyze.cm
Normal file
144
analyze.cm
Normal file
@@ -0,0 +1,144 @@
|
||||
// analyze.cm — Static analysis over index data.
|
||||
//
|
||||
// All functions take an index object (from index.cm) and return structured results.
|
||||
// Does not depend on streamline — operates purely on source-semantic data.
|
||||
|
||||
var analyze = {}
|
||||
|
||||
// Find all references to a name, with optional scope filter.
|
||||
// scope: "top" (enclosing == null), "fn" (enclosing != null), null (all)
|
||||
analyze.find_refs = function(idx, name, scope) {
|
||||
var hits = []
|
||||
var i = 0
|
||||
var ref = null
|
||||
while (i < length(idx.references)) {
|
||||
ref = idx.references[i]
|
||||
if (ref.name == name) {
|
||||
if (scope == null) {
|
||||
hits[] = ref
|
||||
} else if (scope == "top" && ref.enclosing == null) {
|
||||
hits[] = ref
|
||||
} else if (scope == "fn" && ref.enclosing != null) {
|
||||
hits[] = ref
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return hits
|
||||
}
|
||||
|
||||
// Find all <name>.<property> usage patterns (channel analysis).
|
||||
// Only counts unshadowed uses (name not declared as local var in scope).
|
||||
analyze.channels = function(idx, name) {
|
||||
var channels = {}
|
||||
var summary = {}
|
||||
var i = 0
|
||||
var cs = null
|
||||
var callee = null
|
||||
var prop = null
|
||||
var prefix_dot = name + "."
|
||||
while (i < length(idx.call_sites)) {
|
||||
cs = idx.call_sites[i]
|
||||
callee = cs.callee
|
||||
if (callee != null && starts_with(callee, prefix_dot)) {
|
||||
prop = text(callee, length(prefix_dot), length(callee))
|
||||
if (channels[prop] == null) {
|
||||
channels[prop] = []
|
||||
}
|
||||
channels[prop][] = {span: cs.span}
|
||||
if (summary[prop] == null) {
|
||||
summary[prop] = 0
|
||||
}
|
||||
summary[prop] = summary[prop] + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return {channels: channels, summary: summary}
|
||||
}
|
||||
|
||||
// Find declarations by name, with optional kind filter.
|
||||
// kind: "var", "def", "fn", "param", or null (any)
|
||||
analyze.find_decls = function(idx, name, kind) {
|
||||
var hits = []
|
||||
var i = 0
|
||||
var sym = null
|
||||
while (i < length(idx.symbols)) {
|
||||
sym = idx.symbols[i]
|
||||
if (sym.name == name) {
|
||||
if (kind == null || sym.kind == kind) {
|
||||
hits[] = sym
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return hits
|
||||
}
|
||||
|
||||
// Find intrinsic usage by name.
|
||||
analyze.find_intrinsic = function(idx, name) {
|
||||
var hits = []
|
||||
var i = 0
|
||||
var ref = null
|
||||
if (idx.intrinsic_refs == null) return hits
|
||||
while (i < length(idx.intrinsic_refs)) {
|
||||
ref = idx.intrinsic_refs[i]
|
||||
if (ref.name == name) {
|
||||
hits[] = ref
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return hits
|
||||
}
|
||||
|
||||
// Call sites with >4 args — always a compile error (max arity is 4).
|
||||
analyze.excess_args = function(idx) {
|
||||
var hits = []
|
||||
var i = 0
|
||||
var cs = null
|
||||
while (i < length(idx.call_sites)) {
|
||||
cs = idx.call_sites[i]
|
||||
if (cs.args_count > 4) {
|
||||
hits[] = {span: cs.span, callee: cs.callee, args_count: cs.args_count}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return hits
|
||||
}
|
||||
|
||||
// Extract module export shape from index data (for cross-module analysis).
|
||||
analyze.module_summary = function(idx) {
|
||||
var exports = {}
|
||||
var i = 0
|
||||
var j = 0
|
||||
var exp = null
|
||||
var sym = null
|
||||
var found = false
|
||||
if (idx.exports == null) return {exports: exports}
|
||||
while (i < length(idx.exports)) {
|
||||
exp = idx.exports[i]
|
||||
found = false
|
||||
if (exp.symbol_id != null) {
|
||||
j = 0
|
||||
while (j < length(idx.symbols)) {
|
||||
sym = idx.symbols[j]
|
||||
if (sym.symbol_id == exp.symbol_id) {
|
||||
if (sym.kind == "fn" && sym.params != null) {
|
||||
exports[exp.name] = {type: "function", arity: length(sym.params)}
|
||||
} else {
|
||||
exports[exp.name] = {type: sym.kind}
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
exports[exp.name] = {type: "unknown"}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return {exports: exports}
|
||||
}
|
||||
|
||||
return analyze
|
||||
113
archive/miniz.c
113
archive/miniz.c
@@ -1,6 +1,5 @@
|
||||
#include "quickjs.h"
|
||||
#include "miniz.h"
|
||||
#include "cell.h"
|
||||
#include "miniz.h"
|
||||
|
||||
static JSClassID js_reader_class_id;
|
||||
static JSClassID js_writer_class_id;
|
||||
@@ -42,19 +41,19 @@ static JSValue js_miniz_read(JSContext *js, JSValue self, int argc, JSValue *arg
|
||||
{
|
||||
size_t len;
|
||||
void *data = js_get_blob_data(js, &len, argv[0]);
|
||||
if (data == -1)
|
||||
if (data == (void *)-1)
|
||||
return JS_EXCEPTION;
|
||||
|
||||
mz_zip_archive *zip = calloc(sizeof(*zip), 1);
|
||||
if (!zip)
|
||||
return JS_ThrowOutOfMemory(js);
|
||||
return JS_RaiseOOM(js);
|
||||
|
||||
mz_bool success = mz_zip_reader_init_mem(zip, data, len, 0);
|
||||
|
||||
if (!success) {
|
||||
int err = mz_zip_get_last_error(zip);
|
||||
free(zip);
|
||||
return JS_ThrowInternalError(js, "Failed to initialize zip reader: %s", mz_zip_get_error_string(err));
|
||||
return JS_RaiseDisrupt(js, "Failed to initialize zip reader: %s", mz_zip_get_error_string(err));
|
||||
}
|
||||
|
||||
JSValue jszip = JS_NewObjectClass(js, js_reader_class_id);
|
||||
@@ -71,7 +70,7 @@ static JSValue js_miniz_write(JSContext *js, JSValue self, int argc, JSValue *ar
|
||||
mz_zip_archive *zip = calloc(sizeof(*zip), 1);
|
||||
if (!zip) {
|
||||
JS_FreeCString(js, file);
|
||||
return JS_ThrowOutOfMemory(js);
|
||||
return JS_RaiseOOM(js);
|
||||
}
|
||||
|
||||
mz_bool success = mz_zip_writer_init_file(zip, file, 0);
|
||||
@@ -81,7 +80,7 @@ static JSValue js_miniz_write(JSContext *js, JSValue self, int argc, JSValue *ar
|
||||
int err = mz_zip_get_last_error(zip);
|
||||
mz_zip_writer_end(zip);
|
||||
free(zip);
|
||||
return JS_ThrowInternalError(js, "Failed to initialize zip writer: %s", mz_zip_get_error_string(err));
|
||||
return JS_RaiseDisrupt(js, "Failed to initialize zip writer: %s", mz_zip_get_error_string(err));
|
||||
}
|
||||
|
||||
JSValue jszip = JS_NewObjectClass(js, js_writer_class_id);
|
||||
@@ -93,7 +92,7 @@ static JSValue js_miniz_compress(JSContext *js, JSValue this_val,
|
||||
int argc, JSValueConst *argv)
|
||||
{
|
||||
if (argc < 1)
|
||||
return JS_ThrowTypeError(js,
|
||||
return JS_RaiseDisrupt(js,
|
||||
"compress needs a string or ArrayBuffer");
|
||||
|
||||
/* ─── 1. Grab the input data ──────────────────────────────── */
|
||||
@@ -109,35 +108,38 @@ static JSValue js_miniz_compress(JSContext *js, JSValue this_val,
|
||||
in_ptr = cstring;
|
||||
} else {
|
||||
in_ptr = js_get_blob_data(js, &in_len, argv[0]);
|
||||
if (in_ptr == -1)
|
||||
if (in_ptr == (const void *)-1)
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
|
||||
/* ─── 2. Allocate an output buffer big enough ────────────── */
|
||||
/* ─── 2. Allocate output blob (before getting blob input ptr) ── */
|
||||
mz_ulong out_len_est = mz_compressBound(in_len);
|
||||
void *out_buf = js_malloc(js, out_len_est);
|
||||
if (!out_buf) {
|
||||
void *out_ptr;
|
||||
JSValue abuf = js_new_blob_alloc(js, (size_t)out_len_est, &out_ptr);
|
||||
if (JS_IsException(abuf)) {
|
||||
if (cstring) JS_FreeCString(js, cstring);
|
||||
return JS_EXCEPTION;
|
||||
return abuf;
|
||||
}
|
||||
|
||||
/* Re-derive blob input pointer after alloc (GC may have moved it) */
|
||||
if (!cstring) {
|
||||
in_ptr = js_get_blob_data(js, &in_len, argv[0]);
|
||||
}
|
||||
|
||||
/* ─── 3. Do the compression (MZ_DEFAULT_COMPRESSION = level 6) */
|
||||
mz_ulong out_len = out_len_est;
|
||||
int st = mz_compress2(out_buf, &out_len,
|
||||
int st = mz_compress2(out_ptr, &out_len,
|
||||
in_ptr, in_len, MZ_DEFAULT_COMPRESSION);
|
||||
|
||||
/* clean-up for string input */
|
||||
if (cstring) JS_FreeCString(js, cstring);
|
||||
|
||||
if (st != MZ_OK) {
|
||||
js_free(js, out_buf);
|
||||
return JS_ThrowInternalError(js,
|
||||
if (st != MZ_OK)
|
||||
return JS_RaiseDisrupt(js,
|
||||
"miniz: compression failed (%d)", st);
|
||||
}
|
||||
|
||||
/* ─── 4. Hand JavaScript a copy of the compressed data ────── */
|
||||
JSValue abuf = js_new_blob_stoned_copy(js, out_buf, out_len);
|
||||
js_free(js, out_buf);
|
||||
/* ─── 4. Stone with actual compressed size ────────────────── */
|
||||
js_blob_stone(abuf, (size_t)out_len);
|
||||
return abuf;
|
||||
}
|
||||
|
||||
@@ -147,13 +149,13 @@ static JSValue js_miniz_decompress(JSContext *js,
|
||||
JSValueConst *argv)
|
||||
{
|
||||
if (argc < 1)
|
||||
return JS_ThrowTypeError(js,
|
||||
return JS_RaiseDisrupt(js,
|
||||
"decompress: need compressed ArrayBuffer");
|
||||
|
||||
/* grab compressed data */
|
||||
size_t in_len;
|
||||
void *in_ptr = js_get_blob_data(js, &in_len, argv[0]);
|
||||
if (in_ptr == -1)
|
||||
if (in_ptr == (void *)-1)
|
||||
return JS_EXCEPTION;
|
||||
|
||||
/* zlib header present → tell tinfl to parse it */
|
||||
@@ -163,7 +165,7 @@ static JSValue js_miniz_decompress(JSContext *js,
|
||||
TINFL_FLAG_PARSE_ZLIB_HEADER);
|
||||
|
||||
if (!out_ptr)
|
||||
return JS_ThrowInternalError(js,
|
||||
return JS_RaiseDisrupt(js,
|
||||
"miniz: decompression failed");
|
||||
|
||||
JSValue ret;
|
||||
@@ -187,16 +189,16 @@ static const JSCFunctionListEntry js_miniz_funcs[] = {
|
||||
JSValue js_writer_add_file(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
{
|
||||
if (argc < 2)
|
||||
return JS_ThrowTypeError(js, "add_file requires (path, arrayBuffer)");
|
||||
return JS_RaiseDisrupt(js, "add_file requires (path, arrayBuffer)");
|
||||
|
||||
mz_zip_archive *zip = js2writer(js, self);
|
||||
const char *pathInZip = JS_ToCString(js, argv[0]);
|
||||
if (!pathInZip)
|
||||
return JS_ThrowTypeError(js, "Could not parse path argument");
|
||||
return JS_RaiseDisrupt(js, "Could not parse path argument");
|
||||
|
||||
size_t dataLen;
|
||||
void *data = js_get_blob_data(js, &dataLen, argv[1]);
|
||||
if (data == -1) {
|
||||
if (data == (void *)-1) {
|
||||
JS_FreeCString(js, pathInZip);
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
@@ -205,7 +207,7 @@ JSValue js_writer_add_file(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
JS_FreeCString(js, pathInZip);
|
||||
|
||||
if (!success)
|
||||
return JS_ThrowInternalError(js, "Failed to add memory to zip");
|
||||
return JS_RaiseDisrupt(js, "Failed to add memory to zip");
|
||||
|
||||
return JS_NULL;
|
||||
}
|
||||
@@ -225,7 +227,7 @@ JSValue js_reader_mod(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
mz_zip_archive *zip = js2reader(js, self);
|
||||
if (!zip) {
|
||||
JS_FreeCString(js, file);
|
||||
return JS_ThrowInternalError(js, "Invalid zip reader");
|
||||
return JS_RaiseDisrupt(js, "Invalid zip reader");
|
||||
}
|
||||
|
||||
mz_zip_archive_file_stat pstat;
|
||||
@@ -233,19 +235,19 @@ JSValue js_reader_mod(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
|
||||
if (index == (mz_uint)-1) {
|
||||
JS_FreeCString(js, file);
|
||||
return JS_ThrowReferenceError(js, "File '%s' not found in archive", file);
|
||||
return JS_RaiseDisrupt(js, "File '%s' not found in archive", file);
|
||||
}
|
||||
|
||||
JS_FreeCString(js, file);
|
||||
|
||||
if (!mz_zip_reader_file_stat(zip, index, &pstat)) {
|
||||
int err = mz_zip_get_last_error(zip);
|
||||
return JS_ThrowInternalError(js, "Failed to get file stats: %s", mz_zip_get_error_string(err));
|
||||
return JS_RaiseDisrupt(js, "Failed to get file stats: %s", mz_zip_get_error_string(err));
|
||||
}
|
||||
|
||||
return JS_NewFloat64(js, pstat.m_time);
|
||||
#else
|
||||
return JS_ThrowInternalError(js, "MINIZ_NO_TIME is defined");
|
||||
return JS_RaiseDisrupt(js, "MINIZ_NO_TIME is defined");
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -258,7 +260,7 @@ JSValue js_reader_exists(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
mz_zip_archive *zip = js2reader(js, self);
|
||||
if (!zip) {
|
||||
JS_FreeCString(js, file);
|
||||
return JS_ThrowInternalError(js, "Invalid zip reader");
|
||||
return JS_RaiseDisrupt(js, "Invalid zip reader");
|
||||
}
|
||||
|
||||
mz_uint index = mz_zip_reader_locate_file(zip, file, NULL, 0);
|
||||
@@ -276,7 +278,7 @@ JSValue js_reader_slurp(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
mz_zip_archive *zip = js2reader(js, self);
|
||||
if (!zip) {
|
||||
JS_FreeCString(js, file);
|
||||
return JS_ThrowInternalError(js, "Invalid zip reader");
|
||||
return JS_RaiseDisrupt(js, "Invalid zip reader");
|
||||
}
|
||||
|
||||
size_t len;
|
||||
@@ -286,7 +288,7 @@ JSValue js_reader_slurp(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
int err = mz_zip_get_last_error(zip);
|
||||
const char *filename = file;
|
||||
JS_FreeCString(js, file);
|
||||
return JS_ThrowInternalError(js, "Failed to extract file '%s': %s", filename, mz_zip_get_error_string(err));
|
||||
return JS_RaiseDisrupt(js, "Failed to extract file '%s': %s", filename, mz_zip_get_error_string(err));
|
||||
}
|
||||
|
||||
JS_FreeCString(js, file);
|
||||
@@ -300,7 +302,7 @@ JSValue js_reader_list(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
{
|
||||
mz_zip_archive *zip = js2reader(js, self);
|
||||
if (!zip)
|
||||
return JS_ThrowInternalError(js, "Invalid zip reader");
|
||||
return JS_RaiseDisrupt(js, "Invalid zip reader");
|
||||
|
||||
mz_uint num_files = mz_zip_reader_get_num_files(zip);
|
||||
|
||||
@@ -316,10 +318,9 @@ JSValue js_reader_list(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
|
||||
JSValue filename = JS_NewString(js, file_stat.m_filename);
|
||||
if (JS_IsException(filename)) {
|
||||
JS_FreeValue(js, arr);
|
||||
return filename;
|
||||
}
|
||||
JS_SetPropertyUint32(js, arr, arr_index++, filename);
|
||||
JS_SetPropertyNumber(js, arr, arr_index++, filename);
|
||||
}
|
||||
|
||||
return arr;
|
||||
@@ -328,7 +329,7 @@ JSValue js_reader_list(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
JSValue js_reader_is_directory(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
{
|
||||
if (argc < 1)
|
||||
return JS_ThrowTypeError(js, "is_directory requires a file index");
|
||||
return JS_RaiseDisrupt(js, "is_directory requires a file index");
|
||||
|
||||
int32_t index;
|
||||
if (JS_ToInt32(js, &index, argv[0]))
|
||||
@@ -336,7 +337,7 @@ JSValue js_reader_is_directory(JSContext *js, JSValue self, int argc, JSValue *a
|
||||
|
||||
mz_zip_archive *zip = js2reader(js, self);
|
||||
if (!zip)
|
||||
return JS_ThrowInternalError(js, "Invalid zip reader");
|
||||
return JS_RaiseDisrupt(js, "Invalid zip reader");
|
||||
|
||||
return JS_NewBool(js, mz_zip_reader_is_file_a_directory(zip, index));
|
||||
}
|
||||
@@ -344,7 +345,7 @@ JSValue js_reader_is_directory(JSContext *js, JSValue self, int argc, JSValue *a
|
||||
JSValue js_reader_get_filename(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
{
|
||||
if (argc < 1)
|
||||
return JS_ThrowTypeError(js, "get_filename requires a file index");
|
||||
return JS_RaiseDisrupt(js, "get_filename requires a file index");
|
||||
|
||||
int32_t index;
|
||||
if (JS_ToInt32(js, &index, argv[0]))
|
||||
@@ -352,11 +353,11 @@ JSValue js_reader_get_filename(JSContext *js, JSValue self, int argc, JSValue *a
|
||||
|
||||
mz_zip_archive *zip = js2reader(js, self);
|
||||
if (!zip)
|
||||
return JS_ThrowInternalError(js, "Invalid zip reader");
|
||||
return JS_RaiseDisrupt(js, "Invalid zip reader");
|
||||
|
||||
mz_zip_archive_file_stat file_stat;
|
||||
if (!mz_zip_reader_file_stat(zip, index, &file_stat))
|
||||
return JS_ThrowInternalError(js, "Failed to get file stats");
|
||||
return JS_RaiseDisrupt(js, "Failed to get file stats");
|
||||
|
||||
return JS_NewString(js, file_stat.m_filename);
|
||||
}
|
||||
@@ -365,7 +366,7 @@ JSValue js_reader_count(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
{
|
||||
mz_zip_archive *zip = js2reader(js, self);
|
||||
if (!zip)
|
||||
return JS_ThrowInternalError(js, "Invalid zip reader");
|
||||
return JS_RaiseDisrupt(js, "Invalid zip reader");
|
||||
return JS_NewUint32(js, mz_zip_reader_get_num_files(zip));
|
||||
}
|
||||
|
||||
@@ -379,21 +380,23 @@ static const JSCFunctionListEntry js_reader_funcs[] = {
|
||||
JS_CFUNC_DEF("count", 0, js_reader_count),
|
||||
};
|
||||
|
||||
JSValue js_miniz_use(JSContext *js)
|
||||
JSValue js_core_miniz_use(JSContext *js)
|
||||
{
|
||||
JS_FRAME(js);
|
||||
|
||||
JS_NewClassID(&js_reader_class_id);
|
||||
JS_NewClass(js, js_reader_class_id, &js_reader_class);
|
||||
JSValue reader_proto = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, reader_proto, js_reader_funcs, sizeof(js_reader_funcs) / sizeof(JSCFunctionListEntry));
|
||||
JS_SetClassProto(js, js_reader_class_id, reader_proto);
|
||||
JS_ROOT(reader_proto, JS_NewObject(js));
|
||||
JS_SetPropertyFunctionList(js, reader_proto.val, js_reader_funcs, sizeof(js_reader_funcs) / sizeof(JSCFunctionListEntry));
|
||||
JS_SetClassProto(js, js_reader_class_id, reader_proto.val);
|
||||
|
||||
JS_NewClassID(&js_writer_class_id);
|
||||
JS_NewClass(js, js_writer_class_id, &js_writer_class);
|
||||
JSValue writer_proto = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, writer_proto, js_writer_funcs, sizeof(js_writer_funcs) / sizeof(JSCFunctionListEntry));
|
||||
JS_SetClassProto(js, js_writer_class_id, writer_proto);
|
||||
|
||||
JSValue export = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, export, js_miniz_funcs, sizeof(js_miniz_funcs)/sizeof(JSCFunctionListEntry));
|
||||
return export;
|
||||
JS_ROOT(writer_proto, JS_NewObject(js));
|
||||
JS_SetPropertyFunctionList(js, writer_proto.val, js_writer_funcs, sizeof(js_writer_funcs) / sizeof(JSCFunctionListEntry));
|
||||
JS_SetClassProto(js, js_writer_class_id, writer_proto.val);
|
||||
|
||||
JS_ROOT(export, JS_NewObject(js));
|
||||
JS_SetPropertyFunctionList(js, export.val, js_miniz_funcs, sizeof(js_miniz_funcs)/sizeof(JSCFunctionListEntry));
|
||||
JS_RETURN(export.val);
|
||||
}
|
||||
|
||||
149
audit.ce
Normal file
149
audit.ce
Normal file
@@ -0,0 +1,149 @@
|
||||
// cell audit [<locator>] - Test-compile all .ce and .cm scripts
|
||||
//
|
||||
// Usage:
|
||||
// cell audit Audit all packages
|
||||
// cell audit <locator> Audit specific package
|
||||
// cell audit . Audit current directory package
|
||||
// cell audit --function-hoist [<locator>] Report function hoisting usage
|
||||
//
|
||||
// Compiles every script in the package(s) to check for errors.
|
||||
// Continues past failures and reports all issues at the end.
|
||||
|
||||
var shop = use('internal/shop')
|
||||
var pkg = use('package')
|
||||
var fd = use('fd')
|
||||
|
||||
var target_package = null
|
||||
var function_hoist = false
|
||||
var i = 0
|
||||
|
||||
var run = function() {
|
||||
var packages = null
|
||||
var tokenize_mod = null
|
||||
var parse_mod = null
|
||||
var hoist_files = 0
|
||||
var hoist_refs = 0
|
||||
var total_ok = 0
|
||||
var total_errors = 0
|
||||
var total_scripts = 0
|
||||
var all_failures = []
|
||||
var all_unresolved = []
|
||||
var summary = null
|
||||
|
||||
for (i = 0; i < length(args); i++) {
|
||||
if (args[i] == '--help' || args[i] == '-h') {
|
||||
log.console("Usage: cell audit [--function-hoist] [<locator>]")
|
||||
log.console("")
|
||||
log.console("Test-compile all .ce and .cm scripts in package(s).")
|
||||
log.console("Reports all errors without stopping at the first failure.")
|
||||
log.console("")
|
||||
log.console("Flags:")
|
||||
log.console(" --function-hoist Report files that rely on function hoisting")
|
||||
return
|
||||
} else if (args[i] == '--function-hoist') {
|
||||
function_hoist = true
|
||||
} else if (!starts_with(args[i], '-')) {
|
||||
target_package = args[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve local paths
|
||||
if (target_package) {
|
||||
target_package = shop.resolve_locator(target_package)
|
||||
}
|
||||
|
||||
if (target_package) {
|
||||
packages = [target_package]
|
||||
} else {
|
||||
packages = shop.list_packages()
|
||||
}
|
||||
|
||||
if (function_hoist) {
|
||||
tokenize_mod = use('tokenize')
|
||||
parse_mod = use('parse')
|
||||
|
||||
arrfor(packages, function(p) {
|
||||
var scripts = shop.get_package_scripts(p)
|
||||
var pkg_dir = shop.get_package_dir(p)
|
||||
if (length(scripts) == 0) return
|
||||
|
||||
arrfor(scripts, function(script) {
|
||||
var src_path = pkg_dir + '/' + script
|
||||
var src = null
|
||||
var tok_result = null
|
||||
var ast = null
|
||||
var scan = function() {
|
||||
if (!fd.is_file(src_path)) return
|
||||
src = text(fd.slurp(src_path))
|
||||
tok_result = tokenize_mod(src, script)
|
||||
ast = parse_mod(tok_result.tokens, src, script, tokenize_mod)
|
||||
if (ast._hoisted_fns != null && length(ast._hoisted_fns) > 0) {
|
||||
log.console(p + '/' + script + ":")
|
||||
hoist_files = hoist_files + 1
|
||||
arrfor(ast._hoisted_fns, function(ref) {
|
||||
var msg = " " + ref.name
|
||||
if (ref.line != null) msg = msg + " (ref line " + text(ref.line)
|
||||
if (ref.decl_line != null) msg = msg + ", declared line " + text(ref.decl_line)
|
||||
if (ref.line != null) msg = msg + ")"
|
||||
log.console(msg)
|
||||
hoist_refs = hoist_refs + 1
|
||||
})
|
||||
}
|
||||
} disruption {
|
||||
// skip files that fail to parse
|
||||
}
|
||||
scan()
|
||||
})
|
||||
})
|
||||
|
||||
log.console("")
|
||||
log.console("Summary: " + text(hoist_files) + " files with function hoisting, " + text(hoist_refs) + " total forward references")
|
||||
return
|
||||
}
|
||||
|
||||
arrfor(packages, function(p) {
|
||||
var scripts = shop.get_package_scripts(p)
|
||||
var result = null
|
||||
var resolution = null
|
||||
if (length(scripts) == 0) return
|
||||
|
||||
log.console("Auditing " + p + " (" + text(length(scripts)) + " scripts)...")
|
||||
result = shop.build_package_scripts(p)
|
||||
total_ok = total_ok + result.ok
|
||||
total_errors = total_errors + length(result.errors)
|
||||
total_scripts = total_scripts + result.total
|
||||
|
||||
arrfor(result.errors, function(e) {
|
||||
push(all_failures, p + ": " + e)
|
||||
})
|
||||
|
||||
// Check use() resolution
|
||||
resolution = shop.audit_use_resolution(p)
|
||||
arrfor(resolution.unresolved, function(u) {
|
||||
push(all_unresolved, p + '/' + u.script + ": use('" + u.module + "') cannot be resolved")
|
||||
})
|
||||
})
|
||||
|
||||
log.console("")
|
||||
if (length(all_failures) > 0) {
|
||||
log.console("Failed scripts:")
|
||||
arrfor(all_failures, function(f) {
|
||||
log.console(" " + f)
|
||||
})
|
||||
log.console("")
|
||||
}
|
||||
|
||||
if (length(all_unresolved) > 0) {
|
||||
log.console("Unresolved modules:")
|
||||
arrfor(all_unresolved, function(u) {
|
||||
log.console(" " + u)
|
||||
})
|
||||
log.console("")
|
||||
}
|
||||
|
||||
summary = "Audit complete: " + text(total_ok) + "/" + text(total_scripts) + " scripts compiled"
|
||||
if (total_errors > 0) summary = summary + ", " + text(total_errors) + " failed"
|
||||
if (length(all_unresolved) > 0) summary = summary + ", " + text(length(all_unresolved)) + " unresolved use() calls"
|
||||
log.console(summary)
|
||||
}
|
||||
run()
|
||||
315
bench.ce
315
bench.ce
@@ -1,18 +1,39 @@
|
||||
// cell bench - Run benchmarks with statistical analysis
|
||||
|
||||
var shop = use('internal/shop')
|
||||
var pkg = use('package')
|
||||
var fd = use('fd')
|
||||
var time = use('time')
|
||||
var json = use('json')
|
||||
var blob = use('blob')
|
||||
var os = use('os')
|
||||
var os = use('internal/os')
|
||||
var testlib = use('internal/testlib')
|
||||
var math = use('math/radians')
|
||||
|
||||
if (!args) args = []
|
||||
var _args = args == null ? [] : args
|
||||
|
||||
var target_pkg = null // null = current package
|
||||
var target_bench = null // null = all benchmarks, otherwise specific bench file
|
||||
var all_pkgs = false
|
||||
var bench_mode = "bytecode" // "bytecode", "native", or "compare"
|
||||
|
||||
// Strip mode flags from args before parsing
|
||||
function strip_mode_flags() {
|
||||
var filtered = []
|
||||
arrfor(_args, function(a) {
|
||||
if (a == '--native') {
|
||||
bench_mode = "native"
|
||||
} else if (a == '--bytecode') {
|
||||
bench_mode = "bytecode"
|
||||
} else if (a == '--compare') {
|
||||
bench_mode = "compare"
|
||||
} else {
|
||||
push(filtered, a)
|
||||
}
|
||||
})
|
||||
_args = filtered
|
||||
}
|
||||
strip_mode_flags()
|
||||
|
||||
// Benchmark configuration
|
||||
def WARMUP_BATCHES = 3
|
||||
@@ -55,14 +76,19 @@ function stddev(arr, mean_val) {
|
||||
function percentile(arr, p) {
|
||||
if (length(arr) == 0) return 0
|
||||
var sorted = sort(arr)
|
||||
var idx = floor(arr) * p / 100
|
||||
var idx = floor(length(arr) * p / 100)
|
||||
if (idx >= length(arr)) idx = length(arr) - 1
|
||||
return sorted[idx]
|
||||
}
|
||||
|
||||
// Parse arguments similar to test.ce
|
||||
function parse_args() {
|
||||
if (length(args) == 0) {
|
||||
var name = null
|
||||
var lock = null
|
||||
var resolved = null
|
||||
var bench_path = null
|
||||
|
||||
if (length(_args) == 0) {
|
||||
if (!testlib.is_valid_package('.')) {
|
||||
log.console('No cell.toml found in current directory')
|
||||
return false
|
||||
@@ -71,7 +97,7 @@ function parse_args() {
|
||||
return true
|
||||
}
|
||||
|
||||
if (args[0] == 'all') {
|
||||
if (_args[0] == 'all') {
|
||||
if (!testlib.is_valid_package('.')) {
|
||||
log.console('No cell.toml found in current directory')
|
||||
return false
|
||||
@@ -80,28 +106,28 @@ function parse_args() {
|
||||
return true
|
||||
}
|
||||
|
||||
if (args[0] == 'package') {
|
||||
if (length(args) < 2) {
|
||||
if (_args[0] == 'package') {
|
||||
if (length(_args) < 2) {
|
||||
log.console('Usage: cell bench package <name> [bench]')
|
||||
log.console(' cell bench package all')
|
||||
return false
|
||||
}
|
||||
|
||||
if (args[1] == 'all') {
|
||||
if (_args[1] == 'all') {
|
||||
all_pkgs = true
|
||||
log.console('Benchmarking all packages...')
|
||||
return true
|
||||
}
|
||||
|
||||
var name = args[1]
|
||||
var lock = shop.load_lock()
|
||||
name = _args[1]
|
||||
lock = shop.load_lock()
|
||||
if (lock[name]) {
|
||||
target_pkg = name
|
||||
} else if (starts_with(name, '/') && testlib.is_valid_package(name)) {
|
||||
target_pkg = name
|
||||
} else {
|
||||
if (testlib.is_valid_package('.')) {
|
||||
var resolved = pkg.alias_to_package(null, name)
|
||||
resolved = pkg.alias_to_package(null, name)
|
||||
if (resolved) {
|
||||
target_pkg = resolved
|
||||
} else {
|
||||
@@ -114,8 +140,8 @@ function parse_args() {
|
||||
}
|
||||
}
|
||||
|
||||
if (length(args) >= 3) {
|
||||
target_bench = args[2]
|
||||
if (length(_args) >= 3) {
|
||||
target_bench = _args[2]
|
||||
}
|
||||
|
||||
log.console(`Benchmarking package: ${target_pkg}`)
|
||||
@@ -123,7 +149,7 @@ function parse_args() {
|
||||
}
|
||||
|
||||
// cell bench benches/suite or cell bench <path>
|
||||
var bench_path = args[0]
|
||||
bench_path = _args[0]
|
||||
|
||||
// Normalize path - add benches/ prefix if not present
|
||||
if (!starts_with(bench_path, 'benches/') && !starts_with(bench_path, '/')) {
|
||||
@@ -160,12 +186,15 @@ function collect_benches(package_name, specific_bench) {
|
||||
var files = pkg.list_files(package_name)
|
||||
var bench_files = []
|
||||
arrfor(files, function(f) {
|
||||
var bench_name = null
|
||||
var match_name = null
|
||||
var match_base = null
|
||||
if (starts_with(f, "benches/") && ends_with(f, ".cm")) {
|
||||
if (specific_bench) {
|
||||
var bench_name = text(f, 0, -3)
|
||||
var match_name = specific_bench
|
||||
bench_name = text(f, 0, -3)
|
||||
match_name = specific_bench
|
||||
if (!starts_with(match_name, 'benches/')) match_name = 'benches/' + match_name
|
||||
var match_base = ends_with(match_name, '.cm') ? text(match_name, 0, -3) : match_name
|
||||
match_base = ends_with(match_name, '.cm') ? text(match_name, 0, -3) : match_name
|
||||
if (bench_name != match_base) return
|
||||
}
|
||||
push(bench_files, f)
|
||||
@@ -180,24 +209,25 @@ function calibrate_batch_size(bench_fn, is_batch) {
|
||||
|
||||
var n = MIN_BATCH_SIZE
|
||||
var dt = 0
|
||||
var start = 0
|
||||
var new_n = 0
|
||||
var calc = 0
|
||||
var target_n = 0
|
||||
|
||||
// Find a batch size that takes at least MIN_SAMPLE_NS
|
||||
while (n < MAX_BATCH_SIZE) {
|
||||
// Ensure n is a valid number before calling
|
||||
if (!is_number(n) || n < 1) {
|
||||
n = 1
|
||||
break
|
||||
}
|
||||
|
||||
var start = os.now()
|
||||
start = os.now()
|
||||
bench_fn(n)
|
||||
dt = os.now() - start
|
||||
|
||||
if (dt >= MIN_SAMPLE_NS) break
|
||||
|
||||
// Double the batch size
|
||||
var new_n = n * 2
|
||||
// Check if multiplication produced a valid number
|
||||
new_n = n * 2
|
||||
if (!is_number(new_n) || new_n > MAX_BATCH_SIZE) {
|
||||
n = MAX_BATCH_SIZE
|
||||
break
|
||||
@@ -207,10 +237,9 @@ function calibrate_batch_size(bench_fn, is_batch) {
|
||||
|
||||
// Adjust to target sample duration
|
||||
if (dt > 0 && dt < TARGET_SAMPLE_NS && is_number(n) && is_number(dt)) {
|
||||
var calc = n * TARGET_SAMPLE_NS / dt
|
||||
calc = n * TARGET_SAMPLE_NS / dt
|
||||
if (is_number(calc) && calc > 0) {
|
||||
var target_n = floor(calc)
|
||||
// Check if floor returned a valid number
|
||||
target_n = floor(calc)
|
||||
if (is_number(target_n) && target_n > 0) {
|
||||
if (target_n > MAX_BATCH_SIZE) target_n = MAX_BATCH_SIZE
|
||||
if (target_n < MIN_BATCH_SIZE) target_n = MIN_BATCH_SIZE
|
||||
@@ -219,7 +248,6 @@ function calibrate_batch_size(bench_fn, is_batch) {
|
||||
}
|
||||
}
|
||||
|
||||
// Safety check - ensure we always return a valid batch size
|
||||
if (!is_number(n) || n < 1) {
|
||||
n = 1
|
||||
}
|
||||
@@ -230,72 +258,70 @@ function calibrate_batch_size(bench_fn, is_batch) {
|
||||
// Run a single benchmark function
|
||||
function run_single_bench(bench_fn, bench_name) {
|
||||
var timings_per_op = []
|
||||
|
||||
// Detect benchmark format:
|
||||
// 1. Object with { setup, run, teardown } - structured format
|
||||
// 2. Function that accepts (n) - batch format
|
||||
// 3. Function that accepts () - legacy format
|
||||
var is_structured = is_object(bench_fn) && bench_fn.run
|
||||
var is_batch = false
|
||||
var batch_size = 1
|
||||
var setup_fn = null
|
||||
var run_fn = null
|
||||
var teardown_fn = null
|
||||
var calibrate_fn = null
|
||||
var _detect = null
|
||||
var i = 0
|
||||
var state = null
|
||||
var start = 0
|
||||
var duration = 0
|
||||
var ns_per_op = 0
|
||||
|
||||
if (is_structured) {
|
||||
setup_fn = bench_fn.setup || function() { return null }
|
||||
run_fn = bench_fn.run
|
||||
teardown_fn = bench_fn.teardown || function(state) {}
|
||||
teardown_fn = bench_fn.teardown || function(s) {}
|
||||
|
||||
// Check if run function accepts batch size
|
||||
try {
|
||||
_detect = function() {
|
||||
var test_state = setup_fn()
|
||||
run_fn(1, test_state)
|
||||
is_batch = true
|
||||
if (teardown_fn) teardown_fn(test_state)
|
||||
} catch (e) {
|
||||
} disruption {
|
||||
is_batch = false
|
||||
}
|
||||
_detect()
|
||||
|
||||
// Create wrapper for calibration
|
||||
var calibrate_fn = function(n) {
|
||||
var state = setup_fn()
|
||||
run_fn(n, state)
|
||||
if (teardown_fn) teardown_fn(state)
|
||||
calibrate_fn = function(n) {
|
||||
var s = setup_fn()
|
||||
run_fn(n, s)
|
||||
if (teardown_fn) teardown_fn(s)
|
||||
}
|
||||
batch_size = calibrate_batch_size(calibrate_fn, is_batch)
|
||||
|
||||
// Safety check for structured benchmarks
|
||||
if (!is_number(batch_size) || batch_size < 1) {
|
||||
batch_size = 1
|
||||
}
|
||||
} else {
|
||||
// Simple function format
|
||||
try {
|
||||
_detect = function() {
|
||||
bench_fn(1)
|
||||
is_batch = true
|
||||
} catch (e) {
|
||||
} disruption {
|
||||
is_batch = false
|
||||
}
|
||||
_detect()
|
||||
batch_size = calibrate_batch_size(bench_fn, is_batch)
|
||||
}
|
||||
|
||||
// Safety check - ensure batch_size is valid
|
||||
if (!batch_size || batch_size < 1) {
|
||||
batch_size = 1
|
||||
}
|
||||
|
||||
// Warmup phase
|
||||
for (var i = 0; i < WARMUP_BATCHES; i++) {
|
||||
// Ensure batch_size is valid before warmup
|
||||
for (i = 0; i < WARMUP_BATCHES; i++) {
|
||||
if (!is_number(batch_size) || batch_size < 1) {
|
||||
var type_str = is_null(batch_size) ? 'null' : is_number(batch_size) ? 'number' : is_text(batch_size) ? 'text' : is_object(batch_size) ? 'object' : is_array(batch_size) ? 'array' : is_function(batch_size) ? 'function' : is_logical(batch_size) ? 'logical' : 'unknown'
|
||||
log.console(`WARNING: batch_size became ${type_str} = ${batch_size}, resetting to 1`)
|
||||
batch_size = 1
|
||||
}
|
||||
|
||||
if (is_structured) {
|
||||
var state = setup_fn()
|
||||
state = setup_fn()
|
||||
if (is_batch) {
|
||||
run_fn(batch_size, state)
|
||||
} else {
|
||||
@@ -312,35 +338,34 @@ function run_single_bench(bench_fn, bench_name) {
|
||||
}
|
||||
|
||||
// Measurement phase - collect SAMPLES timing samples
|
||||
for (var i = 0; i < SAMPLES; i++) {
|
||||
// Double-check batch_size is valid (should never happen, but defensive)
|
||||
for (i = 0; i < SAMPLES; i++) {
|
||||
if (!is_number(batch_size) || batch_size < 1) {
|
||||
batch_size = 1
|
||||
}
|
||||
|
||||
if (is_structured) {
|
||||
var state = setup_fn()
|
||||
var start = os.now()
|
||||
state = setup_fn()
|
||||
start = os.now()
|
||||
if (is_batch) {
|
||||
run_fn(batch_size, state)
|
||||
} else {
|
||||
run_fn(state)
|
||||
}
|
||||
var duration = os.now() - start
|
||||
duration = os.now() - start
|
||||
if (teardown_fn) teardown_fn(state)
|
||||
|
||||
var ns_per_op = is_batch ? duration / batch_size : duration
|
||||
ns_per_op = is_batch ? duration / batch_size : duration
|
||||
push(timings_per_op, ns_per_op)
|
||||
} else {
|
||||
var start = os.now()
|
||||
start = os.now()
|
||||
if (is_batch) {
|
||||
bench_fn(batch_size)
|
||||
} else {
|
||||
bench_fn()
|
||||
}
|
||||
var duration = os.now() - start
|
||||
duration = os.now() - start
|
||||
|
||||
var ns_per_op = is_batch ? duration / batch_size : duration
|
||||
ns_per_op = is_batch ? duration / batch_size : duration
|
||||
push(timings_per_op, ns_per_op)
|
||||
}
|
||||
}
|
||||
@@ -354,7 +379,6 @@ function run_single_bench(bench_fn, bench_name) {
|
||||
var p95_ns = percentile(timings_per_op, 95)
|
||||
var p99_ns = percentile(timings_per_op, 99)
|
||||
|
||||
// Calculate ops/s from median
|
||||
var ops_per_sec = 0
|
||||
if (median_ns > 0) {
|
||||
ops_per_sec = floor(1000000000 / median_ns)
|
||||
@@ -391,6 +415,53 @@ function format_ops(ops) {
|
||||
return `${round(ops / 1000000000 * 100) / 100}G ops/s`
|
||||
}
|
||||
|
||||
// Load a module for benchmarking in the given mode
|
||||
// Returns the module value, or null on failure
|
||||
function resolve_bench_load(f, package_name) {
|
||||
var mod_path = text(f, 0, -3)
|
||||
var use_pkg = package_name ? package_name : fd.realpath('.')
|
||||
var prefix = testlib.get_pkg_dir(package_name)
|
||||
var src_path = prefix + '/' + f
|
||||
return {mod_path, use_pkg, src_path}
|
||||
}
|
||||
|
||||
function load_bench_module_native(f, package_name) {
|
||||
var r = resolve_bench_load(f, package_name)
|
||||
return shop.use_native(r.src_path, r.use_pkg)
|
||||
}
|
||||
|
||||
function load_bench_module(f, package_name, mode) {
|
||||
var r = resolve_bench_load(f, package_name)
|
||||
if (mode == "native") {
|
||||
return load_bench_module_native(f, package_name)
|
||||
}
|
||||
return shop.use(r.mod_path, r.use_pkg)
|
||||
}
|
||||
|
||||
// Collect benchmark functions from a loaded module
|
||||
function collect_bench_fns(bench_mod) {
|
||||
var benches = []
|
||||
if (is_function(bench_mod)) {
|
||||
push(benches, {name: 'main', fn: bench_mod})
|
||||
} else if (is_object(bench_mod)) {
|
||||
arrfor(array(bench_mod), function(k) {
|
||||
if (is_function(bench_mod[k]))
|
||||
push(benches, {name: k, fn: bench_mod[k]})
|
||||
})
|
||||
}
|
||||
return benches
|
||||
}
|
||||
|
||||
// Print results for a single benchmark
|
||||
function print_bench_result(result, label) {
|
||||
var prefix = label ? `[${label}] ` : ''
|
||||
log.console(` ${prefix}${format_ns(result.median_ns)}/op ${format_ops(result.ops_per_sec)}`)
|
||||
log.console(` ${prefix}min: ${format_ns(result.min_ns)} max: ${format_ns(result.max_ns)} stddev: ${format_ns(result.stddev_ns)}`)
|
||||
if (result.batch_size > 1) {
|
||||
log.console(` ${prefix}batch: ${result.batch_size} samples: ${result.samples}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Run benchmarks for a package
|
||||
function run_benchmarks(package_name, specific_bench) {
|
||||
var bench_files = collect_benches(package_name, specific_bench)
|
||||
@@ -403,66 +474,117 @@ function run_benchmarks(package_name, specific_bench) {
|
||||
|
||||
if (length(bench_files) == 0) return pkg_result
|
||||
|
||||
if (package_name) log.console(`Running benchmarks for ${package_name}`)
|
||||
else log.console(`Running benchmarks for local package`)
|
||||
var mode_label = bench_mode == "compare" ? "bytecode vs native" : bench_mode
|
||||
if (package_name) log.console(`Running benchmarks for ${package_name} (${mode_label})`)
|
||||
else log.console(`Running benchmarks for local package (${mode_label})`)
|
||||
|
||||
arrfor(bench_files, function(f) {
|
||||
var mod_path = text(f, 0, -3)
|
||||
var load_error = false
|
||||
var benches = []
|
||||
var native_benches = []
|
||||
var bench_mod = null
|
||||
var native_mod = null
|
||||
var error_result = null
|
||||
|
||||
var file_result = {
|
||||
name: f,
|
||||
benchmarks: []
|
||||
}
|
||||
|
||||
try {
|
||||
var bench_mod
|
||||
var use_pkg = package_name ? package_name : fd.realpath('.')
|
||||
bench_mod = shop.use(mod_path, use_pkg)
|
||||
|
||||
var benches = []
|
||||
if (is_function(bench_mod)) {
|
||||
push(benches, {name: 'main', fn: bench_mod})
|
||||
} else if (is_object(bench_mod)) {
|
||||
arrfor(array(bench_mod), function(k) {
|
||||
if (is_function(bench_mod[k]))
|
||||
push(benches, {name: k, fn: bench_mod[k]})
|
||||
})
|
||||
var _load_file = function() {
|
||||
var _load_native = null
|
||||
if (bench_mode == "compare") {
|
||||
bench_mod = load_bench_module(f, package_name, "bytecode")
|
||||
benches = collect_bench_fns(bench_mod)
|
||||
_load_native = function() {
|
||||
native_mod = load_bench_module(f, package_name, "native")
|
||||
native_benches = collect_bench_fns(native_mod)
|
||||
} disruption {
|
||||
log.console(` ${f}: native compilation failed, comparing skipped`)
|
||||
native_benches = []
|
||||
}
|
||||
_load_native()
|
||||
} else {
|
||||
bench_mod = load_bench_module(f, package_name, bench_mode)
|
||||
benches = collect_bench_fns(bench_mod)
|
||||
}
|
||||
|
||||
if (length(benches) > 0) {
|
||||
log.console(` ${f}`)
|
||||
arrfor(benches, function(b) {
|
||||
try {
|
||||
var result = run_single_bench(b.fn, b.name)
|
||||
var bench_error = false
|
||||
var result = null
|
||||
var nat_b = null
|
||||
var nat_error = false
|
||||
var nat_result = null
|
||||
|
||||
var _run_bench = function() {
|
||||
var speedup = 0
|
||||
var _run_nat = null
|
||||
result = run_single_bench(b.fn, b.name)
|
||||
result.package = pkg_result.package
|
||||
result.mode = bench_mode == "compare" ? "bytecode" : bench_mode
|
||||
push(file_result.benchmarks, result)
|
||||
pkg_result.total++
|
||||
|
||||
log.console(` ${result.name}`)
|
||||
log.console(` ${format_ns(result.median_ns)}/op ${format_ops(result.ops_per_sec)}`)
|
||||
log.console(` min: ${format_ns(result.min_ns)} max: ${format_ns(result.max_ns)} stddev: ${format_ns(result.stddev_ns)}`)
|
||||
if (result.batch_size > 1) {
|
||||
log.console(` batch: ${result.batch_size} samples: ${result.samples}`)
|
||||
if (bench_mode == "compare") {
|
||||
print_bench_result(result, "bytecode")
|
||||
|
||||
// Find matching native bench and run it
|
||||
nat_b = find(native_benches, function(nb) { return nb.name == b.name })
|
||||
if (nat_b != null) {
|
||||
_run_nat = function() {
|
||||
nat_result = run_single_bench(native_benches[nat_b].fn, b.name)
|
||||
nat_result.package = pkg_result.package
|
||||
nat_result.mode = "native"
|
||||
push(file_result.benchmarks, nat_result)
|
||||
pkg_result.total++
|
||||
print_bench_result(nat_result, "native ")
|
||||
|
||||
if (nat_result.median_ns > 0) {
|
||||
speedup = result.median_ns / nat_result.median_ns
|
||||
log.console(` speedup: ${round(speedup * 100) / 100}x`)
|
||||
}
|
||||
} disruption {
|
||||
nat_error = true
|
||||
}
|
||||
_run_nat()
|
||||
if (nat_error) {
|
||||
log.console(` [native ] ERROR`)
|
||||
}
|
||||
} else {
|
||||
log.console(` [native ] (no matching function)`)
|
||||
}
|
||||
} else {
|
||||
print_bench_result(result, null)
|
||||
}
|
||||
} catch (e) {
|
||||
log.console(` ERROR ${b.name}: ${e}`)
|
||||
log.error(e)
|
||||
var error_result = {
|
||||
} disruption {
|
||||
bench_error = true
|
||||
}
|
||||
_run_bench()
|
||||
if (bench_error) {
|
||||
log.console(` ERROR ${b.name}`)
|
||||
error_result = {
|
||||
package: pkg_result.package,
|
||||
name: b.name,
|
||||
error: e.toString()
|
||||
error: "benchmark disrupted"
|
||||
}
|
||||
push(file_result.benchmarks, error_result)
|
||||
pkg_result.total++
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
log.console(` Error loading ${f}: ${e}`)
|
||||
var error_result = {
|
||||
} disruption {
|
||||
load_error = true
|
||||
}
|
||||
_load_file()
|
||||
if (load_error) {
|
||||
log.console(` Error loading ${f}`)
|
||||
error_result = {
|
||||
package: pkg_result.package,
|
||||
name: "load_module",
|
||||
error: `Error loading module: ${e}`
|
||||
error: "error loading module"
|
||||
}
|
||||
push(file_result.benchmarks, error_result)
|
||||
pkg_result.total++
|
||||
@@ -478,15 +600,16 @@ function run_benchmarks(package_name, specific_bench) {
|
||||
|
||||
// Run all benchmarks
|
||||
var all_results = []
|
||||
var packages = null
|
||||
|
||||
if (all_pkgs) {
|
||||
if (testlib.is_valid_package('.')) {
|
||||
push(all_results, run_benchmarks(null, null))
|
||||
}
|
||||
|
||||
var packages = shop.list_packages()
|
||||
arrfor(packages, function(pkg) {
|
||||
push(all_results, run_benchmarks(pkg, null))
|
||||
packages = shop.list_packages()
|
||||
arrfor(packages, function(p) {
|
||||
push(all_results, run_benchmarks(p, null))
|
||||
})
|
||||
} else {
|
||||
push(all_results, run_benchmarks(target_pkg, target_bench))
|
||||
@@ -507,8 +630,10 @@ function generate_reports() {
|
||||
var report_dir = shop.get_reports_dir() + '/bench_' + timestamp
|
||||
testlib.ensure_dir(report_dir)
|
||||
|
||||
var mode_str = bench_mode == "compare" ? "bytecode vs native" : bench_mode
|
||||
var txt_report = `BENCHMARK REPORT
|
||||
Date: ${time.text(time.number())}
|
||||
Mode: ${mode_str}
|
||||
Total benchmarks: ${total_benches}
|
||||
|
||||
=== SUMMARY ===
|
||||
@@ -519,10 +644,11 @@ Total benchmarks: ${total_benches}
|
||||
arrfor(pkg_res.files, function(f) {
|
||||
txt_report += ` ${f.name}\n`
|
||||
arrfor(f.benchmarks, function(b) {
|
||||
var mode_tag = b.mode ? ` [${b.mode}]` : ''
|
||||
if (b.error) {
|
||||
txt_report += ` ERROR ${b.name}: ${b.error}\n`
|
||||
} else {
|
||||
txt_report += ` ${b.name}: ${format_ns(b.median_ns)}/op (${format_ops(b.ops_per_sec)})\n`
|
||||
txt_report += ` ${b.name}${mode_tag}: ${format_ns(b.median_ns)}/op (${format_ops(b.ops_per_sec)})\n`
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -536,7 +662,8 @@ Total benchmarks: ${total_benches}
|
||||
arrfor(f.benchmarks, function(b) {
|
||||
if (b.error) return
|
||||
|
||||
txt_report += `\n${pkg_res.package}::${b.name}\n`
|
||||
var detail_mode = b.mode ? ` [${b.mode}]` : ''
|
||||
txt_report += `\n${pkg_res.package}::${b.name}${detail_mode}\n`
|
||||
txt_report += ` batch_size: ${b.batch_size} samples: ${b.samples}\n`
|
||||
txt_report += ` median: ${format_ns(b.median_ns)}/op\n`
|
||||
txt_report += ` mean: ${format_ns(b.mean_ns)}/op\n`
|
||||
|
||||
232
benches/actor_patterns.cm
Normal file
232
benches/actor_patterns.cm
Normal file
@@ -0,0 +1,232 @@
|
||||
// actor_patterns.cm — Actor concurrency benchmarks
|
||||
// Message passing, fan-out/fan-in, mailbox throughput.
|
||||
// These use structured benchmarks with setup/run/teardown.
|
||||
|
||||
// Note: actor benchmarks are measured differently from pure compute.
|
||||
// Each iteration sends messages and waits for results, so they're
|
||||
// inherently slower but test real concurrency costs.
|
||||
|
||||
// Simple ping-pong: two actors sending messages back and forth
|
||||
// Since we can't create real actors from a module, we simulate
|
||||
// the message-passing patterns with function call overhead that
|
||||
// mirrors what the actor dispatch does.
|
||||
|
||||
// Simulate message dispatch overhead
|
||||
function make_mailbox() {
|
||||
return {
|
||||
queue: [],
|
||||
delivered: 0
|
||||
}
|
||||
}
|
||||
|
||||
function send(mailbox, msg) {
|
||||
push(mailbox.queue, msg)
|
||||
return null
|
||||
}
|
||||
|
||||
function receive(mailbox) {
|
||||
if (length(mailbox.queue) == 0) return null
|
||||
mailbox.delivered++
|
||||
return pop(mailbox.queue)
|
||||
}
|
||||
|
||||
function drain(mailbox) {
|
||||
var count = 0
|
||||
while (length(mailbox.queue) > 0) {
|
||||
pop(mailbox.queue)
|
||||
count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Ping-pong: simulate two actors exchanging messages
|
||||
function ping_pong(rounds) {
|
||||
var box_a = make_mailbox()
|
||||
var box_b = make_mailbox()
|
||||
var i = 0
|
||||
var msg = null
|
||||
|
||||
send(box_a, {type: "ping", val: 0})
|
||||
|
||||
for (i = 0; i < rounds; i++) {
|
||||
// A receives and sends to B
|
||||
msg = receive(box_a)
|
||||
if (msg) {
|
||||
send(box_b, {type: "pong", val: msg.val + 1})
|
||||
}
|
||||
// B receives and sends to A
|
||||
msg = receive(box_b)
|
||||
if (msg) {
|
||||
send(box_a, {type: "ping", val: msg.val + 1})
|
||||
}
|
||||
}
|
||||
|
||||
return box_a.delivered + box_b.delivered
|
||||
}
|
||||
|
||||
// Fan-out: one sender, N receivers
|
||||
function fan_out(n_receivers, messages_per) {
|
||||
var receivers = []
|
||||
var i = 0
|
||||
var j = 0
|
||||
for (i = 0; i < n_receivers; i++) {
|
||||
push(receivers, make_mailbox())
|
||||
}
|
||||
|
||||
// Send messages to all receivers
|
||||
for (j = 0; j < messages_per; j++) {
|
||||
for (i = 0; i < n_receivers; i++) {
|
||||
send(receivers[i], {seq: j, data: j * 17})
|
||||
}
|
||||
}
|
||||
|
||||
// All receivers drain
|
||||
var total = 0
|
||||
for (i = 0; i < n_receivers; i++) {
|
||||
total += drain(receivers[i])
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
// Fan-in: N senders, one receiver
|
||||
function fan_in(n_senders, messages_per) {
|
||||
var inbox = make_mailbox()
|
||||
var i = 0
|
||||
var j = 0
|
||||
|
||||
// Each sender sends messages
|
||||
for (i = 0; i < n_senders; i++) {
|
||||
for (j = 0; j < messages_per; j++) {
|
||||
send(inbox, {sender: i, seq: j, data: i * 100 + j})
|
||||
}
|
||||
}
|
||||
|
||||
// Receiver processes all
|
||||
var total = 0
|
||||
var msg = null
|
||||
msg = receive(inbox)
|
||||
while (msg) {
|
||||
total += msg.data
|
||||
msg = receive(inbox)
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
// Pipeline: chain of processors
|
||||
function pipeline(stages, items) {
|
||||
var boxes = []
|
||||
var i = 0
|
||||
var j = 0
|
||||
var msg = null
|
||||
|
||||
for (i = 0; i <= stages; i++) {
|
||||
push(boxes, make_mailbox())
|
||||
}
|
||||
|
||||
// Feed input
|
||||
for (i = 0; i < items; i++) {
|
||||
send(boxes[0], {val: i})
|
||||
}
|
||||
|
||||
// Process each stage
|
||||
for (j = 0; j < stages; j++) {
|
||||
msg = receive(boxes[j])
|
||||
while (msg) {
|
||||
send(boxes[j + 1], {val: msg.val * 2 + 1})
|
||||
msg = receive(boxes[j])
|
||||
}
|
||||
}
|
||||
|
||||
// Drain output
|
||||
var total = 0
|
||||
msg = receive(boxes[stages])
|
||||
while (msg) {
|
||||
total += msg.val
|
||||
msg = receive(boxes[stages])
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
// Request-response pattern (simulate RPC)
|
||||
function request_response(n_requests) {
|
||||
var client_box = make_mailbox()
|
||||
var server_box = make_mailbox()
|
||||
var i = 0
|
||||
var req = null
|
||||
var resp = null
|
||||
var total = 0
|
||||
|
||||
for (i = 0; i < n_requests; i++) {
|
||||
// Client sends request
|
||||
send(server_box, {id: i, payload: i * 3, reply_to: client_box})
|
||||
|
||||
// Server processes
|
||||
req = receive(server_box)
|
||||
if (req) {
|
||||
send(req.reply_to, {id: req.id, result: req.payload * 2 + 1})
|
||||
}
|
||||
|
||||
// Client receives response
|
||||
resp = receive(client_box)
|
||||
if (resp) {
|
||||
total += resp.result
|
||||
}
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
return {
|
||||
// Ping-pong: 10K rounds
|
||||
ping_pong_10k: function(n) {
|
||||
var i = 0
|
||||
var x = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
x += ping_pong(10000)
|
||||
}
|
||||
return x
|
||||
},
|
||||
|
||||
// Fan-out: 100 receivers, 100 messages each
|
||||
fan_out_100x100: function(n) {
|
||||
var i = 0
|
||||
var x = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
x += fan_out(100, 100)
|
||||
}
|
||||
return x
|
||||
},
|
||||
|
||||
// Fan-in: 100 senders, 100 messages each
|
||||
fan_in_100x100: function(n) {
|
||||
var i = 0
|
||||
var x = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
x += fan_in(100, 100)
|
||||
}
|
||||
return x
|
||||
},
|
||||
|
||||
// Pipeline: 10 stages, 1000 items
|
||||
pipeline_10x1k: function(n) {
|
||||
var i = 0
|
||||
var x = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
x += pipeline(10, 1000)
|
||||
}
|
||||
return x
|
||||
},
|
||||
|
||||
// Request-response: 5K requests
|
||||
rpc_5k: function(n) {
|
||||
var i = 0
|
||||
var x = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
x += request_response(5000)
|
||||
}
|
||||
return x
|
||||
}
|
||||
}
|
||||
141
benches/cli_tool.cm
Normal file
141
benches/cli_tool.cm
Normal file
@@ -0,0 +1,141 @@
|
||||
// cli_tool.cm — CLI tool simulation (macro benchmark)
|
||||
// Parse args + process data + transform + format output.
|
||||
// Simulates a realistic small utility program.
|
||||
|
||||
var json = use('json')
|
||||
|
||||
// Generate fake records
|
||||
function generate_records(n) {
|
||||
var records = []
|
||||
var x = 42
|
||||
var i = 0
|
||||
var status_vals = ["active", "inactive", "pending", "archived"]
|
||||
var dept_vals = ["eng", "sales", "ops", "hr", "marketing"]
|
||||
for (i = 0; i < n; i++) {
|
||||
x = ((x * 1103515245 + 12345) & 0x7FFFFFFF) | 0
|
||||
push(records, {
|
||||
id: i + 1,
|
||||
name: `user_${i}`,
|
||||
score: (x % 1000) / 10,
|
||||
status: status_vals[i % 4],
|
||||
department: dept_vals[i % 5]
|
||||
})
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
// Filter records by field value
|
||||
function filter_records(records, field, value) {
|
||||
var result = []
|
||||
var i = 0
|
||||
for (i = 0; i < length(records); i++) {
|
||||
if (records[i][field] == value) {
|
||||
push(result, records[i])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Group by a field
|
||||
function group_by(records, field) {
|
||||
var groups = {}
|
||||
var i = 0
|
||||
var key = null
|
||||
for (i = 0; i < length(records); i++) {
|
||||
key = records[i][field]
|
||||
if (!key) key = "unknown"
|
||||
if (!groups[key]) groups[key] = []
|
||||
push(groups[key], records[i])
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
// Aggregate: compute stats per group
|
||||
function aggregate(groups) {
|
||||
var keys = array(groups)
|
||||
var result = []
|
||||
var i = 0
|
||||
var j = 0
|
||||
var grp = null
|
||||
var total = 0
|
||||
var mn = 0
|
||||
var mx = 0
|
||||
for (i = 0; i < length(keys); i++) {
|
||||
grp = groups[keys[i]]
|
||||
total = 0
|
||||
mn = 999999
|
||||
mx = 0
|
||||
for (j = 0; j < length(grp); j++) {
|
||||
total += grp[j].score
|
||||
if (grp[j].score < mn) mn = grp[j].score
|
||||
if (grp[j].score > mx) mx = grp[j].score
|
||||
}
|
||||
push(result, {
|
||||
group: keys[i],
|
||||
count: length(grp),
|
||||
average: total / length(grp),
|
||||
low: mn,
|
||||
high: mx
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Full pipeline: load → filter → sort → group → aggregate → encode
|
||||
function run_pipeline(n_records) {
|
||||
// Generate data
|
||||
var records = generate_records(n_records)
|
||||
|
||||
// Filter to active records
|
||||
var filtered = filter_records(records, "status", "active")
|
||||
|
||||
// Sort by score
|
||||
filtered = sort(filtered, "score")
|
||||
|
||||
// Limit to first 50
|
||||
if (length(filtered) > 50) {
|
||||
filtered = array(filtered, 0, 50)
|
||||
}
|
||||
|
||||
// Group and aggregate
|
||||
var groups = group_by(filtered, "department")
|
||||
var stats = aggregate(groups)
|
||||
stats = sort(stats, "average")
|
||||
|
||||
// Encode as JSON
|
||||
var output = json.encode(stats)
|
||||
|
||||
return length(output)
|
||||
}
|
||||
|
||||
return {
|
||||
// Small dataset (100 records)
|
||||
cli_pipeline_100: function(n) {
|
||||
var i = 0
|
||||
var x = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
x += run_pipeline(100)
|
||||
}
|
||||
return x
|
||||
},
|
||||
|
||||
// Medium dataset (1000 records)
|
||||
cli_pipeline_1k: function(n) {
|
||||
var i = 0
|
||||
var x = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
x += run_pipeline(1000)
|
||||
}
|
||||
return x
|
||||
},
|
||||
|
||||
// Large dataset (10K records)
|
||||
cli_pipeline_10k: function(n) {
|
||||
var i = 0
|
||||
var x = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
x += run_pipeline(10000)
|
||||
}
|
||||
return x
|
||||
}
|
||||
}
|
||||
162
benches/deltablue.cm
Normal file
162
benches/deltablue.cm
Normal file
@@ -0,0 +1,162 @@
|
||||
// deltablue.cm — Constraint solver kernel (DeltaBlue-inspired)
|
||||
// Dynamic dispatch, pointer chasing, object-heavy workload.
|
||||
|
||||
def REQUIRED = 0
|
||||
def STRONG = 1
|
||||
def NORMAL = 2
|
||||
def WEAK = 3
|
||||
def WEAKEST = 4
|
||||
|
||||
function make_variable(name, value) {
|
||||
return {
|
||||
name: name,
|
||||
value: value,
|
||||
constraints: [],
|
||||
determined_by: null,
|
||||
stay: true,
|
||||
mark: 0
|
||||
}
|
||||
}
|
||||
|
||||
function make_constraint(strength, variables, satisfy_fn) {
|
||||
return {
|
||||
strength: strength,
|
||||
variables: variables,
|
||||
satisfy: satisfy_fn,
|
||||
output: null
|
||||
}
|
||||
}
|
||||
|
||||
// Constraint propagation: simple forward solver
|
||||
function propagate(vars, constraints) {
|
||||
var changed = true
|
||||
var passes = 0
|
||||
var max_passes = length(constraints) * 3
|
||||
var i = 0
|
||||
var c = null
|
||||
var old_val = 0
|
||||
|
||||
while (changed && passes < max_passes) {
|
||||
changed = false
|
||||
passes++
|
||||
for (i = 0; i < length(constraints); i++) {
|
||||
c = constraints[i]
|
||||
old_val = c.output ? c.output.value : null
|
||||
c.satisfy(c)
|
||||
if (c.output && c.output.value != old_val) {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return passes
|
||||
}
|
||||
|
||||
// Build a chain of equality constraints: v[i] = v[i-1] + 1
|
||||
function build_chain(n) {
|
||||
var vars = []
|
||||
var constraints = []
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
push(vars, make_variable(`v${i}`, 0))
|
||||
}
|
||||
|
||||
// Set first variable
|
||||
vars[0].value = 1
|
||||
|
||||
var c = null
|
||||
for (i = 1; i < n; i++) {
|
||||
c = make_constraint(NORMAL, [vars[i - 1], vars[i]], function(self) {
|
||||
self.variables[1].value = self.variables[0].value + 1
|
||||
self.output = self.variables[1]
|
||||
})
|
||||
push(constraints, c)
|
||||
push(vars[i].constraints, c)
|
||||
}
|
||||
|
||||
return {vars: vars, constraints: constraints}
|
||||
}
|
||||
|
||||
// Build a projection: pairs of variables with scaling constraints
|
||||
function build_projection(n) {
|
||||
var src = []
|
||||
var dst = []
|
||||
var constraints = []
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
push(src, make_variable(`src${i}`, i * 10))
|
||||
push(dst, make_variable(`dst${i}`, 0))
|
||||
}
|
||||
|
||||
var scale_c = null
|
||||
for (i = 0; i < n; i++) {
|
||||
scale_c = make_constraint(STRONG, [src[i], dst[i]], function(self) {
|
||||
self.variables[1].value = self.variables[0].value * 2 + 1
|
||||
self.output = self.variables[1]
|
||||
})
|
||||
push(constraints, scale_c)
|
||||
push(dst[i].constraints, scale_c)
|
||||
}
|
||||
|
||||
return {src: src, dst: dst, constraints: constraints}
|
||||
}
|
||||
|
||||
// Edit constraint: change a source, re-propagate
|
||||
function run_edits(system, edits) {
|
||||
var i = 0
|
||||
var total_passes = 0
|
||||
for (i = 0; i < edits; i++) {
|
||||
system.vars[0].value = i
|
||||
total_passes += propagate(system.vars, system.constraints)
|
||||
}
|
||||
return total_passes
|
||||
}
|
||||
|
||||
return {
|
||||
// Chain of 100 variables, propagate
|
||||
chain_100: function(n) {
|
||||
var i = 0
|
||||
var chain = null
|
||||
var x = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
chain = build_chain(100)
|
||||
x += propagate(chain.vars, chain.constraints)
|
||||
}
|
||||
return x
|
||||
},
|
||||
|
||||
// Chain of 500 variables, propagate
|
||||
chain_500: function(n) {
|
||||
var i = 0
|
||||
var chain = null
|
||||
var x = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
chain = build_chain(500)
|
||||
x += propagate(chain.vars, chain.constraints)
|
||||
}
|
||||
return x
|
||||
},
|
||||
|
||||
// Projection of 100 pairs
|
||||
projection_100: function(n) {
|
||||
var i = 0
|
||||
var proj = null
|
||||
var x = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
proj = build_projection(100)
|
||||
x += propagate(proj.src, proj.constraints)
|
||||
}
|
||||
return x
|
||||
},
|
||||
|
||||
// Edit and re-propagate (incremental update)
|
||||
chain_edit_100: function(n) {
|
||||
var chain = build_chain(100)
|
||||
var i = 0
|
||||
var x = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
chain.vars[0].value = i
|
||||
x += propagate(chain.vars, chain.constraints)
|
||||
}
|
||||
return x
|
||||
}
|
||||
}
|
||||
405
benches/encoders.cm
Normal file
405
benches/encoders.cm
Normal file
@@ -0,0 +1,405 @@
|
||||
// encoders.cm — nota/wota/json encode+decode benchmark
|
||||
// Isolates per-type bottlenecks across all three serializers.
|
||||
|
||||
var nota = use('internal/nota')
|
||||
var wota = use('internal/wota')
|
||||
var json = use('json')
|
||||
|
||||
// --- Test data shapes ---
|
||||
|
||||
// Small integers: fast path for all encoders
|
||||
var integers_small = array(100, function(i) { return i + 1 })
|
||||
|
||||
// Floats: stresses nota's snprintf path
|
||||
var floats_array = array(100, function(i) {
|
||||
return 3.14159 * (i + 1) + 0.00001 * i
|
||||
})
|
||||
|
||||
// Short strings in records: per-string overhead, property enumeration
|
||||
var strings_short = array(50, function(i) {
|
||||
var r = {}
|
||||
r[`k${i}`] = `value_${i}`
|
||||
return r
|
||||
})
|
||||
|
||||
// Single long string: throughput test (wota byte loop, nota kim)
|
||||
var long_str = ""
|
||||
var li = 0
|
||||
for (li = 0; li < 100; li++) {
|
||||
long_str = `${long_str}abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMN`
|
||||
}
|
||||
var strings_long = long_str
|
||||
|
||||
// Unicode text: nota's kim encoding, wota's byte packing
|
||||
var strings_unicode = "こんにちは世界 🌍🌎🌏 Ñoño café résumé naïve Ω∑∏ 你好世界"
|
||||
|
||||
// Nested records: cycle detection, property enumeration
|
||||
function make_nested(depth, breadth) {
|
||||
var obj = {}
|
||||
var i = 0
|
||||
var k = null
|
||||
if (depth <= 0) {
|
||||
for (i = 0; i < breadth; i++) {
|
||||
k = `v${i}`
|
||||
obj[k] = i * 2.5
|
||||
}
|
||||
return obj
|
||||
}
|
||||
for (i = 0; i < breadth; i++) {
|
||||
k = `n${i}`
|
||||
obj[k] = make_nested(depth - 1, breadth)
|
||||
}
|
||||
return obj
|
||||
}
|
||||
var nested_records = make_nested(3, 4)
|
||||
|
||||
// Flat record: property enumeration cost
|
||||
var flat_record = {}
|
||||
var fi = 0
|
||||
for (fi = 0; fi < 50; fi++) {
|
||||
flat_record[`prop_${fi}`] = fi * 1.1
|
||||
}
|
||||
|
||||
// Mixed payload: realistic workload
|
||||
var mixed_payload = array(50, function(i) {
|
||||
var r = {}
|
||||
r.id = i
|
||||
r.name = `item_${i}`
|
||||
r.active = i % 2 == 0
|
||||
r.score = i * 3.14
|
||||
r.tags = [`t${i % 5}`, `t${(i + 1) % 5}`]
|
||||
return r
|
||||
})
|
||||
|
||||
// --- Pre-encode for decode benchmarks ---
|
||||
|
||||
var nota_enc_integers = nota.encode(integers_small)
|
||||
var nota_enc_floats = nota.encode(floats_array)
|
||||
var nota_enc_strings_short = nota.encode(strings_short)
|
||||
var nota_enc_strings_long = nota.encode(strings_long)
|
||||
var nota_enc_strings_unicode = nota.encode(strings_unicode)
|
||||
var nota_enc_nested = nota.encode(nested_records)
|
||||
var nota_enc_flat = nota.encode(flat_record)
|
||||
var nota_enc_mixed = nota.encode(mixed_payload)
|
||||
|
||||
var wota_enc_integers = wota.encode(integers_small)
|
||||
var wota_enc_floats = wota.encode(floats_array)
|
||||
var wota_enc_strings_short = wota.encode(strings_short)
|
||||
var wota_enc_strings_long = wota.encode(strings_long)
|
||||
var wota_enc_strings_unicode = wota.encode(strings_unicode)
|
||||
var wota_enc_nested = wota.encode(nested_records)
|
||||
var wota_enc_flat = wota.encode(flat_record)
|
||||
var wota_enc_mixed = wota.encode(mixed_payload)
|
||||
|
||||
var json_enc_integers = json.encode(integers_small)
|
||||
var json_enc_floats = json.encode(floats_array)
|
||||
var json_enc_strings_short = json.encode(strings_short)
|
||||
var json_enc_strings_long = json.encode(strings_long)
|
||||
var json_enc_strings_unicode = json.encode(strings_unicode)
|
||||
var json_enc_nested = json.encode(nested_records)
|
||||
var json_enc_flat = json.encode(flat_record)
|
||||
var json_enc_mixed = json.encode(mixed_payload)
|
||||
|
||||
// --- Benchmark functions ---
|
||||
|
||||
return {
|
||||
// NOTA encode
|
||||
nota_encode_integers: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = nota.encode(integers_small) }
|
||||
return r
|
||||
},
|
||||
nota_encode_floats: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = nota.encode(floats_array) }
|
||||
return r
|
||||
},
|
||||
nota_encode_strings_short: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = nota.encode(strings_short) }
|
||||
return r
|
||||
},
|
||||
nota_encode_strings_long: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = nota.encode(strings_long) }
|
||||
return r
|
||||
},
|
||||
nota_encode_strings_unicode: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = nota.encode(strings_unicode) }
|
||||
return r
|
||||
},
|
||||
nota_encode_nested: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = nota.encode(nested_records) }
|
||||
return r
|
||||
},
|
||||
nota_encode_flat: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = nota.encode(flat_record) }
|
||||
return r
|
||||
},
|
||||
nota_encode_mixed: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = nota.encode(mixed_payload) }
|
||||
return r
|
||||
},
|
||||
|
||||
// NOTA decode
|
||||
nota_decode_integers: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = nota.decode(nota_enc_integers) }
|
||||
return r
|
||||
},
|
||||
nota_decode_floats: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = nota.decode(nota_enc_floats) }
|
||||
return r
|
||||
},
|
||||
nota_decode_strings_short: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = nota.decode(nota_enc_strings_short) }
|
||||
return r
|
||||
},
|
||||
nota_decode_strings_long: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = nota.decode(nota_enc_strings_long) }
|
||||
return r
|
||||
},
|
||||
nota_decode_strings_unicode: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = nota.decode(nota_enc_strings_unicode) }
|
||||
return r
|
||||
},
|
||||
nota_decode_nested: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = nota.decode(nota_enc_nested) }
|
||||
return r
|
||||
},
|
||||
nota_decode_flat: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = nota.decode(nota_enc_flat) }
|
||||
return r
|
||||
},
|
||||
nota_decode_mixed: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = nota.decode(nota_enc_mixed) }
|
||||
return r
|
||||
},
|
||||
|
||||
// WOTA encode
|
||||
wota_encode_integers: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = wota.encode(integers_small) }
|
||||
return r
|
||||
},
|
||||
wota_encode_floats: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = wota.encode(floats_array) }
|
||||
return r
|
||||
},
|
||||
wota_encode_strings_short: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = wota.encode(strings_short) }
|
||||
return r
|
||||
},
|
||||
wota_encode_strings_long: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = wota.encode(strings_long) }
|
||||
return r
|
||||
},
|
||||
wota_encode_strings_unicode: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = wota.encode(strings_unicode) }
|
||||
return r
|
||||
},
|
||||
wota_encode_nested: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = wota.encode(nested_records) }
|
||||
return r
|
||||
},
|
||||
wota_encode_flat: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = wota.encode(flat_record) }
|
||||
return r
|
||||
},
|
||||
wota_encode_mixed: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = wota.encode(mixed_payload) }
|
||||
return r
|
||||
},
|
||||
|
||||
// WOTA decode
|
||||
wota_decode_integers: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = wota.decode(wota_enc_integers) }
|
||||
return r
|
||||
},
|
||||
wota_decode_floats: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = wota.decode(wota_enc_floats) }
|
||||
return r
|
||||
},
|
||||
wota_decode_strings_short: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = wota.decode(wota_enc_strings_short) }
|
||||
return r
|
||||
},
|
||||
wota_decode_strings_long: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = wota.decode(wota_enc_strings_long) }
|
||||
return r
|
||||
},
|
||||
wota_decode_strings_unicode: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = wota.decode(wota_enc_strings_unicode) }
|
||||
return r
|
||||
},
|
||||
wota_decode_nested: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = wota.decode(wota_enc_nested) }
|
||||
return r
|
||||
},
|
||||
wota_decode_flat: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = wota.decode(wota_enc_flat) }
|
||||
return r
|
||||
},
|
||||
wota_decode_mixed: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = wota.decode(wota_enc_mixed) }
|
||||
return r
|
||||
},
|
||||
|
||||
// JSON encode
|
||||
json_encode_integers: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = json.encode(integers_small) }
|
||||
return r
|
||||
},
|
||||
json_encode_floats: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = json.encode(floats_array) }
|
||||
return r
|
||||
},
|
||||
json_encode_strings_short: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = json.encode(strings_short) }
|
||||
return r
|
||||
},
|
||||
json_encode_strings_long: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = json.encode(strings_long) }
|
||||
return r
|
||||
},
|
||||
json_encode_strings_unicode: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = json.encode(strings_unicode) }
|
||||
return r
|
||||
},
|
||||
json_encode_nested: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = json.encode(nested_records) }
|
||||
return r
|
||||
},
|
||||
json_encode_flat: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = json.encode(flat_record) }
|
||||
return r
|
||||
},
|
||||
json_encode_mixed: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = json.encode(mixed_payload) }
|
||||
return r
|
||||
},
|
||||
|
||||
// JSON decode
|
||||
json_decode_integers: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = json.decode(json_enc_integers) }
|
||||
return r
|
||||
},
|
||||
json_decode_floats: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = json.decode(json_enc_floats) }
|
||||
return r
|
||||
},
|
||||
json_decode_strings_short: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = json.decode(json_enc_strings_short) }
|
||||
return r
|
||||
},
|
||||
json_decode_strings_long: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = json.decode(json_enc_strings_long) }
|
||||
return r
|
||||
},
|
||||
json_decode_strings_unicode: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = json.decode(json_enc_strings_unicode) }
|
||||
return r
|
||||
},
|
||||
json_decode_nested: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = json.decode(json_enc_nested) }
|
||||
return r
|
||||
},
|
||||
json_decode_flat: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = json.decode(json_enc_flat) }
|
||||
return r
|
||||
},
|
||||
json_decode_mixed: function(n) {
|
||||
var i = 0
|
||||
var r = null
|
||||
for (i = 0; i < n; i++) { r = json.decode(json_enc_mixed) }
|
||||
return r
|
||||
}
|
||||
}
|
||||
126
benches/fibonacci.cm
Normal file
126
benches/fibonacci.cm
Normal file
@@ -0,0 +1,126 @@
|
||||
// fibonacci.cm — Fibonacci variants kernel
|
||||
// Tests recursion overhead, memoization patterns, iteration vs recursion.
|
||||
|
||||
// Naive recursive (exponential) — measures call overhead
|
||||
function fib_naive(n) {
|
||||
if (n <= 1) return n
|
||||
return fib_naive(n - 1) + fib_naive(n - 2)
|
||||
}
|
||||
|
||||
// Iterative (linear)
|
||||
function fib_iter(n) {
|
||||
var a = 0
|
||||
var b = 1
|
||||
var i = 0
|
||||
var tmp = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
tmp = a + b
|
||||
a = b
|
||||
b = tmp
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// Memoized recursive (tests object property lookup + recursion)
|
||||
function make_memo_fib() {
|
||||
var cache = {}
|
||||
var fib = function(n) {
|
||||
var key = text(n)
|
||||
if (cache[key]) return cache[key]
|
||||
var result = null
|
||||
if (n <= 1) {
|
||||
result = n
|
||||
} else {
|
||||
result = fib(n - 1) + fib(n - 2)
|
||||
}
|
||||
cache[key] = result
|
||||
return result
|
||||
}
|
||||
return fib
|
||||
}
|
||||
|
||||
// CPS (continuation passing style) — tests closure creation
|
||||
function fib_cps(n, cont) {
|
||||
if (n <= 1) return cont(n)
|
||||
return fib_cps(n - 1, function(a) {
|
||||
return fib_cps(n - 2, function(b) {
|
||||
return cont(a + b)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Matrix exponentiation style (accumulator)
|
||||
function fib_matrix(n) {
|
||||
var a = 1
|
||||
var b = 0
|
||||
var c = 0
|
||||
var d = 1
|
||||
var ta = 0
|
||||
var tb = 0
|
||||
var m = n
|
||||
while (m > 0) {
|
||||
if (m % 2 == 1) {
|
||||
ta = a * d + b * c // wrong but stresses numeric ops
|
||||
tb = b * d + a * c
|
||||
a = ta
|
||||
b = tb
|
||||
}
|
||||
ta = c * c + d * d
|
||||
tb = d * (2 * c + d)
|
||||
c = ta
|
||||
d = tb
|
||||
m = floor(m / 2)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
return {
|
||||
fib_naive_25: function(n) {
|
||||
var i = 0
|
||||
var x = 0
|
||||
for (i = 0; i < n; i++) x += fib_naive(25)
|
||||
return x
|
||||
},
|
||||
|
||||
fib_naive_30: function(n) {
|
||||
var i = 0
|
||||
var x = 0
|
||||
for (i = 0; i < n; i++) x += fib_naive(30)
|
||||
return x
|
||||
},
|
||||
|
||||
fib_iter_80: function(n) {
|
||||
var i = 0
|
||||
var x = 0
|
||||
for (i = 0; i < n; i++) x += fib_iter(80)
|
||||
return x
|
||||
},
|
||||
|
||||
fib_memo_100: function(n) {
|
||||
var i = 0
|
||||
var x = 0
|
||||
var fib = null
|
||||
for (i = 0; i < n; i++) {
|
||||
fib = make_memo_fib()
|
||||
x += fib(100)
|
||||
}
|
||||
return x
|
||||
},
|
||||
|
||||
fib_cps_20: function(n) {
|
||||
var i = 0
|
||||
var x = 0
|
||||
var identity = function(v) { return v }
|
||||
for (i = 0; i < n; i++) {
|
||||
x += fib_cps(20, identity)
|
||||
}
|
||||
return x
|
||||
},
|
||||
|
||||
fib_matrix_80: function(n) {
|
||||
var i = 0
|
||||
var x = 0
|
||||
for (i = 0; i < n; i++) x += fib_matrix(80)
|
||||
return x
|
||||
}
|
||||
}
|
||||
159
benches/hash_workload.cm
Normal file
159
benches/hash_workload.cm
Normal file
@@ -0,0 +1,159 @@
|
||||
// hash_workload.cm — Hash-heavy / word-count / map-reduce kernel
|
||||
// Stresses record (object) creation, property access, and string handling.
|
||||
|
||||
function make_words(count) {
|
||||
// Generate a repeating word list to simulate text processing
|
||||
var base_words = [
|
||||
"the", "quick", "brown", "fox", "jumps", "over", "lazy", "dog",
|
||||
"and", "cat", "sat", "on", "mat", "with", "hat", "bat",
|
||||
"alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta",
|
||||
"hello", "world", "foo", "bar", "baz", "qux", "quux", "corge"
|
||||
]
|
||||
var words = []
|
||||
var i = 0
|
||||
for (i = 0; i < count; i++) {
|
||||
push(words, base_words[i % length(base_words)])
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
// Word frequency count
|
||||
function word_count(words) {
|
||||
var freq = {}
|
||||
var i = 0
|
||||
var w = null
|
||||
for (i = 0; i < length(words); i++) {
|
||||
w = words[i]
|
||||
if (freq[w]) {
|
||||
freq[w] = freq[w] + 1
|
||||
} else {
|
||||
freq[w] = 1
|
||||
}
|
||||
}
|
||||
return freq
|
||||
}
|
||||
|
||||
// Find top-N words by frequency
|
||||
function top_n(freq, n) {
|
||||
var keys = array(freq)
|
||||
var pairs = []
|
||||
var i = 0
|
||||
for (i = 0; i < length(keys); i++) {
|
||||
push(pairs, {word: keys[i], count: freq[keys[i]]})
|
||||
}
|
||||
var sorted = sort(pairs, "count")
|
||||
// Return last N (highest counts)
|
||||
var result = []
|
||||
var start = length(sorted) - n
|
||||
if (start < 0) start = 0
|
||||
for (i = start; i < length(sorted); i++) {
|
||||
push(result, sorted[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Histogram: group words by length
|
||||
function group_by_length(words) {
|
||||
var groups = {}
|
||||
var i = 0
|
||||
var w = null
|
||||
var k = null
|
||||
for (i = 0; i < length(words); i++) {
|
||||
w = words[i]
|
||||
k = text(length(w))
|
||||
if (!groups[k]) groups[k] = []
|
||||
push(groups[k], w)
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
// Simple hash table with chaining (stress property access patterns)
|
||||
function hash_table_ops(n) {
|
||||
var table = {}
|
||||
var i = 0
|
||||
var k = null
|
||||
var collisions = 0
|
||||
|
||||
// Insert phase
|
||||
for (i = 0; i < n; i++) {
|
||||
k = `key_${i % 512}`
|
||||
if (table[k]) collisions++
|
||||
table[k] = i
|
||||
}
|
||||
|
||||
// Lookup phase
|
||||
var found = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
k = `key_${i % 512}`
|
||||
if (table[k]) found++
|
||||
}
|
||||
|
||||
// Delete phase
|
||||
var deleted = 0
|
||||
for (i = 0; i < n; i += 3) {
|
||||
k = `key_${i % 512}`
|
||||
if (table[k]) {
|
||||
delete table[k]
|
||||
deleted++
|
||||
}
|
||||
}
|
||||
|
||||
return found - deleted + collisions
|
||||
}
|
||||
|
||||
var words_1k = make_words(1000)
|
||||
var words_10k = make_words(10000)
|
||||
|
||||
return {
|
||||
// Word count on 1K words
|
||||
wordcount_1k: function(n) {
|
||||
var i = 0
|
||||
var freq = null
|
||||
for (i = 0; i < n; i++) {
|
||||
freq = word_count(words_1k)
|
||||
}
|
||||
return freq
|
||||
},
|
||||
|
||||
// Word count on 10K words
|
||||
wordcount_10k: function(n) {
|
||||
var i = 0
|
||||
var freq = null
|
||||
for (i = 0; i < n; i++) {
|
||||
freq = word_count(words_10k)
|
||||
}
|
||||
return freq
|
||||
},
|
||||
|
||||
// Word count + top-10 extraction
|
||||
wordcount_top10: function(n) {
|
||||
var i = 0
|
||||
var freq = null
|
||||
var top = null
|
||||
for (i = 0; i < n; i++) {
|
||||
freq = word_count(words_10k)
|
||||
top = top_n(freq, 10)
|
||||
}
|
||||
return top
|
||||
},
|
||||
|
||||
// Group words by length
|
||||
group_by_len: function(n) {
|
||||
var i = 0
|
||||
var groups = null
|
||||
for (i = 0; i < n; i++) {
|
||||
groups = group_by_length(words_10k)
|
||||
}
|
||||
return groups
|
||||
},
|
||||
|
||||
// Hash table insert/lookup/delete
|
||||
hash_table: function(n) {
|
||||
var i = 0
|
||||
var x = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
x += hash_table_ops(2048)
|
||||
}
|
||||
return x
|
||||
}
|
||||
}
|
||||
167
benches/json_walk.cm
Normal file
167
benches/json_walk.cm
Normal file
@@ -0,0 +1,167 @@
|
||||
// json_walk.cm — JSON parse + walk + serialize kernel
|
||||
// Stresses strings, records, arrays, and recursive traversal.
|
||||
|
||||
var json = use('json')
|
||||
|
||||
function make_nested_object(depth, breadth) {
|
||||
var obj = {}
|
||||
var i = 0
|
||||
var k = null
|
||||
if (depth <= 0) {
|
||||
for (i = 0; i < breadth; i++) {
|
||||
k = `key_${i}`
|
||||
obj[k] = i * 3.14
|
||||
}
|
||||
return obj
|
||||
}
|
||||
for (i = 0; i < breadth; i++) {
|
||||
k = `node_${i}`
|
||||
obj[k] = make_nested_object(depth - 1, breadth)
|
||||
}
|
||||
obj.value = depth
|
||||
obj.name = `level_${depth}`
|
||||
return obj
|
||||
}
|
||||
|
||||
function make_array_data(size) {
|
||||
var arr = []
|
||||
var i = 0
|
||||
for (i = 0; i < size; i++) {
|
||||
push(arr, {
|
||||
id: i,
|
||||
name: `item_${i}`,
|
||||
active: i % 2 == 0,
|
||||
score: i * 1.5,
|
||||
tags: [`tag_${i % 5}`, `tag_${(i + 1) % 5}`]
|
||||
})
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// Walk an object tree, counting nodes
|
||||
function walk_count(obj) {
|
||||
var count = 1
|
||||
var keys = null
|
||||
var i = 0
|
||||
var v = null
|
||||
if (is_object(obj)) {
|
||||
keys = array(obj)
|
||||
for (i = 0; i < length(keys); i++) {
|
||||
v = obj[keys[i]]
|
||||
if (is_object(v) || is_array(v)) {
|
||||
count += walk_count(v)
|
||||
}
|
||||
}
|
||||
} else if (is_array(obj)) {
|
||||
for (i = 0; i < length(obj); i++) {
|
||||
v = obj[i]
|
||||
if (is_object(v) || is_array(v)) {
|
||||
count += walk_count(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Walk and extract all numbers
|
||||
function walk_sum(obj) {
|
||||
var sum = 0
|
||||
var keys = null
|
||||
var i = 0
|
||||
var v = null
|
||||
if (is_object(obj)) {
|
||||
keys = array(obj)
|
||||
for (i = 0; i < length(keys); i++) {
|
||||
v = obj[keys[i]]
|
||||
if (is_number(v)) {
|
||||
sum += v
|
||||
} else if (is_object(v) || is_array(v)) {
|
||||
sum += walk_sum(v)
|
||||
}
|
||||
}
|
||||
} else if (is_array(obj)) {
|
||||
for (i = 0; i < length(obj); i++) {
|
||||
v = obj[i]
|
||||
if (is_number(v)) {
|
||||
sum += v
|
||||
} else if (is_object(v) || is_array(v)) {
|
||||
sum += walk_sum(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
// Pre-build test data strings
|
||||
var nested_obj = make_nested_object(3, 4)
|
||||
var nested_json = json.encode(nested_obj)
|
||||
var array_data = make_array_data(200)
|
||||
var array_json = json.encode(array_data)
|
||||
|
||||
return {
|
||||
// Parse nested JSON
|
||||
json_parse_nested: function(n) {
|
||||
var i = 0
|
||||
var obj = null
|
||||
for (i = 0; i < n; i++) {
|
||||
obj = json.decode(nested_json)
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
// Parse array-of-records JSON
|
||||
json_parse_array: function(n) {
|
||||
var i = 0
|
||||
var arr = null
|
||||
for (i = 0; i < n; i++) {
|
||||
arr = json.decode(array_json)
|
||||
}
|
||||
return arr
|
||||
},
|
||||
|
||||
// Encode nested object to JSON
|
||||
json_encode_nested: function(n) {
|
||||
var i = 0
|
||||
var s = null
|
||||
for (i = 0; i < n; i++) {
|
||||
s = json.encode(nested_obj)
|
||||
}
|
||||
return s
|
||||
},
|
||||
|
||||
// Encode array to JSON
|
||||
json_encode_array: function(n) {
|
||||
var i = 0
|
||||
var s = null
|
||||
for (i = 0; i < n; i++) {
|
||||
s = json.encode(array_data)
|
||||
}
|
||||
return s
|
||||
},
|
||||
|
||||
// Parse + walk + count
|
||||
json_roundtrip_walk: function(n) {
|
||||
var i = 0
|
||||
var obj = null
|
||||
var count = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
obj = json.decode(nested_json)
|
||||
count += walk_count(obj)
|
||||
}
|
||||
return count
|
||||
},
|
||||
|
||||
// Parse + sum all numbers + re-encode
|
||||
json_roundtrip_full: function(n) {
|
||||
var i = 0
|
||||
var obj = null
|
||||
var sum = 0
|
||||
var out = null
|
||||
for (i = 0; i < n; i++) {
|
||||
obj = json.decode(array_json)
|
||||
sum += walk_sum(obj)
|
||||
out = json.encode(obj)
|
||||
}
|
||||
return sum
|
||||
}
|
||||
}
|
||||
302
benches/micro_core.cm
Normal file
302
benches/micro_core.cm
Normal file
@@ -0,0 +1,302 @@
|
||||
// micro_core.cm — direct microbenchmarks for core ops
|
||||
|
||||
function blackhole(sink, x) {
|
||||
return (sink + (x | 0)) | 0
|
||||
}
|
||||
|
||||
function make_obj_xy(x, y) {
|
||||
return {x: x, y: y}
|
||||
}
|
||||
|
||||
function make_obj_yx(x, y) {
|
||||
// Different insertion order to force a different shape
|
||||
return {y: y, x: x}
|
||||
}
|
||||
|
||||
function make_packed_array(n) {
|
||||
var a = []
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) push(a, i)
|
||||
return a
|
||||
}
|
||||
|
||||
function make_holey_array(n) {
|
||||
var a = []
|
||||
var i = 0
|
||||
for (i = 0; i < n; i += 2) a[i] = i
|
||||
return a
|
||||
}
|
||||
|
||||
return {
|
||||
loop_empty: function(n) {
|
||||
var sink = 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) {}
|
||||
return blackhole(sink, n)
|
||||
},
|
||||
|
||||
i32_add: function(n) {
|
||||
var sink = 0
|
||||
var x = 1
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = (x + 3) | 0
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
f64_add: function(n) {
|
||||
var sink = 0
|
||||
var x = 1.0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = x + 3.14159
|
||||
return blackhole(sink, x | 0)
|
||||
},
|
||||
|
||||
mixed_add: function(n) {
|
||||
var sink = 0
|
||||
var x = 1
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = x + 0.25
|
||||
return blackhole(sink, x | 0)
|
||||
},
|
||||
|
||||
bit_ops: function(n) {
|
||||
var sink = 0
|
||||
var x = 0x12345678
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = ((x << 5) ^ (x >>> 3)) | 0
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
overflow_path: function(n) {
|
||||
var sink = 0
|
||||
var x = 0x70000000
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = (x + 0x10000000) | 0
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
call_direct: function(n) {
|
||||
var sink = 0
|
||||
var f = function(a) { return (a + 1) | 0 }
|
||||
var x = 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = f(x)
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
call_indirect: function(n) {
|
||||
var sink = 0
|
||||
var f = function(a) { return (a + 1) | 0 }
|
||||
var g = f
|
||||
var x = 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = g(x)
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
call_closure: function(n) {
|
||||
var sink = 0
|
||||
var make_adder = function(k) {
|
||||
return function(a) { return (a + k) | 0 }
|
||||
}
|
||||
var add3 = make_adder(3)
|
||||
var x = 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = add3(x)
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
array_read_packed: function(n) {
|
||||
var sink = 0
|
||||
var a = make_packed_array(1024)
|
||||
var x = 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = (x + a[i & 1023]) | 0
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
array_write_packed: function(n) {
|
||||
var sink = 0
|
||||
var a = make_packed_array(1024)
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) a[i & 1023] = i
|
||||
return blackhole(sink, a[17] | 0)
|
||||
},
|
||||
|
||||
array_read_holey: function(n) {
|
||||
var sink = 0
|
||||
var a = make_holey_array(2048)
|
||||
var x = 0
|
||||
var i = 0
|
||||
var v = null
|
||||
for (i = 0; i < n; i++) {
|
||||
v = a[(i & 2047)]
|
||||
if (v) x = (x + v) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
array_push_steady: function(n) {
|
||||
var sink = 0
|
||||
var x = 0
|
||||
var j = 0
|
||||
var i = 0
|
||||
var a = null
|
||||
for (j = 0; j < n; j++) {
|
||||
a = []
|
||||
for (i = 0; i < 256; i++) push(a, i)
|
||||
x = (x + length(a)) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
array_indexed_sum: function(n) {
|
||||
var sink = 0
|
||||
var a = make_packed_array(1024)
|
||||
var x = 0
|
||||
var j = 0
|
||||
var i = 0
|
||||
for (j = 0; j < n; j++) {
|
||||
x = 0
|
||||
for (i = 0; i < 1024; i++) {
|
||||
x = (x + a[i]) | 0
|
||||
}
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
prop_read_mono: function(n) {
|
||||
var sink = 0
|
||||
var o = make_obj_xy(1, 2)
|
||||
var x = 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = (x + o.x) | 0
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
prop_read_poly_2: function(n) {
|
||||
var sink = 0
|
||||
var a = make_obj_xy(1, 2)
|
||||
var b = make_obj_yx(1, 2)
|
||||
var x = 0
|
||||
var i = 0
|
||||
var o = null
|
||||
for (i = 0; i < n; i++) {
|
||||
o = (i & 1) == 0 ? a : b
|
||||
x = (x + o.x) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
prop_read_poly_4: function(n) {
|
||||
var sink = 0
|
||||
var shapes = [
|
||||
{x: 1, y: 2},
|
||||
{y: 2, x: 1},
|
||||
{x: 1, z: 3, y: 2},
|
||||
{w: 0, x: 1, y: 2}
|
||||
]
|
||||
var x = 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
x = (x + shapes[i & 3].x) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
string_concat_small: function(n) {
|
||||
var sink = 0
|
||||
var x = 0
|
||||
var j = 0
|
||||
var i = 0
|
||||
var s = null
|
||||
for (j = 0; j < n; j++) {
|
||||
s = ""
|
||||
for (i = 0; i < 16; i++) s = s + "x"
|
||||
x = (x + length(s)) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
string_concat_medium: function(n) {
|
||||
var sink = 0
|
||||
var x = 0
|
||||
var j = 0
|
||||
var i = 0
|
||||
var s = null
|
||||
for (j = 0; j < n; j++) {
|
||||
s = ""
|
||||
for (i = 0; i < 100; i++) s = s + "abcdefghij"
|
||||
x = (x + length(s)) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
string_slice: function(n) {
|
||||
var sink = 0
|
||||
var base = "the quick brown fox jumps over the lazy dog"
|
||||
var x = 0
|
||||
var i = 0
|
||||
var s = null
|
||||
for (i = 0; i < n; i++) {
|
||||
s = text(base, i % 10, i % 10 + 10)
|
||||
x = (x + length(s)) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
guard_hot_number: function(n) {
|
||||
var sink = 0
|
||||
var x = 1
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = x + 1
|
||||
return blackhole(sink, x | 0)
|
||||
},
|
||||
|
||||
guard_mixed_types: function(n) {
|
||||
var sink = 0
|
||||
var vals = [1, "a", 2, "b", 3, "c", 4, "d"]
|
||||
var x = 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
if (is_number(vals[i & 7])) x = (x + vals[i & 7]) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
reduce_sum: function(n) {
|
||||
var sink = 0
|
||||
var a = make_packed_array(256)
|
||||
var x = 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
x = (x + reduce(a, function(acc, v) { return acc + v }, 0)) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
filter_evens: function(n) {
|
||||
var sink = 0
|
||||
var a = make_packed_array(256)
|
||||
var x = 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
x = (x + length(filter(a, function(v) { return v % 2 == 0 }))) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
arrfor_sum: function(n) {
|
||||
var sink = 0
|
||||
var a = make_packed_array(256)
|
||||
var x = 0
|
||||
var i = 0
|
||||
var sum = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
sum = 0
|
||||
arrfor(a, function(v) { sum += v })
|
||||
x = (x + sum) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
// micro_ops.bench.ce (or .cm depending on your convention)
|
||||
// micro_ops.cm — microbenchmarks for core operations
|
||||
|
||||
// Note: We use a function-local sink in each benchmark to avoid cross-contamination
|
||||
function blackhole(sink, x) {
|
||||
// Prevent dead-code elimination
|
||||
return (sink + (x | 0)) | 0
|
||||
}
|
||||
|
||||
function make_obj_xy(x, y) {
|
||||
return { x, y }
|
||||
return {x: x, y: y}
|
||||
}
|
||||
|
||||
function make_obj_yx(x, y) {
|
||||
// Different insertion order to force a different shape in many engines
|
||||
return { y, x }
|
||||
// Different insertion order to force a different shape
|
||||
return {y: y, x: x}
|
||||
}
|
||||
|
||||
function make_shapes(n) {
|
||||
var out = []
|
||||
for (var i = 0; i < n; i++) {
|
||||
var o = { a: i }
|
||||
var i = 0
|
||||
var o = null
|
||||
for (i = 0; i < n; i++) {
|
||||
o = {a: i}
|
||||
o[`p${i}`] = i
|
||||
push(out, o)
|
||||
}
|
||||
@@ -27,13 +27,15 @@ function make_shapes(n) {
|
||||
|
||||
function make_packed_array(n) {
|
||||
var a = []
|
||||
for (var i = 0; i < n; i++) push(a, i)
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) push(a, i)
|
||||
return a
|
||||
}
|
||||
|
||||
function make_holey_array(n) {
|
||||
var a = []
|
||||
for (var i = 0; i < n; i += 2) a[i] = i
|
||||
var i = 0
|
||||
for (i = 0; i < n; i += 2) a[i] = i
|
||||
return a
|
||||
}
|
||||
|
||||
@@ -41,7 +43,8 @@ return {
|
||||
// 0) Baseline loop cost
|
||||
loop_empty: function(n) {
|
||||
var sink = 0
|
||||
for (var i = 0; i < n; i++) {}
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) {}
|
||||
return blackhole(sink, n)
|
||||
},
|
||||
|
||||
@@ -49,35 +52,40 @@ return {
|
||||
i32_add: function(n) {
|
||||
var sink = 0
|
||||
var x = 1
|
||||
for (var i = 0; i < n; i++) x = (x + 3) | 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = (x + 3) | 0
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
f64_add: function(n) {
|
||||
var sink = 0
|
||||
var x = 1.0
|
||||
for (var i = 0; i < n; i++) x = x + 3.14159
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = x + 3.14159
|
||||
return blackhole(sink, x | 0)
|
||||
},
|
||||
|
||||
mixed_add: function(n) {
|
||||
var sink = 0
|
||||
var x = 1
|
||||
for (var i = 0; i < n; i++) x = x + 0.25
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = x + 0.25
|
||||
return blackhole(sink, x | 0)
|
||||
},
|
||||
|
||||
bit_ops: function(n) {
|
||||
var sink = 0
|
||||
var x = 0x12345678
|
||||
for (var i = 0; i < n; i++) x = ((x << 5) ^ (x >>> 3)) | 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = ((x << 5) ^ (x >>> 3)) | 0
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
overflow_path: function(n) {
|
||||
var sink = 0
|
||||
var x = 0x70000000
|
||||
for (var i = 0; i < n; i++) x = (x + 0x10000000) | 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = (x + 0x10000000) | 0
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
@@ -85,7 +93,8 @@ return {
|
||||
branch_predictable: function(n) {
|
||||
var sink = 0
|
||||
var x = 0
|
||||
for (var i = 0; i < n; i++) {
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
if ((i & 7) != 0) x++
|
||||
else x += 2
|
||||
}
|
||||
@@ -95,7 +104,8 @@ return {
|
||||
branch_alternating: function(n) {
|
||||
var sink = 0
|
||||
var x = 0
|
||||
for (var i = 0; i < n; i++) {
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
if ((i & 1) == 0) x++
|
||||
else x += 2
|
||||
}
|
||||
@@ -105,29 +115,47 @@ return {
|
||||
// 3) Calls
|
||||
call_direct: function(n) {
|
||||
var sink = 0
|
||||
function f(a) { return (a + 1) | 0 }
|
||||
var f = function(a) { return (a + 1) | 0 }
|
||||
var x = 0
|
||||
for (var i = 0; i < n; i++) x = f(x)
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = f(x)
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
call_indirect: function(n) {
|
||||
var sink = 0
|
||||
function f(a) { return (a + 1) | 0 }
|
||||
var f = function(a) { return (a + 1) | 0 }
|
||||
var g = f
|
||||
var x = 0
|
||||
for (var i = 0; i < n; i++) x = g(x)
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = g(x)
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
call_closure: function(n) {
|
||||
var sink = 0
|
||||
function make_adder(k) {
|
||||
var make_adder = function(k) {
|
||||
return function(a) { return (a + k) | 0 }
|
||||
}
|
||||
var add3 = make_adder(3)
|
||||
var x = 0
|
||||
for (var i = 0; i < n; i++) x = add3(x)
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = add3(x)
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
call_multi_arity: function(n) {
|
||||
var sink = 0
|
||||
var f0 = function() { return 1 }
|
||||
var f1 = function(a) { return a + 1 }
|
||||
var f2 = function(a, b) { return a + b }
|
||||
var f3 = function(a, b, c) { return a + b + c }
|
||||
var f4 = function(a, b, c, d) { return a + b + c + d }
|
||||
var x = 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
x = (x + f0() + f1(i) + f2(i, 1) + f3(i, 1, 2) + f4(i, 1, 2, 3)) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
@@ -136,7 +164,8 @@ return {
|
||||
var sink = 0
|
||||
var o = make_obj_xy(1, 2)
|
||||
var x = 0
|
||||
for (var i = 0; i < n; i++) x = (x + o.x) | 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = (x + o.x) | 0
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
@@ -145,20 +174,38 @@ return {
|
||||
var a = make_obj_xy(1, 2)
|
||||
var b = make_obj_yx(1, 2)
|
||||
var x = 0
|
||||
for (var i = 0; i < n; i++) {
|
||||
var o = (i & 1) == 0 ? a : b
|
||||
var i = 0
|
||||
var o = null
|
||||
for (i = 0; i < n; i++) {
|
||||
o = (i & 1) == 0 ? a : b
|
||||
x = (x + o.x) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
prop_read_poly_4: function(n) {
|
||||
var sink = 0
|
||||
var shapes = [
|
||||
{x: 1, y: 2},
|
||||
{y: 2, x: 1},
|
||||
{x: 1, z: 3, y: 2},
|
||||
{w: 0, x: 1, y: 2}
|
||||
]
|
||||
var x = 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
x = (x + shapes[i & 3].x) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
prop_read_mega: function(n) {
|
||||
var sink = 0
|
||||
var objs = make_shapes(32)
|
||||
var x = 0
|
||||
for (var i = 0; i < n; i++) {
|
||||
var o = objs[i & 31]
|
||||
x = (x + o.a) | 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
x = (x + objs[i & 31].a) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
@@ -166,7 +213,8 @@ return {
|
||||
prop_write_mono: function(n) {
|
||||
var sink = 0
|
||||
var o = make_obj_xy(1, 2)
|
||||
for (var i = 0; i < n; i++) o.x = (o.x + 1) | 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) o.x = (o.x + 1) | 0
|
||||
return blackhole(sink, o.x)
|
||||
},
|
||||
|
||||
@@ -175,14 +223,16 @@ return {
|
||||
var sink = 0
|
||||
var a = make_packed_array(1024)
|
||||
var x = 0
|
||||
for (var i = 0; i < n; i++) x = (x + a[i & 1023]) | 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = (x + a[i & 1023]) | 0
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
array_write_packed: function(n) {
|
||||
var sink = 0
|
||||
var a = make_packed_array(1024)
|
||||
for (var i = 0; i < n; i++) a[i & 1023] = i
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) a[i & 1023] = i
|
||||
return blackhole(sink, a[17] | 0)
|
||||
},
|
||||
|
||||
@@ -190,9 +240,10 @@ return {
|
||||
var sink = 0
|
||||
var a = make_holey_array(2048)
|
||||
var x = 0
|
||||
for (var i = 0; i < n; i++) {
|
||||
var v = a[(i & 2047)]
|
||||
// If "missing" is a special value in your language, this stresses that path too
|
||||
var i = 0
|
||||
var v = null
|
||||
for (i = 0; i < n; i++) {
|
||||
v = a[(i & 2047)]
|
||||
if (v) x = (x + v) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
@@ -201,21 +252,97 @@ return {
|
||||
array_push_steady: function(n) {
|
||||
var sink = 0
|
||||
var x = 0
|
||||
for (var j = 0; j < n; j++) {
|
||||
var a = []
|
||||
for (var i = 0; i < 256; i++) push(a, i)
|
||||
var j = 0
|
||||
var i = 0
|
||||
var a = null
|
||||
for (j = 0; j < n; j++) {
|
||||
a = []
|
||||
for (i = 0; i < 256; i++) push(a, i)
|
||||
x = (x + length(a)) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
array_push_pop: function(n) {
|
||||
var sink = 0
|
||||
var a = []
|
||||
var x = 0
|
||||
var i = 0
|
||||
var v = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
push(a, i)
|
||||
if (length(a) > 64) {
|
||||
v = pop(a)
|
||||
x = (x + v) | 0
|
||||
}
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
array_indexed_sum: function(n) {
|
||||
var sink = 0
|
||||
var a = make_packed_array(1024)
|
||||
var x = 0
|
||||
var j = 0
|
||||
var i = 0
|
||||
for (j = 0; j < n; j++) {
|
||||
x = 0
|
||||
for (i = 0; i < 1024; i++) {
|
||||
x = (x + a[i]) | 0
|
||||
}
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
// 6) Strings
|
||||
string_concat_small: function(n) {
|
||||
var sink = 0
|
||||
var x = 0
|
||||
for (var j = 0; j < n; j++) {
|
||||
var s = ""
|
||||
for (var i = 0; i < 16; i++) s = s + "x"
|
||||
var j = 0
|
||||
var i = 0
|
||||
var s = null
|
||||
for (j = 0; j < n; j++) {
|
||||
s = ""
|
||||
for (i = 0; i < 16; i++) s = s + "x"
|
||||
x = (x + length(s)) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
string_concat_medium: function(n) {
|
||||
var sink = 0
|
||||
var x = 0
|
||||
var j = 0
|
||||
var i = 0
|
||||
var s = null
|
||||
for (j = 0; j < n; j++) {
|
||||
s = ""
|
||||
for (i = 0; i < 100; i++) s = s + "abcdefghij"
|
||||
x = (x + length(s)) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
string_interpolation: function(n) {
|
||||
var sink = 0
|
||||
var x = 0
|
||||
var i = 0
|
||||
var s = null
|
||||
for (i = 0; i < n; i++) {
|
||||
s = `item_${i}_value_${i * 2}`
|
||||
x = (x + length(s)) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
string_slice: function(n) {
|
||||
var sink = 0
|
||||
var base = "the quick brown fox jumps over the lazy dog"
|
||||
var x = 0
|
||||
var i = 0
|
||||
var s = null
|
||||
for (i = 0; i < n; i++) {
|
||||
s = text(base, i % 10, i % 10 + 10)
|
||||
x = (x + length(s)) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
@@ -225,8 +352,10 @@ return {
|
||||
alloc_tiny_objects: function(n) {
|
||||
var sink = 0
|
||||
var x = 0
|
||||
for (var i = 0; i < n; i++) {
|
||||
var o = { a: i, b: i + 1, c: i + 2 }
|
||||
var i = 0
|
||||
var o = null
|
||||
for (i = 0; i < n; i++) {
|
||||
o = {a: i, b: i + 1, c: i + 2}
|
||||
x = (x + o.b) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
@@ -235,9 +364,12 @@ return {
|
||||
alloc_linked_list: function(n) {
|
||||
var sink = 0
|
||||
var head = null
|
||||
for (var i = 0; i < n; i++) head = { v: i, next: head }
|
||||
var i = 0
|
||||
var x = 0
|
||||
var p = head
|
||||
var p = null
|
||||
for (i = 0; i < n; i++) head = {v: i, next: head}
|
||||
x = 0
|
||||
p = head
|
||||
while (p) {
|
||||
x = (x + p.v) | 0
|
||||
p = p.next
|
||||
@@ -245,18 +377,118 @@ return {
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
// 8) meme-specific (adapt these to your exact semantics)
|
||||
meme_clone_read: function(n) {
|
||||
// If meme(obj) clones like Object.create / prototypal clone, this hits it hard.
|
||||
// Replace with your exact meme call form.
|
||||
alloc_arrays: function(n) {
|
||||
var sink = 0
|
||||
var base = { x: 1, y: 2 }
|
||||
var x = 0
|
||||
for (var i = 0; i < n; i++) {
|
||||
var o = meme(base)
|
||||
var i = 0
|
||||
var a = null
|
||||
for (i = 0; i < n; i++) {
|
||||
a = [i, i + 1, i + 2, i + 3]
|
||||
x = (x + a[2]) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
alloc_short_lived: function(n) {
|
||||
var sink = 0
|
||||
var x = 0
|
||||
var i = 0
|
||||
var o = null
|
||||
// Allocate objects that immediately become garbage
|
||||
for (i = 0; i < n; i++) {
|
||||
o = {val: i, data: {inner: i + 1}}
|
||||
x = (x + o.data.inner) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
alloc_long_lived_pressure: function(n) {
|
||||
var sink = 0
|
||||
var store = []
|
||||
var x = 0
|
||||
var i = 0
|
||||
var o = null
|
||||
// Keep first 1024 objects alive, churn the rest
|
||||
for (i = 0; i < n; i++) {
|
||||
o = {val: i, data: i * 2}
|
||||
if (i < 1024) {
|
||||
push(store, o)
|
||||
}
|
||||
x = (x + o.data) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
// 8) Meme (prototype clone)
|
||||
meme_clone_read: function(n) {
|
||||
var sink = 0
|
||||
var base = {x: 1, y: 2}
|
||||
var x = 0
|
||||
var i = 0
|
||||
var o = null
|
||||
for (i = 0; i < n; i++) {
|
||||
o = meme(base)
|
||||
x = (x + o.x) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
// 9) Guard / type check paths
|
||||
guard_hot_number: function(n) {
|
||||
// Monomorphic number path — guards should hoist
|
||||
var sink = 0
|
||||
var x = 1
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) x = x + 1
|
||||
return blackhole(sink, x | 0)
|
||||
},
|
||||
|
||||
guard_mixed_types: function(n) {
|
||||
// Alternating number/text — guards must stay
|
||||
var sink = 0
|
||||
var vals = [1, "a", 2, "b", 3, "c", 4, "d"]
|
||||
var x = 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
if (is_number(vals[i & 7])) x = (x + vals[i & 7]) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
// 10) Reduce / higher-order
|
||||
reduce_sum: function(n) {
|
||||
var sink = 0
|
||||
var a = make_packed_array(256)
|
||||
var x = 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
x = (x + reduce(a, function(acc, v) { return acc + v }, 0)) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
filter_evens: function(n) {
|
||||
var sink = 0
|
||||
var a = make_packed_array(256)
|
||||
var x = 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
x = (x + length(filter(a, function(v) { return v % 2 == 0 }))) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
arrfor_sum: function(n) {
|
||||
var sink = 0
|
||||
var a = make_packed_array(256)
|
||||
var x = 0
|
||||
var i = 0
|
||||
var sum = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
sum = 0
|
||||
arrfor(a, function(v) { sum += v })
|
||||
x = (x + sum) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
}
|
||||
}
|
||||
|
||||
249
benches/module_load.cm
Normal file
249
benches/module_load.cm
Normal file
@@ -0,0 +1,249 @@
|
||||
// module_load.cm — Module loading simulation (macro benchmark)
|
||||
// Simulates parsing many small modules, linking, and running.
|
||||
// Tests the "build scenario" pattern.
|
||||
|
||||
var json = use('json')
|
||||
|
||||
// Simulate a small module: parse token stream + build AST + evaluate
|
||||
function tokenize(src) {
|
||||
var tokens = []
|
||||
var i = 0
|
||||
var ch = null
|
||||
var chars = array(src)
|
||||
var buf = ""
|
||||
|
||||
for (i = 0; i < length(chars); i++) {
|
||||
ch = chars[i]
|
||||
if (ch == " " || ch == "\n" || ch == "\t") {
|
||||
if (length(buf) > 0) {
|
||||
push(tokens, buf)
|
||||
buf = ""
|
||||
}
|
||||
} else if (ch == "(" || ch == ")" || ch == "+" || ch == "-"
|
||||
|| ch == "*" || ch == "=" || ch == ";" || ch == ",") {
|
||||
if (length(buf) > 0) {
|
||||
push(tokens, buf)
|
||||
buf = ""
|
||||
}
|
||||
push(tokens, ch)
|
||||
} else {
|
||||
buf = buf + ch
|
||||
}
|
||||
}
|
||||
if (length(buf) > 0) push(tokens, buf)
|
||||
return tokens
|
||||
}
|
||||
|
||||
// Build a simple AST from tokens
|
||||
function parse_tokens(tokens) {
|
||||
var ast = []
|
||||
var i = 0
|
||||
var tok = null
|
||||
var node = null
|
||||
for (i = 0; i < length(tokens); i++) {
|
||||
tok = tokens[i]
|
||||
if (tok == "var" || tok == "def") {
|
||||
node = {type: "decl", kind: tok, name: null, value: null}
|
||||
i++
|
||||
if (i < length(tokens)) node.name = tokens[i]
|
||||
i++ // skip =
|
||||
i++
|
||||
if (i < length(tokens)) node.value = tokens[i]
|
||||
push(ast, node)
|
||||
} else if (tok == "return") {
|
||||
node = {type: "return", value: null}
|
||||
i++
|
||||
if (i < length(tokens)) node.value = tokens[i]
|
||||
push(ast, node)
|
||||
} else if (tok == "function") {
|
||||
node = {type: "func", name: null, body: []}
|
||||
i++
|
||||
if (i < length(tokens)) node.name = tokens[i]
|
||||
// Skip to matching )
|
||||
while (i < length(tokens) && tokens[i] != ")") i++
|
||||
push(ast, node)
|
||||
} else {
|
||||
push(ast, {type: "expr", value: tok})
|
||||
}
|
||||
}
|
||||
return ast
|
||||
}
|
||||
|
||||
// Evaluate: simple symbol table + resolution
|
||||
function evaluate(ast, env) {
|
||||
var result = null
|
||||
var i = 0
|
||||
var node = null
|
||||
for (i = 0; i < length(ast); i++) {
|
||||
node = ast[i]
|
||||
if (node.type == "decl") {
|
||||
env[node.name] = node.value
|
||||
} else if (node.type == "return") {
|
||||
result = node.value
|
||||
if (env[result]) result = env[result]
|
||||
} else if (node.type == "func") {
|
||||
env[node.name] = node
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Generate fake module source code
|
||||
function generate_module(id, dep_count) {
|
||||
var src = ""
|
||||
var i = 0
|
||||
src = src + "var _id = " + text(id) + ";\n"
|
||||
for (i = 0; i < dep_count; i++) {
|
||||
src = src + "var dep" + text(i) + " = use(mod_" + text(i) + ");\n"
|
||||
}
|
||||
src = src + "var x = " + text(id * 17) + ";\n"
|
||||
src = src + "var y = " + text(id * 31) + ";\n"
|
||||
src = src + "function compute(a, b) { return a + b; }\n"
|
||||
src = src + "var result = compute(x, y);\n"
|
||||
src = src + "return result;\n"
|
||||
return src
|
||||
}
|
||||
|
||||
// Simulate loading N modules with dependency chains
|
||||
function simulate_build(n_modules, deps_per_module) {
|
||||
var modules = []
|
||||
var loaded = {}
|
||||
var i = 0
|
||||
var j = 0
|
||||
var src = null
|
||||
var tokens = null
|
||||
var ast = null
|
||||
var env = null
|
||||
var result = null
|
||||
var total_tokens = 0
|
||||
var total_nodes = 0
|
||||
|
||||
// Generate all module sources
|
||||
for (i = 0; i < n_modules; i++) {
|
||||
src = generate_module(i, deps_per_module)
|
||||
push(modules, src)
|
||||
}
|
||||
|
||||
// "Load" each module: tokenize → parse → evaluate
|
||||
for (i = 0; i < n_modules; i++) {
|
||||
tokens = tokenize(modules[i])
|
||||
total_tokens += length(tokens)
|
||||
|
||||
ast = parse_tokens(tokens)
|
||||
total_nodes += length(ast)
|
||||
|
||||
env = {}
|
||||
// Resolve dependencies
|
||||
for (j = 0; j < deps_per_module; j++) {
|
||||
if (j < i) {
|
||||
env["dep" + text(j)] = loaded["mod_" + text(j)]
|
||||
}
|
||||
}
|
||||
|
||||
result = evaluate(ast, env)
|
||||
loaded["mod_" + text(i)] = result
|
||||
}
|
||||
|
||||
return {
|
||||
modules: n_modules,
|
||||
total_tokens: total_tokens,
|
||||
total_nodes: total_nodes,
|
||||
last_result: result
|
||||
}
|
||||
}
|
||||
|
||||
// Dependency graph analysis (topological sort simulation)
|
||||
function topo_sort(n_modules, deps_per_module) {
|
||||
// Build adjacency list
|
||||
var adj = {}
|
||||
var in_degree = {}
|
||||
var i = 0
|
||||
var j = 0
|
||||
var name = null
|
||||
var dep = null
|
||||
|
||||
for (i = 0; i < n_modules; i++) {
|
||||
name = "mod_" + text(i)
|
||||
adj[name] = []
|
||||
in_degree[name] = 0
|
||||
}
|
||||
|
||||
for (i = 0; i < n_modules; i++) {
|
||||
name = "mod_" + text(i)
|
||||
for (j = 0; j < deps_per_module; j++) {
|
||||
if (j < i) {
|
||||
dep = "mod_" + text(j)
|
||||
push(adj[dep], name)
|
||||
in_degree[name] = in_degree[name] + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kahn's algorithm
|
||||
var queue = []
|
||||
var keys = array(in_degree)
|
||||
for (i = 0; i < length(keys); i++) {
|
||||
if (in_degree[keys[i]] == 0) push(queue, keys[i])
|
||||
}
|
||||
|
||||
var order = []
|
||||
var current = null
|
||||
var neighbors = null
|
||||
var qi = 0
|
||||
while (qi < length(queue)) {
|
||||
current = queue[qi]
|
||||
qi++
|
||||
push(order, current)
|
||||
neighbors = adj[current]
|
||||
if (neighbors) {
|
||||
for (i = 0; i < length(neighbors); i++) {
|
||||
in_degree[neighbors[i]] = in_degree[neighbors[i]] - 1
|
||||
if (in_degree[neighbors[i]] == 0) push(queue, neighbors[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return order
|
||||
}
|
||||
|
||||
return {
|
||||
// Small build: 50 modules, 3 deps each
|
||||
build_50: function(n) {
|
||||
var i = 0
|
||||
var result = null
|
||||
for (i = 0; i < n; i++) {
|
||||
result = simulate_build(50, 3)
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
// Medium build: 200 modules, 5 deps each
|
||||
build_200: function(n) {
|
||||
var i = 0
|
||||
var result = null
|
||||
for (i = 0; i < n; i++) {
|
||||
result = simulate_build(200, 5)
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
// Large build: 500 modules, 5 deps each
|
||||
build_500: function(n) {
|
||||
var i = 0
|
||||
var result = null
|
||||
for (i = 0; i < n; i++) {
|
||||
result = simulate_build(500, 5)
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
// Topo sort of 500 module dependency graph
|
||||
topo_sort_500: function(n) {
|
||||
var i = 0
|
||||
var order = null
|
||||
for (i = 0; i < n; i++) {
|
||||
order = topo_sort(500, 5)
|
||||
}
|
||||
return order
|
||||
}
|
||||
}
|
||||
160
benches/nbody.cm
Normal file
160
benches/nbody.cm
Normal file
@@ -0,0 +1,160 @@
|
||||
// nbody.cm — N-body gravitational simulation kernel
|
||||
// Pure numeric + allocation workload. Classic VM benchmark.
|
||||
|
||||
var math = use('math/radians')
|
||||
|
||||
def PI = 3.141592653589793
|
||||
def SOLAR_MASS = 4 * PI * PI
|
||||
def DAYS_PER_YEAR = 365.24
|
||||
|
||||
function make_system() {
|
||||
// Sun + 4 Jovian planets
|
||||
var sun = {x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0, mass: SOLAR_MASS}
|
||||
|
||||
var jupiter = {
|
||||
x: 4.84143144246472090,
|
||||
y: -1.16032004402742839,
|
||||
z: -0.103622044471123109,
|
||||
vx: 0.00166007664274403694 * DAYS_PER_YEAR,
|
||||
vy: 0.00769901118419740425 * DAYS_PER_YEAR,
|
||||
vz: -0.0000690460016972063023 * DAYS_PER_YEAR,
|
||||
mass: 0.000954791938424326609 * SOLAR_MASS
|
||||
}
|
||||
|
||||
var saturn = {
|
||||
x: 8.34336671824457987,
|
||||
y: 4.12479856412430479,
|
||||
z: -0.403523417114321381,
|
||||
vx: -0.00276742510726862411 * DAYS_PER_YEAR,
|
||||
vy: 0.00499852801234917238 * DAYS_PER_YEAR,
|
||||
vz: 0.0000230417297573763929 * DAYS_PER_YEAR,
|
||||
mass: 0.000285885980666130812 * SOLAR_MASS
|
||||
}
|
||||
|
||||
var uranus = {
|
||||
x: 12.8943695621391310,
|
||||
y: -15.1111514016986312,
|
||||
z: -0.223307578892655734,
|
||||
vx: 0.00296460137564761618 * DAYS_PER_YEAR,
|
||||
vy: 0.00237847173959480950 * DAYS_PER_YEAR,
|
||||
vz: -0.0000296589568540237556 * DAYS_PER_YEAR,
|
||||
mass: 0.0000436624404335156298 * SOLAR_MASS
|
||||
}
|
||||
|
||||
var neptune = {
|
||||
x: 15.3796971148509165,
|
||||
y: -25.9193146099879641,
|
||||
z: 0.179258772950371181,
|
||||
vx: 0.00268067772490389322 * DAYS_PER_YEAR,
|
||||
vy: 0.00162824170038242295 * DAYS_PER_YEAR,
|
||||
vz: -0.0000951592254519715870 * DAYS_PER_YEAR,
|
||||
mass: 0.0000515138902046611451 * SOLAR_MASS
|
||||
}
|
||||
|
||||
var bodies = [sun, jupiter, saturn, uranus, neptune]
|
||||
|
||||
// Offset momentum
|
||||
var px = 0
|
||||
var py = 0
|
||||
var pz = 0
|
||||
var i = 0
|
||||
for (i = 0; i < length(bodies); i++) {
|
||||
px += bodies[i].vx * bodies[i].mass
|
||||
py += bodies[i].vy * bodies[i].mass
|
||||
pz += bodies[i].vz * bodies[i].mass
|
||||
}
|
||||
sun.vx = -px / SOLAR_MASS
|
||||
sun.vy = -py / SOLAR_MASS
|
||||
sun.vz = -pz / SOLAR_MASS
|
||||
|
||||
return bodies
|
||||
}
|
||||
|
||||
function advance(bodies, dt) {
|
||||
var n = length(bodies)
|
||||
var i = 0
|
||||
var j = 0
|
||||
var bi = null
|
||||
var bj = null
|
||||
var dx = 0
|
||||
var dy = 0
|
||||
var dz = 0
|
||||
var dist_sq = 0
|
||||
var dist = 0
|
||||
var mag = 0
|
||||
|
||||
for (i = 0; i < n; i++) {
|
||||
bi = bodies[i]
|
||||
for (j = i + 1; j < n; j++) {
|
||||
bj = bodies[j]
|
||||
dx = bi.x - bj.x
|
||||
dy = bi.y - bj.y
|
||||
dz = bi.z - bj.z
|
||||
dist_sq = dx * dx + dy * dy + dz * dz
|
||||
dist = math.sqrt(dist_sq)
|
||||
mag = dt / (dist_sq * dist)
|
||||
|
||||
bi.vx -= dx * bj.mass * mag
|
||||
bi.vy -= dy * bj.mass * mag
|
||||
bi.vz -= dz * bj.mass * mag
|
||||
bj.vx += dx * bi.mass * mag
|
||||
bj.vy += dy * bi.mass * mag
|
||||
bj.vz += dz * bi.mass * mag
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < n; i++) {
|
||||
bi = bodies[i]
|
||||
bi.x += dt * bi.vx
|
||||
bi.y += dt * bi.vy
|
||||
bi.z += dt * bi.vz
|
||||
}
|
||||
}
|
||||
|
||||
function energy(bodies) {
|
||||
var e = 0
|
||||
var n = length(bodies)
|
||||
var i = 0
|
||||
var j = 0
|
||||
var bi = null
|
||||
var bj = null
|
||||
var dx = 0
|
||||
var dy = 0
|
||||
var dz = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
bi = bodies[i]
|
||||
e += 0.5 * bi.mass * (bi.vx * bi.vx + bi.vy * bi.vy + bi.vz * bi.vz)
|
||||
for (j = i + 1; j < n; j++) {
|
||||
bj = bodies[j]
|
||||
dx = bi.x - bj.x
|
||||
dy = bi.y - bj.y
|
||||
dz = bi.z - bj.z
|
||||
e -= (bi.mass * bj.mass) / math.sqrt(dx * dx + dy * dy + dz * dz)
|
||||
}
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
return {
|
||||
nbody_1k: function(n) {
|
||||
var i = 0
|
||||
var j = 0
|
||||
var bodies = null
|
||||
for (i = 0; i < n; i++) {
|
||||
bodies = make_system()
|
||||
for (j = 0; j < 1000; j++) advance(bodies, 0.01)
|
||||
energy(bodies)
|
||||
}
|
||||
},
|
||||
|
||||
nbody_10k: function(n) {
|
||||
var i = 0
|
||||
var j = 0
|
||||
var bodies = null
|
||||
for (i = 0; i < n; i++) {
|
||||
bodies = make_system()
|
||||
for (j = 0; j < 10000; j++) advance(bodies, 0.01)
|
||||
energy(bodies)
|
||||
}
|
||||
}
|
||||
}
|
||||
154
benches/ray_tracer.cm
Normal file
154
benches/ray_tracer.cm
Normal file
@@ -0,0 +1,154 @@
|
||||
// ray_tracer.cm — Simple ray tracer kernel
|
||||
// Control flow + numeric + allocation. Classic VM benchmark.
|
||||
|
||||
var math = use('math/radians')
|
||||
|
||||
function vec(x, y, z) {
|
||||
return {x: x, y: y, z: z}
|
||||
}
|
||||
|
||||
function vadd(a, b) {
|
||||
return {x: a.x + b.x, y: a.y + b.y, z: a.z + b.z}
|
||||
}
|
||||
|
||||
function vsub(a, b) {
|
||||
return {x: a.x - b.x, y: a.y - b.y, z: a.z - b.z}
|
||||
}
|
||||
|
||||
function vmul(v, s) {
|
||||
return {x: v.x * s, y: v.y * s, z: v.z * s}
|
||||
}
|
||||
|
||||
function vdot(a, b) {
|
||||
return a.x * b.x + a.y * b.y + a.z * b.z
|
||||
}
|
||||
|
||||
function vnorm(v) {
|
||||
var len = math.sqrt(vdot(v, v))
|
||||
if (len == 0) return vec(0, 0, 0)
|
||||
return vmul(v, 1 / len)
|
||||
}
|
||||
|
||||
function make_sphere(center, radius, color) {
|
||||
return {
|
||||
center: center,
|
||||
radius: radius,
|
||||
color: color
|
||||
}
|
||||
}
|
||||
|
||||
function intersect_sphere(origin, dir, sphere) {
|
||||
var oc = vsub(origin, sphere.center)
|
||||
var b = vdot(oc, dir)
|
||||
var c = vdot(oc, oc) - sphere.radius * sphere.radius
|
||||
var disc = b * b - c
|
||||
if (disc < 0) return -1
|
||||
var sq = math.sqrt(disc)
|
||||
var t1 = -b - sq
|
||||
var t2 = -b + sq
|
||||
if (t1 > 0.001) return t1
|
||||
if (t2 > 0.001) return t2
|
||||
return -1
|
||||
}
|
||||
|
||||
function make_scene() {
|
||||
var spheres = [
|
||||
make_sphere(vec(0, -1, 5), 1, vec(1, 0, 0)),
|
||||
make_sphere(vec(2, 0, 6), 1, vec(0, 1, 0)),
|
||||
make_sphere(vec(-2, 0, 4), 1, vec(0, 0, 1)),
|
||||
make_sphere(vec(0, 1, 4.5), 0.5, vec(1, 1, 0)),
|
||||
make_sphere(vec(1, -0.5, 3), 0.3, vec(1, 0, 1)),
|
||||
make_sphere(vec(0, -101, 5), 100, vec(0.5, 0.5, 0.5))
|
||||
]
|
||||
var light = vnorm(vec(1, 1, -1))
|
||||
return {spheres: spheres, light: light}
|
||||
}
|
||||
|
||||
function trace(origin, dir, scene) {
|
||||
var closest_t = 999999
|
||||
var closest_sphere = null
|
||||
var i = 0
|
||||
var t = 0
|
||||
for (i = 0; i < length(scene.spheres); i++) {
|
||||
t = intersect_sphere(origin, dir, scene.spheres[i])
|
||||
if (t > 0 && t < closest_t) {
|
||||
closest_t = t
|
||||
closest_sphere = scene.spheres[i]
|
||||
}
|
||||
}
|
||||
|
||||
if (!closest_sphere) return vec(0.2, 0.3, 0.5) // sky color
|
||||
|
||||
var hit = vadd(origin, vmul(dir, closest_t))
|
||||
var normal = vnorm(vsub(hit, closest_sphere.center))
|
||||
var diffuse = vdot(normal, scene.light)
|
||||
if (diffuse < 0) diffuse = 0
|
||||
|
||||
// Shadow check
|
||||
var shadow_origin = vadd(hit, vmul(normal, 0.001))
|
||||
var in_shadow = false
|
||||
for (i = 0; i < length(scene.spheres); i++) {
|
||||
if (scene.spheres[i] != closest_sphere) {
|
||||
t = intersect_sphere(shadow_origin, scene.light, scene.spheres[i])
|
||||
if (t > 0) {
|
||||
in_shadow = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var ambient = 0.15
|
||||
var intensity = in_shadow ? ambient : ambient + diffuse * 0.85
|
||||
return vmul(closest_sphere.color, intensity)
|
||||
}
|
||||
|
||||
function render(width, height, scene) {
|
||||
var aspect = width / height
|
||||
var fov = 1.0
|
||||
var total_r = 0
|
||||
var total_g = 0
|
||||
var total_b = 0
|
||||
var y = 0
|
||||
var x = 0
|
||||
var u = 0
|
||||
var v = 0
|
||||
var dir = null
|
||||
var color = null
|
||||
var origin = vec(0, 0, 0)
|
||||
|
||||
for (y = 0; y < height; y++) {
|
||||
for (x = 0; x < width; x++) {
|
||||
u = (2 * (x + 0.5) / width - 1) * aspect * fov
|
||||
v = (1 - 2 * (y + 0.5) / height) * fov
|
||||
dir = vnorm(vec(u, v, 1))
|
||||
color = trace(origin, dir, scene)
|
||||
total_r += color.x
|
||||
total_g += color.y
|
||||
total_b += color.z
|
||||
}
|
||||
}
|
||||
|
||||
return {r: total_r, g: total_g, b: total_b}
|
||||
}
|
||||
|
||||
var scene = make_scene()
|
||||
|
||||
return {
|
||||
raytrace_32x32: function(n) {
|
||||
var i = 0
|
||||
var result = null
|
||||
for (i = 0; i < n; i++) {
|
||||
result = render(32, 32, scene)
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
raytrace_64x64: function(n) {
|
||||
var i = 0
|
||||
var result = null
|
||||
for (i = 0; i < n; i++) {
|
||||
result = render(64, 64, scene)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
251
benches/richards.cm
Normal file
251
benches/richards.cm
Normal file
@@ -0,0 +1,251 @@
|
||||
// richards.cm — Richards benchmark (scheduler simulation)
|
||||
// Object-ish workload: dynamic dispatch, state machines, queuing.
|
||||
|
||||
def IDLE = 0
|
||||
def WORKER = 1
|
||||
def HANDLER_A = 2
|
||||
def HANDLER_B = 3
|
||||
def DEVICE_A = 4
|
||||
def DEVICE_B = 5
|
||||
def NUM_TASKS = 6
|
||||
|
||||
def TASK_RUNNING = 0
|
||||
def TASK_WAITING = 1
|
||||
def TASK_HELD = 2
|
||||
def TASK_SUSPENDED = 3
|
||||
|
||||
function make_packet(link, id, kind) {
|
||||
return {link: link, id: id, kind: kind, datum: 0, data: array(4, 0)}
|
||||
}
|
||||
|
||||
function scheduler() {
|
||||
var tasks = array(NUM_TASKS, null)
|
||||
var current = null
|
||||
var queue_count = 0
|
||||
var hold_count = 0
|
||||
var v1 = 0
|
||||
var v2 = 0
|
||||
var w_id = HANDLER_A
|
||||
var w_datum = 0
|
||||
var h_a_queue = null
|
||||
var h_a_count = 0
|
||||
var h_b_queue = null
|
||||
var h_b_count = 0
|
||||
var dev_a_pkt = null
|
||||
var dev_b_pkt = null
|
||||
|
||||
var find_next = function() {
|
||||
var best = null
|
||||
var i = 0
|
||||
for (i = 0; i < NUM_TASKS; i++) {
|
||||
if (tasks[i] && tasks[i].state == TASK_RUNNING) {
|
||||
if (!best || tasks[i].priority > best.priority) {
|
||||
best = tasks[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
var hold_self = function() {
|
||||
hold_count++
|
||||
if (current) current.state = TASK_HELD
|
||||
return find_next()
|
||||
}
|
||||
|
||||
var release = function(id) {
|
||||
var t = tasks[id]
|
||||
if (!t) return find_next()
|
||||
if (t.state == TASK_HELD) t.state = TASK_RUNNING
|
||||
if (t.priority > (current ? current.priority : -1)) return t
|
||||
return current
|
||||
}
|
||||
|
||||
var queue_packet = function(pkt) {
|
||||
var t = tasks[pkt.id]
|
||||
var p = null
|
||||
if (!t) return find_next()
|
||||
queue_count++
|
||||
pkt.link = null
|
||||
pkt.id = current ? current.id : 0
|
||||
if (!t.queue) {
|
||||
t.queue = pkt
|
||||
t.state = TASK_RUNNING
|
||||
if (t.priority > (current ? current.priority : -1)) return t
|
||||
} else {
|
||||
p = t.queue
|
||||
while (p.link) p = p.link
|
||||
p.link = pkt
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
// Idle task
|
||||
tasks[IDLE] = {id: IDLE, priority: 0, queue: null, state: TASK_RUNNING,
|
||||
hold_count: 0, queue_count: 0,
|
||||
fn: function(pkt) {
|
||||
v1--
|
||||
if (v1 == 0) return hold_self()
|
||||
if ((v2 & 1) == 0) {
|
||||
v2 = v2 >> 1
|
||||
return release(DEVICE_A)
|
||||
}
|
||||
v2 = (v2 >> 1) ^ 0xD008
|
||||
return release(DEVICE_B)
|
||||
}
|
||||
}
|
||||
|
||||
// Worker task
|
||||
tasks[WORKER] = {id: WORKER, priority: 1000, queue: null, state: TASK_SUSPENDED,
|
||||
hold_count: 0, queue_count: 0,
|
||||
fn: function(pkt) {
|
||||
var i = 0
|
||||
if (!pkt) return hold_self()
|
||||
w_id = (w_id == HANDLER_A) ? HANDLER_B : HANDLER_A
|
||||
pkt.id = w_id
|
||||
pkt.datum = 0
|
||||
for (i = 0; i < 4; i++) {
|
||||
w_datum++
|
||||
if (w_datum > 26) w_datum = 1
|
||||
pkt.data[i] = 65 + w_datum
|
||||
}
|
||||
return queue_packet(pkt)
|
||||
}
|
||||
}
|
||||
|
||||
// Handler A
|
||||
tasks[HANDLER_A] = {id: HANDLER_A, priority: 2000, queue: null, state: TASK_SUSPENDED,
|
||||
hold_count: 0, queue_count: 0,
|
||||
fn: function(pkt) {
|
||||
var p = null
|
||||
if (pkt) { h_a_queue = pkt; h_a_count++ }
|
||||
if (h_a_queue) {
|
||||
p = h_a_queue
|
||||
h_a_queue = p.link
|
||||
if (h_a_count < 3) return queue_packet(p)
|
||||
return release(DEVICE_A)
|
||||
}
|
||||
return hold_self()
|
||||
}
|
||||
}
|
||||
|
||||
// Handler B
|
||||
tasks[HANDLER_B] = {id: HANDLER_B, priority: 3000, queue: null, state: TASK_SUSPENDED,
|
||||
hold_count: 0, queue_count: 0,
|
||||
fn: function(pkt) {
|
||||
var p = null
|
||||
if (pkt) { h_b_queue = pkt; h_b_count++ }
|
||||
if (h_b_queue) {
|
||||
p = h_b_queue
|
||||
h_b_queue = p.link
|
||||
if (h_b_count < 3) return queue_packet(p)
|
||||
return release(DEVICE_B)
|
||||
}
|
||||
return hold_self()
|
||||
}
|
||||
}
|
||||
|
||||
// Device A
|
||||
tasks[DEVICE_A] = {id: DEVICE_A, priority: 4000, queue: null, state: TASK_SUSPENDED,
|
||||
hold_count: 0, queue_count: 0,
|
||||
fn: function(pkt) {
|
||||
var p = null
|
||||
if (pkt) { dev_a_pkt = pkt; return hold_self() }
|
||||
if (dev_a_pkt) {
|
||||
p = dev_a_pkt
|
||||
dev_a_pkt = null
|
||||
return queue_packet(p)
|
||||
}
|
||||
return hold_self()
|
||||
}
|
||||
}
|
||||
|
||||
// Device B
|
||||
tasks[DEVICE_B] = {id: DEVICE_B, priority: 5000, queue: null, state: TASK_SUSPENDED,
|
||||
hold_count: 0, queue_count: 0,
|
||||
fn: function(pkt) {
|
||||
var p = null
|
||||
if (pkt) { dev_b_pkt = pkt; return hold_self() }
|
||||
if (dev_b_pkt) {
|
||||
p = dev_b_pkt
|
||||
dev_b_pkt = null
|
||||
return queue_packet(p)
|
||||
}
|
||||
return hold_self()
|
||||
}
|
||||
}
|
||||
|
||||
var run = function(iterations) {
|
||||
var i = 0
|
||||
var pkt1 = null
|
||||
var pkt2 = null
|
||||
var steps = 0
|
||||
var pkt = null
|
||||
var next = null
|
||||
|
||||
v1 = iterations
|
||||
v2 = 0xBEEF
|
||||
queue_count = 0
|
||||
hold_count = 0
|
||||
w_id = HANDLER_A
|
||||
w_datum = 0
|
||||
h_a_queue = null
|
||||
h_a_count = 0
|
||||
h_b_queue = null
|
||||
h_b_count = 0
|
||||
dev_a_pkt = null
|
||||
dev_b_pkt = null
|
||||
|
||||
for (i = 0; i < NUM_TASKS; i++) {
|
||||
if (tasks[i]) {
|
||||
tasks[i].state = (i == IDLE) ? TASK_RUNNING : TASK_SUSPENDED
|
||||
tasks[i].queue = null
|
||||
}
|
||||
}
|
||||
|
||||
pkt1 = make_packet(null, WORKER, 1)
|
||||
pkt2 = make_packet(pkt1, WORKER, 1)
|
||||
tasks[WORKER].queue = pkt2
|
||||
tasks[WORKER].state = TASK_RUNNING
|
||||
|
||||
current = find_next()
|
||||
while (current && steps < iterations * 10) {
|
||||
pkt = current.queue
|
||||
if (pkt) {
|
||||
current.queue = pkt.link
|
||||
current.queue_count++
|
||||
}
|
||||
next = current.fn(pkt)
|
||||
if (next) current = next
|
||||
else current = find_next()
|
||||
steps++
|
||||
}
|
||||
return {queue_count: queue_count, hold_count: hold_count, steps: steps}
|
||||
}
|
||||
|
||||
return {run: run}
|
||||
}
|
||||
|
||||
return {
|
||||
richards_100: function(n) {
|
||||
var i = 0
|
||||
var s = null
|
||||
var result = null
|
||||
for (i = 0; i < n; i++) {
|
||||
s = scheduler()
|
||||
result = s.run(100)
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
richards_1k: function(n) {
|
||||
var i = 0
|
||||
var s = null
|
||||
var result = null
|
||||
for (i = 0; i < n; i++) {
|
||||
s = scheduler()
|
||||
result = s.run(1000)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
180
benches/sorting.cm
Normal file
180
benches/sorting.cm
Normal file
@@ -0,0 +1,180 @@
|
||||
// sorting.cm — Sorting and searching kernel
|
||||
// Array manipulation, comparison-heavy, allocation patterns.
|
||||
|
||||
function make_random_array(n, seed) {
|
||||
var a = []
|
||||
var x = seed
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
x = ((x * 1103515245 + 12345) & 0x7FFFFFFF) | 0
|
||||
push(a, x % 10000)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
function make_descending(n) {
|
||||
var a = []
|
||||
var i = 0
|
||||
for (i = n - 1; i >= 0; i--) push(a, i)
|
||||
return a
|
||||
}
|
||||
|
||||
// Manual quicksort (tests recursion + array mutation)
|
||||
function qsort(arr, lo, hi) {
|
||||
var i = lo
|
||||
var j = hi
|
||||
var pivot = arr[floor((lo + hi) / 2)]
|
||||
var tmp = 0
|
||||
if (lo >= hi) return null
|
||||
while (i <= j) {
|
||||
while (arr[i] < pivot) i++
|
||||
while (arr[j] > pivot) j--
|
||||
if (i <= j) {
|
||||
tmp = arr[i]
|
||||
arr[i] = arr[j]
|
||||
arr[j] = tmp
|
||||
i++
|
||||
j--
|
||||
}
|
||||
}
|
||||
if (lo < j) qsort(arr, lo, j)
|
||||
if (i < hi) qsort(arr, i, hi)
|
||||
return null
|
||||
}
|
||||
|
||||
// Merge sort (tests allocation + array creation)
|
||||
function msort(arr) {
|
||||
var n = length(arr)
|
||||
if (n <= 1) return arr
|
||||
var mid = floor(n / 2)
|
||||
var left = msort(array(arr, 0, mid))
|
||||
var right = msort(array(arr, mid, n))
|
||||
return merge(left, right)
|
||||
}
|
||||
|
||||
function merge(a, b) {
|
||||
var result = []
|
||||
var i = 0
|
||||
var j = 0
|
||||
while (i < length(a) && j < length(b)) {
|
||||
if (a[i] <= b[j]) {
|
||||
push(result, a[i])
|
||||
i++
|
||||
} else {
|
||||
push(result, b[j])
|
||||
j++
|
||||
}
|
||||
}
|
||||
while (i < length(a)) {
|
||||
push(result, a[i])
|
||||
i++
|
||||
}
|
||||
while (j < length(b)) {
|
||||
push(result, b[j])
|
||||
j++
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Binary search
|
||||
function bsearch(arr, target) {
|
||||
var lo = 0
|
||||
var hi = length(arr) - 1
|
||||
var mid = 0
|
||||
while (lo <= hi) {
|
||||
mid = floor((lo + hi) / 2)
|
||||
if (arr[mid] == target) return mid
|
||||
if (arr[mid] < target) lo = mid + 1
|
||||
else hi = mid - 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// Sort objects by field
|
||||
function sort_records(n) {
|
||||
var records = []
|
||||
var x = 42
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
x = ((x * 1103515245 + 12345) & 0x7FFFFFFF) | 0
|
||||
push(records, {id: i, score: x % 10000, name: `item_${i}`})
|
||||
}
|
||||
return sort(records, "score")
|
||||
}
|
||||
|
||||
return {
|
||||
// Quicksort 1K random integers
|
||||
qsort_1k: function(n) {
|
||||
var i = 0
|
||||
var a = null
|
||||
for (i = 0; i < n; i++) {
|
||||
a = make_random_array(1000, i)
|
||||
qsort(a, 0, length(a) - 1)
|
||||
}
|
||||
return a
|
||||
},
|
||||
|
||||
// Quicksort 10K random integers
|
||||
qsort_10k: function(n) {
|
||||
var i = 0
|
||||
var a = null
|
||||
for (i = 0; i < n; i++) {
|
||||
a = make_random_array(10000, i)
|
||||
qsort(a, 0, length(a) - 1)
|
||||
}
|
||||
return a
|
||||
},
|
||||
|
||||
// Merge sort 1K (allocation heavy)
|
||||
msort_1k: function(n) {
|
||||
var i = 0
|
||||
var result = null
|
||||
for (i = 0; i < n; i++) {
|
||||
result = msort(make_random_array(1000, i))
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
// Built-in sort 1K
|
||||
builtin_sort_1k: function(n) {
|
||||
var i = 0
|
||||
var result = null
|
||||
for (i = 0; i < n; i++) {
|
||||
result = sort(make_random_array(1000, i))
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
// Sort worst case (descending → ascending)
|
||||
sort_worst_case: function(n) {
|
||||
var i = 0
|
||||
var a = null
|
||||
for (i = 0; i < n; i++) {
|
||||
a = make_descending(1000)
|
||||
qsort(a, 0, length(a) - 1)
|
||||
}
|
||||
return a
|
||||
},
|
||||
|
||||
// Binary search in sorted array
|
||||
bsearch_1k: function(n) {
|
||||
var sorted = make_random_array(1000, 42)
|
||||
sorted = sort(sorted)
|
||||
var found = 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
if (bsearch(sorted, sorted[i % 1000]) >= 0) found++
|
||||
}
|
||||
return found
|
||||
},
|
||||
|
||||
// Sort records by field
|
||||
sort_records_500: function(n) {
|
||||
var i = 0
|
||||
var result = null
|
||||
for (i = 0; i < n; i++) {
|
||||
result = sort_records(500)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
82
benches/spectral_norm.cm
Normal file
82
benches/spectral_norm.cm
Normal file
@@ -0,0 +1,82 @@
|
||||
// spectral_norm.cm — Spectral norm kernel
|
||||
// Pure numeric, dense array access, mathematical computation.
|
||||
|
||||
var math = use('math/radians')
|
||||
|
||||
function eval_a(i, j) {
|
||||
return 1.0 / ((i + j) * (i + j + 1) / 2 + i + 1)
|
||||
}
|
||||
|
||||
function eval_a_times_u(n, u, au) {
|
||||
var i = 0
|
||||
var j = 0
|
||||
var sum = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
sum = 0
|
||||
for (j = 0; j < n; j++) {
|
||||
sum += eval_a(i, j) * u[j]
|
||||
}
|
||||
au[i] = sum
|
||||
}
|
||||
}
|
||||
|
||||
function eval_at_times_u(n, u, atu) {
|
||||
var i = 0
|
||||
var j = 0
|
||||
var sum = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
sum = 0
|
||||
for (j = 0; j < n; j++) {
|
||||
sum += eval_a(j, i) * u[j]
|
||||
}
|
||||
atu[i] = sum
|
||||
}
|
||||
}
|
||||
|
||||
function eval_ata_times_u(n, u, atau) {
|
||||
var v = array(n, 0)
|
||||
eval_a_times_u(n, u, v)
|
||||
eval_at_times_u(n, v, atau)
|
||||
}
|
||||
|
||||
function spectral_norm(n) {
|
||||
var u = array(n, 1)
|
||||
var v = array(n, 0)
|
||||
var i = 0
|
||||
var vbv = 0
|
||||
var vv = 0
|
||||
|
||||
for (i = 0; i < 10; i++) {
|
||||
eval_ata_times_u(n, u, v)
|
||||
eval_ata_times_u(n, v, u)
|
||||
}
|
||||
|
||||
vbv = 0
|
||||
vv = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
vbv += u[i] * v[i]
|
||||
vv += v[i] * v[i]
|
||||
}
|
||||
|
||||
return math.sqrt(vbv / vv)
|
||||
}
|
||||
|
||||
return {
|
||||
spectral_100: function(n) {
|
||||
var i = 0
|
||||
var result = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
result = spectral_norm(100)
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
spectral_200: function(n) {
|
||||
var i = 0
|
||||
var result = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
result = spectral_norm(200)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
188
benches/string_processing.cm
Normal file
188
benches/string_processing.cm
Normal file
@@ -0,0 +1,188 @@
|
||||
// string_processing.cm — String-heavy kernel
|
||||
// Concat, split, search, replace, interning path stress.
|
||||
|
||||
function make_lorem(paragraphs) {
|
||||
var base = "Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat"
|
||||
var result = ""
|
||||
var i = 0
|
||||
for (i = 0; i < paragraphs; i++) {
|
||||
if (i > 0) result = result + " "
|
||||
result = result + base
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Build a lookup table from text
|
||||
function build_index(txt) {
|
||||
var words = array(txt, " ")
|
||||
var index = {}
|
||||
var i = 0
|
||||
var w = null
|
||||
for (i = 0; i < length(words); i++) {
|
||||
w = words[i]
|
||||
if (!index[w]) {
|
||||
index[w] = []
|
||||
}
|
||||
push(index[w], i)
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
// Levenshtein-like distance (simplified)
|
||||
function edit_distance(a, b) {
|
||||
var la = length(a)
|
||||
var lb = length(b)
|
||||
if (la == 0) return lb
|
||||
if (lb == 0) return la
|
||||
|
||||
// Use flat array for 2 rows of DP matrix
|
||||
var prev = array(lb + 1, 0)
|
||||
var curr = array(lb + 1, 0)
|
||||
var i = 0
|
||||
var j = 0
|
||||
var cost = 0
|
||||
var del = 0
|
||||
var ins = 0
|
||||
var sub = 0
|
||||
var tmp = null
|
||||
var ca = array(a)
|
||||
var cb = array(b)
|
||||
|
||||
for (j = 0; j <= lb; j++) prev[j] = j
|
||||
for (i = 1; i <= la; i++) {
|
||||
curr[0] = i
|
||||
for (j = 1; j <= lb; j++) {
|
||||
cost = ca[i - 1] == cb[j - 1] ? 0 : 1
|
||||
del = prev[j] + 1
|
||||
ins = curr[j - 1] + 1
|
||||
sub = prev[j - 1] + cost
|
||||
curr[j] = del
|
||||
if (ins < curr[j]) curr[j] = ins
|
||||
if (sub < curr[j]) curr[j] = sub
|
||||
}
|
||||
tmp = prev
|
||||
prev = curr
|
||||
curr = tmp
|
||||
}
|
||||
return prev[lb]
|
||||
}
|
||||
|
||||
var lorem_5 = make_lorem(5)
|
||||
var lorem_20 = make_lorem(20)
|
||||
|
||||
return {
|
||||
// Split text into words and count
|
||||
string_split_count: function(n) {
|
||||
var i = 0
|
||||
var words = null
|
||||
var count = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
words = array(lorem_5, " ")
|
||||
count += length(words)
|
||||
}
|
||||
return count
|
||||
},
|
||||
|
||||
// Build word index (split + hash + array ops)
|
||||
string_index_build: function(n) {
|
||||
var i = 0
|
||||
var idx = null
|
||||
for (i = 0; i < n; i++) {
|
||||
idx = build_index(lorem_5)
|
||||
}
|
||||
return idx
|
||||
},
|
||||
|
||||
// Search for substrings
|
||||
string_search: function(n) {
|
||||
var targets = ["dolor", "minim", "quis", "magna", "ipsum"]
|
||||
var i = 0
|
||||
var j = 0
|
||||
var count = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
for (j = 0; j < length(targets); j++) {
|
||||
if (search(lorem_20, targets[j])) count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
},
|
||||
|
||||
// Replace operations
|
||||
string_replace: function(n) {
|
||||
var i = 0
|
||||
var result = null
|
||||
for (i = 0; i < n; i++) {
|
||||
result = replace(lorem_5, "dolor", "DOLOR")
|
||||
result = replace(result, "ipsum", "IPSUM")
|
||||
result = replace(result, "amet", "AMET")
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
// String concatenation builder
|
||||
string_builder: function(n) {
|
||||
var i = 0
|
||||
var j = 0
|
||||
var s = null
|
||||
var total = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
s = ""
|
||||
for (j = 0; j < 50; j++) {
|
||||
s = s + "key=" + text(j) + "&value=" + text(j * 17) + "&"
|
||||
}
|
||||
total += length(s)
|
||||
}
|
||||
return total
|
||||
},
|
||||
|
||||
// Edit distance (DP + array + string ops)
|
||||
edit_distance: function(n) {
|
||||
var words = ["kitten", "sitting", "saturday", "sunday", "intention", "execution"]
|
||||
var i = 0
|
||||
var j = 0
|
||||
var total = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
for (j = 0; j < length(words) - 1; j++) {
|
||||
total += edit_distance(words[j], words[j + 1])
|
||||
}
|
||||
}
|
||||
return total
|
||||
},
|
||||
|
||||
// Upper/lower/trim chain
|
||||
string_transforms: function(n) {
|
||||
var src = " Hello World "
|
||||
var i = 0
|
||||
var x = 0
|
||||
var result = null
|
||||
for (i = 0; i < n; i++) {
|
||||
result = trim(src)
|
||||
result = upper(result)
|
||||
result = lower(result)
|
||||
x += length(result)
|
||||
}
|
||||
return x
|
||||
},
|
||||
|
||||
// Starts_with / ends_with (interning path)
|
||||
string_prefix_suffix: function(n) {
|
||||
var strs = [
|
||||
"application/json",
|
||||
"text/html",
|
||||
"image/png",
|
||||
"application/xml",
|
||||
"text/plain"
|
||||
]
|
||||
var i = 0
|
||||
var j = 0
|
||||
var count = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
for (j = 0; j < length(strs); j++) {
|
||||
if (starts_with(strs[j], "application/")) count++
|
||||
if (ends_with(strs[j], "/json")) count++
|
||||
if (starts_with(strs[j], "text/")) count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
}
|
||||
137
benches/tree_ops.cm
Normal file
137
benches/tree_ops.cm
Normal file
@@ -0,0 +1,137 @@
|
||||
// tree_ops.cm — Tree data structure operations kernel
|
||||
// Pointer chasing, recursion, allocation patterns.
|
||||
|
||||
// Binary tree: create, walk, transform, check
|
||||
function make_tree(depth) {
|
||||
if (depth <= 0) return {val: 1, left: null, right: null}
|
||||
return {
|
||||
val: depth,
|
||||
left: make_tree(depth - 1),
|
||||
right: make_tree(depth - 1)
|
||||
}
|
||||
}
|
||||
|
||||
function tree_check(node) {
|
||||
if (!node) return 0
|
||||
if (!node.left) return node.val
|
||||
return node.val + tree_check(node.left) - tree_check(node.right)
|
||||
}
|
||||
|
||||
function tree_sum(node) {
|
||||
if (!node) return 0
|
||||
return node.val + tree_sum(node.left) + tree_sum(node.right)
|
||||
}
|
||||
|
||||
function tree_depth(node) {
|
||||
if (!node) return 0
|
||||
var l = tree_depth(node.left)
|
||||
var r = tree_depth(node.right)
|
||||
return 1 + (l > r ? l : r)
|
||||
}
|
||||
|
||||
function tree_count(node) {
|
||||
if (!node) return 0
|
||||
return 1 + tree_count(node.left) + tree_count(node.right)
|
||||
}
|
||||
|
||||
// Transform tree: map values
|
||||
function tree_map(node, fn) {
|
||||
if (!node) return null
|
||||
return {
|
||||
val: fn(node.val),
|
||||
left: tree_map(node.left, fn),
|
||||
right: tree_map(node.right, fn)
|
||||
}
|
||||
}
|
||||
|
||||
// Flatten tree to array (in-order)
|
||||
function tree_flatten(node, result) {
|
||||
if (!node) return null
|
||||
tree_flatten(node.left, result)
|
||||
push(result, node.val)
|
||||
tree_flatten(node.right, result)
|
||||
return null
|
||||
}
|
||||
|
||||
// Build sorted tree from array (balanced)
|
||||
function build_balanced(arr, lo, hi) {
|
||||
if (lo > hi) return null
|
||||
var mid = floor((lo + hi) / 2)
|
||||
return {
|
||||
val: arr[mid],
|
||||
left: build_balanced(arr, lo, mid - 1),
|
||||
right: build_balanced(arr, mid + 1, hi)
|
||||
}
|
||||
}
|
||||
|
||||
// Find a value in BST
|
||||
function bst_find(node, val) {
|
||||
if (!node) return false
|
||||
if (val == node.val) return true
|
||||
if (val < node.val) return bst_find(node.left, val)
|
||||
return bst_find(node.right, val)
|
||||
}
|
||||
|
||||
return {
|
||||
// Binary tree create + check (allocation heavy)
|
||||
tree_create_check: function(n) {
|
||||
var i = 0
|
||||
var t = null
|
||||
var x = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
t = make_tree(10)
|
||||
x += tree_check(t)
|
||||
}
|
||||
return x
|
||||
},
|
||||
|
||||
// Deep tree traversals
|
||||
tree_traversal: function(n) {
|
||||
var t = make_tree(12)
|
||||
var x = 0
|
||||
var i = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
x += tree_sum(t) + tree_depth(t) + tree_count(t)
|
||||
}
|
||||
return x
|
||||
},
|
||||
|
||||
// Tree map (create new tree from old)
|
||||
tree_transform: function(n) {
|
||||
var t = make_tree(10)
|
||||
var i = 0
|
||||
var mapped = null
|
||||
for (i = 0; i < n; i++) {
|
||||
mapped = tree_map(t, function(v) { return v * 2 + 1 })
|
||||
}
|
||||
return mapped
|
||||
},
|
||||
|
||||
// Flatten + rebuild (array <-> tree conversion)
|
||||
tree_flatten_rebuild: function(n) {
|
||||
var t = make_tree(10)
|
||||
var i = 0
|
||||
var flat = null
|
||||
var rebuilt = null
|
||||
for (i = 0; i < n; i++) {
|
||||
flat = []
|
||||
tree_flatten(t, flat)
|
||||
rebuilt = build_balanced(flat, 0, length(flat) - 1)
|
||||
}
|
||||
return rebuilt
|
||||
},
|
||||
|
||||
// BST search (pointer chasing)
|
||||
bst_search: function(n) {
|
||||
// Build a balanced BST of 1024 elements
|
||||
var data = []
|
||||
var i = 0
|
||||
for (i = 0; i < 1024; i++) push(data, i)
|
||||
var bst = build_balanced(data, 0, 1023)
|
||||
var found = 0
|
||||
for (i = 0; i < n; i++) {
|
||||
if (bst_find(bst, i % 1024)) found++
|
||||
}
|
||||
return found
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
function mainThread() {
|
||||
var maxDepth = max(6, Number(arg[0] || 16));
|
||||
|
||||
var stretchDepth = maxDepth + 1;
|
||||
var check = itemCheck(bottomUpTree(stretchDepth));
|
||||
var longLivedTree = null
|
||||
var depth = null
|
||||
var iterations = null
|
||||
log.console(`stretch tree of depth ${stretchDepth}\t check: ${check}`);
|
||||
|
||||
var longLivedTree = bottomUpTree(maxDepth);
|
||||
longLivedTree = bottomUpTree(maxDepth);
|
||||
|
||||
for (var depth = 4; depth <= maxDepth; depth += 2) {
|
||||
var iterations = 1 << maxDepth - depth + 4;
|
||||
for (depth = 4; depth <= maxDepth; depth += 2) {
|
||||
iterations = 1 << maxDepth - depth + 4;
|
||||
work(iterations, depth);
|
||||
}
|
||||
|
||||
@@ -17,7 +19,8 @@ function mainThread() {
|
||||
|
||||
function work(iterations, depth) {
|
||||
var check = 0;
|
||||
for (var i = 0; i < iterations; i++)
|
||||
var i = 0
|
||||
for (i = 0; i < iterations; i++)
|
||||
check += itemCheck(bottomUpTree(depth));
|
||||
log.console(`${iterations}\t trees of depth ${depth}\t check: ${check}`);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
var blob = use('blob')
|
||||
var math = use('math/radians')
|
||||
|
||||
var i = 0
|
||||
var j = 0
|
||||
|
||||
function eratosthenes (n) {
|
||||
var sieve = blob(n, true)
|
||||
var sqrtN = whole(math.sqrt(n));
|
||||
@@ -9,7 +12,7 @@ function eratosthenes (n) {
|
||||
if (sieve.read_logical(i))
|
||||
for (j = i * i; j <= n; j += i)
|
||||
sieve.write_bit(j, false);
|
||||
|
||||
|
||||
return sieve;
|
||||
}
|
||||
|
||||
@@ -17,9 +20,9 @@ var sieve = eratosthenes(10000000);
|
||||
stone(sieve)
|
||||
|
||||
var c = 0
|
||||
for (var i = 0; i < length(sieve); i++)
|
||||
for (i = 0; i < length(sieve); i++)
|
||||
if (sieve.read_logical(i)) c++
|
||||
|
||||
|
||||
log.console(c)
|
||||
|
||||
$stop()
|
||||
$stop()
|
||||
|
||||
@@ -1,58 +1,65 @@
|
||||
function fannkuch(n) {
|
||||
var perm1 = [n]
|
||||
for (var i = 0; i < n; i++) perm1[i] = i
|
||||
var perm1 = [n]
|
||||
var i = 0
|
||||
var k = null
|
||||
var r = null
|
||||
var t = null
|
||||
var p0 = null
|
||||
var j = null
|
||||
var more = null
|
||||
for (i = 0; i < n; i++) perm1[i] = i
|
||||
var perm = [n]
|
||||
var count = [n]
|
||||
var f = 0, flips = 0, nperm = 0, checksum = 0
|
||||
var i, k, r
|
||||
|
||||
var count = [n]
|
||||
var f = 0
|
||||
var flips = 0
|
||||
var nperm = 0
|
||||
var checksum = 0
|
||||
|
||||
r = n
|
||||
while (r > 0) {
|
||||
i = 0
|
||||
while (r != 1) { count[r-1] = r; r -= 1 }
|
||||
while (i < n) { perm[i] = perm1[i]; i += 1 }
|
||||
|
||||
// Count flips and update max and checksum
|
||||
while (r > 0) {
|
||||
i = 0
|
||||
while (r != 1) { count[r-1] = r; r -= 1 }
|
||||
while (i < n) { perm[i] = perm1[i]; i += 1 }
|
||||
|
||||
f = 0
|
||||
k = perm[0]
|
||||
k = perm[0]
|
||||
while (k != 0) {
|
||||
i = 0
|
||||
while (2*i < k) {
|
||||
var t = perm[i]; perm[i] = perm[k-i]; perm[k-i] = t
|
||||
i += 1
|
||||
i = 0
|
||||
while (2*i < k) {
|
||||
t = perm[i]; perm[i] = perm[k-i]; perm[k-i] = t
|
||||
i += 1
|
||||
}
|
||||
k = perm[0]
|
||||
f += 1
|
||||
}
|
||||
if (f > flips) flips = f
|
||||
f += 1
|
||||
}
|
||||
if (f > flips) flips = f
|
||||
if ((nperm & 0x1) == 0) checksum += f; else checksum -= f
|
||||
|
||||
// Use incremental change to generate another permutation
|
||||
var more = true
|
||||
while (more) {
|
||||
|
||||
more = true
|
||||
while (more) {
|
||||
if (r == n) {
|
||||
log.console( checksum )
|
||||
return flips
|
||||
log.console( checksum )
|
||||
return flips
|
||||
}
|
||||
var p0 = perm1[0]
|
||||
p0 = perm1[0]
|
||||
i = 0
|
||||
while (i < r) {
|
||||
var j = i + 1
|
||||
j = i + 1
|
||||
perm1[i] = perm1[j]
|
||||
i = j
|
||||
i = j
|
||||
}
|
||||
perm1[r] = p0
|
||||
|
||||
count[r] -= 1
|
||||
if (count[r] > 0) more = false; else r += 1
|
||||
perm1[r] = p0
|
||||
|
||||
count[r] -= 1
|
||||
if (count[r] > 0) more = false; else r += 1
|
||||
}
|
||||
nperm += 1
|
||||
}
|
||||
return flips;
|
||||
return flips;
|
||||
}
|
||||
|
||||
|
||||
var n = arg[0] || 10
|
||||
|
||||
|
||||
log.console(`Pfannkuchen(${n}) = ${fannkuch(n)}`)
|
||||
|
||||
$stop()
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run hyperfine with parameter lists
|
||||
# This will create a cross-product of all libraries × all scenarios
|
||||
hyperfine \
|
||||
--warmup 3 \
|
||||
--runs 20 \
|
||||
-i \
|
||||
--export-csv wota_vs_nota_vs_json.csv \
|
||||
--export-json wota_vs_nota_vs_json.json \
|
||||
--export-markdown wota_vs_nota_vs_json.md \
|
||||
--parameter-list lib wota,nota,json \
|
||||
--parameter-list scen empty,integers,floats,strings,objects,nested,large_array \
|
||||
'cell benchmarks/wota_nota_json {lib} {scen}'
|
||||
|
||||
|
||||
echo "Benchmark complete! Results saved to:"
|
||||
echo " - wota_vs_nota_vs_json.csv"
|
||||
echo " - wota_vs_nota_vs_json.json"
|
||||
echo " - wota_vs_nota_vs_json.md"
|
||||
@@ -1,22 +1,12 @@
|
||||
var time = use('time')
|
||||
var math = use('math/radians')
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// JavaScript Performance Benchmark Suite
|
||||
// Tests core JS operations: property access, function calls, arithmetic, etc.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Test configurations
|
||||
def iterations = {
|
||||
simple: 10000000,
|
||||
medium: 1000000,
|
||||
complex: 100000
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Utility: measureTime(fn) => how long fn() takes in seconds
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function measureTime(fn) {
|
||||
var start = time.number();
|
||||
fn();
|
||||
@@ -24,26 +14,24 @@ function measureTime(fn) {
|
||||
return (end - start);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Benchmark: Property Access
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function benchPropertyAccess() {
|
||||
var obj = {
|
||||
a: 1, b: 2, c: 3, d: 4, e: 5,
|
||||
nested: { x: 10, y: 20, z: 30 }
|
||||
};
|
||||
|
||||
|
||||
var readTime = measureTime(function() {
|
||||
var sum = 0;
|
||||
for (var i = 0; i < iterations.simple; i++) {
|
||||
var i = 0
|
||||
for (i = 0; i < iterations.simple; i++) {
|
||||
sum += obj.a + obj.b + obj.c + obj.d + obj.e;
|
||||
sum += obj.nested.x + obj.nested.y + obj.nested.z;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var writeTime = measureTime(function() {
|
||||
for (var i = 0; i < iterations.simple; i++) {
|
||||
var i = 0
|
||||
for (i = 0; i < iterations.simple; i++) {
|
||||
obj.a = i;
|
||||
obj.b = i + 1;
|
||||
obj.c = i + 2;
|
||||
@@ -51,49 +39,48 @@ function benchPropertyAccess() {
|
||||
obj.nested.y = i * 3;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return { readTime: readTime, writeTime: writeTime };
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Benchmark: Function Calls
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function benchFunctionCalls() {
|
||||
function add(a, b) { return a + b; }
|
||||
function multiply(a, b) { return a * b; }
|
||||
function complexCalc(a, b, c) { return (a + b) * c / 2; }
|
||||
|
||||
|
||||
var obj = {
|
||||
method: function(x) { return x * 2; },
|
||||
nested: {
|
||||
deepMethod: function(x, y) { return x + y; }
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var simpleCallTime = measureTime(function() {
|
||||
var result = 0;
|
||||
for (var i = 0; i < iterations.simple; i++) {
|
||||
var i = 0
|
||||
for (i = 0; i < iterations.simple; i++) {
|
||||
result = add(i, 1);
|
||||
result = multiply(result, 2);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var methodCallTime = measureTime(function() {
|
||||
var result = 0;
|
||||
for (var i = 0; i < iterations.simple; i++) {
|
||||
var i = 0
|
||||
for (i = 0; i < iterations.simple; i++) {
|
||||
result = obj.method(i);
|
||||
result = obj.nested.deepMethod(result, i);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var complexCallTime = measureTime(function() {
|
||||
var result = 0;
|
||||
for (var i = 0; i < iterations.medium; i++) {
|
||||
var i = 0
|
||||
for (i = 0; i < iterations.medium; i++) {
|
||||
result = complexCalc(i, i + 1, i + 2);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
simpleCallTime: simpleCallTime,
|
||||
methodCallTime: methodCallTime,
|
||||
@@ -101,37 +88,39 @@ function benchFunctionCalls() {
|
||||
};
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Benchmark: Array Operations
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function benchArrayOps() {
|
||||
var i = 0
|
||||
|
||||
var pushTime = measureTime(function() {
|
||||
var arr = [];
|
||||
for (var i = 0; i < iterations.medium; i++) {
|
||||
push(arr, i);
|
||||
var j = 0
|
||||
for (j = 0; j < iterations.medium; j++) {
|
||||
push(arr, j);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var arr = [];
|
||||
for (var i = 0; i < 10000; i++) push(arr, i);
|
||||
|
||||
for (i = 0; i < 10000; i++) push(arr, i);
|
||||
|
||||
var accessTime = measureTime(function() {
|
||||
var sum = 0;
|
||||
for (var i = 0; i < iterations.medium; i++) {
|
||||
sum += arr[i % 10000];
|
||||
var j = 0
|
||||
for (j = 0; j < iterations.medium; j++) {
|
||||
sum += arr[j % 10000];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var iterateTime = measureTime(function() {
|
||||
var sum = 0;
|
||||
for (var j = 0; j < 1000; j++) {
|
||||
for (var i = 0; i < length(arr); i++) {
|
||||
sum += arr[i];
|
||||
var j = 0
|
||||
var k = 0
|
||||
for (j = 0; j < 1000; j++) {
|
||||
for (k = 0; k < length(arr); k++) {
|
||||
sum += arr[k];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
pushTime: pushTime,
|
||||
accessTime: accessTime,
|
||||
@@ -139,27 +128,27 @@ function benchArrayOps() {
|
||||
};
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Benchmark: Object Creation
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function benchObjectCreation() {
|
||||
var literalTime = measureTime(function() {
|
||||
for (var i = 0; i < iterations.medium; i++) {
|
||||
var obj = { x: i, y: i * 2, z: i * 3 };
|
||||
var i = 0
|
||||
var obj = null
|
||||
for (i = 0; i < iterations.medium; i++) {
|
||||
obj = { x: i, y: i * 2, z: i * 3 };
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function Point(x, y) {
|
||||
return {x,y}
|
||||
}
|
||||
|
||||
|
||||
var defructorTime = measureTime(function() {
|
||||
for (var i = 0; i < iterations.medium; i++) {
|
||||
var p = Point(i, i * 2);
|
||||
var i = 0
|
||||
var p = null
|
||||
for (i = 0; i < iterations.medium; i++) {
|
||||
p = Point(i, i * 2);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var protoObj = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
@@ -168,15 +157,17 @@ function benchObjectCreation() {
|
||||
this.y += dy;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var prototypeTime = measureTime(function() {
|
||||
for (var i = 0; i < iterations.medium; i++) {
|
||||
var obj = meme(protoObj);
|
||||
var i = 0
|
||||
var obj = null
|
||||
for (i = 0; i < iterations.medium; i++) {
|
||||
obj = meme(protoObj);
|
||||
obj.x = i;
|
||||
obj.y = i * 2;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
literalTime: literalTime,
|
||||
defructorTime: defructorTime,
|
||||
@@ -184,36 +175,39 @@ function benchObjectCreation() {
|
||||
};
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Benchmark: String Operations
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function benchStringOps() {
|
||||
var i = 0
|
||||
var strings = [];
|
||||
|
||||
var concatTime = measureTime(function() {
|
||||
var str = "";
|
||||
for (var i = 0; i < iterations.complex; i++) {
|
||||
str = "test" + i + "value";
|
||||
var j = 0
|
||||
for (j = 0; j < iterations.complex; j++) {
|
||||
str = "test" + j + "value";
|
||||
}
|
||||
});
|
||||
|
||||
var strings = [];
|
||||
for (var i = 0; i < 1000; i++) {
|
||||
|
||||
for (i = 0; i < 1000; i++) {
|
||||
push(strings, "string" + i);
|
||||
}
|
||||
|
||||
|
||||
var joinTime = measureTime(function() {
|
||||
for (var i = 0; i < iterations.complex; i++) {
|
||||
var result = text(strings, ",");
|
||||
var j = 0
|
||||
var result = null
|
||||
for (j = 0; j < iterations.complex; j++) {
|
||||
result = text(strings, ",");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var splitTime = measureTime(function() {
|
||||
var str = "a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p";
|
||||
for (var i = 0; i < iterations.medium; i++) {
|
||||
var parts = array(str, ",");
|
||||
var j = 0
|
||||
var parts = null
|
||||
for (j = 0; j < iterations.medium; j++) {
|
||||
parts = array(str, ",");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
concatTime: concatTime,
|
||||
joinTime: joinTime,
|
||||
@@ -221,35 +215,34 @@ function benchStringOps() {
|
||||
};
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Benchmark: Arithmetic Operations
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function benchArithmetic() {
|
||||
var intMathTime = measureTime(function() {
|
||||
var result = 1;
|
||||
for (var i = 0; i < iterations.simple; i++) {
|
||||
var i = 0
|
||||
for (i = 0; i < iterations.simple; i++) {
|
||||
result = ((result + i) * 2 - 1) / 3;
|
||||
result = result % 1000 + 1;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var floatMathTime = measureTime(function() {
|
||||
var result = 1.5;
|
||||
for (var i = 0; i < iterations.simple; i++) {
|
||||
var i = 0
|
||||
for (i = 0; i < iterations.simple; i++) {
|
||||
result = math.sine(result) + math.cosine(i * 0.01);
|
||||
result = math.sqrt(abs(result)) + 0.1;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var bitwiseTime = measureTime(function() {
|
||||
var result = 0;
|
||||
for (var i = 0; i < iterations.simple; i++) {
|
||||
var i = 0
|
||||
for (i = 0; i < iterations.simple; i++) {
|
||||
result = (result ^ i) & 0xFFFF;
|
||||
result = (result << 1) | (result >> 15);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
intMathTime: intMathTime,
|
||||
floatMathTime: floatMathTime,
|
||||
@@ -257,134 +250,123 @@ function benchArithmetic() {
|
||||
};
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Benchmark: Closure Operations
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function benchClosures() {
|
||||
var i = 0
|
||||
|
||||
function makeAdder(x) {
|
||||
return function(y) { return x + y; };
|
||||
}
|
||||
|
||||
|
||||
var closureCreateTime = measureTime(function() {
|
||||
var funcs = [];
|
||||
for (var i = 0; i < iterations.medium; i++) {
|
||||
push(funcs, makeAdder(i));
|
||||
var j = 0
|
||||
for (j = 0; j < iterations.medium; j++) {
|
||||
push(funcs, makeAdder(j));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var adders = [];
|
||||
for (var i = 0; i < 1000; i++) {
|
||||
for (i = 0; i < 1000; i++) {
|
||||
push(adders, makeAdder(i));
|
||||
}
|
||||
|
||||
|
||||
var closureCallTime = measureTime(function() {
|
||||
var sum = 0;
|
||||
for (var i = 0; i < iterations.medium; i++) {
|
||||
sum += adders[i % 1000](i);
|
||||
var j = 0
|
||||
for (j = 0; j < iterations.medium; j++) {
|
||||
sum += adders[j % 1000](j);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
closureCreateTime: closureCreateTime,
|
||||
closureCallTime: closureCallTime
|
||||
};
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Main benchmark runner
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
log.console("JavaScript Performance Benchmark");
|
||||
log.console("======================\n");
|
||||
|
||||
// Property Access
|
||||
log.console("BENCHMARK: Property Access");
|
||||
var propResults = benchPropertyAccess();
|
||||
log.console(" Read time: " + propResults.readTime.toFixed(3) + "s => " +
|
||||
log.console(" Read time: " + propResults.readTime.toFixed(3) + "s => " +
|
||||
(iterations.simple / propResults.readTime).toFixed(1) + " reads/sec [" +
|
||||
(propResults.readTime / iterations.simple * 1e9).toFixed(1) + " ns/op]");
|
||||
log.console(" Write time: " + propResults.writeTime.toFixed(3) + "s => " +
|
||||
log.console(" Write time: " + propResults.writeTime.toFixed(3) + "s => " +
|
||||
(iterations.simple / propResults.writeTime).toFixed(1) + " writes/sec [" +
|
||||
(propResults.writeTime / iterations.simple * 1e9).toFixed(1) + " ns/op]");
|
||||
log.console("");
|
||||
|
||||
// Function Calls
|
||||
log.console("BENCHMARK: Function Calls");
|
||||
var funcResults = benchFunctionCalls();
|
||||
log.console(" Simple calls: " + funcResults.simpleCallTime.toFixed(3) + "s => " +
|
||||
log.console(" Simple calls: " + funcResults.simpleCallTime.toFixed(3) + "s => " +
|
||||
(iterations.simple / funcResults.simpleCallTime).toFixed(1) + " calls/sec [" +
|
||||
(funcResults.simpleCallTime / iterations.simple * 1e9).toFixed(1) + " ns/op]");
|
||||
log.console(" Method calls: " + funcResults.methodCallTime.toFixed(3) + "s => " +
|
||||
log.console(" Method calls: " + funcResults.methodCallTime.toFixed(3) + "s => " +
|
||||
(iterations.simple / funcResults.methodCallTime).toFixed(1) + " calls/sec [" +
|
||||
(funcResults.methodCallTime / iterations.simple * 1e9).toFixed(1) + " ns/op]");
|
||||
log.console(" Complex calls: " + funcResults.complexCallTime.toFixed(3) + "s => " +
|
||||
log.console(" Complex calls: " + funcResults.complexCallTime.toFixed(3) + "s => " +
|
||||
(iterations.medium / funcResults.complexCallTime).toFixed(1) + " calls/sec [" +
|
||||
(funcResults.complexCallTime / iterations.medium * 1e9).toFixed(1) + " ns/op]");
|
||||
log.console("");
|
||||
|
||||
// Array Operations
|
||||
log.console("BENCHMARK: Array Operations");
|
||||
var arrayResults = benchArrayOps();
|
||||
log.console(" Push: " + arrayResults.pushTime.toFixed(3) + "s => " +
|
||||
log.console(" Push: " + arrayResults.pushTime.toFixed(3) + "s => " +
|
||||
(iterations.medium / arrayResults.pushTime).toFixed(1) + " pushes/sec [" +
|
||||
(arrayResults.pushTime / iterations.medium * 1e9).toFixed(1) + " ns/op]");
|
||||
log.console(" Access: " + arrayResults.accessTime.toFixed(3) + "s => " +
|
||||
log.console(" Access: " + arrayResults.accessTime.toFixed(3) + "s => " +
|
||||
(iterations.medium / arrayResults.accessTime).toFixed(1) + " accesses/sec [" +
|
||||
(arrayResults.accessTime / iterations.medium * 1e9).toFixed(1) + " ns/op]");
|
||||
log.console(" Iterate: " + arrayResults.iterateTime.toFixed(3) + "s => " +
|
||||
log.console(" Iterate: " + arrayResults.iterateTime.toFixed(3) + "s => " +
|
||||
(1000 / arrayResults.iterateTime).toFixed(1) + " full iterations/sec");
|
||||
log.console("");
|
||||
|
||||
// Object Creation
|
||||
log.console("BENCHMARK: Object Creation");
|
||||
var objResults = benchObjectCreation();
|
||||
log.console(" Literal: " + objResults.literalTime.toFixed(3) + "s => " +
|
||||
log.console(" Literal: " + objResults.literalTime.toFixed(3) + "s => " +
|
||||
(iterations.medium / objResults.literalTime).toFixed(1) + " creates/sec [" +
|
||||
(objResults.literalTime / iterations.medium * 1e9).toFixed(1) + " ns/op]");
|
||||
log.console(" Constructor: " + objResults.defructorTime.toFixed(3) + "s => " +
|
||||
log.console(" Constructor: " + objResults.defructorTime.toFixed(3) + "s => " +
|
||||
(iterations.medium / objResults.defructorTime).toFixed(1) + " creates/sec [" +
|
||||
(objResults.defructorTime / iterations.medium * 1e9).toFixed(1) + " ns/op]");
|
||||
log.console(" Prototype: " + objResults.prototypeTime.toFixed(3) + "s => " +
|
||||
log.console(" Prototype: " + objResults.prototypeTime.toFixed(3) + "s => " +
|
||||
(iterations.medium / objResults.prototypeTime).toFixed(1) + " creates/sec [" +
|
||||
(objResults.prototypeTime / iterations.medium * 1e9).toFixed(1) + " ns/op]");
|
||||
log.console("");
|
||||
|
||||
// String Operations
|
||||
log.console("BENCHMARK: String Operations");
|
||||
var strResults = benchStringOps();
|
||||
log.console(" Concat: " + strResults.concatTime.toFixed(3) + "s => " +
|
||||
log.console(" Concat: " + strResults.concatTime.toFixed(3) + "s => " +
|
||||
(iterations.complex / strResults.concatTime).toFixed(1) + " concats/sec [" +
|
||||
(strResults.concatTime / iterations.complex * 1e9).toFixed(1) + " ns/op]");
|
||||
log.console(" Join: " + strResults.joinTime.toFixed(3) + "s => " +
|
||||
log.console(" Join: " + strResults.joinTime.toFixed(3) + "s => " +
|
||||
(iterations.complex / strResults.joinTime).toFixed(1) + " joins/sec [" +
|
||||
(strResults.joinTime / iterations.complex * 1e9).toFixed(1) + " ns/op]");
|
||||
log.console(" Split: " + strResults.splitTime.toFixed(3) + "s => " +
|
||||
log.console(" Split: " + strResults.splitTime.toFixed(3) + "s => " +
|
||||
(iterations.medium / strResults.splitTime).toFixed(1) + " splits/sec [" +
|
||||
(strResults.splitTime / iterations.medium * 1e9).toFixed(1) + " ns/op]");
|
||||
log.console("");
|
||||
|
||||
// Arithmetic Operations
|
||||
log.console("BENCHMARK: Arithmetic Operations");
|
||||
var mathResults = benchArithmetic();
|
||||
log.console(" Integer math: " + mathResults.intMathTime.toFixed(3) + "s => " +
|
||||
log.console(" Integer math: " + mathResults.intMathTime.toFixed(3) + "s => " +
|
||||
(iterations.simple / mathResults.intMathTime).toFixed(1) + " ops/sec [" +
|
||||
(mathResults.intMathTime / iterations.simple * 1e9).toFixed(1) + " ns/op]");
|
||||
log.console(" Float math: " + mathResults.floatMathTime.toFixed(3) + "s => " +
|
||||
log.console(" Float math: " + mathResults.floatMathTime.toFixed(3) + "s => " +
|
||||
(iterations.simple / mathResults.floatMathTime).toFixed(1) + " ops/sec [" +
|
||||
(mathResults.floatMathTime / iterations.simple * 1e9).toFixed(1) + " ns/op]");
|
||||
log.console(" Bitwise: " + mathResults.bitwiseTime.toFixed(3) + "s => " +
|
||||
log.console(" Bitwise: " + mathResults.bitwiseTime.toFixed(3) + "s => " +
|
||||
(iterations.simple / mathResults.bitwiseTime).toFixed(1) + " ops/sec [" +
|
||||
(mathResults.bitwiseTime / iterations.simple * 1e9).toFixed(1) + " ns/op]");
|
||||
log.console("");
|
||||
|
||||
// Closures
|
||||
log.console("BENCHMARK: Closures");
|
||||
var closureResults = benchClosures();
|
||||
log.console(" Create: " + closureResults.closureCreateTime.toFixed(3) + "s => " +
|
||||
log.console(" Create: " + closureResults.closureCreateTime.toFixed(3) + "s => " +
|
||||
(iterations.medium / closureResults.closureCreateTime).toFixed(1) + " creates/sec [" +
|
||||
(closureResults.closureCreateTime / iterations.medium * 1e9).toFixed(1) + " ns/op]");
|
||||
log.console(" Call: " + closureResults.closureCallTime.toFixed(3) + "s => " +
|
||||
log.console(" Call: " + closureResults.closureCallTime.toFixed(3) + "s => " +
|
||||
(iterations.medium / closureResults.closureCallTime).toFixed(1) + " calls/sec [" +
|
||||
(closureResults.closureCallTime / iterations.medium * 1e9).toFixed(1) + " ns/op]");
|
||||
log.console("");
|
||||
|
||||
@@ -1,40 +1,46 @@
|
||||
var blob = use('blob')
|
||||
|
||||
var iter = 50, limit = 2.0;
|
||||
var zr, zi, cr, ci, tr, ti;
|
||||
var iter = 50
|
||||
var limit = 2.0
|
||||
var zr = null
|
||||
var zi = null
|
||||
var cr = null
|
||||
var ci = null
|
||||
var tr = null
|
||||
var ti = null
|
||||
var y = 0
|
||||
var x = 0
|
||||
var i = 0
|
||||
var row = null
|
||||
|
||||
var h = Number(arg[0]) || 500
|
||||
var w = h
|
||||
|
||||
log.console(`P4\n${w} ${h}`);
|
||||
|
||||
for (var y = 0; y < h; ++y) {
|
||||
// Create a blob for the row - we need w bits
|
||||
var row = blob(w);
|
||||
for (y = 0; y < h; ++y) {
|
||||
row = blob(w);
|
||||
|
||||
for (var x = 0; x < w; ++x) {
|
||||
zr = zi = tr = ti = 0;
|
||||
for (x = 0; x < w; ++x) {
|
||||
zr = 0; zi = 0; tr = 0; ti = 0;
|
||||
cr = 2 * x / w - 1.5;
|
||||
ci = 2 * y / h - 1;
|
||||
for (var i = 0; i < iter && (tr + ti <= limit * limit); ++i) {
|
||||
for (i = 0; i < iter && (tr + ti <= limit * limit); ++i) {
|
||||
zi = 2 * zr * zi + ci;
|
||||
zr = tr - ti + cr;
|
||||
tr = zr * zr;
|
||||
ti = zi * zi;
|
||||
}
|
||||
|
||||
// Write a 1 bit if inside the set, 0 if outside
|
||||
if (tr + ti <= limit * limit)
|
||||
row.write_bit(1);
|
||||
else
|
||||
row.write_bit(0);
|
||||
}
|
||||
|
||||
// Convert the blob to stone (immutable) to prepare for output
|
||||
|
||||
stone(row)
|
||||
|
||||
// Output the blob data as raw bytes
|
||||
log.console(text(row, 'b'));
|
||||
|
||||
log.console(text(row, 'b'));
|
||||
}
|
||||
|
||||
$stop()
|
||||
@@ -1,9 +1,12 @@
|
||||
var math = use('math/radians')
|
||||
var N = 1000000;
|
||||
var num = 0;
|
||||
for (var i = 0; i < N; i ++) {
|
||||
var x = 2 * $random();
|
||||
var y = $random();
|
||||
var i = 0
|
||||
var x = null
|
||||
var y = null
|
||||
for (i = 0; i < N; i++) {
|
||||
x = 2 * $random();
|
||||
y = $random();
|
||||
if (y < math.sine(x * x))
|
||||
num++;
|
||||
}
|
||||
|
||||
@@ -2,60 +2,60 @@ var math = use('math/radians')
|
||||
var SOLAR_MASS = 4 * pi * pi;
|
||||
var DAYS_PER_YEAR = 365.24;
|
||||
|
||||
function Body(x, y, z, vx, vy, vz, mass) {
|
||||
return {x, y, z, vx, vy, vz, mass};
|
||||
function Body(p) {
|
||||
return {x: p.x, y: p.y, z: p.z, vx: p.vx, vy: p.vy, vz: p.vz, mass: p.mass};
|
||||
}
|
||||
|
||||
function Jupiter() {
|
||||
return Body(
|
||||
4.84143144246472090e+00,
|
||||
-1.16032004402742839e+00,
|
||||
-1.03622044471123109e-01,
|
||||
1.66007664274403694e-03 * DAYS_PER_YEAR,
|
||||
7.69901118419740425e-03 * DAYS_PER_YEAR,
|
||||
-6.90460016972063023e-05 * DAYS_PER_YEAR,
|
||||
9.54791938424326609e-04 * SOLAR_MASS
|
||||
);
|
||||
return Body({
|
||||
x: 4.84143144246472090e+00,
|
||||
y: -1.16032004402742839e+00,
|
||||
z: -1.03622044471123109e-01,
|
||||
vx: 1.66007664274403694e-03 * DAYS_PER_YEAR,
|
||||
vy: 7.69901118419740425e-03 * DAYS_PER_YEAR,
|
||||
vz: -6.90460016972063023e-05 * DAYS_PER_YEAR,
|
||||
mass: 9.54791938424326609e-04 * SOLAR_MASS
|
||||
});
|
||||
}
|
||||
|
||||
function Saturn() {
|
||||
return Body(
|
||||
8.34336671824457987e+00,
|
||||
4.12479856412430479e+00,
|
||||
-4.03523417114321381e-01,
|
||||
-2.76742510726862411e-03 * DAYS_PER_YEAR,
|
||||
4.99852801234917238e-03 * DAYS_PER_YEAR,
|
||||
2.30417297573763929e-05 * DAYS_PER_YEAR,
|
||||
2.85885980666130812e-04 * SOLAR_MASS
|
||||
);
|
||||
return Body({
|
||||
x: 8.34336671824457987e+00,
|
||||
y: 4.12479856412430479e+00,
|
||||
z: -4.03523417114321381e-01,
|
||||
vx: -2.76742510726862411e-03 * DAYS_PER_YEAR,
|
||||
vy: 4.99852801234917238e-03 * DAYS_PER_YEAR,
|
||||
vz: 2.30417297573763929e-05 * DAYS_PER_YEAR,
|
||||
mass: 2.85885980666130812e-04 * SOLAR_MASS
|
||||
});
|
||||
}
|
||||
|
||||
function Uranus() {
|
||||
return Body(
|
||||
1.28943695621391310e+01,
|
||||
-1.51111514016986312e+01,
|
||||
-2.23307578892655734e-01,
|
||||
2.96460137564761618e-03 * DAYS_PER_YEAR,
|
||||
2.37847173959480950e-03 * DAYS_PER_YEAR,
|
||||
-2.96589568540237556e-05 * DAYS_PER_YEAR,
|
||||
4.36624404335156298e-05 * SOLAR_MASS
|
||||
);
|
||||
return Body({
|
||||
x: 1.28943695621391310e+01,
|
||||
y: -1.51111514016986312e+01,
|
||||
z: -2.23307578892655734e-01,
|
||||
vx: 2.96460137564761618e-03 * DAYS_PER_YEAR,
|
||||
vy: 2.37847173959480950e-03 * DAYS_PER_YEAR,
|
||||
vz: -2.96589568540237556e-05 * DAYS_PER_YEAR,
|
||||
mass: 4.36624404335156298e-05 * SOLAR_MASS
|
||||
});
|
||||
}
|
||||
|
||||
function Neptune() {
|
||||
return Body(
|
||||
1.53796971148509165e+01,
|
||||
-2.59193146099879641e+01,
|
||||
1.79258772950371181e-01,
|
||||
2.68067772490389322e-03 * DAYS_PER_YEAR,
|
||||
1.62824170038242295e-03 * DAYS_PER_YEAR,
|
||||
-9.51592254519715870e-05 * DAYS_PER_YEAR,
|
||||
5.15138902046611451e-05 * SOLAR_MASS
|
||||
);
|
||||
return Body({
|
||||
x: 1.53796971148509165e+01,
|
||||
y: -2.59193146099879641e+01,
|
||||
z: 1.79258772950371181e-01,
|
||||
vx: 2.68067772490389322e-03 * DAYS_PER_YEAR,
|
||||
vy: 1.62824170038242295e-03 * DAYS_PER_YEAR,
|
||||
vz: -9.51592254519715870e-05 * DAYS_PER_YEAR,
|
||||
mass: 5.15138902046611451e-05 * SOLAR_MASS
|
||||
});
|
||||
}
|
||||
|
||||
function Sun() {
|
||||
return Body(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, SOLAR_MASS);
|
||||
return Body({x: 0.0, y: 0.0, z: 0.0, vx: 0.0, vy: 0.0, vz: 0.0, mass: SOLAR_MASS});
|
||||
}
|
||||
|
||||
var bodies = Array(Sun(), Jupiter(), Saturn(), Uranus(), Neptune());
|
||||
@@ -65,15 +65,18 @@ function offsetMomentum() {
|
||||
var py = 0;
|
||||
var pz = 0;
|
||||
var size = length(bodies);
|
||||
for (var i = 0; i < size; i++) {
|
||||
var body = bodies[i];
|
||||
var mass = body.mass;
|
||||
var i = 0
|
||||
var body = null
|
||||
var mass = null
|
||||
for (i = 0; i < size; i++) {
|
||||
body = bodies[i];
|
||||
mass = body.mass;
|
||||
px += body.vx * mass;
|
||||
py += body.vy * mass;
|
||||
pz += body.vz * mass;
|
||||
}
|
||||
|
||||
var body = bodies[0];
|
||||
body = bodies[0];
|
||||
body.vx = -px / SOLAR_MASS;
|
||||
body.vy = -py / SOLAR_MASS;
|
||||
body.vz = -pz / SOLAR_MASS;
|
||||
@@ -81,27 +84,42 @@ function offsetMomentum() {
|
||||
|
||||
function advance(dt) {
|
||||
var size = length(bodies);
|
||||
var i = 0
|
||||
var j = 0
|
||||
var bodyi = null
|
||||
var bodyj = null
|
||||
var vxi = null
|
||||
var vyi = null
|
||||
var vzi = null
|
||||
var dx = null
|
||||
var dy = null
|
||||
var dz = null
|
||||
var d2 = null
|
||||
var mag = null
|
||||
var massj = null
|
||||
var massi = null
|
||||
var body = null
|
||||
|
||||
for (var i = 0; i < size; i++) {
|
||||
var bodyi = bodies[i];
|
||||
var vxi = bodyi.vx;
|
||||
var vyi = bodyi.vy;
|
||||
var vzi = bodyi.vz;
|
||||
for (var j = i + 1; j < size; j++) {
|
||||
var bodyj = bodies[j];
|
||||
var dx = bodyi.x - bodyj.x;
|
||||
var dy = bodyi.y - bodyj.y;
|
||||
var dz = bodyi.z - bodyj.z;
|
||||
for (i = 0; i < size; i++) {
|
||||
bodyi = bodies[i];
|
||||
vxi = bodyi.vx;
|
||||
vyi = bodyi.vy;
|
||||
vzi = bodyi.vz;
|
||||
for (j = i + 1; j < size; j++) {
|
||||
bodyj = bodies[j];
|
||||
dx = bodyi.x - bodyj.x;
|
||||
dy = bodyi.y - bodyj.y;
|
||||
dz = bodyi.z - bodyj.z;
|
||||
|
||||
var d2 = dx * dx + dy * dy + dz * dz;
|
||||
var mag = dt / (d2 * math.sqrt(d2));
|
||||
d2 = dx * dx + dy * dy + dz * dz;
|
||||
mag = dt / (d2 * math.sqrt(d2));
|
||||
|
||||
var massj = bodyj.mass;
|
||||
massj = bodyj.mass;
|
||||
vxi -= dx * massj * mag;
|
||||
vyi -= dy * massj * mag;
|
||||
vzi -= dz * massj * mag;
|
||||
|
||||
var massi = bodyi.mass;
|
||||
massi = bodyi.mass;
|
||||
bodyj.vx += dx * massi * mag;
|
||||
bodyj.vy += dy * massi * mag;
|
||||
bodyj.vz += dz * massi * mag;
|
||||
@@ -111,8 +129,8 @@ function advance(dt) {
|
||||
bodyi.vz = vzi;
|
||||
}
|
||||
|
||||
for (var i = 0; i < size; i++) {
|
||||
var body = bodies[i];
|
||||
for (i = 0; i < size; i++) {
|
||||
body = bodies[i];
|
||||
body.x += dt * body.vx;
|
||||
body.y += dt * body.vy;
|
||||
body.z += dt * body.vz;
|
||||
@@ -122,20 +140,28 @@ function advance(dt) {
|
||||
function energy() {
|
||||
var e = 0;
|
||||
var size = length(bodies);
|
||||
var i = 0
|
||||
var j = 0
|
||||
var bodyi = null
|
||||
var bodyj = null
|
||||
var dx = null
|
||||
var dy = null
|
||||
var dz = null
|
||||
var distance = null
|
||||
|
||||
for (var i = 0; i < size; i++) {
|
||||
var bodyi = bodies[i];
|
||||
for (i = 0; i < size; i++) {
|
||||
bodyi = bodies[i];
|
||||
|
||||
e += 0.5 * bodyi.mass * ( bodyi.vx * bodyi.vx +
|
||||
e += 0.5 * bodyi.mass * ( bodyi.vx * bodyi.vx +
|
||||
bodyi.vy * bodyi.vy + bodyi.vz * bodyi.vz );
|
||||
|
||||
for (var j = i + 1; j < size; j++) {
|
||||
var bodyj = bodies[j];
|
||||
var dx = bodyi.x - bodyj.x;
|
||||
var dy = bodyi.y - bodyj.y;
|
||||
var dz = bodyi.z - bodyj.z;
|
||||
for (j = i + 1; j < size; j++) {
|
||||
bodyj = bodies[j];
|
||||
dx = bodyi.x - bodyj.x;
|
||||
dy = bodyi.y - bodyj.y;
|
||||
dz = bodyi.z - bodyj.z;
|
||||
|
||||
var distance = math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
distance = math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
e -= (bodyi.mass * bodyj.mass) / distance;
|
||||
}
|
||||
}
|
||||
@@ -143,12 +169,13 @@ function energy() {
|
||||
}
|
||||
|
||||
var n = arg[0] || 100000
|
||||
var i = 0
|
||||
|
||||
offsetMomentum();
|
||||
|
||||
log.console(`n = ${n}`)
|
||||
log.console(energy().toFixed(9))
|
||||
for (var i = 0; i < n; i++)
|
||||
for (i = 0; i < n; i++)
|
||||
advance(0.01);
|
||||
log.console(energy().toFixed(9))
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
var nota = use('nota')
|
||||
var os = use('os')
|
||||
var nota = use('internal/nota')
|
||||
var os = use('internal/os')
|
||||
var io = use('fd')
|
||||
var json = use('json')
|
||||
|
||||
@@ -7,41 +7,40 @@ var ll = io.slurp('benchmarks/nota.json')
|
||||
|
||||
var newarr = []
|
||||
var accstr = ""
|
||||
for (var i = 0; i < 10000; i++) {
|
||||
var i = 0
|
||||
var start = null
|
||||
var jll = null
|
||||
var jsonStr = null
|
||||
var nll = null
|
||||
var oll = null
|
||||
for (i = 0; i < 10000; i++) {
|
||||
accstr += i;
|
||||
newarrpush(i.toString())
|
||||
push(newarr, text(i))
|
||||
}
|
||||
// Arrays to store timing results
|
||||
var jsonDecodeTimes = [];
|
||||
var jsonEncodeTimes = [];
|
||||
var notaEncodeTimes = [];
|
||||
var notaDecodeTimes = [];
|
||||
var notaSizes = [];
|
||||
|
||||
// Run 100 tests
|
||||
for (var i = 0; i < 100; i++) {
|
||||
// JSON Decode test
|
||||
var start = os.now();
|
||||
var jll = json.decode(ll);
|
||||
jsonDecodeTimespush((os.now() - start) * 1000);
|
||||
|
||||
// JSON Encode test
|
||||
for (i = 0; i < 100; i++) {
|
||||
start = os.now();
|
||||
var jsonStr = JSON.stringify(jll);
|
||||
jsonEncodeTimespush((os.now() - start) * 1000);
|
||||
jll = json.decode(ll);
|
||||
push(jsonDecodeTimes, (os.now() - start) * 1000);
|
||||
|
||||
// NOTA Encode test
|
||||
start = os.now();
|
||||
var nll = nota.encode(jll);
|
||||
notaEncodeTimespush((os.now() - start) * 1000);
|
||||
jsonStr = JSON.stringify(jll);
|
||||
push(jsonEncodeTimes, (os.now() - start) * 1000);
|
||||
|
||||
// NOTA Decode test
|
||||
start = os.now();
|
||||
var oll = nota.decode(nll);
|
||||
notaDecodeTimespush((os.now() - start) * 1000);
|
||||
nll = nota.encode(jll);
|
||||
push(notaEncodeTimes, (os.now() - start) * 1000);
|
||||
|
||||
start = os.now();
|
||||
oll = nota.decode(nll);
|
||||
push(notaDecodeTimes, (os.now() - start) * 1000);
|
||||
}
|
||||
|
||||
// Calculate statistics
|
||||
function getStats(arr) {
|
||||
return {
|
||||
avg: reduce(arr, (a,b) => a+b, 0) / length(arr),
|
||||
@@ -50,7 +49,6 @@ function getStats(arr) {
|
||||
};
|
||||
}
|
||||
|
||||
// Pretty print results
|
||||
log.console("\n== Performance Test Results (100 iterations) ==");
|
||||
log.console("\nJSON Decoding (ms):");
|
||||
def jsonDecStats = getStats(jsonDecodeTimes);
|
||||
@@ -75,4 +73,3 @@ def notaDecStats = getStats(notaDecodeTimes);
|
||||
log.console(`Average: ${notaDecStats.avg.toFixed(2)} ms`);
|
||||
log.console(`Min: ${notaDecStats.min.toFixed(2)} ms`);
|
||||
log.console(`Max: ${notaDecStats.max.toFixed(2)} ms`);
|
||||
|
||||
|
||||
2132
benchmarks/nota.json
2132
benchmarks/nota.json
File diff suppressed because it is too large
Load Diff
@@ -5,21 +5,27 @@ function A(i,j) {
|
||||
}
|
||||
|
||||
function Au(u,v) {
|
||||
for (var i=0; i<length(u); ++i) {
|
||||
var t = 0;
|
||||
for (var j=0; j<length(u); ++j)
|
||||
var i = 0
|
||||
var j = 0
|
||||
var t = null
|
||||
for (i = 0; i < length(u); ++i) {
|
||||
t = 0;
|
||||
for (j = 0; j < length(u); ++j)
|
||||
t += A(i,j) * u[j];
|
||||
|
||||
|
||||
v[i] = t;
|
||||
}
|
||||
}
|
||||
|
||||
function Atu(u,v) {
|
||||
for (var i=0; i<length(u); ++i) {
|
||||
var t = 0;
|
||||
for (var j=0; j<length(u); ++j)
|
||||
var i = 0
|
||||
var j = 0
|
||||
var t = null
|
||||
for (i = 0; i < length(u); ++i) {
|
||||
t = 0;
|
||||
for (j = 0; j < length(u); ++j)
|
||||
t += A(j,i) * u[j];
|
||||
|
||||
|
||||
v[i] = t;
|
||||
}
|
||||
}
|
||||
@@ -30,20 +36,26 @@ function AtAu(u,v,w) {
|
||||
}
|
||||
|
||||
function spectralnorm(n) {
|
||||
var i, u=[], v=[], w=[], vv=0, vBv=0;
|
||||
for (i=0; i<n; ++i)
|
||||
u[i] = 1; v[i] = w[i] = 0;
|
||||
var i = 0
|
||||
var u = []
|
||||
var v = []
|
||||
var w = []
|
||||
var vv = 0
|
||||
var vBv = 0
|
||||
for (i = 0; i < n; ++i) {
|
||||
u[i] = 1; v[i] = 0; w[i] = 0;
|
||||
}
|
||||
|
||||
for (i=0; i<10; ++i) {
|
||||
for (i = 0; i < 10; ++i) {
|
||||
AtAu(u,v,w);
|
||||
AtAu(v,u,w);
|
||||
}
|
||||
|
||||
for (i=0; i<n; ++i) {
|
||||
|
||||
for (i = 0; i < n; ++i) {
|
||||
vBv += u[i]*v[i];
|
||||
vv += v[i]*v[i];
|
||||
}
|
||||
|
||||
|
||||
return math.sqrt(vBv/vv);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,41 +1,22 @@
|
||||
//
|
||||
// wota_benchmark.js
|
||||
//
|
||||
// Usage in QuickJS:
|
||||
// qjs wota_benchmark.js
|
||||
//
|
||||
// Prerequisite:
|
||||
var wota = use('wota');
|
||||
var os = use('os');
|
||||
// or otherwise ensure `wota` and `os` are available.
|
||||
// Make sure wota_benchmark.js is loaded after wota.js or combined with it.
|
||||
//
|
||||
var wota = use('internal/wota');
|
||||
var os = use('internal/os');
|
||||
|
||||
var i = 0
|
||||
|
||||
// Helper to run a function repeatedly and measure total time in seconds.
|
||||
// Returns elapsed time in seconds.
|
||||
function measureTime(fn, iterations) {
|
||||
var t1 = os.now();
|
||||
for (var i = 0; i < iterations; i++) {
|
||||
for (i = 0; i < iterations; i++) {
|
||||
fn();
|
||||
}
|
||||
var t2 = os.now();
|
||||
return t2 - t1;
|
||||
}
|
||||
|
||||
// We'll define a function that does `encode -> decode` for a given value:
|
||||
function roundTripWota(value) {
|
||||
var encoded = wota.encode(value);
|
||||
var decoded = wota.decode(encoded);
|
||||
// Not doing a deep compare here, just measuring performance.
|
||||
// (We trust the test suite to verify correctness.)
|
||||
}
|
||||
|
||||
// A small suite of data we want to benchmark. Each entry includes:
|
||||
// name: label for printing
|
||||
// data: the test value(s) to encode/decode
|
||||
// iterations: how many times to loop
|
||||
//
|
||||
// You can tweak these as you like for heavier or lighter tests.
|
||||
def benchmarks = [
|
||||
{
|
||||
name: "Small Integers",
|
||||
@@ -62,22 +43,17 @@ def benchmarks = [
|
||||
},
|
||||
{
|
||||
name: "Large Array (1k numbers)",
|
||||
// A thousand random numbers
|
||||
data: [ array(1000, i => i *0.5) ],
|
||||
iterations: 1000
|
||||
},
|
||||
];
|
||||
|
||||
// Print a header
|
||||
log.console("Wota Encode/Decode Benchmark");
|
||||
log.console("===================\n");
|
||||
|
||||
// We'll run each benchmark scenario in turn.
|
||||
arrfor(benchmarks, function(bench) {
|
||||
var totalIterations = bench.iterations * length(bench.data);
|
||||
|
||||
// We'll define a function that does a roundTrip for *each* data item in bench.data
|
||||
// to measure in one loop iteration. Then we multiply by bench.iterations.
|
||||
|
||||
function runAllData() {
|
||||
arrfor(bench.data, roundTripWota)
|
||||
}
|
||||
@@ -91,5 +67,4 @@ arrfor(benchmarks, function(bench) {
|
||||
log.console(` Throughput: ${opsPerSec} encode+decode ops/sec\n`);
|
||||
})
|
||||
|
||||
// All done
|
||||
log.console("Benchmark completed.\n");
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
//
|
||||
// benchmark_wota_nota_json.js
|
||||
//
|
||||
// Usage in QuickJS:
|
||||
// qjs benchmark_wota_nota_json.js <LibraryName> <ScenarioName>
|
||||
//
|
||||
// Ensure wota, nota, json, and os are all available, e.g.:
|
||||
var wota = use('wota');
|
||||
var nota = use('nota');
|
||||
var json = use('json');
|
||||
var jswota = use('jswota')
|
||||
var os = use('os');
|
||||
//
|
||||
var wota = use('internal/wota');
|
||||
var nota = use('internal/nota');
|
||||
var json = use('json');
|
||||
var jswota = use('jswota')
|
||||
var os = use('internal/os');
|
||||
|
||||
// Parse command line arguments
|
||||
if (length(arg) != 2) {
|
||||
log.console('Usage: cell benchmark_wota_nota_json.ce <LibraryName> <ScenarioName>');
|
||||
$stop()
|
||||
@@ -21,16 +12,11 @@ if (length(arg) != 2) {
|
||||
var lib_name = arg[0];
|
||||
var scenario_name = arg[1];
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 1. Setup "libraries" array to easily switch among wota, nota, and json
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
def libraries = [
|
||||
{
|
||||
name: "wota",
|
||||
encode: wota.encode,
|
||||
decode: wota.decode,
|
||||
// wota produces an ArrayBuffer. We'll count `buffer.byteLength` as size.
|
||||
getSize(encoded) {
|
||||
return length(encoded);
|
||||
}
|
||||
@@ -39,7 +25,6 @@ def libraries = [
|
||||
name: "nota",
|
||||
encode: nota.encode,
|
||||
decode: nota.decode,
|
||||
// nota also produces an ArrayBuffer:
|
||||
getSize(encoded) {
|
||||
return length(encoded);
|
||||
}
|
||||
@@ -48,19 +33,12 @@ def libraries = [
|
||||
name: "json",
|
||||
encode: json.encode,
|
||||
decode: json.decode,
|
||||
// json produces a JS string. We'll measure its UTF-16 code unit length
|
||||
// as a rough "size". Alternatively, you could convert to UTF-8 for
|
||||
getSize(encodedStr) {
|
||||
return length(encodedStr);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 2. Test data sets (similar to wota benchmarks).
|
||||
// Each scenario has { name, data, iterations }
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
def benchmarks = [
|
||||
{
|
||||
name: "empty",
|
||||
@@ -102,42 +80,24 @@ def benchmarks = [
|
||||
},
|
||||
];
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 3. Utility: measureTime(fn) => how long fn() takes in seconds.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function measureTime(fn) {
|
||||
var start = os.now();
|
||||
fn();
|
||||
var end = os.now();
|
||||
return (end - start); // in seconds
|
||||
return (end - start);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 4. For each library, we run each benchmark scenario and measure:
|
||||
// - Encoding time (seconds)
|
||||
// - Decoding time (seconds)
|
||||
// - Total encoded size (bytes or code units for json)
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function runBenchmarkForLibrary(lib, bench) {
|
||||
// We'll encode and decode each item in `bench.data`.
|
||||
// We do 'bench.iterations' times. Then sum up total time.
|
||||
|
||||
// Pre-store the encoded results for all items so we can measure decode time
|
||||
// in a separate pass. Also measure total size once.
|
||||
var encodedList = [];
|
||||
var totalSize = 0;
|
||||
var i = 0
|
||||
var j = 0
|
||||
var e = null
|
||||
|
||||
// 1) Measure ENCODING
|
||||
var encodeTime = measureTime(() => {
|
||||
for (var i = 0; i < bench.iterations; i++) {
|
||||
// For each data item, encode it
|
||||
for (var j = 0; j < length(bench.data); j++) {
|
||||
var e = lib.encode(bench.data[j]);
|
||||
// store only in the very first iteration, so we can decode them later
|
||||
// but do not store them every iteration or we blow up memory.
|
||||
for (i = 0; i < bench.iterations; i++) {
|
||||
for (j = 0; j < length(bench.data); j++) {
|
||||
e = lib.encode(bench.data[j]);
|
||||
if (i == 0) {
|
||||
push(encodedList, e);
|
||||
totalSize += lib.getSize(e);
|
||||
@@ -146,9 +106,8 @@ function runBenchmarkForLibrary(lib, bench) {
|
||||
}
|
||||
});
|
||||
|
||||
// 2) Measure DECODING
|
||||
var decodeTime = measureTime(() => {
|
||||
for (var i = 0; i < bench.iterations; i++) {
|
||||
for (i = 0; i < bench.iterations; i++) {
|
||||
arrfor(encodedList, lib.decode)
|
||||
}
|
||||
});
|
||||
@@ -156,11 +115,6 @@ function runBenchmarkForLibrary(lib, bench) {
|
||||
return { encodeTime, decodeTime, totalSize };
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 5. Main driver: run only the specified library and scenario
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Find the requested library and scenario
|
||||
var lib = libraries[find(libraries, l => l.name == lib_name)];
|
||||
var bench = benchmarks[find(benchmarks, b => b.name == scenario_name)];
|
||||
|
||||
@@ -176,10 +130,11 @@ if (!bench) {
|
||||
$stop()
|
||||
}
|
||||
|
||||
// Run the benchmark for this library/scenario combination
|
||||
var { encodeTime, decodeTime, totalSize } = runBenchmarkForLibrary(lib, bench);
|
||||
var bench_result = runBenchmarkForLibrary(lib, bench);
|
||||
var encodeTime = bench_result.encodeTime;
|
||||
var decodeTime = bench_result.decodeTime;
|
||||
var totalSize = bench_result.totalSize;
|
||||
|
||||
// Output json for easy parsing by hyperfine or other tools
|
||||
var totalOps = bench.iterations * length(bench.data);
|
||||
var result = {
|
||||
lib: lib_name,
|
||||
|
||||
192
boot.ce
Normal file
192
boot.ce
Normal file
@@ -0,0 +1,192 @@
|
||||
// cell boot [--native] <program> - Pre-compile all module dependencies in parallel
|
||||
//
|
||||
// Discovers all transitive module dependencies for a program,
|
||||
// checks which are not yet cached, and compiles uncached ones
|
||||
// in parallel using worker actors composed via parallel() requestors.
|
||||
//
|
||||
// Also used as a child actor by engine.cm for auto-boot.
|
||||
|
||||
var shop = use('internal/shop')
|
||||
var fd = use('fd')
|
||||
var pkg_tools = use('package')
|
||||
var build = use('build')
|
||||
|
||||
var is_native = false
|
||||
var target_prog = null
|
||||
var target_pkg = null
|
||||
var i = 0
|
||||
|
||||
// Child actor mode: receive message from engine.cm
|
||||
var _child_mode = false
|
||||
var run_boot = null
|
||||
|
||||
$receiver(function(msg) {
|
||||
_child_mode = true
|
||||
is_native = msg.native || false
|
||||
target_prog = msg.program
|
||||
target_pkg = msg.package
|
||||
run_boot()
|
||||
})
|
||||
|
||||
// CLI mode: parse arguments
|
||||
if (args && length(args) > 0) {
|
||||
for (i = 0; i < length(args); i = i + 1) {
|
||||
if (args[i] == '--native') {
|
||||
is_native = true
|
||||
} else if (args[i] == '--help' || args[i] == '-h') {
|
||||
log.console("Usage: cell boot [--native] <program>")
|
||||
log.console("")
|
||||
log.console("Pre-compile all module dependencies for a program.")
|
||||
log.console("Uncached modules are compiled in parallel.")
|
||||
$stop()
|
||||
} else if (!starts_with(args[i], '-')) {
|
||||
target_prog = args[i]
|
||||
}
|
||||
}
|
||||
if (!target_prog) {
|
||||
log.error("boot: no program specified")
|
||||
$stop()
|
||||
}
|
||||
}
|
||||
|
||||
// Discover all transitive module dependencies for a file
|
||||
function discover_deps(file_path) {
|
||||
return shop.trace_deps(file_path)
|
||||
}
|
||||
|
||||
// Filter out already-cached modules
|
||||
function filter_uncached(deps) {
|
||||
var uncached = []
|
||||
var j = 0
|
||||
var s = null
|
||||
|
||||
j = 0
|
||||
while (j < length(deps.scripts)) {
|
||||
s = deps.scripts[j]
|
||||
if (is_native) {
|
||||
if (!shop.is_native_cached(s.path, s.package)) {
|
||||
uncached[] = {type: 'native_script', path: s.path, package: s.package}
|
||||
}
|
||||
} else {
|
||||
if (!shop.is_cached(s.path)) {
|
||||
uncached[] = {type: 'script', path: s.path, package: s.package}
|
||||
}
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
|
||||
// Expand C packages into individual files for parallel compilation
|
||||
var target = build.detect_host_target()
|
||||
var pkg = null
|
||||
var c_files = null
|
||||
var k = 0
|
||||
j = 0
|
||||
while (j < length(deps.c_packages)) {
|
||||
pkg = deps.c_packages[j]
|
||||
if (pkg != 'core') {
|
||||
c_files = pkg_tools.get_c_files(pkg, target, true)
|
||||
k = 0
|
||||
while (k < length(c_files)) {
|
||||
uncached[] = {type: 'c_file', package: pkg, file: c_files[k]}
|
||||
k = k + 1
|
||||
}
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
|
||||
return uncached
|
||||
}
|
||||
|
||||
function item_name(item) {
|
||||
if (item.path) return item.path
|
||||
if (item.file) return item.package + '/' + item.file
|
||||
return item.package
|
||||
}
|
||||
|
||||
// Create a requestor that spawns a compile_worker actor for one item
|
||||
function make_compile_requestor(item) {
|
||||
var worker = null
|
||||
var name = item_name(item)
|
||||
return function(callback, value) {
|
||||
log.console('boot: spawning worker for ' + name)
|
||||
$start(function(event) {
|
||||
if (event.type == 'greet') {
|
||||
worker = event.actor
|
||||
send(event.actor, {
|
||||
type: item.type,
|
||||
path: item.path,
|
||||
package: item.package,
|
||||
file: item.file
|
||||
})
|
||||
}
|
||||
if (event.type == 'stop') {
|
||||
callback(name)
|
||||
}
|
||||
if (event.type == 'disrupt') {
|
||||
log.error('boot: worker failed for ' + name)
|
||||
callback(null, {message: 'compile failed: ' + name})
|
||||
}
|
||||
}, 'compile_worker')
|
||||
return function cancel(reason) {
|
||||
if (worker) $stop(worker)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run_boot = function() {
|
||||
var prog_path = null
|
||||
var prog_info = null
|
||||
var deps = null
|
||||
var uncached = null
|
||||
var requestors = null
|
||||
var p = null
|
||||
|
||||
// Resolve the program path
|
||||
if (target_prog) {
|
||||
p = target_prog
|
||||
if (ends_with(p, '.ce')) p = text(p, 0, -3)
|
||||
prog_info = shop.resolve_program ? shop.resolve_program(p, target_pkg) : null
|
||||
if (prog_info) {
|
||||
prog_path = prog_info.path
|
||||
if (!target_pkg && prog_info.pkg) target_pkg = prog_info.pkg
|
||||
} else {
|
||||
prog_path = p + '.ce'
|
||||
if (!fd.is_file(prog_path)) {
|
||||
prog_path = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!prog_path || !fd.is_file(prog_path)) {
|
||||
log.error('boot: could not find program: ' + text(target_prog || ''))
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
|
||||
// Discover all transitive deps
|
||||
deps = discover_deps(prog_path)
|
||||
uncached = filter_uncached(deps)
|
||||
|
||||
if (length(uncached) == 0) {
|
||||
log.console('boot: all modules cached')
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
|
||||
// Compile uncached modules in parallel using worker actors
|
||||
log.console('boot: ' + text(length(uncached)) + ' modules to compile')
|
||||
requestors = array(uncached, make_compile_requestor)
|
||||
parallel(requestors)(function(results, reason) {
|
||||
if (reason) {
|
||||
log.error('boot: ' + (reason.message || text(reason)))
|
||||
} else {
|
||||
log.console('boot: compiled ' + text(length(results)) + ' modules')
|
||||
}
|
||||
$stop()
|
||||
}, null)
|
||||
}
|
||||
|
||||
// CLI mode: start immediately
|
||||
if (!_child_mode && target_prog) {
|
||||
run_boot()
|
||||
}
|
||||
5485
boot/bootstrap.cm.mcode
Normal file
5485
boot/bootstrap.cm.mcode
Normal file
File diff suppressed because one or more lines are too long
5963
boot/fold.cm.mcode
Normal file
5963
boot/fold.cm.mcode
Normal file
File diff suppressed because it is too large
Load Diff
14410
boot/mcode.cm.mcode
Normal file
14410
boot/mcode.cm.mcode
Normal file
File diff suppressed because one or more lines are too long
12058
boot/parse.cm.mcode
Normal file
12058
boot/parse.cm.mcode
Normal file
File diff suppressed because one or more lines are too long
2764
boot/qbe.cm.mcode
Normal file
2764
boot/qbe.cm.mcode
Normal file
File diff suppressed because it is too large
Load Diff
34967
boot/qbe_emit.cm.mcode
Normal file
34967
boot/qbe_emit.cm.mcode
Normal file
File diff suppressed because one or more lines are too long
16776
boot/streamline.cm.mcode
Normal file
16776
boot/streamline.cm.mcode
Normal file
File diff suppressed because one or more lines are too long
4254
boot/tokenize.cm.mcode
Normal file
4254
boot/tokenize.cm.mcode
Normal file
File diff suppressed because one or more lines are too long
74
boot_miscompile_bad.cm
Normal file
74
boot_miscompile_bad.cm
Normal file
@@ -0,0 +1,74 @@
|
||||
// boot_miscompile_bad.cm — Documents a boot compiler miscompilation bug.
|
||||
//
|
||||
// BUG SUMMARY:
|
||||
// The boot compiler's optimizer (likely compress_slots, eliminate_moves,
|
||||
// or infer_param_types) miscompiles a specific pattern when it appears
|
||||
// inside streamline.cm. The pattern: an array-loaded value used as a
|
||||
// dynamic index for another array store, inside a guarded block:
|
||||
//
|
||||
// sv = instr[j]
|
||||
// if (is_number(sv) && sv >= 0 && sv < nr_slots) {
|
||||
// last_ref[sv] = i // <-- miscompiled: sv reads wrong slot
|
||||
// }
|
||||
//
|
||||
// The bug is CONTEXT-DEPENDENT on streamline.cm's exact function/closure
|
||||
// structure. A standalone module with the same pattern does NOT trigger it.
|
||||
// The boot optimizer's cross-function analysis (infer_param_types, type
|
||||
// propagation, etc.) makes different decisions in the full streamline.cm
|
||||
// context, leading to the miscompilation.
|
||||
//
|
||||
// SYMPTOMS:
|
||||
// - 'log' is not defined (comparison error path fires on non-comparable values)
|
||||
// - array index must be a number (store_dynamic with corrupted index)
|
||||
// - Error line has NO reference to 'log' — the reference comes from the
|
||||
// error-reporting code path of the < operator
|
||||
// - Non-deterministic: different error messages on different runs
|
||||
// - NOT a GC bug: persists with --heap 4GB
|
||||
// - NOT slot overflow: function has only 85 raw slots
|
||||
//
|
||||
// TO REPRODUCE:
|
||||
// In streamline.cm, replace the build_slot_liveness function body with
|
||||
// this version (raw operand scanning instead of get_slot_refs):
|
||||
//
|
||||
// var build_slot_liveness = function(instructions, nr_slots) {
|
||||
// var last_ref = array(nr_slots, -1)
|
||||
// var n = length(instructions)
|
||||
// var i = 0
|
||||
// var j = 0
|
||||
// var limit = 0
|
||||
// var sv = 0
|
||||
// var instr = null
|
||||
//
|
||||
// while (i < n) {
|
||||
// instr = instructions[i]
|
||||
// if (is_array(instr)) {
|
||||
// j = 1
|
||||
// limit = length(instr) - 2
|
||||
// while (j < limit) {
|
||||
// sv = instr[j]
|
||||
// if (is_number(sv) && sv >= 0 && sv < nr_slots) {
|
||||
// last_ref[sv] = i
|
||||
// }
|
||||
// j = j + 1
|
||||
// }
|
||||
// }
|
||||
// i = i + 1
|
||||
// }
|
||||
// return last_ref
|
||||
// }
|
||||
//
|
||||
// Then: rm -rf .cell/build && ./cell --dev vm_suite
|
||||
//
|
||||
// WORKAROUND:
|
||||
// Use get_slot_refs(instr) to iterate only over known slot-reference
|
||||
// positions. This produces different IR that the boot optimizer handles
|
||||
// correctly, and is also more semantically correct.
|
||||
//
|
||||
// FIXING:
|
||||
// To find the root cause, compare the boot-compiled bytecodes of
|
||||
// build_slot_liveness (in the full streamline.cm context) vs the
|
||||
// source-compiled bytecodes. Use disasm.ce with --optimized to see
|
||||
// what the source compiler produces. The boot-compiled bytecodes
|
||||
// would need a C-level MachCode dump to inspect.
|
||||
|
||||
return null
|
||||
42
bootstrap.ce
42
bootstrap.ce
@@ -1,42 +0,0 @@
|
||||
// bootstrap.ce — regenerate .mach bytecode files consumed by the mach engine
|
||||
// usage: cell bootstrap.ce
|
||||
|
||||
var fd = use("fd")
|
||||
var json = use("json")
|
||||
var tokenize = use("tokenize")
|
||||
var parse = use("parse")
|
||||
var fold = use("fold")
|
||||
|
||||
var files = [
|
||||
{src: "tokenize.cm", name: "tokenize", out: "tokenize.mach"},
|
||||
{src: "parse.cm", name: "parse", out: "parse.mach"},
|
||||
{src: "fold.cm", name: "fold", out: "fold.mach"},
|
||||
{src: "mcode.cm", name: "mcode", out: "mcode.mach"},
|
||||
{src: "internal/bootstrap.cm", name: "bootstrap", out: "internal/bootstrap.mach"},
|
||||
{src: "internal/engine.cm", name: "engine", out: "internal/engine.mach"}
|
||||
]
|
||||
|
||||
var i = 0
|
||||
var entry = null
|
||||
var src = null
|
||||
var tok_result = null
|
||||
var ast = null
|
||||
var folded = null
|
||||
var ast_json = null
|
||||
var bytecode = null
|
||||
var f = null
|
||||
|
||||
while (i < length(files)) {
|
||||
entry = files[i]
|
||||
src = text(fd.slurp(entry.src))
|
||||
tok_result = tokenize(src, entry.src)
|
||||
ast = parse(tok_result.tokens, src, entry.src, tokenize)
|
||||
folded = fold(ast)
|
||||
ast_json = json.encode(folded)
|
||||
bytecode = mach_compile_ast(entry.name, ast_json)
|
||||
f = fd.open(entry.out, "w")
|
||||
fd.write(f, bytecode)
|
||||
fd.close(f)
|
||||
print(`wrote ${entry.out}`)
|
||||
i = i + 1
|
||||
}
|
||||
148
build.ce
148
build.ce
@@ -6,6 +6,7 @@
|
||||
// cell build <locator> Build dynamic library for specific package
|
||||
// cell build -t <target> Cross-compile dynamic libraries for target platform
|
||||
// cell build -b <type> Build type: release (default), debug, or minsize
|
||||
// cell build --verbose Print resolved flags, commands, and cache status
|
||||
|
||||
var build = use('build')
|
||||
var shop = use('internal/shop')
|
||||
@@ -15,62 +16,66 @@ var fd = use('fd')
|
||||
var target = null
|
||||
var target_package = null
|
||||
var buildtype = 'release'
|
||||
var verbose = false
|
||||
var force_rebuild = false
|
||||
var dry_run = false
|
||||
var i = 0
|
||||
var targets = null
|
||||
var t = 0
|
||||
var lib = null
|
||||
var results = null
|
||||
var success = 0
|
||||
var failed = 0
|
||||
|
||||
for (var i = 0; i < length(args); i++) {
|
||||
if (args[i] == '-t' || args[i] == '--target') {
|
||||
if (i + 1 < length(args)) {
|
||||
target = args[++i]
|
||||
} else {
|
||||
log.error('-t requires a target')
|
||||
$stop()
|
||||
}
|
||||
} else if (args[i] == '-p' || args[i] == '--package') {
|
||||
// Legacy support for -p flag
|
||||
if (i + 1 < length(args)) {
|
||||
target_package = args[++i]
|
||||
} else {
|
||||
log.error('-p requires a package name')
|
||||
$stop()
|
||||
}
|
||||
} else if (args[i] == '-b' || args[i] == '--buildtype') {
|
||||
if (i + 1 < length(args)) {
|
||||
buildtype = args[++i]
|
||||
if (buildtype != 'release' && buildtype != 'debug' && buildtype != 'minsize') {
|
||||
log.error('Invalid buildtype: ' + buildtype + '. Must be release, debug, or minsize')
|
||||
$stop()
|
||||
var run = function() {
|
||||
for (i = 0; i < length(args); i++) {
|
||||
if (args[i] == '-t' || args[i] == '--target') {
|
||||
if (i + 1 < length(args)) {
|
||||
target = args[++i]
|
||||
} else {
|
||||
log.error('-t requires a target')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
log.error('-b requires a buildtype (release, debug, minsize)')
|
||||
$stop()
|
||||
} else if (args[i] == '-p' || args[i] == '--package') {
|
||||
// Legacy support for -p flag
|
||||
if (i + 1 < length(args)) {
|
||||
target_package = args[++i]
|
||||
} else {
|
||||
log.error('-p requires a package name')
|
||||
return
|
||||
}
|
||||
} else if (args[i] == '-b' || args[i] == '--buildtype') {
|
||||
if (i + 1 < length(args)) {
|
||||
buildtype = args[++i]
|
||||
if (buildtype != 'release' && buildtype != 'debug' && buildtype != 'minsize') {
|
||||
log.error('Invalid buildtype: ' + buildtype + '. Must be release, debug, or minsize')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
log.error('-b requires a buildtype (release, debug, minsize)')
|
||||
return
|
||||
}
|
||||
} else if (args[i] == '--force') {
|
||||
force_rebuild = true
|
||||
} else if (args[i] == '--verbose' || args[i] == '-v') {
|
||||
verbose = true
|
||||
} else if (args[i] == '--dry-run') {
|
||||
dry_run = true
|
||||
} else if (args[i] == '--list-targets') {
|
||||
log.console('Available targets:')
|
||||
targets = build.list_targets()
|
||||
for (t = 0; t < length(targets); t++) {
|
||||
log.console(' ' + targets[t])
|
||||
}
|
||||
return
|
||||
} else if (!starts_with(args[i], '-') && !target_package) {
|
||||
// Positional argument - treat as package locator
|
||||
target_package = args[i]
|
||||
}
|
||||
} else if (args[i] == '--force') {
|
||||
force_rebuild = true
|
||||
} else if (args[i] == '--dry-run') {
|
||||
dry_run = true
|
||||
} else if (args[i] == '--list-targets') {
|
||||
log.console('Available targets:')
|
||||
var targets = build.list_targets()
|
||||
for (var t = 0; t < length(targets); t++) {
|
||||
log.console(' ' + targets[t])
|
||||
}
|
||||
$stop()
|
||||
} else if (!starts_with(args[i], '-') && !target_package) {
|
||||
// Positional argument - treat as package locator
|
||||
target_package = args[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve local paths to absolute paths
|
||||
if (target_package) {
|
||||
if (target_package == '.' || starts_with(target_package, './') || starts_with(target_package, '../') || fd.is_dir(target_package)) {
|
||||
var resolved = fd.realpath(target_package)
|
||||
if (resolved) {
|
||||
target_package = resolved
|
||||
}
|
||||
}
|
||||
}
|
||||
if (target_package)
|
||||
target_package = shop.resolve_locator(target_package)
|
||||
|
||||
// Detect target if not specified
|
||||
if (!target) {
|
||||
@@ -78,47 +83,48 @@ if (!target) {
|
||||
if (target) log.console('Target: ' + target)
|
||||
}
|
||||
|
||||
if (target && !build.has_target(target)) {
|
||||
log.error('Invalid target: ' + target)
|
||||
log.console('Available targets: ' + text(build.list_targets(), ', '))
|
||||
$stop()
|
||||
}
|
||||
if (target && !build.has_target(target)) {
|
||||
log.error('Invalid target: ' + target)
|
||||
log.console('Available targets: ' + text(build.list_targets(), ', '))
|
||||
return
|
||||
}
|
||||
|
||||
var packages = shop.list_packages()
|
||||
log.console('Preparing packages...')
|
||||
arrfor(packages, function(package) {
|
||||
if (package == 'core') return
|
||||
shop.extract(package)
|
||||
shop.sync(package, {no_build: true})
|
||||
})
|
||||
|
||||
var _build = null
|
||||
if (target_package) {
|
||||
// Build single package
|
||||
log.console('Building ' + target_package + '...')
|
||||
try {
|
||||
var lib = build.build_dynamic(target_package, target, buildtype)
|
||||
_build = function() {
|
||||
lib = build.build_dynamic(target_package, target, buildtype, {verbose: verbose, force: force_rebuild})
|
||||
if (lib) {
|
||||
log.console('Built: ' + lib)
|
||||
log.console(`Built ${text(length(lib))} module(s)`)
|
||||
}
|
||||
} catch (e) {
|
||||
log.error('Build failed: ' + e)
|
||||
} disruption {
|
||||
log.error('Build failed')
|
||||
$stop()
|
||||
}
|
||||
_build()
|
||||
} else {
|
||||
// Build all packages
|
||||
log.console('Building all packages...')
|
||||
var results = build.build_all_dynamic(target, buildtype)
|
||||
|
||||
var success = 0
|
||||
var failed = 0
|
||||
for (var i = 0; i < length(results); i++) {
|
||||
if (results[i].library) {
|
||||
success++
|
||||
} else if (results[i].error) {
|
||||
failed++
|
||||
results = build.build_all_dynamic(target, buildtype, {verbose: verbose, force: force_rebuild})
|
||||
|
||||
success = 0
|
||||
failed = 0
|
||||
for (i = 0; i < length(results); i++) {
|
||||
if (results[i].modules) {
|
||||
success = success + length(results[i].modules)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
log.console(`Build complete: ${success} libraries built${failed > 0 ? `, ${failed} failed` : ''}`)
|
||||
}
|
||||
}
|
||||
run()
|
||||
|
||||
$stop()
|
||||
|
||||
11
cell.toml
11
cell.toml
@@ -1,13 +1,2 @@
|
||||
[compilation]
|
||||
CFLAGS = "-Isource -Wno-incompatible-pointer-types -Wno-missing-braces -Wno-strict-prototypes -Wno-unused-function -Wno-int-conversion"
|
||||
LDFLAGS = "-lstdc++ -lm"
|
||||
|
||||
[compilation.macos_arm64]
|
||||
CFLAGS = "-x objective-c"
|
||||
LDFLAGS = "-framework CoreFoundation -framework CFNetwork"
|
||||
|
||||
[compilation.playdate]
|
||||
CFLAGS = "-DMINIZ_NO_TIME -DTARGET_EXTENSION -DTARGET_PLAYDATE -I$LOCAL/PlaydateSDK/C_API"
|
||||
|
||||
[compilation.windows]
|
||||
LDFLAGS = "-lws2_32 -lwinmm -liphlpapi -lbcrypt -lwinhttp -static-libgcc -static-libstdc++"
|
||||
|
||||
489
cellfs.cm
489
cellfs.cm
@@ -1,136 +1,132 @@
|
||||
var cellfs = {}
|
||||
|
||||
// CellFS: A filesystem implementation using miniz and raw OS filesystem
|
||||
// Supports mounting multiple sources (fs, zip) and named mounts (@name)
|
||||
|
||||
var fd = use('fd')
|
||||
var miniz = use('miniz')
|
||||
var qop = use('qop')
|
||||
var wildstar = use('wildstar')
|
||||
var qop = use('internal/qop')
|
||||
var wildstar = use('internal/wildstar')
|
||||
var blib = use('blob')
|
||||
|
||||
// Internal state
|
||||
var mounts = [] // Array of {source, type, handle, name}
|
||||
var mounts = []
|
||||
|
||||
var writepath = "."
|
||||
var write_mount = null
|
||||
|
||||
// Helper to normalize paths
|
||||
function normalize_path(path) {
|
||||
if (!path) return ""
|
||||
// Remove leading/trailing slashes and normalize
|
||||
return replace(path, /^\/+|\/+$/, "")
|
||||
}
|
||||
|
||||
// Check if a file exists in a specific mount
|
||||
function mount_exists(mount, path) {
|
||||
var result = false
|
||||
var full_path = null
|
||||
var st = null
|
||||
var _check = null
|
||||
if (mount.type == 'zip') {
|
||||
try {
|
||||
_check = function() {
|
||||
mount.handle.mod(path)
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
result = true
|
||||
} disruption {}
|
||||
_check()
|
||||
} else if (mount.type == 'qop') {
|
||||
try {
|
||||
return mount.handle.stat(path) != null
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
} else { // fs
|
||||
var full_path = fd.join_paths(mount.source, path)
|
||||
try {
|
||||
var st = fd.stat(full_path)
|
||||
return st.isFile || st.isDirectory
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
_check = function() {
|
||||
result = mount.handle.stat(path) != null
|
||||
} disruption {}
|
||||
_check()
|
||||
} else if (mount.type == 'fs') {
|
||||
full_path = fd.join_paths(mount.source, path)
|
||||
_check = function() {
|
||||
st = fd.stat(full_path)
|
||||
result = st.isFile || st.isDirectory
|
||||
} disruption {}
|
||||
_check()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Check if a path refers to a directory in a specific mount
|
||||
function is_directory(path) {
|
||||
var res = resolve(path)
|
||||
var mount = res.mount
|
||||
var result = false
|
||||
var full_path = null
|
||||
var st = null
|
||||
var _check = null
|
||||
if (mount.type == 'zip') {
|
||||
try {
|
||||
return mount.handle.is_directory(path);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
_check = function() {
|
||||
result = mount.handle.is_directory(path)
|
||||
} disruption {}
|
||||
_check()
|
||||
} else if (mount.type == 'qop') {
|
||||
try {
|
||||
return mount.handle.is_directory(path);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
} else { // fs
|
||||
var full_path = fd.join_paths(mount.source, path)
|
||||
try {
|
||||
var st = fd.stat(full_path)
|
||||
return st.isDirectory
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
_check = function() {
|
||||
result = mount.handle.is_directory(path)
|
||||
} disruption {}
|
||||
_check()
|
||||
} else {
|
||||
full_path = fd.join_paths(mount.source, path)
|
||||
_check = function() {
|
||||
st = fd.stat(full_path)
|
||||
result = st.isDirectory
|
||||
} disruption {}
|
||||
_check()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Resolve a path to a specific mount and relative path
|
||||
// Returns { mount, path } or throws/returns null
|
||||
function resolve(path, must_exist) {
|
||||
path = normalize_path(path)
|
||||
|
||||
// Check for named mount
|
||||
if (starts_with(path, "@")) {
|
||||
var idx = search(path, "/")
|
||||
var mount_name = ""
|
||||
var rel_path = ""
|
||||
|
||||
var idx = null
|
||||
var mount_name = ""
|
||||
var rel_path = ""
|
||||
var mount = null
|
||||
var found_mount = null
|
||||
var npath = normalize_path(path)
|
||||
|
||||
if (starts_with(npath, "@")) {
|
||||
idx = search(npath, "/")
|
||||
|
||||
if (idx == null) {
|
||||
mount_name = text(path, 1)
|
||||
mount_name = text(npath, 1)
|
||||
rel_path = ""
|
||||
} else {
|
||||
mount_name = text(path, 1, idx)
|
||||
rel_path = text(path, idx + 1)
|
||||
mount_name = text(npath, 1, idx)
|
||||
rel_path = text(npath, idx + 1)
|
||||
}
|
||||
|
||||
// Find named mount
|
||||
var mount = null
|
||||
|
||||
arrfor(mounts, function(m) {
|
||||
if (m.name == mount_name) {
|
||||
mount = m
|
||||
return true
|
||||
}
|
||||
}, false, true)
|
||||
|
||||
|
||||
if (!mount) {
|
||||
throw Error("Unknown mount point: @" + mount_name)
|
||||
log.error("Unknown mount point: @" + mount_name); disrupt
|
||||
}
|
||||
|
||||
|
||||
return { mount: mount, path: rel_path }
|
||||
}
|
||||
|
||||
// Search path
|
||||
var found_mount = null
|
||||
arrfor(mounts, function(mount) {
|
||||
if (mount_exists(mount, path)) {
|
||||
found_mount = { mount: mount, path: path }
|
||||
|
||||
arrfor(mounts, function(m) {
|
||||
if (mount_exists(m, npath)) {
|
||||
found_mount = { mount: m, path: npath }
|
||||
return true
|
||||
}
|
||||
}, false, true)
|
||||
|
||||
|
||||
if (found_mount) {
|
||||
return found_mount
|
||||
}
|
||||
|
||||
|
||||
if (must_exist) {
|
||||
throw Error("File not found in any mount: " + path)
|
||||
log.error("File not found in any mount: " + npath); disrupt
|
||||
}
|
||||
}
|
||||
|
||||
// Mount a source
|
||||
function mount(source, name) {
|
||||
// Check if source exists
|
||||
var st = fd.stat(source)
|
||||
|
||||
var st = null
|
||||
var blob = null
|
||||
var qop_archive = null
|
||||
var zip = null
|
||||
var _try_qop = null
|
||||
var http = null
|
||||
|
||||
var mount_info = {
|
||||
source: source,
|
||||
name: name || null,
|
||||
@@ -138,74 +134,96 @@ function mount(source, name) {
|
||||
handle: null,
|
||||
zip_blob: null
|
||||
}
|
||||
|
||||
|
||||
if (starts_with(source, 'http://') || starts_with(source, 'https://')) {
|
||||
http = use('http')
|
||||
mount_info.type = 'http'
|
||||
mount_info.handle = {
|
||||
base_url: source,
|
||||
get: function(path, callback) {
|
||||
var url = source + '/' + path
|
||||
$clock(function(_t) {
|
||||
var resp = http.request('GET', url, null, null)
|
||||
if (resp && resp.status == 200) {
|
||||
callback(resp.body)
|
||||
} else {
|
||||
callback(null, "HTTP " + text(resp ? resp.status : 0) + ": " + url)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
push(mounts, mount_info)
|
||||
return
|
||||
}
|
||||
|
||||
st = fd.stat(source)
|
||||
|
||||
if (st.isDirectory) {
|
||||
mount_info.type = 'fs'
|
||||
} else if (st.isFile) {
|
||||
var blob = fd.slurp(source)
|
||||
|
||||
// Try QOP first (it's likely faster to fail?) or Zip?
|
||||
// QOP open checks magic.
|
||||
var qop_archive = null
|
||||
try {
|
||||
qop_archive = qop.open(blob)
|
||||
} catch(e) {}
|
||||
blob = fd.slurp(source)
|
||||
|
||||
qop_archive = null
|
||||
_try_qop = function() {
|
||||
qop_archive = qop.open(blob)
|
||||
} disruption {}
|
||||
_try_qop()
|
||||
|
||||
if (qop_archive) {
|
||||
mount_info.type = 'qop'
|
||||
mount_info.handle = qop_archive
|
||||
mount_info.zip_blob = blob // keep blob alive
|
||||
mount_info.type = 'qop'
|
||||
mount_info.handle = qop_archive
|
||||
mount_info.zip_blob = blob
|
||||
} else {
|
||||
var zip = miniz.read(blob)
|
||||
if (!is_object(zip) || !is_function(zip.count)) {
|
||||
throw Error("Invalid archive file (not zip or qop): " + source)
|
||||
}
|
||||
|
||||
mount_info.type = 'zip'
|
||||
mount_info.handle = zip
|
||||
mount_info.zip_blob = blob // keep blob alive
|
||||
zip = miniz.read(blob)
|
||||
if (!is_object(zip) || !is_function(zip.count)) {
|
||||
log.error("Invalid archive file (not zip or qop): " + source); disrupt
|
||||
}
|
||||
|
||||
mount_info.type = 'zip'
|
||||
mount_info.handle = zip
|
||||
mount_info.zip_blob = blob
|
||||
}
|
||||
} else {
|
||||
throw Error("Unsupported mount source type: " + source)
|
||||
log.error("Unsupported mount source type: " + source); disrupt
|
||||
}
|
||||
|
||||
|
||||
push(mounts, mount_info)
|
||||
}
|
||||
|
||||
// Unmount
|
||||
function unmount(name_or_source) {
|
||||
mounts = filter(mounts, function(mount) {
|
||||
return mount.name != name_or_source && mount.source != name_or_source
|
||||
mounts = filter(mounts, function(m) {
|
||||
return m.name != name_or_source && m.source != name_or_source
|
||||
})
|
||||
}
|
||||
|
||||
// Read file
|
||||
function slurp(path) {
|
||||
var res = resolve(path, true)
|
||||
if (!res) throw Error("File not found: " + path)
|
||||
|
||||
var data = null
|
||||
var full_path = null
|
||||
if (!res) { log.error("File not found: " + path); disrupt }
|
||||
|
||||
if (res.mount.type == 'zip') {
|
||||
return res.mount.handle.slurp(res.path)
|
||||
} else if (res.mount.type == 'qop') {
|
||||
var data = res.mount.handle.read(res.path)
|
||||
if (!data) throw Error("File not found in qop: " + path)
|
||||
data = res.mount.handle.read(res.path)
|
||||
if (!data) { log.error("File not found in qop: " + path); disrupt }
|
||||
return data
|
||||
} else {
|
||||
var full_path = fd.join_paths(res.mount.source, res.path)
|
||||
full_path = fd.join_paths(res.mount.source, res.path)
|
||||
return fd.slurp(full_path)
|
||||
}
|
||||
}
|
||||
|
||||
// Write file
|
||||
function slurpwrite(path, data) {
|
||||
var full_path = writepath + "/" + path
|
||||
|
||||
var f = fd.open(full_path, 'w')
|
||||
fd.write(f, data)
|
||||
fd.close(f)
|
||||
var full_path = null
|
||||
if (write_mount) {
|
||||
full_path = fd.join_paths(write_mount.source, path)
|
||||
} else {
|
||||
full_path = fd.join_paths(".", path)
|
||||
}
|
||||
fd.slurpwrite(full_path, data)
|
||||
}
|
||||
|
||||
// Check existence
|
||||
function exists(path) {
|
||||
var res = resolve(path, false)
|
||||
if (starts_with(path, "@")) {
|
||||
@@ -214,29 +232,31 @@ function exists(path) {
|
||||
return res != null
|
||||
}
|
||||
|
||||
// Stat
|
||||
function stat(path) {
|
||||
var res = resolve(path, true)
|
||||
if (!res) throw Error("File not found: " + path)
|
||||
|
||||
var mod = null
|
||||
var s = null
|
||||
var full_path = null
|
||||
if (!res) { log.error("File not found: " + path); disrupt }
|
||||
|
||||
if (res.mount.type == 'zip') {
|
||||
var mod = res.mount.handle.mod(res.path)
|
||||
mod = res.mount.handle.mod(res.path)
|
||||
return {
|
||||
filesize: 0,
|
||||
filesize: 0,
|
||||
modtime: mod * 1000,
|
||||
isDirectory: false
|
||||
isDirectory: false
|
||||
}
|
||||
} else if (res.mount.type == 'qop') {
|
||||
var s = res.mount.handle.stat(res.path)
|
||||
if (!s) throw Error("File not found in qop: " + path)
|
||||
s = res.mount.handle.stat(res.path)
|
||||
if (!s) { log.error("File not found in qop: " + path); disrupt }
|
||||
return {
|
||||
filesize: s.size,
|
||||
modtime: s.modtime,
|
||||
isDirectory: s.isDirectory
|
||||
}
|
||||
} else {
|
||||
var full_path = fd.join_paths(res.mount.source, res.path)
|
||||
var s = fd.stat(full_path)
|
||||
full_path = fd.join_paths(res.mount.source, res.path)
|
||||
s = fd.stat(full_path)
|
||||
return {
|
||||
filesize: s.size,
|
||||
modtime: s.mtime,
|
||||
@@ -245,51 +265,62 @@ function stat(path) {
|
||||
}
|
||||
}
|
||||
|
||||
// Get search paths
|
||||
function searchpath() {
|
||||
return array(mounts)
|
||||
}
|
||||
|
||||
// Mount a package using the shop system
|
||||
function mount_package(name) {
|
||||
if (name == null) {
|
||||
mount('.', null)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
var shop = use('internal/shop')
|
||||
var dir = shop.get_package_dir(name)
|
||||
|
||||
if (!dir) {
|
||||
throw Error("Package not found: " + name)
|
||||
log.error("Package not found: " + name); disrupt
|
||||
}
|
||||
|
||||
mount(dir, name)
|
||||
}
|
||||
|
||||
// New functions for qjs_io compatibility
|
||||
|
||||
function match(str, pattern) {
|
||||
return wildstar.match(pattern, str, wildstar.WM_PATHNAME | wildstar.WM_PERIOD | wildstar.WM_WILDSTAR)
|
||||
}
|
||||
|
||||
function rm(path) {
|
||||
var res = resolve(path, true)
|
||||
if (res.mount.type != 'fs') throw Error("Cannot delete from non-fs mount")
|
||||
|
||||
var full_path = fd.join_paths(res.mount.source, res.path)
|
||||
var st = fd.stat(full_path)
|
||||
var full_path = null
|
||||
var st = null
|
||||
if (res.mount.type != 'fs') { log.error("Cannot delete from non-fs mount"); disrupt }
|
||||
|
||||
full_path = fd.join_paths(res.mount.source, res.path)
|
||||
st = fd.stat(full_path)
|
||||
if (st.isDirectory) fd.rmdir(full_path)
|
||||
else fd.unlink(full_path)
|
||||
}
|
||||
|
||||
function mkdir(path) {
|
||||
var full = fd.join_paths(writepath, path)
|
||||
var full = null
|
||||
if (write_mount) {
|
||||
full = fd.join_paths(write_mount.source, path)
|
||||
} else {
|
||||
full = fd.join_paths(".", path)
|
||||
}
|
||||
fd.mkdir(full)
|
||||
}
|
||||
|
||||
function set_writepath(path) {
|
||||
writepath = path
|
||||
function set_writepath(mount_name) {
|
||||
var found = null
|
||||
if (mount_name == null) { write_mount = null; return }
|
||||
arrfor(mounts, function(m) {
|
||||
if (m.name == mount_name) { found = m; return true }
|
||||
}, false, true)
|
||||
if (!found || found.type != 'fs') {
|
||||
log.error("writepath: must be an fs mount"); disrupt
|
||||
}
|
||||
write_mount = found
|
||||
}
|
||||
|
||||
function basedir() {
|
||||
@@ -306,55 +337,63 @@ function realdir(path) {
|
||||
return fd.join_paths(res.mount.source, res.path)
|
||||
}
|
||||
|
||||
function enumerate(path, recurse) {
|
||||
if (path == null) path = ""
|
||||
|
||||
function enumerate(_path, recurse) {
|
||||
var path = _path == null ? "" : _path
|
||||
|
||||
var res = resolve(path, true)
|
||||
var results = []
|
||||
|
||||
var full = null
|
||||
var st = null
|
||||
var all = null
|
||||
var prefix = null
|
||||
var prefix_len = null
|
||||
var seen = null
|
||||
|
||||
function visit(curr_full, rel_prefix) {
|
||||
var list = fd.readdir(curr_full)
|
||||
if (!list) return
|
||||
|
||||
|
||||
arrfor(list, function(item) {
|
||||
var item_rel = rel_prefix ? rel_prefix + "/" + item : item
|
||||
var child_st = null
|
||||
push(results, item_rel)
|
||||
|
||||
|
||||
if (recurse) {
|
||||
var st = fd.stat(fd.join_paths(curr_full, item))
|
||||
if (st.isDirectory) {
|
||||
child_st = fd.stat(fd.join_paths(curr_full, item))
|
||||
if (child_st.isDirectory) {
|
||||
visit(fd.join_paths(curr_full, item), item_rel)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
if (res.mount.type == 'fs') {
|
||||
var full = fd.join_paths(res.mount.source, res.path)
|
||||
var st = fd.stat(full)
|
||||
full = fd.join_paths(res.mount.source, res.path)
|
||||
st = fd.stat(full)
|
||||
if (st && st.isDirectory) {
|
||||
visit(full, "")
|
||||
}
|
||||
} else if (res.mount.type == 'qop') {
|
||||
var all = res.mount.handle.list()
|
||||
var prefix = res.path ? res.path + "/" : ""
|
||||
var prefix_len = length(prefix)
|
||||
|
||||
// Use a set to avoid duplicates if we are simulating directories
|
||||
var seen = {}
|
||||
|
||||
all = res.mount.handle.list()
|
||||
prefix = res.path ? res.path + "/" : ""
|
||||
prefix_len = length(prefix)
|
||||
|
||||
seen = {}
|
||||
|
||||
arrfor(all, function(p) {
|
||||
var rel = null
|
||||
var slash = null
|
||||
if (starts_with(p, prefix)) {
|
||||
var rel = text(p, prefix_len)
|
||||
rel = text(p, prefix_len)
|
||||
if (length(rel) == 0) return
|
||||
|
||||
if (!recurse) {
|
||||
var slash = search(rel, '/')
|
||||
slash = search(rel, '/')
|
||||
if (slash != null) {
|
||||
rel = text(rel, 0, slash)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!seen[rel]) {
|
||||
seen[rel] = true
|
||||
push(results, rel)
|
||||
@@ -362,15 +401,20 @@ function enumerate(path, recurse) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
function globfs(globs, dir) {
|
||||
if (dir == null) dir = ""
|
||||
function globfs(globs, _dir) {
|
||||
var dir = _dir == null ? "" : _dir
|
||||
var res = resolve(dir, true)
|
||||
var results = []
|
||||
|
||||
var full = null
|
||||
var st = null
|
||||
var all = null
|
||||
var prefix = null
|
||||
var prefix_len = null
|
||||
|
||||
function check_neg(path) {
|
||||
var result = false
|
||||
arrfor(globs, function(g) {
|
||||
@@ -381,7 +425,7 @@ function globfs(globs, dir) {
|
||||
}, false, true)
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
function check_pos(path) {
|
||||
var result = false
|
||||
arrfor(globs, function(g) {
|
||||
@@ -398,14 +442,14 @@ function globfs(globs, dir) {
|
||||
|
||||
var list = fd.readdir(curr_full)
|
||||
if (!list) return
|
||||
|
||||
|
||||
arrfor(list, function(item) {
|
||||
var item_rel = rel_prefix ? rel_prefix + "/" + item : item
|
||||
|
||||
|
||||
var child_full = fd.join_paths(curr_full, item)
|
||||
var st = fd.stat(child_full)
|
||||
|
||||
if (st.isDirectory) {
|
||||
var child_st = fd.stat(child_full)
|
||||
|
||||
if (child_st.isDirectory) {
|
||||
if (!check_neg(item_rel)) {
|
||||
visit(child_full, item_rel)
|
||||
}
|
||||
@@ -416,21 +460,22 @@ function globfs(globs, dir) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
if (res.mount.type == 'fs') {
|
||||
var full = fd.join_paths(res.mount.source, res.path)
|
||||
var st = fd.stat(full)
|
||||
full = fd.join_paths(res.mount.source, res.path)
|
||||
st = fd.stat(full)
|
||||
if (st && st.isDirectory) {
|
||||
visit(full, "")
|
||||
}
|
||||
} else if (res.mount.type == 'qop') {
|
||||
var all = res.mount.handle.list()
|
||||
var prefix = res.path ? res.path + "/" : ""
|
||||
var prefix_len = length(prefix)
|
||||
|
||||
all = res.mount.handle.list()
|
||||
prefix = res.path ? res.path + "/" : ""
|
||||
prefix_len = length(prefix)
|
||||
|
||||
arrfor(all, function(p) {
|
||||
var rel = null
|
||||
if (starts_with(p, prefix)) {
|
||||
var rel = text(p, prefix_len)
|
||||
rel = text(p, prefix_len)
|
||||
if (length(rel) == 0) return
|
||||
|
||||
if (!check_neg(rel) && check_pos(rel)) {
|
||||
@@ -439,11 +484,86 @@ function globfs(globs, dir) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// Exports
|
||||
// Requestor factory: returns a requestor for reading a file at path
|
||||
function get(path) {
|
||||
return function get_requestor(callback, value) {
|
||||
var res = resolve(path, false)
|
||||
var full = null
|
||||
var f = null
|
||||
var acc = null
|
||||
var cancelled = false
|
||||
var data = null
|
||||
var _close = null
|
||||
|
||||
if (!res) { callback(null, "not found: " + path); return }
|
||||
|
||||
if (res.mount.type == 'zip') {
|
||||
callback(res.mount.handle.slurp(res.path))
|
||||
return
|
||||
}
|
||||
if (res.mount.type == 'qop') {
|
||||
data = res.mount.handle.read(res.path)
|
||||
if (data) {
|
||||
callback(data)
|
||||
} else {
|
||||
callback(null, "not found in qop: " + path)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (res.mount.type == 'http') {
|
||||
res.mount.handle.get(res.path, callback)
|
||||
return
|
||||
}
|
||||
|
||||
full = fd.join_paths(res.mount.source, res.path)
|
||||
f = fd.open(full, 'r')
|
||||
acc = blob()
|
||||
|
||||
function next(_t) {
|
||||
var chunk = null
|
||||
if (cancelled) return
|
||||
chunk = fd.read(f, 65536)
|
||||
if (length(chunk) == 0) {
|
||||
fd.close(f)
|
||||
stone(acc)
|
||||
callback(acc)
|
||||
return
|
||||
}
|
||||
acc.write_blob(chunk)
|
||||
$clock(next)
|
||||
}
|
||||
|
||||
next()
|
||||
return function cancel() {
|
||||
cancelled = true
|
||||
_close = function() { fd.close(f) } disruption {}
|
||||
_close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Requestor factory: returns a requestor for writing data to path
|
||||
function put(path, data) {
|
||||
return function put_requestor(callback, value) {
|
||||
var _data = data != null ? data : value
|
||||
var full = null
|
||||
var _do = null
|
||||
if (!write_mount) { callback(null, "no write mount set"); return }
|
||||
full = fd.join_paths(write_mount.source, path)
|
||||
_do = function() {
|
||||
fd.slurpwrite(full, _data)
|
||||
callback(true)
|
||||
} disruption {
|
||||
callback(null, "write failed: " + path)
|
||||
}
|
||||
_do()
|
||||
}
|
||||
}
|
||||
|
||||
cellfs.mount = mount
|
||||
cellfs.mount_package = mount_package
|
||||
cellfs.unmount = unmount
|
||||
@@ -462,7 +582,8 @@ cellfs.writepath = set_writepath
|
||||
cellfs.basedir = basedir
|
||||
cellfs.prefdir = prefdir
|
||||
cellfs.realdir = realdir
|
||||
|
||||
cellfs.mount('.')
|
||||
cellfs.get = get
|
||||
cellfs.put = put
|
||||
cellfs.resolve = resolve
|
||||
|
||||
return cellfs
|
||||
|
||||
456
cfg.ce
Normal file
456
cfg.ce
Normal file
@@ -0,0 +1,456 @@
|
||||
// cfg.ce — control flow graph
|
||||
//
|
||||
// Usage:
|
||||
// cell cfg --fn <N|name> <file> Text CFG for function
|
||||
// cell cfg --dot --fn <N|name> <file> DOT output for graphviz
|
||||
// cell cfg <file> Text CFG for all functions
|
||||
|
||||
var shop = use("internal/shop")
|
||||
|
||||
var pad_right = function(s, w) {
|
||||
var r = s
|
||||
while (length(r) < w) {
|
||||
r = r + " "
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
var fmt_val = function(v) {
|
||||
if (is_null(v)) return "null"
|
||||
if (is_number(v)) return text(v)
|
||||
if (is_text(v)) return `"${v}"`
|
||||
if (is_object(v)) return text(v)
|
||||
if (is_logical(v)) return v ? "true" : "false"
|
||||
return text(v)
|
||||
}
|
||||
|
||||
var is_jump_op = function(op) {
|
||||
return op == "jump" || op == "jump_true" || op == "jump_false" || op == "jump_null" || op == "jump_not_null"
|
||||
}
|
||||
|
||||
var is_conditional_jump = function(op) {
|
||||
return op == "jump_true" || op == "jump_false" || op == "jump_null" || op == "jump_not_null"
|
||||
}
|
||||
|
||||
var is_terminator = function(op) {
|
||||
return op == "return" || op == "disrupt" || op == "tail_invoke" || op == "goinvoke"
|
||||
}
|
||||
|
||||
var run = function() {
|
||||
var filename = null
|
||||
var fn_filter = null
|
||||
var show_dot = false
|
||||
var use_optimized = false
|
||||
var i = 0
|
||||
var compiled = null
|
||||
var main_name = null
|
||||
var fi = 0
|
||||
var func = null
|
||||
var fname = null
|
||||
|
||||
while (i < length(args)) {
|
||||
if (args[i] == '--fn') {
|
||||
i = i + 1
|
||||
fn_filter = args[i]
|
||||
} else if (args[i] == '--dot') {
|
||||
show_dot = true
|
||||
} else if (args[i] == '--optimized') {
|
||||
use_optimized = true
|
||||
} else if (args[i] == '--help' || args[i] == '-h') {
|
||||
log.console("Usage: cell cfg [--fn <N|name>] [--dot] [--optimized] <file>")
|
||||
log.console("")
|
||||
log.console(" --fn <N|name> Filter to function by index or name")
|
||||
log.console(" --dot Output DOT format for graphviz")
|
||||
log.console(" --optimized Use optimized IR")
|
||||
return null
|
||||
} else if (!starts_with(args[i], '-')) {
|
||||
filename = args[i]
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
if (!filename) {
|
||||
log.console("Usage: cell cfg [--fn <N|name>] [--dot] [--optimized] <file>")
|
||||
return null
|
||||
}
|
||||
|
||||
if (use_optimized) {
|
||||
compiled = shop.compile_file(filename)
|
||||
} else {
|
||||
compiled = shop.mcode_file(filename)
|
||||
}
|
||||
|
||||
var fn_matches = function(index, name) {
|
||||
var match = null
|
||||
if (fn_filter == null) return true
|
||||
if (index >= 0 && fn_filter == text(index)) return true
|
||||
if (name != null) {
|
||||
match = search(name, fn_filter)
|
||||
if (match != null && match >= 0) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var build_cfg = function(func) {
|
||||
var instrs = func.instructions
|
||||
var blocks = []
|
||||
var label_to_block = {}
|
||||
var pc_to_block = {}
|
||||
var label_to_pc = {}
|
||||
var block_start_pcs = {}
|
||||
var after_terminator = false
|
||||
var current_block = null
|
||||
var current_label = null
|
||||
var pc = 0
|
||||
var ii = 0
|
||||
var bi = 0
|
||||
var instr = null
|
||||
var op = null
|
||||
var n = 0
|
||||
var line_num = null
|
||||
var blk = null
|
||||
var last_instr_data = null
|
||||
var last_op = null
|
||||
var target_label = null
|
||||
var target_bi = null
|
||||
var edge_type = null
|
||||
|
||||
if (instrs == null || length(instrs) == 0) return []
|
||||
|
||||
// Pass 1: identify block start PCs
|
||||
block_start_pcs["0"] = true
|
||||
pc = 0
|
||||
ii = 0
|
||||
while (ii < length(instrs)) {
|
||||
instr = instrs[ii]
|
||||
if (is_array(instr)) {
|
||||
op = instr[0]
|
||||
if (after_terminator) {
|
||||
block_start_pcs[text(pc)] = true
|
||||
after_terminator = false
|
||||
}
|
||||
if (is_jump_op(op) || is_terminator(op)) {
|
||||
after_terminator = true
|
||||
}
|
||||
pc = pc + 1
|
||||
}
|
||||
ii = ii + 1
|
||||
}
|
||||
|
||||
// Pass 2: map labels to PCs and mark as block starts
|
||||
pc = 0
|
||||
ii = 0
|
||||
while (ii < length(instrs)) {
|
||||
instr = instrs[ii]
|
||||
if (is_text(instr) && !starts_with(instr, "_nop_")) {
|
||||
label_to_pc[instr] = pc
|
||||
block_start_pcs[text(pc)] = true
|
||||
} else if (is_array(instr)) {
|
||||
pc = pc + 1
|
||||
}
|
||||
ii = ii + 1
|
||||
}
|
||||
|
||||
// Pass 3: build basic blocks
|
||||
pc = 0
|
||||
ii = 0
|
||||
current_label = null
|
||||
while (ii < length(instrs)) {
|
||||
instr = instrs[ii]
|
||||
if (is_text(instr)) {
|
||||
if (!starts_with(instr, "_nop_")) {
|
||||
current_label = instr
|
||||
}
|
||||
ii = ii + 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (is_array(instr)) {
|
||||
if (block_start_pcs[text(pc)]) {
|
||||
if (current_block != null) {
|
||||
push(blocks, current_block)
|
||||
}
|
||||
current_block = {
|
||||
id: length(blocks),
|
||||
label: current_label,
|
||||
start_pc: pc,
|
||||
end_pc: pc,
|
||||
instrs: [],
|
||||
edges: [],
|
||||
first_line: null,
|
||||
last_line: null
|
||||
}
|
||||
current_label = null
|
||||
}
|
||||
|
||||
if (current_block != null) {
|
||||
push(current_block.instrs, {pc: pc, instr: instr})
|
||||
current_block.end_pc = pc
|
||||
n = length(instr)
|
||||
line_num = instr[n - 2]
|
||||
if (line_num != null) {
|
||||
if (current_block.first_line == null) {
|
||||
current_block.first_line = line_num
|
||||
}
|
||||
current_block.last_line = line_num
|
||||
}
|
||||
}
|
||||
pc = pc + 1
|
||||
}
|
||||
ii = ii + 1
|
||||
}
|
||||
if (current_block != null) {
|
||||
push(blocks, current_block)
|
||||
}
|
||||
|
||||
// Build block index
|
||||
bi = 0
|
||||
while (bi < length(blocks)) {
|
||||
pc_to_block[text(blocks[bi].start_pc)] = bi
|
||||
if (blocks[bi].label != null) {
|
||||
label_to_block[blocks[bi].label] = bi
|
||||
}
|
||||
bi = bi + 1
|
||||
}
|
||||
|
||||
// Pass 4: compute edges
|
||||
bi = 0
|
||||
while (bi < length(blocks)) {
|
||||
blk = blocks[bi]
|
||||
if (length(blk.instrs) > 0) {
|
||||
last_instr_data = blk.instrs[length(blk.instrs) - 1]
|
||||
last_op = last_instr_data.instr[0]
|
||||
n = length(last_instr_data.instr)
|
||||
|
||||
if (is_jump_op(last_op)) {
|
||||
if (last_op == "jump") {
|
||||
target_label = last_instr_data.instr[1]
|
||||
} else {
|
||||
target_label = last_instr_data.instr[2]
|
||||
}
|
||||
|
||||
target_bi = label_to_block[target_label]
|
||||
if (target_bi != null) {
|
||||
edge_type = "jump"
|
||||
if (target_bi <= bi) {
|
||||
edge_type = "loop back-edge"
|
||||
}
|
||||
push(blk.edges, {target: target_bi, kind: edge_type})
|
||||
}
|
||||
|
||||
if (is_conditional_jump(last_op)) {
|
||||
if (bi + 1 < length(blocks)) {
|
||||
push(blk.edges, {target: bi + 1, kind: "fallthrough"})
|
||||
}
|
||||
}
|
||||
} else if (is_terminator(last_op)) {
|
||||
push(blk.edges, {target: -1, kind: "EXIT (" + last_op + ")"})
|
||||
} else {
|
||||
if (bi + 1 < length(blocks)) {
|
||||
push(blk.edges, {target: bi + 1, kind: "fallthrough"})
|
||||
}
|
||||
}
|
||||
}
|
||||
bi = bi + 1
|
||||
}
|
||||
|
||||
return blocks
|
||||
}
|
||||
|
||||
var print_cfg_text = function(blocks, name) {
|
||||
var bi = 0
|
||||
var blk = null
|
||||
var header = null
|
||||
var ii = 0
|
||||
var idata = null
|
||||
var instr = null
|
||||
var op = null
|
||||
var n = 0
|
||||
var parts = null
|
||||
var j = 0
|
||||
var operands = null
|
||||
var ei = 0
|
||||
var edge = null
|
||||
var target_label = null
|
||||
|
||||
log.compile(`\n=== ${name} ===`)
|
||||
|
||||
if (length(blocks) == 0) {
|
||||
log.compile(" (empty)")
|
||||
return null
|
||||
}
|
||||
|
||||
bi = 0
|
||||
while (bi < length(blocks)) {
|
||||
blk = blocks[bi]
|
||||
header = ` B${text(bi)}`
|
||||
if (blk.label != null) {
|
||||
header = header + ` "${blk.label}"`
|
||||
}
|
||||
header = header + ` [pc ${text(blk.start_pc)}-${text(blk.end_pc)}`
|
||||
if (blk.first_line != null) {
|
||||
if (blk.first_line == blk.last_line) {
|
||||
header = header + `, line ${text(blk.first_line)}`
|
||||
} else {
|
||||
header = header + `, lines ${text(blk.first_line)}-${text(blk.last_line)}`
|
||||
}
|
||||
}
|
||||
header = header + "]:"
|
||||
|
||||
log.compile(header)
|
||||
|
||||
ii = 0
|
||||
while (ii < length(blk.instrs)) {
|
||||
idata = blk.instrs[ii]
|
||||
instr = idata.instr
|
||||
op = instr[0]
|
||||
n = length(instr)
|
||||
parts = []
|
||||
j = 1
|
||||
while (j < n - 2) {
|
||||
push(parts, fmt_val(instr[j]))
|
||||
j = j + 1
|
||||
}
|
||||
operands = text(parts, ", ")
|
||||
log.compile(` ${pad_right(text(idata.pc), 6)}${pad_right(op, 15)}${operands}`)
|
||||
ii = ii + 1
|
||||
}
|
||||
|
||||
ei = 0
|
||||
while (ei < length(blk.edges)) {
|
||||
edge = blk.edges[ei]
|
||||
if (edge.target == -1) {
|
||||
log.compile(` -> ${edge.kind}`)
|
||||
} else {
|
||||
target_label = blocks[edge.target].label
|
||||
if (target_label != null) {
|
||||
log.compile(` -> B${text(edge.target)} "${target_label}" (${edge.kind})`)
|
||||
} else {
|
||||
log.compile(` -> B${text(edge.target)} (${edge.kind})`)
|
||||
}
|
||||
}
|
||||
ei = ei + 1
|
||||
}
|
||||
|
||||
log.compile("")
|
||||
bi = bi + 1
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
var print_cfg_dot = function(blocks, name) {
|
||||
var safe_name = replace(replace(name, '"', '\\"'), ' ', '_')
|
||||
var bi = 0
|
||||
var blk = null
|
||||
var label_text = null
|
||||
var ii = 0
|
||||
var idata = null
|
||||
var instr = null
|
||||
var op = null
|
||||
var n = 0
|
||||
var parts = null
|
||||
var j = 0
|
||||
var operands = null
|
||||
var ei = 0
|
||||
var edge = null
|
||||
var style = null
|
||||
|
||||
log.compile(`digraph "${safe_name}" {`)
|
||||
log.compile(" rankdir=TB;")
|
||||
log.compile(" node [shape=record, fontname=monospace, fontsize=10];")
|
||||
|
||||
bi = 0
|
||||
while (bi < length(blocks)) {
|
||||
blk = blocks[bi]
|
||||
label_text = "B" + text(bi)
|
||||
if (blk.label != null) {
|
||||
label_text = label_text + " (" + blk.label + ")"
|
||||
}
|
||||
label_text = label_text + "\\npc " + text(blk.start_pc) + "-" + text(blk.end_pc)
|
||||
if (blk.first_line != null) {
|
||||
label_text = label_text + "\\nline " + text(blk.first_line)
|
||||
}
|
||||
label_text = label_text + "|"
|
||||
|
||||
ii = 0
|
||||
while (ii < length(blk.instrs)) {
|
||||
idata = blk.instrs[ii]
|
||||
instr = idata.instr
|
||||
op = instr[0]
|
||||
n = length(instr)
|
||||
parts = []
|
||||
j = 1
|
||||
while (j < n - 2) {
|
||||
push(parts, fmt_val(instr[j]))
|
||||
j = j + 1
|
||||
}
|
||||
operands = text(parts, ", ")
|
||||
label_text = label_text + text(idata.pc) + " " + op + " " + replace(operands, '"', '\\"') + "\\l"
|
||||
ii = ii + 1
|
||||
}
|
||||
|
||||
log.compile(" B" + text(bi) + " [label=\"{" + label_text + "}\"];")
|
||||
bi = bi + 1
|
||||
}
|
||||
|
||||
// Edges
|
||||
bi = 0
|
||||
while (bi < length(blocks)) {
|
||||
blk = blocks[bi]
|
||||
ei = 0
|
||||
while (ei < length(blk.edges)) {
|
||||
edge = blk.edges[ei]
|
||||
if (edge.target >= 0) {
|
||||
style = ""
|
||||
if (edge.kind == "loop back-edge") {
|
||||
style = " [style=bold, color=red, label=\"loop\"]"
|
||||
} else if (edge.kind == "fallthrough") {
|
||||
style = " [style=dashed]"
|
||||
}
|
||||
log.compile(` B${text(bi)} -> B${text(edge.target)}${style};`)
|
||||
}
|
||||
ei = ei + 1
|
||||
}
|
||||
bi = bi + 1
|
||||
}
|
||||
|
||||
log.compile("}")
|
||||
return null
|
||||
}
|
||||
|
||||
var process_function = function(func, name, index) {
|
||||
var blocks = build_cfg(func)
|
||||
if (show_dot) {
|
||||
print_cfg_dot(blocks, name)
|
||||
} else {
|
||||
print_cfg_text(blocks, name)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Process functions
|
||||
main_name = compiled.name != null ? compiled.name : "<main>"
|
||||
|
||||
if (compiled.main != null) {
|
||||
if (fn_matches(-1, main_name)) {
|
||||
process_function(compiled.main, main_name, -1)
|
||||
}
|
||||
}
|
||||
|
||||
if (compiled.functions != null) {
|
||||
fi = 0
|
||||
while (fi < length(compiled.functions)) {
|
||||
func = compiled.functions[fi]
|
||||
fname = func.name != null ? func.name : "<anonymous>"
|
||||
if (fn_matches(fi, fname)) {
|
||||
process_function(func, `[${text(fi)}] ${fname}`, fi)
|
||||
}
|
||||
fi = fi + 1
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
run()
|
||||
$stop()
|
||||
139
clean.ce
139
clean.ce
@@ -23,40 +23,43 @@ var clean_build = false
|
||||
var clean_fetch = false
|
||||
var deep = false
|
||||
var dry_run = false
|
||||
var i = 0
|
||||
var deps = null
|
||||
|
||||
for (var i = 0; i < length(args); i++) {
|
||||
if (args[i] == '--build') {
|
||||
clean_build = true
|
||||
} else if (args[i] == '--fetch') {
|
||||
clean_fetch = true
|
||||
} else if (args[i] == '--all') {
|
||||
clean_build = true
|
||||
clean_fetch = true
|
||||
} else if (args[i] == '--deep') {
|
||||
deep = true
|
||||
} else if (args[i] == '--dry-run') {
|
||||
dry_run = true
|
||||
} else if (args[i] == '--help' || args[i] == '-h') {
|
||||
log.console("Usage: cell clean [<scope>] [options]")
|
||||
log.console("")
|
||||
log.console("Remove cached material to force refetch/rebuild.")
|
||||
log.console("")
|
||||
log.console("Scopes:")
|
||||
log.console(" <locator> Clean specific package")
|
||||
log.console(" shop Clean entire shop")
|
||||
log.console(" world Clean all world packages")
|
||||
log.console("")
|
||||
log.console("Options:")
|
||||
log.console(" --build Remove build outputs only (default)")
|
||||
log.console(" --fetch Remove fetched sources only")
|
||||
log.console(" --all Remove both build outputs and fetched sources")
|
||||
log.console(" --deep Apply to full dependency closure")
|
||||
log.console(" --dry-run Show what would be deleted")
|
||||
$stop()
|
||||
} else if (!starts_with(args[i], '-')) {
|
||||
scope = args[i]
|
||||
var run = function() {
|
||||
for (i = 0; i < length(args); i++) {
|
||||
if (args[i] == '--build') {
|
||||
clean_build = true
|
||||
} else if (args[i] == '--fetch') {
|
||||
clean_fetch = true
|
||||
} else if (args[i] == '--all') {
|
||||
clean_build = true
|
||||
clean_fetch = true
|
||||
} else if (args[i] == '--deep') {
|
||||
deep = true
|
||||
} else if (args[i] == '--dry-run') {
|
||||
dry_run = true
|
||||
} else if (args[i] == '--help' || args[i] == '-h') {
|
||||
log.console("Usage: cell clean [<scope>] [options]")
|
||||
log.console("")
|
||||
log.console("Remove cached material to force refetch/rebuild.")
|
||||
log.console("")
|
||||
log.console("Scopes:")
|
||||
log.console(" <locator> Clean specific package")
|
||||
log.console(" shop Clean entire shop")
|
||||
log.console(" world Clean all world packages")
|
||||
log.console("")
|
||||
log.console("Options:")
|
||||
log.console(" --build Remove build outputs only (default)")
|
||||
log.console(" --fetch Remove fetched sources only")
|
||||
log.console(" --all Remove both build outputs and fetched sources")
|
||||
log.console(" --deep Apply to full dependency closure")
|
||||
log.console(" --dry-run Show what would be deleted")
|
||||
return
|
||||
} else if (!starts_with(args[i], '-')) {
|
||||
scope = args[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to --build if nothing specified
|
||||
if (!clean_build && !clean_fetch) {
|
||||
@@ -73,12 +76,7 @@ var is_shop_scope = (scope == 'shop')
|
||||
var is_world_scope = (scope == 'world')
|
||||
|
||||
if (!is_shop_scope && !is_world_scope) {
|
||||
if (scope == '.' || starts_with(scope, './') || starts_with(scope, '../') || fd.is_dir(scope)) {
|
||||
var resolved = fd.realpath(scope)
|
||||
if (resolved) {
|
||||
scope = resolved
|
||||
}
|
||||
}
|
||||
scope = shop.resolve_locator(scope)
|
||||
}
|
||||
|
||||
var files_to_delete = []
|
||||
@@ -86,6 +84,7 @@ var dirs_to_delete = []
|
||||
|
||||
// Gather packages to clean
|
||||
var packages_to_clean = []
|
||||
var _gather = null
|
||||
|
||||
if (is_shop_scope) {
|
||||
packages_to_clean = shop.list_packages()
|
||||
@@ -97,14 +96,15 @@ if (is_shop_scope) {
|
||||
push(packages_to_clean, scope)
|
||||
|
||||
if (deep) {
|
||||
try {
|
||||
var deps = pkg.gather_dependencies(scope)
|
||||
_gather = function() {
|
||||
deps = pkg.gather_dependencies(scope)
|
||||
arrfor(deps, function(dep) {
|
||||
push(packages_to_clean, dep)
|
||||
})
|
||||
} catch (e) {
|
||||
} disruption {
|
||||
// Skip if can't read dependencies
|
||||
}
|
||||
_gather()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,37 +114,13 @@ var build_dir = shop.get_build_dir()
|
||||
var packages_dir = replace(shop.get_package_dir(''), /\/$/, '') // Get base packages dir
|
||||
|
||||
if (clean_build) {
|
||||
if (is_shop_scope) {
|
||||
// Clean entire build and lib directories
|
||||
if (fd.is_dir(build_dir)) {
|
||||
push(dirs_to_delete, build_dir)
|
||||
}
|
||||
if (fd.is_dir(lib_dir)) {
|
||||
push(dirs_to_delete, lib_dir)
|
||||
}
|
||||
} else {
|
||||
// Clean specific package libraries
|
||||
arrfor(packages_to_clean, function(p) {
|
||||
if (p == 'core') return
|
||||
|
||||
var lib_name = shop.lib_name_for_package(p)
|
||||
var dylib_ext = '.dylib'
|
||||
var lib_path = lib_dir + '/' + lib_name + dylib_ext
|
||||
|
||||
if (fd.is_file(lib_path)) {
|
||||
push(files_to_delete, lib_path)
|
||||
}
|
||||
|
||||
// Also check for .so and .dll
|
||||
var so_path = lib_dir + '/' + lib_name + '.so'
|
||||
var dll_path = lib_dir + '/' + lib_name + '.dll'
|
||||
if (fd.is_file(so_path)) {
|
||||
push(files_to_delete, so_path)
|
||||
}
|
||||
if (fd.is_file(dll_path)) {
|
||||
push(files_to_delete, dll_path)
|
||||
}
|
||||
})
|
||||
// Nuke entire build cache (content-addressed, per-package clean impractical)
|
||||
if (fd.is_dir(build_dir)) {
|
||||
push(dirs_to_delete, build_dir)
|
||||
}
|
||||
// Clean orphaned lib/ directory if it exists (legacy)
|
||||
if (fd.is_dir(lib_dir)) {
|
||||
push(dirs_to_delete, lib_dir)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +144,7 @@ if (clean_fetch) {
|
||||
}
|
||||
|
||||
// Execute or report
|
||||
var deleted_count = 0
|
||||
if (dry_run) {
|
||||
log.console("Would delete:")
|
||||
if (length(files_to_delete) == 0 && length(dirs_to_delete) == 0) {
|
||||
@@ -181,20 +158,19 @@ if (dry_run) {
|
||||
})
|
||||
}
|
||||
} else {
|
||||
var deleted_count = 0
|
||||
|
||||
arrfor(files_to_delete, function(f) {
|
||||
try {
|
||||
var _del = function() {
|
||||
fd.unlink(f)
|
||||
log.console("Deleted: " + f)
|
||||
deleted_count++
|
||||
} catch (e) {
|
||||
log.error("Failed to delete " + f + ": " + e)
|
||||
} disruption {
|
||||
log.error("Failed to delete " + f)
|
||||
}
|
||||
_del()
|
||||
})
|
||||
|
||||
arrfor(dirs_to_delete, function(d) {
|
||||
try {
|
||||
var _del = function() {
|
||||
if (fd.is_link(d)) {
|
||||
fd.unlink(d)
|
||||
} else {
|
||||
@@ -202,9 +178,10 @@ if (dry_run) {
|
||||
}
|
||||
log.console("Deleted: " + d)
|
||||
deleted_count++
|
||||
} catch (e) {
|
||||
log.error("Failed to delete " + d + ": " + e)
|
||||
} disruption {
|
||||
log.error("Failed to delete " + d)
|
||||
}
|
||||
_del()
|
||||
})
|
||||
|
||||
if (deleted_count == 0) {
|
||||
@@ -214,5 +191,7 @@ if (dry_run) {
|
||||
log.console("Clean complete: " + text(deleted_count) + " item(s) deleted.")
|
||||
}
|
||||
}
|
||||
}
|
||||
run()
|
||||
|
||||
$stop()
|
||||
|
||||
112
clone.ce
112
clone.ce
@@ -5,117 +5,67 @@ var shop = use('internal/shop')
|
||||
var link = use('link')
|
||||
var fd = use('fd')
|
||||
var http = use('http')
|
||||
var miniz = use('miniz')
|
||||
|
||||
if (length(args) < 2) {
|
||||
log.console("Usage: cell clone <origin> <path>")
|
||||
log.console("Clones a cell package to a local path and links it.")
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
var run = function() {
|
||||
if (length(args) < 2) {
|
||||
log.console("Usage: cell clone <origin> <path>")
|
||||
log.console("Clones a cell package to a local path and links it.")
|
||||
return
|
||||
}
|
||||
|
||||
var origin = args[0]
|
||||
var target_path = args[1]
|
||||
|
||||
// Resolve target path to absolute
|
||||
if (target_path == '.' || starts_with(target_path, './') || starts_with(target_path, '../')) {
|
||||
var resolved = fd.realpath(target_path)
|
||||
if (resolved) {
|
||||
target_path = resolved
|
||||
} else {
|
||||
// Path doesn't exist yet, resolve relative to cwd
|
||||
var cwd = fd.realpath('.')
|
||||
if (target_path == '.') {
|
||||
target_path = cwd
|
||||
} else if (starts_with(target_path, './')) {
|
||||
target_path = cwd + text(target_path, 1)
|
||||
} else if (starts_with(target_path, '../')) {
|
||||
// Go up one directory from cwd
|
||||
var parent = fd.dirname(cwd)
|
||||
target_path = parent + text(target_path, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
target_path = shop.resolve_locator(target_path)
|
||||
|
||||
// Check if target already exists
|
||||
if (fd.is_dir(target_path)) {
|
||||
log.console("Error: " + target_path + " already exists")
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
if (fd.is_dir(target_path)) {
|
||||
log.console("Error: " + target_path + " already exists")
|
||||
return
|
||||
}
|
||||
|
||||
log.console("Cloning " + origin + " to " + target_path + "...")
|
||||
|
||||
// Get the latest commit
|
||||
var info = shop.resolve_package_info(origin)
|
||||
if (!info || info == 'local') {
|
||||
log.console("Error: " + origin + " is not a remote package")
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
if (!info || info == 'local') {
|
||||
log.console("Error: " + origin + " is not a remote package")
|
||||
return
|
||||
}
|
||||
|
||||
// Update to get the commit hash
|
||||
var update_result = shop.update(origin)
|
||||
if (!update_result) {
|
||||
log.console("Error: Could not fetch " + origin)
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
if (!update_result) {
|
||||
log.console("Error: Could not fetch " + origin)
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch and extract to the target path
|
||||
var lock = shop.load_lock()
|
||||
var entry = lock[origin]
|
||||
if (!entry || !entry.commit) {
|
||||
log.console("Error: No commit found for " + origin)
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
if (!entry || !entry.commit) {
|
||||
log.console("Error: No commit found for " + origin)
|
||||
return
|
||||
}
|
||||
|
||||
var download_url = shop.get_download_url(origin, entry.commit)
|
||||
log.console("Downloading from " + download_url)
|
||||
|
||||
try {
|
||||
var _clone = function() {
|
||||
var zip_blob = http.fetch(download_url)
|
||||
|
||||
// Extract zip to target path
|
||||
var zip = miniz.read(zip_blob)
|
||||
if (!zip) {
|
||||
log.console("Error: Failed to read zip archive")
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
|
||||
// Create target directory
|
||||
fd.mkdir(target_path)
|
||||
|
||||
var count = zip.count()
|
||||
for (var i = 0; i < count; i++) {
|
||||
if (zip.is_directory(i)) continue
|
||||
var filename = zip.get_filename(i)
|
||||
var first_slash = search(filename, '/')
|
||||
if (first_slash == null) continue
|
||||
if (first_slash + 1 >= length(filename)) continue
|
||||
shop.install_zip(zip_blob, target_path)
|
||||
|
||||
var rel_path = text(filename, first_slash + 1)
|
||||
var full_path = target_path + '/' + rel_path
|
||||
var dir_path = fd.dirname(full_path)
|
||||
|
||||
// Ensure directory exists
|
||||
if (!fd.is_dir(dir_path)) {
|
||||
fd.mkdir(dir_path)
|
||||
}
|
||||
fd.slurpwrite(full_path, zip.slurp(filename))
|
||||
}
|
||||
|
||||
log.console("Extracted to " + target_path)
|
||||
|
||||
|
||||
// Link the origin to the cloned path
|
||||
link.add(origin, target_path, shop)
|
||||
log.console("Linked " + origin + " -> " + target_path)
|
||||
|
||||
} catch (e) {
|
||||
log.console("Error: " + e.message)
|
||||
if (e.stack) log.console(e.stack)
|
||||
} disruption {
|
||||
log.console("Error during clone")
|
||||
}
|
||||
_clone()
|
||||
}
|
||||
run()
|
||||
|
||||
$stop()
|
||||
|
||||
130
compare_aot.ce
Normal file
130
compare_aot.ce
Normal file
@@ -0,0 +1,130 @@
|
||||
// compare_aot.ce — compile a .ce/.cm file via both paths and compare results
|
||||
//
|
||||
// Usage:
|
||||
// cell --dev compare_aot.ce <file.ce>
|
||||
|
||||
var build = use('build')
|
||||
var fd_mod = use('fd')
|
||||
var os = use('internal/os')
|
||||
var json = use('json')
|
||||
var time = use('time')
|
||||
|
||||
var show = function(v) {
|
||||
if (v == null) return "null"
|
||||
return json.encode(v)
|
||||
}
|
||||
|
||||
if (length(args) < 1) {
|
||||
log.compile('usage: cell --dev compare_aot.ce <file>')
|
||||
return
|
||||
}
|
||||
|
||||
var file = args[0]
|
||||
if (!fd_mod.is_file(file)) {
|
||||
if (!ends_with(file, '.ce') && fd_mod.is_file(file + '.ce'))
|
||||
file = file + '.ce'
|
||||
else if (!ends_with(file, '.cm') && fd_mod.is_file(file + '.cm'))
|
||||
file = file + '.cm'
|
||||
else {
|
||||
log.error('file not found: ' + file)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var abs = fd_mod.realpath(file)
|
||||
|
||||
// Shared compilation front-end — uses raw modules for per-stage timing
|
||||
var tokenize = use('tokenize')
|
||||
var parse_mod = use('parse')
|
||||
var fold = use('fold')
|
||||
var mcode_mod = use('mcode')
|
||||
var streamline_mod = use('streamline')
|
||||
|
||||
var t0 = time.number()
|
||||
var src = text(fd_mod.slurp(abs))
|
||||
var t1 = time.number()
|
||||
var tok = tokenize(src, abs)
|
||||
var t2 = time.number()
|
||||
var ast = parse_mod(tok.tokens, src, abs, tokenize)
|
||||
var t3 = time.number()
|
||||
var folded = fold(ast)
|
||||
var t4 = time.number()
|
||||
var compiled = mcode_mod(folded)
|
||||
var t5 = time.number()
|
||||
var optimized = streamline_mod(compiled)
|
||||
var t6 = time.number()
|
||||
|
||||
log.compile('--- front-end timing ---')
|
||||
log.compile(' read: ' + text(t1 - t0) + 's')
|
||||
log.compile(' tokenize: ' + text(t2 - t1) + 's')
|
||||
log.compile(' parse: ' + text(t3 - t2) + 's')
|
||||
log.compile(' fold: ' + text(t4 - t3) + 's')
|
||||
log.compile(' mcode: ' + text(t5 - t4) + 's')
|
||||
log.compile(' streamline: ' + text(t6 - t5) + 's')
|
||||
log.compile(' total: ' + text(t6 - t0) + 's')
|
||||
|
||||
// Shared env for both paths — only non-intrinsic runtime functions.
|
||||
// Intrinsics (starts_with, ends_with, logical, some, every, etc.) live on
|
||||
// the stoned global and are found via GETINTRINSIC/cell_rt_get_intrinsic.
|
||||
var env = stone({
|
||||
log: log,
|
||||
fallback: fallback,
|
||||
parallel: parallel,
|
||||
race: race,
|
||||
sequence: sequence,
|
||||
use
|
||||
})
|
||||
|
||||
// --- Interpreted (mach VM) ---
|
||||
var result_interp = null
|
||||
var interp_ok = false
|
||||
var run_interp = function() {
|
||||
log.compile('--- interpreted ---')
|
||||
var mcode_json = json.encode(optimized)
|
||||
var mach_blob = mach_compile_mcode_bin(abs, mcode_json)
|
||||
result_interp = mach_load(mach_blob, env)
|
||||
interp_ok = true
|
||||
log.compile('result: ' + show(result_interp))
|
||||
} disruption {
|
||||
interp_ok = true
|
||||
log.compile('(disruption escaped from interpreted run)')
|
||||
}
|
||||
run_interp()
|
||||
|
||||
// --- Native (AOT via QBE) ---
|
||||
var result_native = null
|
||||
var native_ok = false
|
||||
var run_native = function() {
|
||||
log.compile('\n--- native ---')
|
||||
var dylib_path = build.compile_native_ir(optimized, abs, null)
|
||||
log.compile('dylib: ' + dylib_path)
|
||||
var handle = os.dylib_open(dylib_path)
|
||||
if (!handle) {
|
||||
log.error('failed to open dylib')
|
||||
return
|
||||
}
|
||||
result_native = os.native_module_load(handle, env)
|
||||
native_ok = true
|
||||
log.compile('result: ' + show(result_native))
|
||||
} disruption {
|
||||
native_ok = true
|
||||
log.compile('(disruption escaped from native run)')
|
||||
}
|
||||
run_native()
|
||||
|
||||
// --- Comparison ---
|
||||
log.compile('\n--- comparison ---')
|
||||
var s_interp = show(result_interp)
|
||||
var s_native = show(result_native)
|
||||
if (interp_ok && native_ok) {
|
||||
if (s_interp == s_native) {
|
||||
log.compile('MATCH')
|
||||
} else {
|
||||
log.error('MISMATCH')
|
||||
log.error(' interp: ' + s_interp)
|
||||
log.error(' native: ' + s_native)
|
||||
}
|
||||
} else {
|
||||
if (!interp_ok) log.error('interpreted run failed')
|
||||
if (!native_ok) log.error('native run failed')
|
||||
}
|
||||
99
compile.ce
99
compile.ce
@@ -1,102 +1,27 @@
|
||||
// compile.ce — compile a .cm module to native .dylib via QBE
|
||||
// compile.ce — compile a .cm or .ce file to native .dylib via QBE
|
||||
//
|
||||
// Usage:
|
||||
// cell --core . compile.ce <file.cm>
|
||||
// cell compile <file.cm|file.ce>
|
||||
//
|
||||
// Produces <file>.dylib in the current directory.
|
||||
// Installs the dylib to .cell/lib/<pkg>/<stem>.dylib
|
||||
|
||||
var shop = use('internal/shop')
|
||||
var build = use('build')
|
||||
var fd = use('fd')
|
||||
var os = use('os')
|
||||
|
||||
if (length(args) < 1) {
|
||||
print('usage: cell --core . compile.ce <file.cm>')
|
||||
log.compile('usage: cell compile <file.cm|file.ce>')
|
||||
return
|
||||
}
|
||||
|
||||
var file = args[0]
|
||||
var base = file
|
||||
if (ends_with(base, '.cm')) {
|
||||
base = text(base, 0, length(base) - 3)
|
||||
}
|
||||
|
||||
var safe = replace(replace(base, '/', '_'), '-', '_')
|
||||
var symbol = 'js_' + safe + '_use'
|
||||
var tmp = '/tmp/qbe_' + safe
|
||||
var ssa_path = tmp + '.ssa'
|
||||
var s_path = tmp + '.s'
|
||||
var o_path = tmp + '.o'
|
||||
var rt_o_path = '/tmp/qbe_rt.o'
|
||||
var dylib_path = base + '.dylib'
|
||||
var cwd = fd.getcwd()
|
||||
var rc = 0
|
||||
|
||||
// Step 1: emit QBE IL
|
||||
print('emit qbe...')
|
||||
rc = os.system('cd ' + cwd + ' && ./cell --core . --emit-qbe ' + file + ' > ' + ssa_path)
|
||||
if (rc != 0) {
|
||||
print('failed to emit qbe il')
|
||||
if (!fd.is_file(file)) {
|
||||
log.error('file not found: ' + file)
|
||||
return
|
||||
}
|
||||
|
||||
// Step 2: post-process — insert dead labels after ret/jmp, append wrapper
|
||||
// Use awk via shell to avoid blob/slurpwrite issues with long strings
|
||||
print('post-process...')
|
||||
var awk_cmd = `awk '
|
||||
need_label && /^[[:space:]]*[^@}]/ && NF > 0 {
|
||||
print "@_dead_" dead_id; dead_id++; need_label=0
|
||||
}
|
||||
/^@/ || /^}/ || NF==0 { need_label=0 }
|
||||
/^[[:space:]]*ret / || /^[[:space:]]*jmp / { need_label=1; print; next }
|
||||
{ print }
|
||||
' ` + ssa_path + ` > ` + tmp + `_fixed.ssa`
|
||||
rc = os.system(awk_cmd)
|
||||
if (rc != 0) {
|
||||
print('post-process failed')
|
||||
return
|
||||
}
|
||||
var abs = fd.realpath(file)
|
||||
var file_info = shop.file_info(abs)
|
||||
var pkg = file_info.package
|
||||
|
||||
// Append wrapper function — called as symbol(ctx) by os.dylib_symbol.
|
||||
// Delegates to cell_rt_module_entry which heap-allocates a frame
|
||||
// (so closures survive) and calls cell_main.
|
||||
var wrapper_cmd = `printf '\nexport function l $` + symbol + `(l %%ctx) {\n@entry\n %%result =l call $cell_rt_module_entry(l %%ctx)\n ret %%result\n}\n' >> ` + tmp + `_fixed.ssa`
|
||||
rc = os.system(wrapper_cmd)
|
||||
if (rc != 0) {
|
||||
print('wrapper append failed')
|
||||
return
|
||||
}
|
||||
|
||||
// Step 3: compile QBE IL to assembly
|
||||
print('qbe compile...')
|
||||
rc = os.system('~/.local/bin/qbe -o ' + s_path + ' ' + tmp + '_fixed.ssa')
|
||||
if (rc != 0) {
|
||||
print('qbe compilation failed')
|
||||
return
|
||||
}
|
||||
|
||||
// Step 4: assemble
|
||||
print('assemble...')
|
||||
rc = os.system('cc -c ' + s_path + ' -o ' + o_path)
|
||||
if (rc != 0) {
|
||||
print('assembly failed')
|
||||
return
|
||||
}
|
||||
|
||||
// Step 5: compile runtime stubs (cached — skip if already built)
|
||||
if (!fd.is_file(rt_o_path)) {
|
||||
print('compile runtime stubs...')
|
||||
rc = os.system('cc -c ' + cwd + '/qbe_rt.c -o ' + rt_o_path + ' -fPIC')
|
||||
if (rc != 0) {
|
||||
print('runtime stubs compilation failed')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6: link dylib
|
||||
print('link...')
|
||||
rc = os.system('cc -shared -fPIC -undefined dynamic_lookup ' + o_path + ' ' + rt_o_path + ' -o ' + cwd + '/' + dylib_path)
|
||||
if (rc != 0) {
|
||||
print('linking failed')
|
||||
return
|
||||
}
|
||||
|
||||
print('built: ' + dylib_path)
|
||||
build.compile_native(abs, null, null, pkg)
|
||||
|
||||
40
compile_worker.ce
Normal file
40
compile_worker.ce
Normal file
@@ -0,0 +1,40 @@
|
||||
// compile_worker - Worker actor that compiles a single module and replies
|
||||
//
|
||||
// Receives a message with:
|
||||
// {type: 'script', path, package} — bytecode compile
|
||||
// {type: 'native_script', path, package} — native compile
|
||||
// {type: 'c_package', package} — C package build
|
||||
// {type: 'c_file', package, file} — single C module build
|
||||
//
|
||||
// Replies with {ok: true/false, path} and stops.
|
||||
|
||||
var shop = use('internal/shop')
|
||||
var build = use('build')
|
||||
|
||||
$receiver(function(msg) {
|
||||
var name = msg.path || (msg.file ? msg.package + '/' + msg.file : msg.package)
|
||||
var _work = function() {
|
||||
if (msg.type == 'script') {
|
||||
log.console('compile_worker: compiling ' + name)
|
||||
shop.precompile(msg.path, msg.package)
|
||||
} else if (msg.type == 'native_script') {
|
||||
log.console('compile_worker: native compiling ' + name)
|
||||
build.compile_native(msg.path, null, null, msg.package)
|
||||
} else if (msg.type == 'c_package') {
|
||||
log.console('compile_worker: building package ' + name)
|
||||
build.build_dynamic(msg.package, null, null, null)
|
||||
} else if (msg.type == 'c_file') {
|
||||
log.console('compile_worker: building ' + name)
|
||||
build.compile_c_module(msg.package, msg.file)
|
||||
}
|
||||
log.console('compile_worker: done ' + name)
|
||||
send(msg, {ok: true, path: name})
|
||||
} disruption {
|
||||
log.error('compile_worker: failed ' + name)
|
||||
send(msg, {ok: false, error: 'compile failed'})
|
||||
}
|
||||
_work()
|
||||
$stop()
|
||||
})
|
||||
|
||||
var _t = $delay($stop, 120)
|
||||
261
config.ce
261
config.ce
@@ -47,8 +47,10 @@ function get_nested(obj, path) {
|
||||
// Set a value in nested object using path
|
||||
function set_nested(obj, path, value) {
|
||||
var current = obj
|
||||
for (var i = 0; i < length(path) - 1; i++) {
|
||||
var segment = path[i]
|
||||
var i = 0
|
||||
var segment = null
|
||||
for (i = 0; i < length(path) - 1; i++) {
|
||||
segment = path[i]
|
||||
if (is_null(current[segment]) || !is_object(current[segment])) {
|
||||
current[segment] = {}
|
||||
}
|
||||
@@ -59,15 +61,17 @@ function set_nested(obj, path, value) {
|
||||
|
||||
// Parse value string into appropriate type
|
||||
function parse_value(str) {
|
||||
var num_str = null
|
||||
var n = null
|
||||
// Boolean
|
||||
if (str == 'true') return true
|
||||
if (str == 'false') return false
|
||||
|
||||
// Number (including underscores)
|
||||
var num_str = replace(str, /_/g, '')
|
||||
if (/^-?\d+$/.test(num_str)) return parseInt(num_str)
|
||||
if (/^-?\d*\.\d+$/.test(num_str)) return parseFloat(num_str)
|
||||
|
||||
|
||||
// Number
|
||||
num_str = replace(str, /_/g, '')
|
||||
n = number(num_str)
|
||||
if (n != null) return n
|
||||
|
||||
// String
|
||||
return str
|
||||
}
|
||||
@@ -75,22 +79,19 @@ function parse_value(str) {
|
||||
// Format value for display
|
||||
function format_value(val) {
|
||||
if (is_text(val)) return '"' + val + '"'
|
||||
if (is_number(val) && val >= 1000) {
|
||||
// Add underscores to large numbers
|
||||
return replace(val.toString(), /\B(?=(\d{3})+(?!\d))/g, '_')
|
||||
}
|
||||
return text(val)
|
||||
}
|
||||
|
||||
// Print configuration tree recursively
|
||||
function print_config(obj, prefix = '') {
|
||||
function print_config(obj, pfx) {
|
||||
var p = pfx || ''
|
||||
arrfor(array(obj), function(key) {
|
||||
var val = obj[key]
|
||||
var full_key = prefix ? prefix + '.' + key : key
|
||||
|
||||
var full_key = p ? p + '.' + key : key
|
||||
|
||||
if (is_object(val))
|
||||
print_config(val, full_key)
|
||||
else
|
||||
else if (!is_null(val))
|
||||
log.console(full_key + ' = ' + format_value(val))
|
||||
})
|
||||
}
|
||||
@@ -99,151 +100,123 @@ function print_config(obj, prefix = '') {
|
||||
if (length(args) == 0) {
|
||||
print_help()
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
|
||||
var config = pkg.load_config()
|
||||
if (!config) {
|
||||
log.error("Failed to load cell.toml")
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
|
||||
var command = args[0]
|
||||
var key
|
||||
var path
|
||||
var value
|
||||
var key = null
|
||||
var path = null
|
||||
var value = null
|
||||
var value_str = null
|
||||
var valid_system_keys = null
|
||||
var actor_name = null
|
||||
var actor_cmd = null
|
||||
|
||||
switch (command) {
|
||||
case 'help':
|
||||
case '-h':
|
||||
case '--help':
|
||||
print_help()
|
||||
break
|
||||
|
||||
case 'list':
|
||||
log.console("# Cell Configuration")
|
||||
log.console("")
|
||||
print_config(config)
|
||||
break
|
||||
|
||||
case 'get':
|
||||
if (length(args) < 2) {
|
||||
log.error("Usage: cell config get <key>")
|
||||
if (command == 'help' || command == '-h' || command == '--help') {
|
||||
print_help()
|
||||
} else if (command == 'list') {
|
||||
log.console("# Cell Configuration")
|
||||
log.console("")
|
||||
print_config(config)
|
||||
} else if (command == 'get') {
|
||||
if (length(args) < 2) {
|
||||
log.error("Usage: cell config get <key>")
|
||||
$stop()
|
||||
}
|
||||
key = args[1]
|
||||
path = parse_key(key)
|
||||
value = get_nested(config, path)
|
||||
|
||||
if (value == null) {
|
||||
log.error("Key not found: " + key)
|
||||
} else if (is_object(value)) {
|
||||
print_config(value, key)
|
||||
} else {
|
||||
log.console(key + ' = ' + format_value(value))
|
||||
}
|
||||
} else if (command == 'set') {
|
||||
if (length(args) < 3) {
|
||||
log.error("Usage: cell config set <key> <value>")
|
||||
$stop()
|
||||
}
|
||||
key = args[1]
|
||||
value_str = args[2]
|
||||
path = parse_key(key)
|
||||
value = parse_value(value_str)
|
||||
|
||||
if (path[0] == 'system') {
|
||||
valid_system_keys = [
|
||||
'ar_timer', 'actor_memory', 'net_service',
|
||||
'reply_timeout', 'actor_max', 'stack_max'
|
||||
]
|
||||
if (find(valid_system_keys, path[1]) == null) {
|
||||
log.error("Invalid system key. Valid keys: " + text(valid_system_keys, ', '))
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
key = args[1]
|
||||
path = parse_key(key)
|
||||
value = get_nested(config, path)
|
||||
|
||||
if (value == null) {
|
||||
log.error("Key not found: " + key)
|
||||
} else if (isa(value, object)) {
|
||||
// Print all nested values
|
||||
print_config(value, key)
|
||||
}
|
||||
|
||||
set_nested(config, path, value)
|
||||
pkg.save_config(config)
|
||||
log.console("Set " + key + " = " + format_value(value))
|
||||
} else if (command == 'actor') {
|
||||
if (length(args) < 3) {
|
||||
log.error("Usage: cell config actor <name> <command> [options]")
|
||||
$stop()
|
||||
}
|
||||
|
||||
actor_name = args[1]
|
||||
actor_cmd = args[2]
|
||||
|
||||
config.actors = config.actors || {}
|
||||
config.actors[actor_name] = config.actors[actor_name] || {}
|
||||
|
||||
if (actor_cmd == 'list') {
|
||||
if (length(array(config.actors[actor_name])) == 0) {
|
||||
log.console("No configuration for actor: " + actor_name)
|
||||
} else {
|
||||
log.console(key + ' = ' + format_value(value))
|
||||
log.console("# Configuration for actor: " + actor_name)
|
||||
log.console("")
|
||||
print_config(config.actors[actor_name], 'actors.' + actor_name)
|
||||
}
|
||||
break
|
||||
|
||||
case 'set':
|
||||
if (length(args) < 3) {
|
||||
log.error("Usage: cell config set <key> <value>")
|
||||
} else if (actor_cmd == 'get') {
|
||||
if (length(args) < 4) {
|
||||
log.error("Usage: cell config actor <name> get <key>")
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
var key = args[1]
|
||||
var value_str = args[2]
|
||||
var path = parse_key(key)
|
||||
var value = parse_value(value_str)
|
||||
|
||||
// Validate system keys
|
||||
if (path[0] == 'system') {
|
||||
var valid_system_keys = [
|
||||
'ar_timer', 'actor_memory', 'net_service',
|
||||
'reply_timeout', 'actor_max', 'stack_max'
|
||||
]
|
||||
if (find(valid_system_keys, path[1]) == null) {
|
||||
log.error("Invalid system key. Valid keys: " + text(valid_system_keys, ', '))
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
key = args[3]
|
||||
path = parse_key(key)
|
||||
value = get_nested(config.actors[actor_name], path)
|
||||
|
||||
if (value == null) {
|
||||
log.error("Key not found for actor " + actor_name + ": " + key)
|
||||
} else {
|
||||
log.console('actors.' + actor_name + '.' + key + ' = ' + format_value(value))
|
||||
}
|
||||
|
||||
set_nested(config, path, value)
|
||||
} else if (actor_cmd == 'set') {
|
||||
if (length(args) < 5) {
|
||||
log.error("Usage: cell config actor <name> set <key> <value>")
|
||||
$stop()
|
||||
}
|
||||
key = args[3]
|
||||
value_str = args[4]
|
||||
path = parse_key(key)
|
||||
value = parse_value(value_str)
|
||||
|
||||
set_nested(config.actors[actor_name], path, value)
|
||||
pkg.save_config(config)
|
||||
log.console("Set " + key + " = " + format_value(value))
|
||||
break
|
||||
|
||||
case 'actor':
|
||||
// Handle actor-specific configuration
|
||||
if (length(args) < 3) {
|
||||
log.error("Usage: cell config actor <name> <command> [options]")
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
|
||||
var actor_name = args[1]
|
||||
var actor_cmd = args[2]
|
||||
|
||||
// Initialize actors section if needed
|
||||
config.actors = config.actors || {}
|
||||
config.actors[actor_name] = config.actors[actor_name] || {}
|
||||
|
||||
switch (actor_cmd) {
|
||||
case 'list':
|
||||
if (length(array(config.actors[actor_name])) == 0) {
|
||||
log.console("No configuration for actor: " + actor_name)
|
||||
} else {
|
||||
log.console("# Configuration for actor: " + actor_name)
|
||||
log.console("")
|
||||
print_config(config.actors[actor_name], 'actors.' + actor_name)
|
||||
}
|
||||
break
|
||||
|
||||
case 'get':
|
||||
if (length(args) < 4) {
|
||||
log.error("Usage: cell config actor <name> get <key>")
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
key = args[3]
|
||||
path = parse_key(key)
|
||||
value = get_nested(config.actors[actor_name], path)
|
||||
|
||||
if (value == null) {
|
||||
log.error("Key not found for actor " + actor_name + ": " + key)
|
||||
} else {
|
||||
log.console('actors.' + actor_name + '.' + key + ' = ' + format_value(value))
|
||||
}
|
||||
break
|
||||
|
||||
case 'set':
|
||||
if (length(args) < 5) {
|
||||
log.error("Usage: cell config actor <name> set <key> <value>")
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
key = args[3]
|
||||
var value_str = args[4]
|
||||
path = parse_key(key)
|
||||
value = parse_value(value_str)
|
||||
|
||||
set_nested(config.actors[actor_name], path, value)
|
||||
pkg.save_config(config)
|
||||
log.console("Set actors." + actor_name + "." + key + " = " + format_value(value))
|
||||
break
|
||||
|
||||
default:
|
||||
log.error("Unknown actor command: " + actor_cmd)
|
||||
log.console("Valid commands: list, get, set")
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
log.error("Unknown command: " + command)
|
||||
print_help()
|
||||
log.console("Set actors." + actor_name + "." + key + " = " + format_value(value))
|
||||
} else {
|
||||
log.error("Unknown actor command: " + actor_cmd)
|
||||
log.console("Valid commands: list, get, set")
|
||||
}
|
||||
} else {
|
||||
log.error("Unknown command: " + command)
|
||||
print_help()
|
||||
}
|
||||
|
||||
$stop()
|
||||
$stop()
|
||||
|
||||
12
cross/mingw64.ini
Normal file
12
cross/mingw64.ini
Normal file
@@ -0,0 +1,12 @@
|
||||
[binaries]
|
||||
c = 'x86_64-w64-mingw32-gcc'
|
||||
cpp = 'x86_64-w64-mingw32-g++'
|
||||
ar = 'x86_64-w64-mingw32-ar'
|
||||
strip = 'x86_64-w64-mingw32-strip'
|
||||
windres = 'x86_64-w64-mingw32-windres'
|
||||
|
||||
[host_machine]
|
||||
system = 'windows'
|
||||
cpu_family = 'x86_64'
|
||||
cpu = 'x86_64'
|
||||
endian = 'little'
|
||||
@@ -1,27 +1,15 @@
|
||||
#include "cell.h"
|
||||
|
||||
// Return the current stack depth.
|
||||
JSC_CCALL(debug_stack_depth, return number2js(js,js_debugger_stack_depth(js)))
|
||||
// TODO: Reimplement stack depth for register VM
|
||||
JSC_CCALL(debug_stack_depth, return number2js(js, 0))
|
||||
|
||||
// Return a backtrace of the current call stack.
|
||||
JSC_CCALL(debug_build_backtrace, return js_debugger_build_backtrace(js,NULL))
|
||||
|
||||
// Return the closure variables for a given function.
|
||||
JSC_CCALL(debug_closure_vars, return js_debugger_closure_variables(js,argv[0]))
|
||||
|
||||
JSC_CCALL(debug_set_closure_var,
|
||||
js_debugger_set_closure_variable(js,argv[0],argv[1],argv[2]);
|
||||
return JS_NULL;
|
||||
)
|
||||
|
||||
// Return the local variables for a specific stack frame.
|
||||
JSC_CCALL(debug_local_vars, return js_debugger_local_variables(js, js2number(js,argv[0])))
|
||||
|
||||
// Return metadata about a given function.
|
||||
JSC_CCALL(debug_fn_info, return js_debugger_fn_info(js, argv[0]))
|
||||
|
||||
// Return an array of functions in the current backtrace.
|
||||
JSC_CCALL(debug_backtrace_fns, return js_debugger_backtrace_fns(js,NULL))
|
||||
// TODO: Reimplement debug introspection for register VM
|
||||
JSC_CCALL(debug_build_backtrace, return JS_NewArray(js))
|
||||
JSC_CCALL(debug_closure_vars, return JS_NewObject(js))
|
||||
JSC_CCALL(debug_set_closure_var, return JS_NULL;)
|
||||
JSC_CCALL(debug_local_vars, return JS_NewObject(js))
|
||||
JSC_CCALL(debug_fn_info, return JS_NewObject(js))
|
||||
JSC_CCALL(debug_backtrace_fns, return JS_NewArray(js))
|
||||
|
||||
static const JSCFunctionListEntry js_debug_funcs[] = {
|
||||
MIST_FUNC_DEF(debug, stack_depth, 0),
|
||||
@@ -33,8 +21,9 @@ static const JSCFunctionListEntry js_debug_funcs[] = {
|
||||
MIST_FUNC_DEF(debug, backtrace_fns,0),
|
||||
};
|
||||
|
||||
JSValue js_debug_use(JSContext *js) {
|
||||
JSValue mod = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js,mod,js_debug_funcs,countof(js_debug_funcs));
|
||||
return mod;
|
||||
}
|
||||
JSValue js_core_debug_use(JSContext *js) {
|
||||
JS_FRAME(js);
|
||||
JS_ROOT(mod, JS_NewObject(js));
|
||||
JS_SetPropertyFunctionList(js, mod.val, js_debug_funcs, countof(js_debug_funcs));
|
||||
JS_RETURN(mod.val);
|
||||
}
|
||||
|
||||
52
debug/js.c
52
debug/js.c
@@ -1,48 +1,17 @@
|
||||
#include "cell.h"
|
||||
#include "pit_internal.h"
|
||||
|
||||
JSC_CCALL(os_mem_limit, JS_SetMemoryLimit(JS_GetRuntime(js), js2number(js,argv[0])))
|
||||
JSC_CCALL(os_max_stacksize, JS_SetMaxStackSize(JS_GetRuntime(js), js2number(js,argv[0])))
|
||||
JSC_CCALL(os_max_stacksize, JS_SetMaxStackSize(js, js2number(js,argv[0])))
|
||||
|
||||
// Compute the approximate size of a single JS value in memory.
|
||||
// TODO: Reimplement memory usage reporting for new allocator
|
||||
JSC_CCALL(os_calc_mem,
|
||||
JSMemoryUsage mu;
|
||||
JS_ComputeMemoryUsage(JS_GetRuntime(js),&mu);
|
||||
ret = JS_NewObject(js);
|
||||
JS_SetPropertyStr(js,ret,"malloc_size",number2js(js,mu.malloc_size));
|
||||
JS_SetPropertyStr(js,ret,"malloc_limit",number2js(js,mu.malloc_limit));
|
||||
JS_SetPropertyStr(js,ret,"memory_used_size",number2js(js,mu.memory_used_size));
|
||||
JS_SetPropertyStr(js,ret,"malloc_count",number2js(js,mu.malloc_count));
|
||||
JS_SetPropertyStr(js,ret,"memory_used_count",number2js(js,mu.memory_used_count));
|
||||
JS_SetPropertyStr(js,ret,"str_count",number2js(js,mu.str_count));
|
||||
JS_SetPropertyStr(js,ret,"str_size",number2js(js,mu.str_size));
|
||||
JS_SetPropertyStr(js,ret,"obj_count",number2js(js,mu.obj_count));
|
||||
JS_SetPropertyStr(js,ret,"obj_size",number2js(js,mu.obj_size));
|
||||
JS_SetPropertyStr(js,ret,"prop_count",number2js(js,mu.prop_count));
|
||||
JS_SetPropertyStr(js,ret,"prop_size",number2js(js,mu.prop_size));
|
||||
JS_SetPropertyStr(js,ret,"shape_count",number2js(js,mu.shape_count));
|
||||
JS_SetPropertyStr(js,ret,"shape_size",number2js(js,mu.shape_size));
|
||||
JS_SetPropertyStr(js,ret,"js_func_count",number2js(js,mu.js_func_count));
|
||||
JS_SetPropertyStr(js,ret,"js_func_size",number2js(js,mu.js_func_size));
|
||||
JS_SetPropertyStr(js,ret,"js_func_code_size",number2js(js,mu.js_func_code_size));
|
||||
JS_SetPropertyStr(js,ret,"js_func_pc2line_count",number2js(js,mu.js_func_pc2line_count));
|
||||
JS_SetPropertyStr(js,ret,"js_func_pc2line_size",number2js(js,mu.js_func_pc2line_size));
|
||||
JS_SetPropertyStr(js,ret,"c_func_count",number2js(js,mu.c_func_count));
|
||||
JS_SetPropertyStr(js,ret,"array_count",number2js(js,mu.array_count));
|
||||
JS_SetPropertyStr(js,ret,"fast_array_count",number2js(js,mu.fast_array_count));
|
||||
JS_SetPropertyStr(js,ret,"fast_array_elements",number2js(js,mu.fast_array_elements));
|
||||
JS_SetPropertyStr(js,ret,"binary_object_count",number2js(js,mu.binary_object_count));
|
||||
JS_SetPropertyStr(js,ret,"binary_object_size",number2js(js,mu.binary_object_size));
|
||||
)
|
||||
|
||||
// Disassemble a function object into a string.
|
||||
JSC_CCALL(js_disassemble,
|
||||
return js_debugger_fn_bytecode(js, argv[0]);
|
||||
)
|
||||
|
||||
// Return metadata about a given function.
|
||||
JSC_CCALL(js_fn_info,
|
||||
return js_debugger_fn_info(js, argv[0]);
|
||||
)
|
||||
// TODO: Reimplement for register VM
|
||||
JSC_CCALL(js_disassemble, return JS_NewArray(js);)
|
||||
JSC_CCALL(js_fn_info, return JS_NewObject(js);)
|
||||
|
||||
static const JSCFunctionListEntry js_js_funcs[] = {
|
||||
MIST_FUNC_DEF(os, calc_mem, 0),
|
||||
@@ -52,8 +21,9 @@ static const JSCFunctionListEntry js_js_funcs[] = {
|
||||
MIST_FUNC_DEF(js, fn_info, 1),
|
||||
};
|
||||
|
||||
JSValue js_js_use(JSContext *js) {
|
||||
JSValue mod = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js,mod,js_js_funcs,countof(js_js_funcs));
|
||||
return mod;
|
||||
JSValue js_core_js_use(JSContext *js) {
|
||||
JS_FRAME(js);
|
||||
JS_ROOT(mod, JS_NewObject(js));
|
||||
JS_SetPropertyFunctionList(js, mod.val, js_js_funcs, countof(js_js_funcs));
|
||||
JS_RETURN(mod.val);
|
||||
}
|
||||
|
||||
223
diff.ce
Normal file
223
diff.ce
Normal file
@@ -0,0 +1,223 @@
|
||||
// diff.ce — differential testing: run tests optimized vs unoptimized, compare results
|
||||
//
|
||||
// Usage:
|
||||
// cell diff - diff all test files in current package
|
||||
// cell diff suite - diff a specific test file (tests/suite.cm)
|
||||
// cell diff tests/foo - diff a specific test file by path
|
||||
var shop = use('internal/shop')
|
||||
var pkg = use('package')
|
||||
var fd = use('fd')
|
||||
var time = use('time')
|
||||
var testlib = use('internal/testlib')
|
||||
|
||||
var _args = args == null ? [] : args
|
||||
|
||||
var analyze = use('internal/os').analyze
|
||||
var run_ast_fn = use('internal/os').run_ast_fn
|
||||
var run_ast_noopt_fn = use('internal/os').run_ast_noopt_fn
|
||||
|
||||
if (!run_ast_noopt_fn) {
|
||||
log.console("error: run_ast_noopt_fn not available (rebuild bootstrap)")
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
|
||||
// Parse arguments: diff [test_path]
|
||||
var target_test = null
|
||||
if (length(_args) > 0) {
|
||||
target_test = _args[0]
|
||||
}
|
||||
|
||||
var is_valid_package = testlib.is_valid_package
|
||||
|
||||
if (!is_valid_package('.')) {
|
||||
log.console('No cell.toml found in current directory')
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
|
||||
// Collect test files
|
||||
function collect_tests(specific_test) {
|
||||
var files = pkg.list_files(null)
|
||||
var test_files = []
|
||||
var i = 0
|
||||
var f = null
|
||||
var test_name = null
|
||||
var match_name = null
|
||||
var match_base = null
|
||||
for (i = 0; i < length(files); i++) {
|
||||
f = files[i]
|
||||
if (starts_with(f, "tests/") && ends_with(f, ".cm")) {
|
||||
if (specific_test) {
|
||||
test_name = text(f, 0, -3)
|
||||
match_name = specific_test
|
||||
if (!starts_with(match_name, 'tests/')) match_name = 'tests/' + match_name
|
||||
match_base = ends_with(match_name, '.cm') ? text(match_name, 0, -3) : match_name
|
||||
if (test_name != match_base) continue
|
||||
}
|
||||
push(test_files, f)
|
||||
}
|
||||
}
|
||||
return test_files
|
||||
}
|
||||
|
||||
var values_equal = testlib.values_equal
|
||||
var describe = testlib.describe
|
||||
|
||||
// Run a single test file through both paths
|
||||
function diff_test_file(file_path) {
|
||||
var mod_path = text(file_path, 0, -3)
|
||||
var src_path = fd.realpath('.') + '/' + file_path
|
||||
var src = null
|
||||
var ast = null
|
||||
var mod_opt = null
|
||||
var mod_noopt = null
|
||||
var results = {file: file_path, tests: [], passed: 0, failed: 0, errors: []}
|
||||
var use_pkg = fd.realpath('.')
|
||||
var opt_error = null
|
||||
var noopt_error = null
|
||||
var keys = null
|
||||
var i = 0
|
||||
var k = null
|
||||
var opt_result = null
|
||||
var noopt_result = null
|
||||
var opt_err = null
|
||||
var noopt_err = null
|
||||
var _run_one_opt = null
|
||||
var _run_one_noopt = null
|
||||
|
||||
// Build env for module loading
|
||||
var make_env = function() {
|
||||
return stone({
|
||||
use: function(path) {
|
||||
return shop.use(path, use_pkg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Read and parse
|
||||
var _read = function() {
|
||||
src = text(fd.slurp(src_path))
|
||||
ast = analyze(src, src_path)
|
||||
} disruption {
|
||||
push(results.errors, `failed to parse ${file_path}`)
|
||||
return results
|
||||
}
|
||||
_read()
|
||||
if (length(results.errors) > 0) return results
|
||||
|
||||
// Run optimized
|
||||
var _run_opt = function() {
|
||||
mod_opt = run_ast_fn(mod_path, ast, make_env())
|
||||
} disruption {
|
||||
opt_error = "disrupted"
|
||||
}
|
||||
_run_opt()
|
||||
|
||||
// Run unoptimized
|
||||
var _run_noopt = function() {
|
||||
mod_noopt = run_ast_noopt_fn(mod_path, ast, make_env())
|
||||
} disruption {
|
||||
noopt_error = "disrupted"
|
||||
}
|
||||
_run_noopt()
|
||||
|
||||
// Compare module-level behavior
|
||||
if (opt_error != noopt_error) {
|
||||
push(results.errors, `module load mismatch: opt=${opt_error != null ? opt_error : "ok"} noopt=${noopt_error != null ? noopt_error : "ok"}`)
|
||||
results.failed = results.failed + 1
|
||||
return results
|
||||
}
|
||||
if (opt_error != null) {
|
||||
// Both disrupted during load — that's consistent
|
||||
results.passed = results.passed + 1
|
||||
push(results.tests, {name: "<module>", status: "passed"})
|
||||
return results
|
||||
}
|
||||
|
||||
// If module returns a record of functions, test each one
|
||||
if (is_object(mod_opt) && is_object(mod_noopt)) {
|
||||
keys = array(mod_opt)
|
||||
while (i < length(keys)) {
|
||||
k = keys[i]
|
||||
if (is_function(mod_opt[k]) && is_function(mod_noopt[k])) {
|
||||
opt_result = null
|
||||
noopt_result = null
|
||||
opt_err = null
|
||||
noopt_err = null
|
||||
|
||||
_run_one_opt = function() {
|
||||
opt_result = mod_opt[k]()
|
||||
} disruption {
|
||||
opt_err = "disrupted"
|
||||
}
|
||||
_run_one_opt()
|
||||
|
||||
_run_one_noopt = function() {
|
||||
noopt_result = mod_noopt[k]()
|
||||
} disruption {
|
||||
noopt_err = "disrupted"
|
||||
}
|
||||
_run_one_noopt()
|
||||
|
||||
if (opt_err != noopt_err) {
|
||||
push(results.tests, {name: k, status: "failed"})
|
||||
push(results.errors, `${k}: disruption mismatch opt=${opt_err != null ? opt_err : "ok"} noopt=${noopt_err != null ? noopt_err : "ok"}`)
|
||||
results.failed = results.failed + 1
|
||||
} else if (!values_equal(opt_result, noopt_result)) {
|
||||
push(results.tests, {name: k, status: "failed"})
|
||||
push(results.errors, `${k}: result mismatch opt=${describe(opt_result)} noopt=${describe(noopt_result)}`)
|
||||
results.failed = results.failed + 1
|
||||
} else {
|
||||
push(results.tests, {name: k, status: "passed"})
|
||||
results.passed = results.passed + 1
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
} else {
|
||||
// Compare direct return values
|
||||
if (!values_equal(mod_opt, mod_noopt)) {
|
||||
push(results.tests, {name: "<return>", status: "failed"})
|
||||
push(results.errors, `return value mismatch: opt=${describe(mod_opt)} noopt=${describe(mod_noopt)}`)
|
||||
results.failed = results.failed + 1
|
||||
} else {
|
||||
push(results.tests, {name: "<return>", status: "passed"})
|
||||
results.passed = results.passed + 1
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// Main
|
||||
var test_files = collect_tests(target_test)
|
||||
log.console(`Differential testing: ${text(length(test_files))} file(s)`)
|
||||
|
||||
var total_passed = 0
|
||||
var total_failed = 0
|
||||
var i = 0
|
||||
var result = null
|
||||
var j = 0
|
||||
|
||||
while (i < length(test_files)) {
|
||||
result = diff_test_file(test_files[i])
|
||||
log.console(` ${result.file}: ${text(result.passed)} passed, ${text(result.failed)} failed`)
|
||||
j = 0
|
||||
while (j < length(result.errors)) {
|
||||
log.console(` MISMATCH: ${result.errors[j]}`)
|
||||
j = j + 1
|
||||
}
|
||||
total_passed = total_passed + result.passed
|
||||
total_failed = total_failed + result.failed
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
log.console(`----------------------------------------`)
|
||||
log.console(`Diff: ${text(total_passed)} passed, ${text(total_failed)} failed, ${text(total_passed + total_failed)} total`)
|
||||
|
||||
if (total_failed > 0) {
|
||||
log.console(`DIFFERENTIAL FAILURES DETECTED`)
|
||||
}
|
||||
|
||||
$stop()
|
||||
310
diff_ir.ce
Normal file
310
diff_ir.ce
Normal file
@@ -0,0 +1,310 @@
|
||||
// diff_ir.ce — mcode vs streamline diff
|
||||
//
|
||||
// Usage:
|
||||
// cell diff_ir <file> Diff all functions
|
||||
// cell diff_ir --fn <N|name> <file> Diff only one function
|
||||
// cell diff_ir --summary <file> Counts only
|
||||
|
||||
var fd = use("fd")
|
||||
var shop = use("internal/shop")
|
||||
|
||||
var pad_right = function(s, w) {
|
||||
var r = s
|
||||
while (length(r) < w) {
|
||||
r = r + " "
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
var fmt_val = function(v) {
|
||||
if (is_null(v)) return "null"
|
||||
if (is_number(v)) return text(v)
|
||||
if (is_text(v)) return `"${v}"`
|
||||
if (is_object(v)) return text(v)
|
||||
if (is_logical(v)) return v ? "true" : "false"
|
||||
return text(v)
|
||||
}
|
||||
|
||||
var run = function() {
|
||||
var fn_filter = null
|
||||
var show_summary = false
|
||||
var filename = null
|
||||
var i = 0
|
||||
var mcode_ir = null
|
||||
var opt_ir = null
|
||||
var source_text = null
|
||||
var source_lines = null
|
||||
var main_name = null
|
||||
var fi = 0
|
||||
var func = null
|
||||
var opt_func = null
|
||||
var fname = null
|
||||
|
||||
while (i < length(args)) {
|
||||
if (args[i] == '--fn') {
|
||||
i = i + 1
|
||||
fn_filter = args[i]
|
||||
} else if (args[i] == '--summary') {
|
||||
show_summary = true
|
||||
} else if (args[i] == '--help' || args[i] == '-h') {
|
||||
log.console("Usage: cell diff_ir [--fn <N|name>] [--summary] <file>")
|
||||
log.console("")
|
||||
log.console(" --fn <N|name> Filter to function by index or name")
|
||||
log.console(" --summary Show counts only")
|
||||
return null
|
||||
} else if (!starts_with(args[i], '-')) {
|
||||
filename = args[i]
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
if (!filename) {
|
||||
log.console("Usage: cell diff_ir [--fn <N|name>] [--summary] <file>")
|
||||
return null
|
||||
}
|
||||
|
||||
mcode_ir = shop.mcode_file(filename)
|
||||
opt_ir = shop.compile_file(filename)
|
||||
|
||||
source_text = text(fd.slurp(filename))
|
||||
source_lines = array(source_text, "\n")
|
||||
|
||||
var get_source_line = function(line_num) {
|
||||
if (line_num < 1 || line_num > length(source_lines)) return null
|
||||
return source_lines[line_num - 1]
|
||||
}
|
||||
|
||||
var fn_matches = function(index, name) {
|
||||
var match = null
|
||||
if (fn_filter == null) return true
|
||||
if (index >= 0 && fn_filter == text(index)) return true
|
||||
if (name != null) {
|
||||
match = search(name, fn_filter)
|
||||
if (match != null && match >= 0) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var fmt_instr = function(instr) {
|
||||
var op = instr[0]
|
||||
var n = length(instr)
|
||||
var parts = []
|
||||
var j = 1
|
||||
var operands = null
|
||||
var line_str = null
|
||||
while (j < n - 2) {
|
||||
push(parts, fmt_val(instr[j]))
|
||||
j = j + 1
|
||||
}
|
||||
operands = text(parts, ", ")
|
||||
line_str = instr[n - 2] != null ? `:${text(instr[n - 2])}` : ""
|
||||
return pad_right(`${pad_right(op, 15)}${operands}`, 45) + line_str
|
||||
}
|
||||
|
||||
var classify = function(before, after) {
|
||||
var bn = 0
|
||||
var an = 0
|
||||
var k = 0
|
||||
if (is_text(after) && starts_with(after, "_nop_")) return "eliminated"
|
||||
if (is_array(before) && is_array(after)) {
|
||||
if (before[0] != after[0]) return "rewritten"
|
||||
bn = length(before)
|
||||
an = length(after)
|
||||
if (bn != an) return "rewritten"
|
||||
k = 1
|
||||
while (k < bn - 2) {
|
||||
if (before[k] != after[k]) return "rewritten"
|
||||
k = k + 1
|
||||
}
|
||||
return "identical"
|
||||
}
|
||||
return "identical"
|
||||
}
|
||||
|
||||
var total_eliminated = 0
|
||||
var total_rewritten = 0
|
||||
var total_funcs = 0
|
||||
|
||||
var diff_function = function(mcode_func, opt_func, name, index) {
|
||||
var nr_args = mcode_func.nr_args != null ? mcode_func.nr_args : 0
|
||||
var nr_slots = mcode_func.nr_slots != null ? mcode_func.nr_slots : 0
|
||||
var m_instrs = mcode_func.instructions
|
||||
var o_instrs = opt_func.instructions
|
||||
var eliminated = 0
|
||||
var rewritten = 0
|
||||
var mi = 0
|
||||
var oi = 0
|
||||
var pc = 0
|
||||
var m_instr = null
|
||||
var o_instr = null
|
||||
var kind = null
|
||||
var last_line = null
|
||||
var instr_line = null
|
||||
var n = 0
|
||||
var src = null
|
||||
var annotation = null
|
||||
|
||||
if (m_instrs == null) m_instrs = []
|
||||
if (o_instrs == null) o_instrs = []
|
||||
|
||||
// First pass: count changes
|
||||
mi = 0
|
||||
oi = 0
|
||||
while (mi < length(m_instrs) && oi < length(o_instrs)) {
|
||||
m_instr = m_instrs[mi]
|
||||
o_instr = o_instrs[oi]
|
||||
|
||||
if (is_text(m_instr)) {
|
||||
mi = mi + 1
|
||||
oi = oi + 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (is_text(o_instr) && starts_with(o_instr, "_nop_")) {
|
||||
if (is_array(m_instr)) {
|
||||
eliminated = eliminated + 1
|
||||
}
|
||||
mi = mi + 1
|
||||
oi = oi + 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (is_array(m_instr) && is_array(o_instr)) {
|
||||
kind = classify(m_instr, o_instr)
|
||||
if (kind == "rewritten") {
|
||||
rewritten = rewritten + 1
|
||||
}
|
||||
}
|
||||
mi = mi + 1
|
||||
oi = oi + 1
|
||||
}
|
||||
|
||||
total_eliminated = total_eliminated + eliminated
|
||||
total_rewritten = total_rewritten + rewritten
|
||||
total_funcs = total_funcs + 1
|
||||
|
||||
if (show_summary) {
|
||||
if (eliminated == 0 && rewritten == 0) {
|
||||
log.compile(` ${pad_right(name + ":", 40)} 0 eliminated, 0 rewritten (unchanged)`)
|
||||
} else {
|
||||
log.compile(` ${pad_right(name + ":", 40)} ${text(eliminated)} eliminated, ${text(rewritten)} rewritten`)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
if (eliminated == 0 && rewritten == 0) return null
|
||||
|
||||
log.compile(`\n=== ${name} (args=${text(nr_args)}, slots=${text(nr_slots)}) ===`)
|
||||
log.compile(` ${text(eliminated)} eliminated, ${text(rewritten)} rewritten`)
|
||||
|
||||
// Second pass: show diffs
|
||||
mi = 0
|
||||
oi = 0
|
||||
pc = 0
|
||||
last_line = null
|
||||
while (mi < length(m_instrs) && oi < length(o_instrs)) {
|
||||
m_instr = m_instrs[mi]
|
||||
o_instr = o_instrs[oi]
|
||||
|
||||
if (is_text(m_instr) && !starts_with(m_instr, "_nop_")) {
|
||||
mi = mi + 1
|
||||
oi = oi + 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (is_text(m_instr) && starts_with(m_instr, "_nop_")) {
|
||||
mi = mi + 1
|
||||
oi = oi + 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (is_text(o_instr) && starts_with(o_instr, "_nop_")) {
|
||||
if (is_array(m_instr)) {
|
||||
n = length(m_instr)
|
||||
instr_line = m_instr[n - 2]
|
||||
if (instr_line != last_line && instr_line != null) {
|
||||
src = get_source_line(instr_line)
|
||||
if (src != null) src = trim(src)
|
||||
if (last_line != null) log.compile("")
|
||||
if (src != null && length(src) > 0) {
|
||||
log.compile(` --- line ${text(instr_line)}: ${src} ---`)
|
||||
}
|
||||
last_line = instr_line
|
||||
}
|
||||
log.compile(` - ${pad_right(text(pc), 6)}${fmt_instr(m_instr)}`)
|
||||
log.compile(` + ${pad_right(text(pc), 6)}${pad_right(o_instr, 45)} (eliminated)`)
|
||||
}
|
||||
mi = mi + 1
|
||||
oi = oi + 1
|
||||
pc = pc + 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (is_array(m_instr) && is_array(o_instr)) {
|
||||
kind = classify(m_instr, o_instr)
|
||||
if (kind != "identical") {
|
||||
n = length(m_instr)
|
||||
instr_line = m_instr[n - 2]
|
||||
if (instr_line != last_line && instr_line != null) {
|
||||
src = get_source_line(instr_line)
|
||||
if (src != null) src = trim(src)
|
||||
if (last_line != null) log.compile("")
|
||||
if (src != null && length(src) > 0) {
|
||||
log.compile(` --- line ${text(instr_line)}: ${src} ---`)
|
||||
}
|
||||
last_line = instr_line
|
||||
}
|
||||
|
||||
annotation = ""
|
||||
if (kind == "rewritten") {
|
||||
if (o_instr[0] == "concat" && m_instr[0] != "concat") {
|
||||
annotation = "(specialized)"
|
||||
} else {
|
||||
annotation = "(rewritten)"
|
||||
}
|
||||
}
|
||||
|
||||
log.compile(` - ${pad_right(text(pc), 6)}${fmt_instr(m_instr)}`)
|
||||
log.compile(` + ${pad_right(text(pc), 6)}${fmt_instr(o_instr)} ${annotation}`)
|
||||
}
|
||||
pc = pc + 1
|
||||
}
|
||||
|
||||
mi = mi + 1
|
||||
oi = oi + 1
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Process functions
|
||||
main_name = mcode_ir.name != null ? mcode_ir.name : "<main>"
|
||||
|
||||
if (mcode_ir.main != null && opt_ir.main != null) {
|
||||
if (fn_matches(-1, main_name)) {
|
||||
diff_function(mcode_ir.main, opt_ir.main, main_name, -1)
|
||||
}
|
||||
}
|
||||
|
||||
if (mcode_ir.functions != null && opt_ir.functions != null) {
|
||||
fi = 0
|
||||
while (fi < length(mcode_ir.functions) && fi < length(opt_ir.functions)) {
|
||||
func = mcode_ir.functions[fi]
|
||||
opt_func = opt_ir.functions[fi]
|
||||
fname = func.name != null ? func.name : "<anonymous>"
|
||||
if (fn_matches(fi, fname)) {
|
||||
diff_function(func, opt_func, `[${text(fi)}] ${fname}`, fi)
|
||||
}
|
||||
fi = fi + 1
|
||||
}
|
||||
}
|
||||
|
||||
if (show_summary) {
|
||||
log.compile(`\n total: ${text(total_eliminated)} eliminated, ${text(total_rewritten)} rewritten across ${text(total_funcs)} functions`)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
run()
|
||||
$stop()
|
||||
265
disasm.ce
Normal file
265
disasm.ce
Normal file
@@ -0,0 +1,265 @@
|
||||
// disasm.ce — source-interleaved disassembly
|
||||
//
|
||||
// Usage:
|
||||
// cell disasm <file> Disassemble all functions (mcode)
|
||||
// cell disasm --optimized <file> Disassemble optimized IR (streamline)
|
||||
// cell disasm --fn <N|name> <file> Show only function N or named function
|
||||
// cell disasm --line <N> <file> Show instructions from source line N
|
||||
|
||||
var fd = use("fd")
|
||||
var shop = use("internal/shop")
|
||||
|
||||
var pad_right = function(s, w) {
|
||||
var r = s
|
||||
while (length(r) < w) {
|
||||
r = r + " "
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
var fmt_val = function(v) {
|
||||
if (is_null(v)) return "null"
|
||||
if (is_number(v)) return text(v)
|
||||
if (is_text(v)) return `"${v}"`
|
||||
if (is_object(v)) return text(v)
|
||||
if (is_logical(v)) return v ? "true" : "false"
|
||||
return text(v)
|
||||
}
|
||||
|
||||
var run = function() {
|
||||
var use_optimized = false
|
||||
var fn_filter = null
|
||||
var line_filter = null
|
||||
var filename = null
|
||||
var i = 0
|
||||
var compiled = null
|
||||
var source_text = null
|
||||
var source_lines = null
|
||||
var main_name = null
|
||||
var fi = 0
|
||||
var func = null
|
||||
var fname = null
|
||||
|
||||
while (i < length(args)) {
|
||||
if (args[i] == '--optimized') {
|
||||
use_optimized = true
|
||||
} else if (args[i] == '--fn') {
|
||||
i = i + 1
|
||||
fn_filter = args[i]
|
||||
} else if (args[i] == '--line') {
|
||||
i = i + 1
|
||||
line_filter = number(args[i])
|
||||
} else if (args[i] == '--help' || args[i] == '-h') {
|
||||
log.console("Usage: cell disasm [--optimized] [--fn <N|name>] [--line <N>] <file>")
|
||||
log.console("")
|
||||
log.console(" --optimized Use optimized IR (streamline) instead of raw mcode")
|
||||
log.console(" --fn <N|name> Filter to function by index or name")
|
||||
log.console(" --line <N> Show only instructions from source line N")
|
||||
return null
|
||||
} else if (!starts_with(args[i], '-')) {
|
||||
filename = args[i]
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
if (!filename) {
|
||||
log.console("Usage: cell disasm [--optimized] [--fn <N|name>] [--line <N>] <file>")
|
||||
return null
|
||||
}
|
||||
|
||||
// Compile
|
||||
|
||||
if (use_optimized) {
|
||||
compiled = shop.compile_file(filename)
|
||||
} else {
|
||||
compiled = shop.mcode_file(filename)
|
||||
}
|
||||
|
||||
// Read source file
|
||||
|
||||
source_text = text(fd.slurp(filename))
|
||||
source_lines = array(source_text, "\n")
|
||||
|
||||
// Helpers
|
||||
|
||||
var get_source_line = function(line_num) {
|
||||
if (line_num < 1 || line_num > length(source_lines)) return null
|
||||
return source_lines[line_num - 1]
|
||||
}
|
||||
|
||||
var first_instr_line = function(func) {
|
||||
var instrs = func.instructions
|
||||
var i = 0
|
||||
var n = 0
|
||||
if (instrs == null) return null
|
||||
while (i < length(instrs)) {
|
||||
if (is_array(instrs[i])) {
|
||||
n = length(instrs[i])
|
||||
return instrs[i][n - 2]
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
var func_has_line = function(func, target) {
|
||||
var instrs = func.instructions
|
||||
var i = 0
|
||||
var n = 0
|
||||
if (instrs == null) return false
|
||||
while (i < length(instrs)) {
|
||||
if (is_array(instrs[i])) {
|
||||
n = length(instrs[i])
|
||||
if (instrs[i][n - 2] == target) return true
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var fn_matches = function(index, name) {
|
||||
var match = null
|
||||
if (fn_filter == null) return true
|
||||
if (index >= 0 && fn_filter == text(index)) return true
|
||||
if (name != null) {
|
||||
match = search(name, fn_filter)
|
||||
if (match != null && match >= 0) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var func_name_by_index = function(fi) {
|
||||
var f = null
|
||||
if (compiled.functions == null) return null
|
||||
if (fi < 0 || fi >= length(compiled.functions)) return null
|
||||
f = compiled.functions[fi]
|
||||
return f.name
|
||||
}
|
||||
|
||||
var dump_function = function(func, name, index) {
|
||||
var nr_args = func.nr_args != null ? func.nr_args : 0
|
||||
var nr_slots = func.nr_slots != null ? func.nr_slots : 0
|
||||
var nr_close = func.nr_close_slots != null ? func.nr_close_slots : 0
|
||||
var instrs = func.instructions
|
||||
var start_line = first_instr_line(func)
|
||||
var header = null
|
||||
var i = 0
|
||||
var pc = 0
|
||||
var instr = null
|
||||
var op = null
|
||||
var n = 0
|
||||
var parts = null
|
||||
var j = 0
|
||||
var operands = null
|
||||
var instr_line = null
|
||||
var last_line = null
|
||||
var src = null
|
||||
var line_str = null
|
||||
var instr_text = null
|
||||
var target_name = null
|
||||
|
||||
header = `\n=== ${name} (args=${text(nr_args)}, slots=${text(nr_slots)}, closures=${text(nr_close)})`
|
||||
if (start_line != null) {
|
||||
header = header + ` [line ${text(start_line)}]`
|
||||
}
|
||||
header = header + " ==="
|
||||
log.compile(header)
|
||||
|
||||
if (instrs == null || length(instrs) == 0) {
|
||||
log.compile(" (empty)")
|
||||
return null
|
||||
}
|
||||
|
||||
while (i < length(instrs)) {
|
||||
instr = instrs[i]
|
||||
if (is_text(instr)) {
|
||||
if (!starts_with(instr, "_nop_") && line_filter == null) {
|
||||
log.compile(` ${instr}:`)
|
||||
}
|
||||
} else if (is_array(instr)) {
|
||||
op = instr[0]
|
||||
n = length(instr)
|
||||
instr_line = instr[n - 2]
|
||||
|
||||
if (line_filter != null && instr_line != line_filter) {
|
||||
pc = pc + 1
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (instr_line != last_line && instr_line != null) {
|
||||
src = get_source_line(instr_line)
|
||||
if (src != null) {
|
||||
src = trim(src)
|
||||
}
|
||||
if (last_line != null) {
|
||||
log.compile("")
|
||||
}
|
||||
if (src != null && length(src) > 0) {
|
||||
log.compile(` --- line ${text(instr_line)}: ${src} ---`)
|
||||
} else {
|
||||
log.compile(` --- line ${text(instr_line)} ---`)
|
||||
}
|
||||
last_line = instr_line
|
||||
}
|
||||
|
||||
parts = []
|
||||
j = 1
|
||||
while (j < n - 2) {
|
||||
push(parts, fmt_val(instr[j]))
|
||||
j = j + 1
|
||||
}
|
||||
operands = text(parts, ", ")
|
||||
line_str = instr_line != null ? `:${text(instr_line)}` : ""
|
||||
instr_text = ` ${pad_right(text(pc), 6)}${pad_right(op, 15)}${operands}`
|
||||
|
||||
// Cross-reference for function creation instructions
|
||||
target_name = null
|
||||
if (op == "function" && n >= 5) {
|
||||
target_name = func_name_by_index(instr[2])
|
||||
}
|
||||
if (target_name != null) {
|
||||
instr_text = pad_right(instr_text, 65) + line_str + ` ; -> [${text(instr[2])}] ${target_name}`
|
||||
} else {
|
||||
instr_text = pad_right(instr_text, 65) + line_str
|
||||
}
|
||||
|
||||
log.compile(instr_text)
|
||||
pc = pc + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Process functions
|
||||
|
||||
main_name = compiled.name != null ? compiled.name : "<main>"
|
||||
|
||||
if (compiled.main != null) {
|
||||
if (fn_matches(-1, main_name)) {
|
||||
if (line_filter == null || func_has_line(compiled.main, line_filter)) {
|
||||
dump_function(compiled.main, main_name, -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (compiled.functions != null) {
|
||||
fi = 0
|
||||
while (fi < length(compiled.functions)) {
|
||||
func = compiled.functions[fi]
|
||||
fname = func.name != null ? func.name : "<anonymous>"
|
||||
if (fn_matches(fi, fname)) {
|
||||
if (line_filter == null || func_has_line(func, line_filter)) {
|
||||
dump_function(func, `[${text(fi)}] ${fname}`, fi)
|
||||
}
|
||||
}
|
||||
fi = fi + 1
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
run()
|
||||
$stop()
|
||||
@@ -34,6 +34,7 @@ pit hello
|
||||
- [**Actors and Modules**](/docs/actors/) — the execution model
|
||||
- [**Requestors**](/docs/requestors/) — asynchronous composition
|
||||
- [**Packages**](/docs/packages/) — code organization and sharing
|
||||
- [**Shop Architecture**](/docs/shop/) — module resolution, compilation, and caching
|
||||
|
||||
## Reference
|
||||
|
||||
@@ -56,6 +57,9 @@ Modules loaded with `use()`:
|
||||
## Tools
|
||||
|
||||
- [**Command Line**](/docs/cli/) — the `pit` tool
|
||||
- [**Semantic Index**](/docs/semantic-index/) — index and query symbols, references, and call sites
|
||||
- [**Testing**](/docs/testing/) — writing and running tests
|
||||
- [**Compiler Inspection**](/docs/compiler-tools/) — dump AST, mcode, and optimizer reports
|
||||
- [**Writing C Modules**](/docs/c-modules/) — native extensions
|
||||
|
||||
## Architecture
|
||||
@@ -77,7 +81,7 @@ cd cell
|
||||
make bootstrap
|
||||
```
|
||||
|
||||
The ƿit shop is stored at `~/.pit/`.
|
||||
The ƿit shop is stored at `~/.cell/`.
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
187
docs/actors.md
187
docs/actors.md
@@ -67,9 +67,9 @@ An actor is a script that **does not return a value**. It runs as an independent
|
||||
// worker.ce
|
||||
print("Worker started")
|
||||
|
||||
$receiver(function(msg, reply) {
|
||||
$receiver(function(msg) {
|
||||
print("Received:", msg)
|
||||
// Process message...
|
||||
send(msg, {status: "ok"})
|
||||
})
|
||||
```
|
||||
|
||||
@@ -83,106 +83,140 @@ $receiver(function(msg, reply) {
|
||||
|
||||
Actors have access to special functions prefixed with `$`:
|
||||
|
||||
### $me
|
||||
### $self
|
||||
|
||||
Reference to the current actor.
|
||||
Reference to the current actor. This is a stone (immutable) actor object.
|
||||
|
||||
```javascript
|
||||
print($me) // actor reference
|
||||
print($self) // actor reference
|
||||
print(is_actor($self)) // true
|
||||
```
|
||||
|
||||
### $overling
|
||||
|
||||
Reference to the parent actor that started this actor. `null` for the root actor. Child actors are automatically coupled to their overling — if the parent dies, the child dies too.
|
||||
|
||||
```javascript
|
||||
if ($overling != null) {
|
||||
send($overling, {status: "ready"})
|
||||
}
|
||||
```
|
||||
|
||||
### $stop()
|
||||
|
||||
Stop the current actor.
|
||||
Stop the current actor. When called with an actor argument, stops that underling (child) instead.
|
||||
|
||||
```javascript
|
||||
$stop()
|
||||
$stop() // stop self
|
||||
$stop(child) // stop a child actor
|
||||
```
|
||||
|
||||
### $send(actor, message, callback)
|
||||
|
||||
Send a message to another actor.
|
||||
**Important:** `$stop()` does not halt execution immediately. Code after the call continues running in the current turn — it only prevents the actor from receiving future messages. Structure your code so that nothing runs after `$stop()`, or use `return` to exit the current function first.
|
||||
|
||||
```javascript
|
||||
$send(other_actor, {type: "ping", data: 42}, function(reply) {
|
||||
print("Got reply:", reply)
|
||||
})
|
||||
```
|
||||
// Wrong — code after $stop() still runs
|
||||
if (done) $stop()
|
||||
do_more_work() // this still executes!
|
||||
|
||||
Messages are automatically **splatted** — flattened to plain data without prototypes.
|
||||
// Right — return after $stop()
|
||||
if (done) { $stop(); return }
|
||||
do_more_work()
|
||||
```
|
||||
|
||||
### $start(callback, program)
|
||||
|
||||
Start a new actor from a script.
|
||||
Start a new child actor from a script. The callback receives lifecycle events:
|
||||
|
||||
- `{type: "greet", actor: <ref>}` — child started successfully
|
||||
- `{type: "stop"}` — child stopped cleanly
|
||||
- `{type: "disrupt", reason: ...}` — child crashed
|
||||
|
||||
```javascript
|
||||
$start(function(new_actor) {
|
||||
print("Started:", new_actor)
|
||||
$start(function(event) {
|
||||
if (event.type == 'greet') {
|
||||
print("Child started:", event.actor)
|
||||
send(event.actor, {task: "work"})
|
||||
}
|
||||
if (event.type == 'stop') {
|
||||
print("Child stopped")
|
||||
}
|
||||
if (event.type == 'disrupt') {
|
||||
print("Child crashed:", event.reason)
|
||||
}
|
||||
}, "worker")
|
||||
```
|
||||
|
||||
### $delay(callback, seconds)
|
||||
|
||||
Schedule a callback after a delay.
|
||||
Schedule a callback after a delay. Returns a cancel function that can be called to prevent the callback from firing.
|
||||
|
||||
```javascript
|
||||
$delay(function() {
|
||||
var cancel = $delay(function() {
|
||||
print("5 seconds later")
|
||||
}, 5)
|
||||
|
||||
// To cancel before it fires:
|
||||
cancel()
|
||||
```
|
||||
|
||||
### $clock(callback)
|
||||
|
||||
Get called every frame/tick.
|
||||
Get called every frame/tick. The callback receives the current time as a number.
|
||||
|
||||
```javascript
|
||||
$clock(function(dt) {
|
||||
// Called each tick with delta time
|
||||
$clock(function(t) {
|
||||
// called each tick with current time
|
||||
})
|
||||
```
|
||||
|
||||
### $receiver(callback)
|
||||
|
||||
Set up a message receiver.
|
||||
Set up a message receiver. The callback is called with the incoming message whenever another actor sends a message to this actor.
|
||||
|
||||
To reply to a message, call `send(message, reply_data)` — the message object contains routing information that directs the reply back to the sender.
|
||||
|
||||
```javascript
|
||||
$receiver(function(message, reply) {
|
||||
// Handle incoming message
|
||||
reply({status: "ok"})
|
||||
$receiver(function(message) {
|
||||
// handle incoming message
|
||||
send(message, {status: "ok"})
|
||||
})
|
||||
```
|
||||
|
||||
### $portal(callback, port)
|
||||
|
||||
Open a network port.
|
||||
Open a network port to receive connections from remote actors.
|
||||
|
||||
```javascript
|
||||
$portal(function(connection) {
|
||||
// Handle new connection
|
||||
// handle new connection
|
||||
}, 8080)
|
||||
```
|
||||
|
||||
### $contact(callback, record)
|
||||
|
||||
Connect to a remote address.
|
||||
Connect to a remote actor at a given address.
|
||||
|
||||
```javascript
|
||||
$contact(function(connection) {
|
||||
// Connected
|
||||
// connected
|
||||
}, {host: "example.com", port: 80})
|
||||
```
|
||||
|
||||
### $time_limit(requestor, seconds)
|
||||
|
||||
Wrap a requestor with a timeout. See [Requestors](/docs/requestors/) for details.
|
||||
Wrap a requestor with a timeout. Returns a new requestor that will cancel the original and call its callback with a failure if the time limit is exceeded. See [Requestors](/docs/requestors/) for details.
|
||||
|
||||
```javascript
|
||||
$time_limit(my_requestor, 10) // 10 second timeout
|
||||
var timed = $time_limit(my_requestor, 10)
|
||||
|
||||
timed(function(result, reason) {
|
||||
// reason will explain timeout if it fires
|
||||
}, initial_value)
|
||||
```
|
||||
|
||||
### $couple(actor)
|
||||
|
||||
Couple the current actor to another actor. When the coupled actor dies, the current actor also dies. Coupling is automatic between an actor and its overling (parent).
|
||||
Couple the current actor to another actor. When the coupled actor dies, the current actor also dies. Coupling is automatic between a child actor and its overling (parent).
|
||||
|
||||
```javascript
|
||||
$couple(other_actor)
|
||||
@@ -190,7 +224,7 @@ $couple(other_actor)
|
||||
|
||||
### $unneeded(callback, seconds)
|
||||
|
||||
Schedule the actor for removal after a specified time.
|
||||
Schedule the actor for removal after a specified time. The callback fires when the time elapses.
|
||||
|
||||
```javascript
|
||||
$unneeded(function() {
|
||||
@@ -200,20 +234,78 @@ $unneeded(function() {
|
||||
|
||||
### $connection(callback, actor, config)
|
||||
|
||||
Get information about the connection to another actor, such as latency, bandwidth, and activity.
|
||||
Get information about the connection to another actor. For local actors, returns `{type: "local"}`. For remote actors, returns connection details including latency, bandwidth, and activity.
|
||||
|
||||
```javascript
|
||||
$connection(function(info) {
|
||||
print(info.latency)
|
||||
if (info.type == "local") {
|
||||
print("same machine")
|
||||
} else {
|
||||
print(info.latency)
|
||||
}
|
||||
}, other_actor, {})
|
||||
```
|
||||
|
||||
## Runtime Functions
|
||||
|
||||
These functions are available in actors without the `$` prefix:
|
||||
|
||||
### send(actor, message, callback)
|
||||
|
||||
Send a message to another actor. The message must be an object record.
|
||||
|
||||
The optional callback receives the reply when the recipient responds.
|
||||
|
||||
```javascript
|
||||
send(other_actor, {type: "ping"}, function(reply) {
|
||||
print("Got reply:", reply)
|
||||
})
|
||||
```
|
||||
|
||||
To reply to a received message, pass the message itself as the first argument — it contains routing information:
|
||||
|
||||
```javascript
|
||||
$receiver(function(message) {
|
||||
send(message, {result: 42})
|
||||
})
|
||||
```
|
||||
|
||||
Messages are automatically flattened to plain data.
|
||||
|
||||
### is_actor(value)
|
||||
|
||||
Returns `true` if the value is an actor reference.
|
||||
|
||||
```javascript
|
||||
if (is_actor(some_value)) {
|
||||
send(some_value, {ping: true})
|
||||
}
|
||||
```
|
||||
|
||||
### log
|
||||
|
||||
Channel-based logging. Any `log.X(value)` writes to channel `"X"`. Three channels are conventional: `log.console(msg)`, `log.error(msg)`, `log.system(msg)` — but any name works.
|
||||
|
||||
Channels are routed to configurable **sinks** (console or file) defined in `.cell/log.toml`. See [Logging](/docs/logging/) for the full guide.
|
||||
|
||||
### use(path)
|
||||
|
||||
Import a module. See [Module Resolution](#module-resolution) below.
|
||||
|
||||
### args
|
||||
|
||||
Array of command-line arguments passed to the actor.
|
||||
|
||||
### sequence(), parallel(), race(), fallback()
|
||||
|
||||
Requestor composition functions. See [Requestors](/docs/requestors/) for details.
|
||||
|
||||
## Module Resolution
|
||||
|
||||
When you call `use('name')`, ƿit searches:
|
||||
|
||||
1. **Current package** — files relative to package root
|
||||
2. **Dependencies** — packages declared in `pit.toml`
|
||||
2. **Dependencies** — packages declared in `cell.toml`
|
||||
3. **Core** — built-in ƿit modules
|
||||
|
||||
```javascript
|
||||
@@ -224,7 +316,7 @@ use('json') // core json module
|
||||
use('otherlib/foo') // dependency 'otherlib', file foo.cm
|
||||
```
|
||||
|
||||
Files starting with underscore (`_helper.cm`) are private to the package.
|
||||
Files in the `internal/` directory are private to the package.
|
||||
|
||||
## Example: Simple Actor System
|
||||
|
||||
@@ -234,8 +326,14 @@ var config = use('config')
|
||||
|
||||
print("Starting application...")
|
||||
|
||||
$start(function(worker) {
|
||||
$send(worker, {task: "process", data: [1, 2, 3]})
|
||||
$start(function(event) {
|
||||
if (event.type == 'greet') {
|
||||
send(event.actor, {task: "process", data: [1, 2, 3]})
|
||||
}
|
||||
if (event.type == 'stop') {
|
||||
print("Worker finished")
|
||||
$stop()
|
||||
}
|
||||
}, "worker")
|
||||
|
||||
$delay(function() {
|
||||
@@ -246,11 +344,12 @@ $delay(function() {
|
||||
|
||||
```javascript
|
||||
// worker.ce - Worker actor
|
||||
$receiver(function(msg, reply) {
|
||||
$receiver(function(msg) {
|
||||
if (msg.task == "process") {
|
||||
var result = array(msg.data, x => x * 2)
|
||||
reply({result: result})
|
||||
var result = array(msg.data, function(x) { return x * 2 })
|
||||
send(msg, {result: result})
|
||||
}
|
||||
$stop()
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -52,6 +52,14 @@ Where:
|
||||
Examples:
|
||||
- `mypackage/math.c` -> `js_mypackage_math_use`
|
||||
- `gitea.pockle.world/john/lib/render.c` -> `js_gitea_pockle_world_john_lib_render_use`
|
||||
- `mypackage/internal/helpers.c` -> `js_mypackage_internal_helpers_use`
|
||||
- `mypackage/game.ce` (AOT actor) -> `js_mypackage_game_program`
|
||||
|
||||
Actor files (`.ce`) use the `_program` suffix instead of `_use`.
|
||||
|
||||
Internal modules (in `internal/` subdirectories) follow the same convention — the `internal` directory name becomes part of the symbol. For example, `internal/os.c` in the core package has the symbol `js_core_internal_os_use`.
|
||||
|
||||
**Note:** Having both a `.cm` and `.c` file with the same stem at the same scope is a build error.
|
||||
|
||||
## Required Headers
|
||||
|
||||
@@ -182,10 +190,12 @@ 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_SetPropertyStr(js, result, "x", number2js(js, x/len));
|
||||
JS_SetPropertyStr(js, result, "y", number2js(js, y/len));
|
||||
ret = result;
|
||||
JS_FRAME(js);
|
||||
JS_ROOT(result, JS_NewObject(js));
|
||||
JS_SetPropertyStr(js, result.val, "x", number2js(js, x/len));
|
||||
JS_SetPropertyStr(js, result.val, "y", number2js(js, y/len));
|
||||
JS_RestoreFrame(_js_ctx, _js_gc_frame, _js_local_frame);
|
||||
ret = result.val;
|
||||
}
|
||||
)
|
||||
|
||||
@@ -216,44 +226,116 @@ var n = vector.normalize(3, 4) // {x: 0.6, y: 0.8}
|
||||
var d = vector.dot(1, 0, 0, 1) // 0
|
||||
```
|
||||
|
||||
## Combining C and ƿit
|
||||
|
||||
A common pattern is to have a C file provide low-level functions and a `.cm` file provide a higher-level API:
|
||||
|
||||
```c
|
||||
// _vector_native.c
|
||||
// ... raw C functions ...
|
||||
```
|
||||
|
||||
```javascript
|
||||
// vector.cm
|
||||
var native = this // C module passed as 'this'
|
||||
|
||||
var Vector = function(x, y) {
|
||||
return {x: x, y: y}
|
||||
}
|
||||
|
||||
Vector.length = function(v) {
|
||||
return native.length(v.x, v.y)
|
||||
}
|
||||
|
||||
Vector.normalize = function(v) {
|
||||
return native.normalize(v.x, v.y)
|
||||
}
|
||||
|
||||
return Vector
|
||||
```
|
||||
|
||||
## Build Process
|
||||
|
||||
C files are automatically compiled when you run:
|
||||
|
||||
```bash
|
||||
pit build
|
||||
pit update
|
||||
cell --dev build
|
||||
```
|
||||
|
||||
The resulting dynamic library is placed in `~/.pit/lib/`.
|
||||
Each C file is compiled into a per-file dynamic library at a content-addressed path in `~/.cell/build/<hash>`. A manifest is written for each package so the runtime can find dylibs without rerunning the build pipeline — see [Dylib Manifests](/docs/shop/#dylib-manifests).
|
||||
|
||||
## Compilation Flags (cell.toml)
|
||||
|
||||
Use the `[compilation]` section in `cell.toml` to pass compiler and linker flags:
|
||||
|
||||
```toml
|
||||
[compilation]
|
||||
CFLAGS = "-Isrc -Ivendor/include"
|
||||
LDFLAGS = "-lz -lm"
|
||||
```
|
||||
|
||||
### Include paths
|
||||
|
||||
Relative `-I` paths are resolved from the package root:
|
||||
|
||||
```toml
|
||||
CFLAGS = "-Isdk/public"
|
||||
```
|
||||
|
||||
If your package is at `/path/to/mypkg`, this becomes `-I/path/to/mypkg/sdk/public`.
|
||||
|
||||
Absolute paths are passed through unchanged.
|
||||
|
||||
The build system also auto-discovers `include/` directories — if your package has an `include/` directory, it is automatically added to the include path. No need to add `-I$PACKAGE/include` in cell.toml.
|
||||
|
||||
### Library paths
|
||||
|
||||
Relative `-L` paths work the same way:
|
||||
|
||||
```toml
|
||||
LDFLAGS = "-Lsdk/lib -lmylib"
|
||||
```
|
||||
|
||||
### Target-specific flags
|
||||
|
||||
Add sections named `[compilation.<target>]` for platform-specific flags:
|
||||
|
||||
```toml
|
||||
[compilation]
|
||||
CFLAGS = "-Isdk/public"
|
||||
|
||||
[compilation.macos_arm64]
|
||||
LDFLAGS = "-Lsdk/lib/osx -lmylib"
|
||||
|
||||
[compilation.linux]
|
||||
LDFLAGS = "-Lsdk/lib/linux64 -lmylib"
|
||||
|
||||
[compilation.windows]
|
||||
LDFLAGS = "-Lsdk/lib/win64 -lmylib64"
|
||||
```
|
||||
|
||||
Available targets: `macos_arm64`, `macos_x86_64`, `linux`, `linux_arm64`, `windows`.
|
||||
|
||||
### Sigils
|
||||
|
||||
Use sigils in flags to refer to standard directories:
|
||||
|
||||
- `$LOCAL` — absolute path to `.cell/local` (for prebuilt libraries)
|
||||
- `$PACKAGE` — absolute path to the package root
|
||||
|
||||
```toml
|
||||
CFLAGS = "-I$PACKAGE/vendor/include"
|
||||
LDFLAGS = "-L$LOCAL -lmyprebuilt"
|
||||
```
|
||||
|
||||
### Example: vendored SDK
|
||||
|
||||
A package wrapping an external SDK with platform-specific shared libraries:
|
||||
|
||||
```
|
||||
mypkg/
|
||||
├── cell.toml
|
||||
├── wrapper.cpp
|
||||
└── sdk/
|
||||
├── public/
|
||||
│ └── mylib/
|
||||
│ └── api.h
|
||||
└── lib/
|
||||
├── osx/
|
||||
│ └── libmylib.dylib
|
||||
└── linux64/
|
||||
└── libmylib.so
|
||||
```
|
||||
|
||||
```toml
|
||||
[compilation]
|
||||
CFLAGS = "-Isdk/public"
|
||||
|
||||
[compilation.macos_arm64]
|
||||
LDFLAGS = "-Lsdk/lib/osx -lmylib"
|
||||
|
||||
[compilation.linux]
|
||||
LDFLAGS = "-Lsdk/lib/linux64 -lmylib"
|
||||
```
|
||||
|
||||
```cpp
|
||||
// wrapper.cpp
|
||||
#include "cell.h"
|
||||
#include <mylib/api.h>
|
||||
// ...
|
||||
```
|
||||
|
||||
## Platform-Specific Code
|
||||
|
||||
@@ -267,6 +349,198 @@ audio_emscripten.c # Web/Emscripten
|
||||
|
||||
ƿit selects the appropriate file based on the target platform.
|
||||
|
||||
## Multi-File C Modules
|
||||
|
||||
If your module wraps a C library, place the library's source files in a `src/` directory. Files in `src/` are compiled as support objects and linked into your module's dylib — they are not treated as standalone modules.
|
||||
|
||||
```
|
||||
mypackage/
|
||||
rtree.c # module (exports js_mypackage_rtree_use)
|
||||
src/
|
||||
rtree.c # support file (linked into rtree.dylib)
|
||||
rtree.h # header
|
||||
```
|
||||
|
||||
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_NewInt32`, `JS_SetPropertyStr`, `js_new_blob_stoned_copy`, etc. — can trigger GC, which **moves** heap objects to new addresses. Bare C locals holding `JSValue` become **dangling pointers** after any allocating call. This is not a theoretical concern — it causes real crashes that are difficult to reproduce because they depend on heap pressure.
|
||||
|
||||
### Checklist (apply to EVERY C function you write or modify)
|
||||
|
||||
1. Count the `JS_New*`, `JS_SetProperty*`, and `js_new_blob*` calls in the function
|
||||
2. If there are **2 or more**, the function **MUST** use `JS_FRAME` / `JS_ROOT` / `JS_RETURN`
|
||||
3. Every `JSValue` held in a C local across an allocating call must be rooted
|
||||
|
||||
### 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 creates an object and then sets properties on it, you **must** root it — each `JS_SetPropertyStr` call is an allocating call that can trigger GC:
|
||||
|
||||
```c
|
||||
// UNSAFE — will crash under GC pressure:
|
||||
JSValue obj = JS_NewObject(js);
|
||||
JS_SetPropertyStr(js, obj, "x", JS_NewInt32(js, 1)); // can GC → obj is stale
|
||||
JS_SetPropertyStr(js, obj, "y", JS_NewInt32(js, 2)); // obj may be garbage
|
||||
return obj;
|
||||
|
||||
// SAFE:
|
||||
JS_FRAME(js);
|
||||
JS_ROOT(obj, JS_NewObject(js));
|
||||
JS_SetPropertyStr(js, obj.val, "x", JS_NewInt32(js, 1));
|
||||
JS_SetPropertyStr(js, obj.val, "y", JS_NewInt32(js, 2));
|
||||
JS_RETURN(obj.val);
|
||||
```
|
||||
|
||||
### Patterns
|
||||
|
||||
**Object with properties** — the most common pattern in this codebase:
|
||||
|
||||
```c
|
||||
JS_FRAME(js);
|
||||
JS_ROOT(result, JS_NewObject(js));
|
||||
JS_SetPropertyStr(js, result.val, "width", JS_NewInt32(js, w));
|
||||
JS_SetPropertyStr(js, result.val, "height", JS_NewInt32(js, h));
|
||||
JS_SetPropertyStr(js, result.val, "pixels", js_new_blob_stoned_copy(js, data, len));
|
||||
JS_RETURN(result.val);
|
||||
```
|
||||
|
||||
**Array with loop** — root the element variable *before* the loop, then reassign `.val` each iteration:
|
||||
|
||||
```c
|
||||
JS_FRAME(js);
|
||||
JS_ROOT(arr, JS_NewArray(js));
|
||||
JS_ROOT(item, JS_NULL);
|
||||
for (int i = 0; i < count; i++) {
|
||||
item.val = JS_NewObject(js);
|
||||
JS_SetPropertyStr(js, item.val, "index", JS_NewInt32(js, i));
|
||||
JS_SetPropertyStr(js, item.val, "data", js_new_blob_stoned_copy(js, ptr, sz));
|
||||
JS_SetPropertyNumber(js, arr.val, i, item.val);
|
||||
}
|
||||
JS_RETURN(arr.val);
|
||||
```
|
||||
|
||||
**WARNING — NEVER put `JS_ROOT` inside a loop.** `JS_ROOT` declares a `JSGCRef` local and calls `JS_PushGCRef(&name)`, which pushes its address onto a linked list. Inside a loop the compiler reuses the same stack address, so on iteration 2+ the list becomes self-referential (`ref->prev == ref`). When GC triggers it walks the chain and **hangs forever**. This bug is intermittent — it only manifests when GC happens to run during the loop — making it very hard to reproduce.
|
||||
|
||||
**Nested objects** — root every object that persists across an allocating call:
|
||||
|
||||
```c
|
||||
JS_FRAME(js);
|
||||
JS_ROOT(outer, JS_NewObject(js));
|
||||
JS_ROOT(inner, JS_NewArray(js));
|
||||
// ... populate inner ...
|
||||
JS_SetPropertyStr(js, outer.val, "items", inner.val);
|
||||
JS_RETURN(outer.val);
|
||||
```
|
||||
|
||||
**Inside `JSC_CCALL`** — use `JS_RestoreFrame` and assign to `ret`:
|
||||
|
||||
```c
|
||||
JSC_CCALL(mymod_make,
|
||||
JS_FRAME(js);
|
||||
JS_ROOT(obj, JS_NewObject(js));
|
||||
JS_SetPropertyStr(js, obj.val, "x", number2js(js, 42));
|
||||
JS_RestoreFrame(_js_ctx, _js_gc_frame, _js_local_frame);
|
||||
ret = obj.val;
|
||||
)
|
||||
```
|
||||
|
||||
### C Argument Evaluation Order (critical)
|
||||
|
||||
In C, the **order of evaluation of function arguments is unspecified**. This interacts with the copying GC to create intermittent crashes that are extremely difficult to diagnose.
|
||||
|
||||
```c
|
||||
// UNSAFE — crashes intermittently:
|
||||
JS_FRAME(js);
|
||||
JS_ROOT(obj, JS_NewObject(js));
|
||||
JS_SetPropertyStr(js, obj.val, "format", JS_NewString(js, "rgba32"));
|
||||
// ^^^^^^^ may be evaluated BEFORE JS_NewString runs
|
||||
// If JS_NewString triggers GC, the already-read obj.val is a dangling pointer.
|
||||
```
|
||||
|
||||
The compiler is free to evaluate `obj.val` into a register, *then* call `JS_NewString`. If `JS_NewString` triggers GC, the object moves to a new address. The rooted `obj` is updated by GC, but the **register copy** is not — it still holds the old address. `JS_SetPropertyStr` then writes to freed memory.
|
||||
|
||||
**Fix:** always separate the allocating call into a local variable:
|
||||
|
||||
```c
|
||||
// SAFE:
|
||||
JS_FRAME(js);
|
||||
JS_ROOT(obj, JS_NewObject(js));
|
||||
JSValue fmt = JS_NewString(js, "rgba32");
|
||||
JS_SetPropertyStr(js, obj.val, "format", fmt);
|
||||
// obj.val is read AFTER JS_NewString completes — guaranteed correct.
|
||||
```
|
||||
|
||||
This applies to **any** allocating function used as an argument when another argument references a rooted `.val`:
|
||||
|
||||
```c
|
||||
// ALL of these are UNSAFE:
|
||||
JS_SetPropertyStr(js, obj.val, "pixels", js_new_blob_stoned_copy(js, data, len));
|
||||
JS_SetPropertyStr(js, obj.val, "x", JS_NewFloat64(js, 3.14));
|
||||
JS_SetPropertyStr(js, obj.val, "name", JS_NewString(js, name));
|
||||
|
||||
// SAFE versions — separate the allocation:
|
||||
JSValue pixels = js_new_blob_stoned_copy(js, data, len);
|
||||
JS_SetPropertyStr(js, obj.val, "pixels", pixels);
|
||||
JSValue x = JS_NewFloat64(js, 3.14);
|
||||
JS_SetPropertyStr(js, obj.val, "x", x);
|
||||
JSValue s = JS_NewString(js, name);
|
||||
JS_SetPropertyStr(js, obj.val, "name", s);
|
||||
```
|
||||
|
||||
**Functions that allocate** (must be separated): `JS_NewString`, `JS_NewFloat64`, `JS_NewInt64`, `JS_NewObject`, `JS_NewArray`, `JS_NewCFunction`, `js_new_blob_stoned_copy`
|
||||
|
||||
**Functions that do NOT allocate** (safe inline): `JS_NewInt32`, `JS_NewUint32`, `JS_NewBool`, `JS_NULL`, `JS_TRUE`, `JS_FALSE`
|
||||
|
||||
### Macros
|
||||
|
||||
| Macro | Purpose |
|
||||
|-------|---------|
|
||||
| `JS_FRAME(js)` | Save the GC frame. Required before any `JS_ROOT`. |
|
||||
| `JS_ROOT(name, init)` | Declare a `JSGCRef` and root its value. Access via `name.val`. |
|
||||
| `JS_LOCAL(name, init)` | Declare a rooted `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 =`). |
|
||||
|
||||
### Error return rules
|
||||
|
||||
- Error returns **before** `JS_FRAME` can use plain `return JS_ThrowTypeError(...)` etc.
|
||||
- Error returns **after** `JS_FRAME` must use `JS_RETURN_EX()` or `JS_RETURN_NULL()` — never plain `return`, which would leak the GC frame.
|
||||
|
||||
### 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`:
|
||||
@@ -280,3 +554,32 @@ static int module_state = 0;
|
||||
```
|
||||
|
||||
This prevents symbol conflicts between packages.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Missing header / SDK not installed
|
||||
|
||||
If a package wraps a third-party SDK that isn't installed on your system, the build will show:
|
||||
|
||||
```
|
||||
module.c: fatal error: 'sdk/header.h' file not found (SDK not installed?)
|
||||
```
|
||||
|
||||
Install the required SDK or skip that package. These warnings are harmless — other packages continue building normally.
|
||||
|
||||
### CFLAGS not applied
|
||||
|
||||
If your `cell.toml` has a `[compilation]` section but flags aren't being picked up, check:
|
||||
|
||||
1. The TOML syntax is valid (strings must be quoted)
|
||||
2. The section header is exactly `[compilation]` (not `[compile]` etc.)
|
||||
3. Target-specific sections use valid target names: `macos_arm64`, `macos_x86_64`, `linux`, `linux_arm64`, `windows`
|
||||
|
||||
### API changes from older versions
|
||||
|
||||
If C modules fail with errors about function signatures:
|
||||
|
||||
- `JS_IsArray` takes one argument (the value), not two — remove the context argument
|
||||
- Use `JS_GetPropertyNumber` / `JS_SetPropertyNumber` instead of `JS_GetPropertyUint32` / `JS_SetPropertyUint32`
|
||||
- Use `JS_NewString` instead of `JS_NewAtomString`
|
||||
- There is no `undefined` — use `JS_IsNull` and `JS_NULL` only
|
||||
|
||||
481
docs/cli.md
481
docs/cli.md
@@ -13,7 +13,7 @@ type: "docs"
|
||||
pit <command> [arguments]
|
||||
```
|
||||
|
||||
## Commands
|
||||
## General
|
||||
|
||||
### pit version
|
||||
|
||||
@@ -24,39 +24,62 @@ pit version
|
||||
# 0.1.0
|
||||
```
|
||||
|
||||
### pit install
|
||||
### pit help
|
||||
|
||||
Install a package to the shop.
|
||||
Display help information.
|
||||
|
||||
```bash
|
||||
pit install gitea.pockle.world/john/prosperon
|
||||
pit install /Users/john/local/mypackage # local path
|
||||
pit help
|
||||
pit help <command>
|
||||
```
|
||||
|
||||
### pit update
|
||||
## Package Commands
|
||||
|
||||
Update packages from remote sources.
|
||||
These commands operate on a package's `cell.toml`, source files, or build artifacts.
|
||||
|
||||
### pit add
|
||||
|
||||
Add a dependency to the current package. Installs the package to the shop, builds any C modules, and updates `cell.toml`.
|
||||
|
||||
```bash
|
||||
pit update # update all packages
|
||||
pit update <package> # update specific package
|
||||
pit add gitea.pockle.world/john/prosperon # remote, default alias
|
||||
pit add gitea.pockle.world/john/prosperon myalias # remote, custom alias
|
||||
pit add /Users/john/work/mylib # local path (symlinked)
|
||||
pit add . # current directory
|
||||
pit add ../sibling-package # relative path
|
||||
```
|
||||
|
||||
### pit remove
|
||||
For local paths, the package is symlinked into the shop rather than copied. Changes to the source directory are immediately visible.
|
||||
|
||||
Remove a package from the shop.
|
||||
### pit build
|
||||
|
||||
Build C modules for a package. Compiles each C file into a per-file dynamic library stored in the content-addressed build cache at `~/.cell/build/<hash>`. A per-package manifest is written so the runtime can find dylibs by package name. C files in `src/` directories are compiled as support objects and linked into the module dylibs. Files that previously failed to compile are skipped automatically (cached failure markers); they are retried when the source or compiler flags change.
|
||||
|
||||
```bash
|
||||
pit remove gitea.pockle.world/john/oldpackage
|
||||
pit build # build all packages
|
||||
pit build <package> # build specific package
|
||||
pit build /Users/john/work/mylib # build local package
|
||||
pit build . # build current directory
|
||||
pit build -t macos_arm64 # cross-compile for target
|
||||
pit build -b debug # build type: release (default), debug, minsize
|
||||
pit build --list-targets # list available targets
|
||||
pit build --force # force rebuild
|
||||
pit build --dry-run # show what would be built
|
||||
pit build --verbose # print resolved flags, commands, cache status
|
||||
```
|
||||
|
||||
### pit list
|
||||
### pit test
|
||||
|
||||
List installed packages.
|
||||
Run tests. See [Testing](/docs/testing/) for the full guide.
|
||||
|
||||
```bash
|
||||
pit list # list all installed packages
|
||||
pit list <package> # list dependencies of a package
|
||||
pit test # run tests in current package
|
||||
pit test suite # run specific test file
|
||||
pit test all # run all tests in current package
|
||||
pit test package <name> # run tests in a named package
|
||||
pit test package /Users/john/work/mylib # run tests for a local package
|
||||
pit test package all # run tests from all packages
|
||||
pit test suite --verify --diff # with IR verification and differential testing
|
||||
```
|
||||
|
||||
### pit ls
|
||||
@@ -68,22 +91,135 @@ pit ls # list files in current project
|
||||
pit ls <package> # list files in specified package
|
||||
```
|
||||
|
||||
### pit build
|
||||
### pit audit
|
||||
|
||||
Build the current package.
|
||||
Test-compile all `.ce` and `.cm` scripts in package(s). Continues past failures and reports all errors at the end.
|
||||
|
||||
```bash
|
||||
pit build
|
||||
pit audit # audit all installed packages
|
||||
pit audit <package> # audit specific package
|
||||
pit audit . # audit current directory
|
||||
```
|
||||
|
||||
### pit test
|
||||
### pit resolve
|
||||
|
||||
Run tests.
|
||||
Print the fully resolved dependency closure for a package.
|
||||
|
||||
```bash
|
||||
pit test # run tests in current package
|
||||
pit test all # run all tests
|
||||
pit test <package> # run tests in specific package
|
||||
pit resolve # resolve current package
|
||||
pit resolve <package> # resolve specific package
|
||||
pit resolve --locked # show lock state without links
|
||||
```
|
||||
|
||||
### pit graph
|
||||
|
||||
Emit a dependency graph.
|
||||
|
||||
```bash
|
||||
pit graph # tree of current package
|
||||
pit graph --format dot # graphviz dot output
|
||||
pit graph --format json # json output
|
||||
pit graph --world # graph all installed packages
|
||||
pit graph --locked # show lock view without links
|
||||
```
|
||||
|
||||
### pit verify
|
||||
|
||||
Verify integrity and consistency of packages, links, and builds.
|
||||
|
||||
```bash
|
||||
pit verify # verify current package
|
||||
pit verify shop # verify entire shop
|
||||
pit verify --deep # traverse full dependency closure
|
||||
pit verify --target <triple>
|
||||
```
|
||||
|
||||
### pit pack
|
||||
|
||||
Build a statically linked binary from a package and all its dependencies.
|
||||
|
||||
```bash
|
||||
pit pack <package> # build static binary (output: app)
|
||||
pit pack <package> -o myapp # specify output name
|
||||
pit pack <package> -t <triple> # cross-compile for target
|
||||
```
|
||||
|
||||
### pit config
|
||||
|
||||
Manage system and actor configuration values in `cell.toml`.
|
||||
|
||||
```bash
|
||||
pit config list # list all config
|
||||
pit config get system.ar_timer # get a value
|
||||
pit config set system.ar_timer 5.0 # set a value
|
||||
pit config actor <name> list # list actor config
|
||||
pit config actor <name> get <key> # get actor config
|
||||
pit config actor <name> set <key> <val> # set actor config
|
||||
```
|
||||
|
||||
### pit bench
|
||||
|
||||
Run benchmarks with statistical analysis. Benchmark files are `.cm` modules in a package's `benches/` directory.
|
||||
|
||||
```bash
|
||||
pit bench # run all benchmarks in current package
|
||||
pit bench all # same as above
|
||||
pit bench <suite> # run specific benchmark file
|
||||
pit bench package <name> # benchmark a named package
|
||||
pit bench package <name> <suite> # specific benchmark in a package
|
||||
pit bench package all # benchmark all packages
|
||||
pit bench --bytecode <suite> # force bytecode-only benchmark run
|
||||
pit bench --native <suite> # force native-only benchmark run
|
||||
pit bench --compare <suite> # run bytecode and native side-by-side
|
||||
```
|
||||
|
||||
Output includes median, mean, standard deviation, and percentiles for each benchmark.
|
||||
|
||||
## Shop Commands
|
||||
|
||||
These commands operate on the global shop (`~/.cell/`) or system-level state.
|
||||
|
||||
### pit install
|
||||
|
||||
Install a package to the shop.
|
||||
|
||||
```bash
|
||||
pit install gitea.pockle.world/john/prosperon
|
||||
pit install /Users/john/local/mypackage # local path
|
||||
```
|
||||
|
||||
### pit remove
|
||||
|
||||
Remove a package from the shop. Removes the lock entry, the package directory (or symlink), and any built dylibs.
|
||||
|
||||
```bash
|
||||
pit remove gitea.pockle.world/john/oldpackage
|
||||
pit remove /Users/john/work/mylib # local path
|
||||
pit remove . # current directory
|
||||
pit remove mypackage --dry-run # show what would be removed
|
||||
pit remove mypackage --prune # also remove orphaned dependencies
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--prune` — also remove packages that are no longer needed by any remaining root
|
||||
- `--dry-run` — show what would be removed without removing anything
|
||||
|
||||
### pit update
|
||||
|
||||
Update packages from remote sources.
|
||||
|
||||
```bash
|
||||
pit update # update all packages
|
||||
pit update <package> # update specific package
|
||||
```
|
||||
|
||||
### pit list
|
||||
|
||||
List installed packages.
|
||||
|
||||
```bash
|
||||
pit list # list all installed packages
|
||||
pit list <package> # list dependencies of a package
|
||||
```
|
||||
|
||||
### pit link
|
||||
@@ -97,6 +233,22 @@ pit link delete <canonical> # remove a link
|
||||
pit link clear # remove all links
|
||||
```
|
||||
|
||||
### pit unlink
|
||||
|
||||
Remove a link created by `pit link` or `pit clone` and restore the original package.
|
||||
|
||||
```bash
|
||||
pit unlink gitea.pockle.world/john/prosperon
|
||||
```
|
||||
|
||||
### pit clone
|
||||
|
||||
Clone a package to a local path and link it for development.
|
||||
|
||||
```bash
|
||||
pit clone gitea.pockle.world/john/prosperon ./prosperon
|
||||
```
|
||||
|
||||
### pit fetch
|
||||
|
||||
Fetch package sources without extracting.
|
||||
@@ -105,6 +257,22 @@ Fetch package sources without extracting.
|
||||
pit fetch <package>
|
||||
```
|
||||
|
||||
### pit search
|
||||
|
||||
Search for packages, actors, or modules matching a query.
|
||||
|
||||
```bash
|
||||
pit search math
|
||||
```
|
||||
|
||||
### pit why
|
||||
|
||||
Show which installed packages depend on a given package (reverse dependency lookup).
|
||||
|
||||
```bash
|
||||
pit why gitea.pockle.world/john/prosperon
|
||||
```
|
||||
|
||||
### pit upgrade
|
||||
|
||||
Upgrade the ƿit installation itself.
|
||||
@@ -121,23 +289,261 @@ Clean build artifacts.
|
||||
pit clean
|
||||
```
|
||||
|
||||
### pit help
|
||||
## Logging
|
||||
|
||||
Display help information.
|
||||
### pit log
|
||||
|
||||
Manage log sinks and read log files. See [Logging](/docs/logging/) for the full guide.
|
||||
|
||||
### pit log list
|
||||
|
||||
List configured sinks.
|
||||
|
||||
```bash
|
||||
pit help
|
||||
pit help <command>
|
||||
pit log list
|
||||
```
|
||||
|
||||
## Running Scripts
|
||||
### pit log add
|
||||
|
||||
Any `.ce` file in the ƿit core can be run as a command:
|
||||
Add a log sink.
|
||||
|
||||
```bash
|
||||
pit version # runs version.ce
|
||||
pit build # runs build.ce
|
||||
pit test # runs test.ce
|
||||
pit log add <name> console [options] # add a console sink
|
||||
pit log add <name> file <path> [options] # add a file sink
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
- `--format=pretty|bare|json` — output format (default: `pretty` for console, `json` for file)
|
||||
- `--channels=ch1,ch2` — channels to subscribe (default: `console,error,system`). Use `'*'` for all channels (quote to prevent shell glob expansion).
|
||||
- `--exclude=ch1,ch2` — channels to exclude (useful with `'*'`)
|
||||
- `--stack=ch1,ch2` — channels that capture a full stack trace (default: `error`)
|
||||
|
||||
```bash
|
||||
pit log add terminal console --format=bare --channels=console
|
||||
pit log add errors file .cell/logs/errors.jsonl --channels=error
|
||||
pit log add dump file .cell/logs/dump.jsonl '--channels=*' --exclude=console
|
||||
pit log add debug console --channels=error,debug --stack=error,debug
|
||||
```
|
||||
|
||||
### pit log remove
|
||||
|
||||
Remove a sink.
|
||||
|
||||
```bash
|
||||
pit log remove <name>
|
||||
```
|
||||
|
||||
### pit log read
|
||||
|
||||
Read entries from a file sink.
|
||||
|
||||
```bash
|
||||
pit log read <sink> [options]
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
- `--lines=N` — show last N entries
|
||||
- `--channel=X` — filter by channel
|
||||
- `--since=timestamp` — only show entries after timestamp (seconds since epoch)
|
||||
|
||||
```bash
|
||||
pit log read errors --lines=50
|
||||
pit log read dump --channel=debug --lines=10
|
||||
pit log read errors --since=1702656000
|
||||
```
|
||||
|
||||
### pit log tail
|
||||
|
||||
Follow a file sink in real time.
|
||||
|
||||
```bash
|
||||
pit log tail <sink> [--lines=N]
|
||||
```
|
||||
|
||||
`--lines=N` controls how many existing entries to show on start (default: 10).
|
||||
|
||||
```bash
|
||||
pit log tail dump
|
||||
pit log tail errors --lines=20
|
||||
```
|
||||
|
||||
## Developer Commands
|
||||
|
||||
Compiler pipeline tools, analysis, and testing. These are primarily useful for developing the ƿit compiler and runtime.
|
||||
|
||||
### Compiler Pipeline
|
||||
|
||||
Each of these commands runs the compilation pipeline up to a specific stage and prints the intermediate output. They take a source file as input.
|
||||
|
||||
### pit tokenize
|
||||
|
||||
Tokenize a source file and output the token stream as JSON.
|
||||
|
||||
```bash
|
||||
pit tokenize <file.cm>
|
||||
```
|
||||
|
||||
### pit parse
|
||||
|
||||
Parse a source file and output the AST as JSON.
|
||||
|
||||
```bash
|
||||
pit parse <file.cm>
|
||||
```
|
||||
|
||||
### pit fold
|
||||
|
||||
Run constant folding and semantic analysis on a source file and output the simplified AST as JSON.
|
||||
|
||||
```bash
|
||||
pit fold <file.cm>
|
||||
```
|
||||
|
||||
### pit mcode
|
||||
|
||||
Compile a source file to mcode (machine-independent intermediate representation) and output as JSON.
|
||||
|
||||
```bash
|
||||
pit mcode <file.cm>
|
||||
```
|
||||
|
||||
### pit streamline
|
||||
|
||||
Apply the full optimization pipeline to a source file and output optimized mcode as JSON.
|
||||
|
||||
```bash
|
||||
pit streamline <file.cm> # full optimized IR as JSON (default)
|
||||
pit streamline --stats <file.cm> # summary stats per function
|
||||
pit streamline --ir <file.cm> # human-readable IR
|
||||
pit streamline --check <file.cm> # warnings only (e.g. high slot count)
|
||||
```
|
||||
|
||||
Flags can be combined. `--stats` output includes function name, args, slots, instruction counts by category, and nops eliminated. `--check` warns when `nr_slots > 200` (approaching the 255 limit).
|
||||
|
||||
### pit qbe
|
||||
|
||||
Compile a source file to QBE intermediate language (for native code generation).
|
||||
|
||||
```bash
|
||||
pit qbe <file.cm>
|
||||
```
|
||||
|
||||
### pit compile
|
||||
|
||||
Compile a source file to a native dynamic library.
|
||||
|
||||
```bash
|
||||
pit compile <file.cm> # outputs .dylib to ~/.cell/build/
|
||||
pit compile <file.ce>
|
||||
```
|
||||
|
||||
### pit run_native
|
||||
|
||||
Compile a module natively and compare execution against interpreted mode, showing timing differences.
|
||||
|
||||
```bash
|
||||
pit run_native <module> # compare interpreted vs native
|
||||
pit run_native <module> <test_arg> # pass argument to module function
|
||||
```
|
||||
|
||||
### pit run_aot
|
||||
|
||||
Ahead-of-time compile and execute a program natively.
|
||||
|
||||
```bash
|
||||
pit run_aot <program.ce>
|
||||
```
|
||||
|
||||
### pit seed
|
||||
|
||||
Regenerate the boot seed files in `boot/`. Seeds are pre-compiled mcode IR (JSON) that bootstrap the compilation pipeline on cold start. They only need regenerating when the pipeline source changes in a way the existing seeds can't compile, or before distribution.
|
||||
|
||||
```bash
|
||||
pit seed # regenerate all boot seeds
|
||||
pit seed --clean # also clear the build cache after
|
||||
```
|
||||
|
||||
The engine recompiles pipeline modules automatically when source changes (via content-addressed cache). Seeds are a fallback for cold start when no cache exists.
|
||||
|
||||
### Analysis
|
||||
|
||||
### pit explain
|
||||
|
||||
Query the semantic index for symbol information at a specific source location or by name.
|
||||
|
||||
```bash
|
||||
pit explain --span <file:line:col> # find symbol at position
|
||||
pit explain --symbol <name> [files...] # find symbol by name
|
||||
pit explain --help # show usage
|
||||
```
|
||||
|
||||
### pit index
|
||||
|
||||
Build a semantic index for a source file and output symbol information as JSON.
|
||||
|
||||
```bash
|
||||
pit index <file.cm> # output to stdout
|
||||
pit index <file.cm> -o index.json # output to file
|
||||
pit index --help # show usage
|
||||
```
|
||||
|
||||
### pit ir_report
|
||||
|
||||
Optimizer flight recorder — capture detailed information about IR transformations during optimization.
|
||||
|
||||
```bash
|
||||
pit ir_report <file.cm> # per-pass JSON summaries (default)
|
||||
pit ir_report <file.cm> --events # include rewrite events
|
||||
pit ir_report <file.cm> --types # include type deltas
|
||||
pit ir_report <file.cm> --ir-before=PASS # print IR before specific pass
|
||||
pit ir_report <file.cm> --ir-after=PASS # print IR after specific pass
|
||||
pit ir_report <file.cm> --ir-all # print IR before/after every pass
|
||||
pit ir_report <file.cm> --full # all options combined
|
||||
```
|
||||
|
||||
Output is NDJSON (newline-delimited JSON).
|
||||
|
||||
### Testing
|
||||
|
||||
### pit diff
|
||||
|
||||
Differential testing — run tests with and without optimizations and compare results.
|
||||
|
||||
```bash
|
||||
pit diff # diff all test files in current package
|
||||
pit diff <suite> # diff specific test file
|
||||
pit diff tests/<path> # diff by path
|
||||
```
|
||||
|
||||
### pit fuzz
|
||||
|
||||
Random program fuzzer — generates random programs and checks for optimization correctness by comparing optimized vs unoptimized execution.
|
||||
|
||||
```bash
|
||||
pit fuzz # 100 iterations with random seed
|
||||
pit fuzz <iterations> # specific number of iterations
|
||||
pit fuzz --seed <N> # start at specific seed
|
||||
pit fuzz <iterations> --seed <N>
|
||||
```
|
||||
|
||||
Failures are saved to `tests/fuzz_failures/`.
|
||||
|
||||
### pit vm_suite
|
||||
|
||||
Run the VM stability test suite (641 tests covering arithmetic, strings, control flow, closures, objects, and more).
|
||||
|
||||
```bash
|
||||
pit vm_suite
|
||||
```
|
||||
|
||||
### pit syntax_suite
|
||||
|
||||
Run the syntax feature test suite (covers all literal types, operators, control flow, functions, prototypes, and more).
|
||||
|
||||
```bash
|
||||
pit syntax_suite
|
||||
```
|
||||
|
||||
## Package Locators
|
||||
@@ -154,13 +560,12 @@ pit install /Users/john/work/mylib
|
||||
|
||||
## Configuration
|
||||
|
||||
ƿit stores its data in `~/.pit/`:
|
||||
ƿit stores its data in `~/.cell/`:
|
||||
|
||||
```
|
||||
~/.pit/
|
||||
├── packages/ # installed packages
|
||||
├── lib/ # compiled dynamic libraries
|
||||
├── build/ # build cache
|
||||
~/.cell/
|
||||
├── packages/ # installed package sources
|
||||
├── build/ # content-addressed cache (bytecode, dylibs, manifests)
|
||||
├── cache/ # downloaded archives
|
||||
├── lock.toml # installed package versions
|
||||
└── link.toml # local development links
|
||||
|
||||
495
docs/compiler-tools.md
Normal file
495
docs/compiler-tools.md
Normal file
@@ -0,0 +1,495 @@
|
||||
---
|
||||
title: "Compiler Inspection Tools"
|
||||
description: "Tools for inspecting and debugging the compiler pipeline"
|
||||
weight: 50
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
ƿit includes a set of tools for inspecting the compiler pipeline at every stage. These are useful for debugging, testing optimizations, and understanding what the compiler does with your code.
|
||||
|
||||
## Pipeline Overview
|
||||
|
||||
The compiler runs in stages:
|
||||
|
||||
```
|
||||
source → tokenize → parse → fold → mcode → streamline → output
|
||||
```
|
||||
|
||||
Each stage has a corresponding CLI tool that lets you see its output.
|
||||
|
||||
| Stage | Tool | What it shows |
|
||||
|-------------|---------------------------|----------------------------------------|
|
||||
| tokenize | `tokenize.ce` | Token stream as JSON |
|
||||
| parse | `parse.ce` | Unfolded AST as JSON |
|
||||
| fold | `fold.ce` | Folded AST as JSON |
|
||||
| mcode | `mcode.ce` | Raw mcode IR as JSON |
|
||||
| mcode | `mcode.ce --pretty` | Human-readable mcode IR |
|
||||
| streamline | `streamline.ce` | Full optimized IR as JSON |
|
||||
| streamline | `streamline.ce --types` | Optimized IR with type annotations |
|
||||
| streamline | `streamline.ce --stats` | Per-function summary stats |
|
||||
| streamline | `streamline.ce --ir` | Human-readable canonical IR |
|
||||
| disasm | `disasm.ce` | Source-interleaved disassembly |
|
||||
| disasm | `disasm.ce --optimized` | Optimized source-interleaved disassembly |
|
||||
| diff | `diff_ir.ce` | Mcode vs streamline instruction diff |
|
||||
| xref | `xref.ce` | Cross-reference / call creation graph |
|
||||
| cfg | `cfg.ce` | Control flow graph (basic blocks) |
|
||||
| slots | `slots.ce` | Slot data flow / use-def chains |
|
||||
| all | `ir_report.ce` | Structured optimizer flight recorder |
|
||||
|
||||
All tools take a source file as input and run the pipeline up to the relevant stage.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# see raw mcode IR (pretty-printed)
|
||||
cell mcode --pretty myfile.ce
|
||||
|
||||
# source-interleaved disassembly
|
||||
cell disasm myfile.ce
|
||||
|
||||
# see optimized IR with type annotations
|
||||
cell streamline --types myfile.ce
|
||||
|
||||
# full optimizer report with events
|
||||
cell ir_report --full myfile.ce
|
||||
```
|
||||
|
||||
## fold.ce
|
||||
|
||||
Prints the folded AST as JSON. This is the output of the parser and constant folder, before mcode generation.
|
||||
|
||||
```bash
|
||||
cell fold <file.ce|file.cm>
|
||||
```
|
||||
|
||||
## mcode.ce
|
||||
|
||||
Prints mcode IR. Default output is JSON; use `--pretty` for human-readable format with opcodes, operands, and program counter.
|
||||
|
||||
```bash
|
||||
cell mcode <file.ce|file.cm> # JSON (default)
|
||||
cell mcode --pretty <file.ce|file.cm> # human-readable IR
|
||||
```
|
||||
|
||||
## streamline.ce
|
||||
|
||||
Runs the full pipeline (tokenize, parse, fold, mcode, streamline) and outputs the optimized IR as JSON. Useful for piping to `jq` or saving for comparison.
|
||||
|
||||
```bash
|
||||
cell streamline <file.ce|file.cm> # full JSON (default)
|
||||
cell streamline --stats <file.ce|file.cm> # summary stats per function
|
||||
cell streamline --ir <file.ce|file.cm> # human-readable IR
|
||||
cell streamline --check <file.ce|file.cm> # warnings only
|
||||
cell streamline --types <file.ce|file.cm> # IR with type annotations
|
||||
cell streamline --diagnose <file.ce|file.cm> # compile-time diagnostics
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| (none) | Full optimized IR as JSON (backward compatible) |
|
||||
| `--stats` | Per-function summary: args, slots, instruction counts by category, nops eliminated |
|
||||
| `--ir` | Human-readable canonical IR (same format as `ir_report.ce`) |
|
||||
| `--check` | Warnings only (e.g. `nr_slots > 200` approaching 255 limit) |
|
||||
| `--types` | Optimized IR with inferred type annotations per slot |
|
||||
| `--diagnose` | Run compile-time diagnostics (type errors and warnings) |
|
||||
|
||||
Flags can be combined.
|
||||
|
||||
## disasm.ce
|
||||
|
||||
Source-interleaved disassembly. Shows mcode or optimized IR with source lines interleaved, making it easy to see which instructions were generated from which source code.
|
||||
|
||||
```bash
|
||||
cell disasm <file> # disassemble all functions (mcode)
|
||||
cell disasm --optimized <file> # disassemble optimized IR (streamline)
|
||||
cell disasm --fn 87 <file> # show only function 87
|
||||
cell disasm --fn my_func <file> # show only functions named "my_func"
|
||||
cell disasm --line 235 <file> # show instructions generated from line 235
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| (none) | Raw mcode IR with source interleaving (default) |
|
||||
| `--optimized` | Use optimized IR (streamline) instead of raw mcode |
|
||||
| `--fn <N\|name>` | Filter to specific function by index or name substring |
|
||||
| `--line <N>` | Show only instructions generated from a specific source line |
|
||||
|
||||
### Output Format
|
||||
|
||||
Functions are shown with a header including argument count, slot count, and the source line where the function begins. Instructions are grouped by source line, with the source text shown before each group:
|
||||
|
||||
```
|
||||
=== [87] <anonymous> (args=0, slots=12, closures=0) [line 234] ===
|
||||
|
||||
--- line 235: var result = compute(x, y) ---
|
||||
0 access 2, "compute" :235
|
||||
1 get 3, 1, 0 :235
|
||||
2 get 4, 1, 1 :235
|
||||
3 invoke 3, 2, 2 :235
|
||||
|
||||
--- line 236: if (result > 0) { ---
|
||||
4 access 5, 0 :236
|
||||
5 gt 6, 4, 5 :236
|
||||
6 jump_false 6, "else_1" :236
|
||||
```
|
||||
|
||||
Each instruction line shows:
|
||||
- Program counter (left-aligned)
|
||||
- Opcode
|
||||
- Operands (comma-separated)
|
||||
- Source line number (`:N` suffix, right-aligned)
|
||||
|
||||
Function creation instructions include a cross-reference annotation showing the target function's name:
|
||||
|
||||
```
|
||||
3 function 5, 12 :235 ; -> [12] helper_fn
|
||||
```
|
||||
|
||||
## diff_ir.ce
|
||||
|
||||
Compares mcode IR (before optimization) with streamline IR (after optimization), showing what the optimizer changed. Useful for understanding which instructions were eliminated, specialized, or rewritten.
|
||||
|
||||
```bash
|
||||
cell diff_ir <file> # diff all functions
|
||||
cell diff_ir --fn <N|name> <file> # diff only one function
|
||||
cell diff_ir --summary <file> # counts only
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| (none) | Show all diffs with source interleaving |
|
||||
| `--fn <N\|name>` | Filter to specific function by index or name |
|
||||
| `--summary` | Show only eliminated/rewritten counts per function |
|
||||
|
||||
### Output Format
|
||||
|
||||
Changed instructions are shown in diff style with `-` (before) and `+` (after) lines:
|
||||
|
||||
```
|
||||
=== [0] <anonymous> (args=1, slots=40) ===
|
||||
17 eliminated, 51 rewritten
|
||||
|
||||
--- line 4: if (n <= 1) { ---
|
||||
- 1 is_int 4, 1 :4
|
||||
+ 1 is_int 3, 1 :4 (specialized)
|
||||
- 3 is_int 5, 2 :4
|
||||
+ 3 _nop_tc_1 (eliminated)
|
||||
```
|
||||
|
||||
Summary mode gives a quick overview:
|
||||
|
||||
```
|
||||
[0] <anonymous>: 17 eliminated, 51 rewritten
|
||||
[1] <anonymous>: 65 eliminated, 181 rewritten
|
||||
total: 86 eliminated, 250 rewritten across 4 functions
|
||||
```
|
||||
|
||||
## xref.ce
|
||||
|
||||
Cross-reference / call graph tool. Shows which functions create other functions (via `function` instructions), building a creation tree.
|
||||
|
||||
```bash
|
||||
cell xref <file> # full creation tree
|
||||
cell xref --callers <N> <file> # who creates function [N]?
|
||||
cell xref --callees <N> <file> # what does [N] create/call?
|
||||
cell xref --dot <file> # DOT graph for graphviz
|
||||
cell xref --optimized <file> # use optimized IR
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| (none) | Indented creation tree from main |
|
||||
| `--callers <N>` | Show which functions create function [N] |
|
||||
| `--callees <N>` | Show what function [N] creates (use -1 for main) |
|
||||
| `--dot` | Output DOT format for graphviz |
|
||||
| `--optimized` | Use optimized IR instead of raw mcode |
|
||||
|
||||
### Output Format
|
||||
|
||||
Default tree view:
|
||||
|
||||
```
|
||||
demo_disasm.cm
|
||||
[0] <anonymous>
|
||||
[1] <anonymous>
|
||||
[2] <anonymous>
|
||||
```
|
||||
|
||||
Caller/callee query:
|
||||
|
||||
```
|
||||
Callers of [0] <anonymous>:
|
||||
demo_disasm.cm at line 3
|
||||
```
|
||||
|
||||
DOT output can be piped to graphviz: `cell xref --dot file.cm | dot -Tpng -o xref.png`
|
||||
|
||||
## cfg.ce
|
||||
|
||||
Control flow graph tool. Identifies basic blocks from labels and jumps, computes edges, and detects loop back-edges.
|
||||
|
||||
```bash
|
||||
cell cfg --fn <N|name> <file> # text CFG for function
|
||||
cell cfg --dot --fn <N|name> <file> # DOT output for graphviz
|
||||
cell cfg <file> # text CFG for all functions
|
||||
cell cfg --optimized <file> # use optimized IR
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--fn <N\|name>` | Filter to specific function by index or name |
|
||||
| `--dot` | Output DOT format for graphviz |
|
||||
| `--optimized` | Use optimized IR instead of raw mcode |
|
||||
|
||||
### Output Format
|
||||
|
||||
```
|
||||
=== [0] <anonymous> ===
|
||||
B0 [pc 0-2, line 4]:
|
||||
0 access 2, 1
|
||||
1 is_int 4, 1
|
||||
2 jump_false 4, "rel_ni_2"
|
||||
-> B3 "rel_ni_2" (jump)
|
||||
-> B1 (fallthrough)
|
||||
|
||||
B1 [pc 3-4, line 4]:
|
||||
3 is_int 5, 2
|
||||
4 jump_false 5, "rel_ni_2"
|
||||
-> B3 "rel_ni_2" (jump)
|
||||
-> B2 (fallthrough)
|
||||
```
|
||||
|
||||
Each block shows its ID, PC range, source lines, instructions, and outgoing edges. Loop back-edges (target PC <= source PC) are annotated.
|
||||
|
||||
## slots.ce
|
||||
|
||||
Slot data flow analysis. Builds use-def chains for every slot in a function, showing where each slot is defined and used. Optionally captures type information from streamline.
|
||||
|
||||
```bash
|
||||
cell slots --fn <N|name> <file> # slot summary for function
|
||||
cell slots --slot <N> --fn <N|name> <file> # trace slot N
|
||||
cell slots <file> # slot summary for all functions
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--fn <N\|name>` | Filter to specific function by index or name |
|
||||
| `--slot <N>` | Show chronological DEF/USE trace for a specific slot |
|
||||
|
||||
### Output Format
|
||||
|
||||
Summary shows each slot with its def count, use count, inferred type, and first definition. Dead slots (defined but never used) are flagged:
|
||||
|
||||
```
|
||||
=== [0] <anonymous> (args=1, slots=40) ===
|
||||
slot defs uses type first-def
|
||||
s0 0 0 - (this)
|
||||
s1 0 10 - (arg 0)
|
||||
s2 1 6 - pc 0: access
|
||||
s10 1 0 - pc 29: invoke <- dead
|
||||
```
|
||||
|
||||
Slot trace (`--slot N`) shows every DEF and USE in program order:
|
||||
|
||||
```
|
||||
=== slot 3 in [0] <anonymous> ===
|
||||
DEF pc 5: le_int 3, 1, 2 :4
|
||||
DEF pc 11: le_float 3, 1, 2 :4
|
||||
DEF pc 17: le_text 3, 1, 2 :4
|
||||
USE pc 31: jump_false 3, "if_else_0" :4
|
||||
```
|
||||
|
||||
## seed.ce
|
||||
|
||||
Regenerates the boot seed files in `boot/`. These are pre-compiled mcode IR (JSON) files that bootstrap the compilation pipeline on cold start.
|
||||
|
||||
```bash
|
||||
cell seed # regenerate all boot seeds
|
||||
cell seed --clean # also clear the build cache after
|
||||
```
|
||||
|
||||
The script compiles each pipeline module (tokenize, parse, fold, mcode, streamline) and `internal/bootstrap.cm` through the current pipeline, encodes the output as JSON, and writes it to `boot/<name>.cm.mcode`.
|
||||
|
||||
**When to regenerate seeds:**
|
||||
- Before a release or distribution
|
||||
- When the pipeline source changes in a way the existing seeds can't compile the new source (e.g. language-level changes)
|
||||
- Seeds do NOT need regenerating for normal development — the engine recompiles pipeline modules from source automatically via the content-addressed cache
|
||||
|
||||
## ir_report.ce
|
||||
|
||||
The optimizer flight recorder. Runs the full pipeline with structured logging and outputs machine-readable, diff-friendly JSON. This is the most detailed tool for understanding what the optimizer did and why.
|
||||
|
||||
```bash
|
||||
cell ir_report [options] <file.ce|file.cm>
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--summary` | Per-pass JSON summaries with instruction counts and timing (default) |
|
||||
| `--events` | Include rewrite events showing each optimization applied |
|
||||
| `--types` | Include type delta records showing inferred slot types |
|
||||
| `--ir-before=PASS` | Print canonical IR before a specific pass |
|
||||
| `--ir-after=PASS` | Print canonical IR after a specific pass |
|
||||
| `--ir-all` | Print canonical IR before and after all passes |
|
||||
| `--full` | Everything: summary + events + types + ir-all |
|
||||
|
||||
With no flags, `--summary` is the default.
|
||||
|
||||
### Output Format
|
||||
|
||||
Output is line-delimited JSON. Each line is a self-contained JSON object with a `type` field:
|
||||
|
||||
**`type: "pass"`** — Per-pass summary with categorized instruction counts before and after:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "pass",
|
||||
"pass": "eliminate_type_checks",
|
||||
"fn": "fib",
|
||||
"ms": 0.12,
|
||||
"changed": true,
|
||||
"before": {"instr": 77, "nop": 0, "guard": 16, "branch": 28, ...},
|
||||
"after": {"instr": 77, "nop": 1, "guard": 15, "branch": 28, ...},
|
||||
"changes": {"guards_removed": 1, "nops_added": 1}
|
||||
}
|
||||
```
|
||||
|
||||
**`type: "event"`** — Individual rewrite event with before/after instructions and reasoning:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "event",
|
||||
"pass": "eliminate_type_checks",
|
||||
"rule": "incompatible_type_forces_jump",
|
||||
"at": 3,
|
||||
"before": [["is_int", 5, 2, 4, 9], ["jump_false", 5, "rel_ni_2", 4, 9]],
|
||||
"after": ["_nop_tc_1", ["jump", "rel_ni_2", 4, 9]],
|
||||
"why": {"slot": 2, "known_type": "float", "checked_type": "int"}
|
||||
}
|
||||
```
|
||||
|
||||
**`type: "types"`** — Inferred type information for a function:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "types",
|
||||
"fn": "fib",
|
||||
"param_types": {},
|
||||
"slot_types": {"25": "null"}
|
||||
}
|
||||
```
|
||||
|
||||
**`type: "ir"`** — Canonical IR text for a function at a specific point:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ir",
|
||||
"when": "before",
|
||||
"pass": "all",
|
||||
"fn": "fib",
|
||||
"text": "fn fib (args=1, slots=26)\n @0 access s2, 2\n ..."
|
||||
}
|
||||
```
|
||||
|
||||
### Rewrite Rules
|
||||
|
||||
Each pass records events with named rules:
|
||||
|
||||
**eliminate_type_checks:**
|
||||
- `known_type_eliminates_guard` — type already known, guard removed
|
||||
- `incompatible_type_forces_jump` — type conflicts, conditional jump becomes unconditional
|
||||
- `num_subsumes_int_float` — num check satisfied by int or float
|
||||
- `dynamic_to_field` — load_dynamic/store_dynamic narrowed to field access
|
||||
- `dynamic_to_index` — load_dynamic/store_dynamic narrowed to index access
|
||||
|
||||
**simplify_algebra:**
|
||||
- `add_zero`, `sub_zero`, `mul_one`, `div_one` — identity operations become moves
|
||||
- `mul_zero` — multiplication by zero becomes constant
|
||||
- `self_eq`, `self_ne` — same-slot comparisons become constants
|
||||
|
||||
**simplify_booleans:**
|
||||
- `not_jump_false_fusion` — not + jump_false fused into jump_true
|
||||
- `not_jump_true_fusion` — not + jump_true fused into jump_false
|
||||
- `double_not` — not + not collapsed to move
|
||||
|
||||
**eliminate_moves:**
|
||||
- `self_move` — move to same slot becomes nop
|
||||
|
||||
**eliminate_dead_jumps:**
|
||||
- `jump_to_next` — jump to immediately following label becomes nop
|
||||
|
||||
### Canonical IR Format
|
||||
|
||||
The `--ir-all`, `--ir-before`, and `--ir-after` flags produce a deterministic text representation of the IR:
|
||||
|
||||
```
|
||||
fn fib (args=1, slots=26)
|
||||
@0 access s2, 2
|
||||
@1 is_int s4, s1 ; [guard]
|
||||
@2 jump_false s4, "rel_ni_2" ; [branch]
|
||||
@3 --- nop (tc) ---
|
||||
@4 jump "rel_ni_2" ; [branch]
|
||||
@5 lt_int s3, s1, s2
|
||||
@6 jump "rel_done_4" ; [branch]
|
||||
rel_ni_2:
|
||||
@8 is_num s4, s1 ; [guard]
|
||||
```
|
||||
|
||||
Properties:
|
||||
- `@N` is the raw array index, stable across passes (passes replace, never insert or delete)
|
||||
- `sN` prefix distinguishes slot operands from literal values
|
||||
- String operands are quoted
|
||||
- Labels appear as indented headers with a colon
|
||||
- Category tags in brackets: `[guard]`, `[branch]`, `[load]`, `[store]`, `[call]`, `[arith]`, `[move]`, `[const]`
|
||||
- Nops shown as `--- nop (reason) ---` with reason codes: `tc` (type check), `bl` (boolean), `mv` (move), `dj` (dead jump), `ur` (unreachable)
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# what passes changed something?
|
||||
cell ir_report --summary myfile.ce | jq 'select(.changed)'
|
||||
|
||||
# list all rewrite rules that fired
|
||||
cell ir_report --events myfile.ce | jq 'select(.type == "event") | .rule'
|
||||
|
||||
# diff IR before and after optimization
|
||||
cell ir_report --ir-all myfile.ce | jq -r 'select(.type == "ir") | .text'
|
||||
|
||||
# full report for analysis
|
||||
cell ir_report --full myfile.ce > report.json
|
||||
```
|
||||
|
||||
## ir_stats.cm
|
||||
|
||||
A utility module used by `ir_report.ce` and available for custom tooling. Not a standalone tool.
|
||||
|
||||
```javascript
|
||||
var ir_stats = use("ir_stats")
|
||||
|
||||
ir_stats.detailed_stats(func) // categorized instruction counts
|
||||
ir_stats.ir_fingerprint(func) // djb2 hash of instruction array
|
||||
ir_stats.canonical_ir(func, name, opts) // deterministic text representation
|
||||
ir_stats.type_snapshot(slot_types) // frozen copy of type map
|
||||
ir_stats.type_delta(before_types, after_types) // compute type changes
|
||||
ir_stats.category_tag(op) // classify an opcode
|
||||
```
|
||||
|
||||
### Instruction Categories
|
||||
|
||||
`detailed_stats` classifies each instruction into one of these categories:
|
||||
|
||||
| Category | Opcodes |
|
||||
|----------|---------|
|
||||
| load | `load_field`, `load_index`, `load_dynamic`, `get`, `access` (non-constant) |
|
||||
| store | `store_field`, `store_index`, `store_dynamic`, `set_var`, `put`, `push` |
|
||||
| branch | `jump`, `jump_true`, `jump_false`, `jump_not_null` |
|
||||
| call | `invoke`, `goinvoke` |
|
||||
| guard | `is_int`, `is_text`, `is_num`, `is_bool`, `is_null`, `is_array`, `is_func`, `is_record`, `is_stone` |
|
||||
| arith | `add_int`, `sub_int`, ..., `add_float`, ..., `concat`, `neg_int`, `neg_float`, bitwise ops |
|
||||
| move | `move` |
|
||||
| const | `int`, `true`, `false`, `null`, `access` (with constant value) |
|
||||
| label | string entries that are not nops |
|
||||
| nop | strings starting with `_nop_` |
|
||||
| other | everything else (`frame`, `setarg`, `array`, `record`, `function`, `return`, etc.) |
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -395,16 +395,6 @@ Returns the opposite logical. Returns null for non-logicals.
|
||||
|
||||
Returns a requestor that starts all requestors in the array. Results are collected into an array matching the input order. Optional throttle limits concurrent requestors. Optional need specifies the minimum number of successes required. See [Requestors](/docs/requestors/) for usage.
|
||||
|
||||
### print(value)
|
||||
|
||||
Print a value to standard output.
|
||||
|
||||
```javascript
|
||||
print("hello")
|
||||
print(42)
|
||||
print(`result: ${x}`)
|
||||
```
|
||||
|
||||
### race(requestor_array, throttle, need)
|
||||
|
||||
Like parallel, but returns as soon as the needed number of results are obtained. Default need is 1. Unfinished requestors are cancelled. See [Requestors](/docs/requestors/) for usage.
|
||||
|
||||
@@ -398,6 +398,8 @@ fn2() // 1
|
||||
|
||||
### Arguments
|
||||
|
||||
Functions can have at most **4 parameters**. Use a record to pass more values.
|
||||
|
||||
Extra arguments are ignored. Missing arguments are `null`.
|
||||
|
||||
```javascript
|
||||
@@ -406,6 +408,11 @@ fn(1, 2, 3) // 3 (extra arg ignored)
|
||||
|
||||
var fn2 = function(a, b) { return a }
|
||||
fn2(1) // 1 (b is null)
|
||||
|
||||
// More than 4 parameters — use a record
|
||||
var draw = function(shape, opts) {
|
||||
// opts.x, opts.y, opts.color, ...
|
||||
}
|
||||
```
|
||||
|
||||
### Immediately Invoked Function Expression
|
||||
@@ -505,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
|
||||
@@ -587,7 +595,7 @@ var safe_divide = function(a, b) {
|
||||
if (b == 0) disrupt
|
||||
return a / b
|
||||
} disruption {
|
||||
print("something went wrong")
|
||||
log.error("something went wrong")
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -15,10 +15,14 @@ var json = use('json')
|
||||
|
||||
### json.encode(value, space, replacer, whitelist)
|
||||
|
||||
Convert a value to JSON text.
|
||||
Convert a value to JSON text. With no `space` argument, output is pretty-printed with 1-space indent. Pass `false` or `0` for compact single-line output.
|
||||
|
||||
```javascript
|
||||
json.encode({a: 1, b: 2})
|
||||
// '{ "a": 1, "b": 2 }'
|
||||
|
||||
// Compact (no whitespace)
|
||||
json.encode({a: 1, b: 2}, false)
|
||||
// '{"a":1,"b":2}'
|
||||
|
||||
// Pretty print with 2-space indent
|
||||
@@ -32,7 +36,7 @@ json.encode({a: 1, b: 2}, 2)
|
||||
**Parameters:**
|
||||
|
||||
- **value** — the value to encode
|
||||
- **space** — indentation (number of spaces or string)
|
||||
- **space** — indentation: number of spaces, string, or `false`/`0` for compact output. Default is pretty-printed.
|
||||
- **replacer** — function to transform values
|
||||
- **whitelist** — array of keys to include
|
||||
|
||||
|
||||
233
docs/library/probe.md
Normal file
233
docs/library/probe.md
Normal file
@@ -0,0 +1,233 @@
|
||||
---
|
||||
title: "probe"
|
||||
description: "Runtime observability for actors"
|
||||
weight: 90
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
Runtime observability for actors. Register named probe functions on any actor and query them over HTTP while the program runs.
|
||||
|
||||
```javascript
|
||||
var probe = use('probe')
|
||||
```
|
||||
|
||||
The probe server starts automatically on the first `register()` call, listening on `127.0.0.1:9000`.
|
||||
|
||||
## Registering Probes
|
||||
|
||||
### probe.register(target, probes)
|
||||
|
||||
Register a group of probe functions under a target name. Each probe is a function that receives an `args` record and returns a value.
|
||||
|
||||
```javascript
|
||||
var probe = use('probe')
|
||||
|
||||
var world = {
|
||||
entities: [
|
||||
{id: 1, name: "player", x: 10, y: 20, hp: 100},
|
||||
{id: 2, name: "goblin", x: 55, y: 30, hp: 40}
|
||||
],
|
||||
tick: 0
|
||||
}
|
||||
|
||||
probe.register("game", {
|
||||
state: function(args) {
|
||||
return world
|
||||
},
|
||||
entities: function(args) {
|
||||
return world.entities
|
||||
},
|
||||
entity: function(args) {
|
||||
return find(world.entities, function(e) {
|
||||
return e.id == args.id
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
probe.register("render", {
|
||||
info: function(args) {
|
||||
return {fps: 60, draw_calls: 128, batches: 12}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
A target is just a namespace — group related probes under the same target name. Register as many targets as you like; the server starts once and serves them all.
|
||||
|
||||
## HTTP Endpoints
|
||||
|
||||
All responses are JSON with an `ok` field.
|
||||
|
||||
### GET /discover
|
||||
|
||||
Lists all registered targets and their probe names. Designed for tooling — an LLM or dashboard can call this first to learn what's available, then query specific probes.
|
||||
|
||||
```
|
||||
$ curl http://127.0.0.1:9000/discover
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"targets": {
|
||||
"game": ["state", "entities", "entity"],
|
||||
"render": ["info"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /probe
|
||||
|
||||
Call a single probe function by target and name. Optionally pass arguments.
|
||||
|
||||
```
|
||||
$ curl -X POST -H "Content-Type: application/json" \
|
||||
-d '{"target":"game","name":"state"}' \
|
||||
http://127.0.0.1:9000/probe
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"result": {
|
||||
"entities": [
|
||||
{"id": 1, "name": "player", "x": 10, "y": 20, "hp": 100},
|
||||
{"id": 2, "name": "goblin", "x": 55, "y": 30, "hp": 40}
|
||||
],
|
||||
"tick": 4821
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
With arguments:
|
||||
|
||||
```
|
||||
$ curl -X POST -H "Content-Type: application/json" \
|
||||
-d '{"target":"game","name":"entity","args":{"id":1}}' \
|
||||
http://127.0.0.1:9000/probe
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"result": {"id": 1, "name": "player", "x": 10, "y": 20, "hp": 100}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /snapshot
|
||||
|
||||
Call multiple probes in one request. Returns all results keyed by `target/name`.
|
||||
|
||||
```
|
||||
$ curl -X POST -H "Content-Type: application/json" \
|
||||
-d '{"probes":[{"target":"game","name":"state"},{"target":"render","name":"info"}]}' \
|
||||
http://127.0.0.1:9000/snapshot
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"results": {
|
||||
"game/state": {
|
||||
"entities": [
|
||||
{"id": 1, "name": "player", "x": 10, "y": 20, "hp": 100},
|
||||
{"id": 2, "name": "goblin", "x": 55, "y": 30, "hp": 40}
|
||||
],
|
||||
"tick": 4821
|
||||
},
|
||||
"render/info": {
|
||||
"fps": 60,
|
||||
"draw_calls": 128,
|
||||
"batches": 12
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Errors
|
||||
|
||||
Unknown paths return 404:
|
||||
|
||||
```json
|
||||
{"ok": false, "error": "not found"}
|
||||
```
|
||||
|
||||
Unknown targets or probe names:
|
||||
|
||||
```json
|
||||
{"ok": false, "error": "unknown probe: game/nonexistent"}
|
||||
```
|
||||
|
||||
If a probe function disrupts:
|
||||
|
||||
```json
|
||||
{"ok": false, "error": "probe failed"}
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
A game actor with a simulation loop and probe observability:
|
||||
|
||||
```javascript
|
||||
// game.ce
|
||||
var probe = use('probe')
|
||||
|
||||
var state = {
|
||||
entities: [
|
||||
{id: 1, name: "player", x: 0, y: 0, hp: 100},
|
||||
{id: 2, name: "enemy", x: 50, y: 50, hp: 60}
|
||||
],
|
||||
frame: 0,
|
||||
paused: false
|
||||
}
|
||||
|
||||
probe.register("game", {
|
||||
state: function(args) {
|
||||
return state
|
||||
},
|
||||
entities: function(args) {
|
||||
return state.entities
|
||||
},
|
||||
entity: function(args) {
|
||||
return find(state.entities, function(e) {
|
||||
return e.id == args.id
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// game loop
|
||||
def tick = function(_) {
|
||||
if (!state.paused) {
|
||||
state.frame = state.frame + 1
|
||||
// ... update entities, physics, AI ...
|
||||
}
|
||||
$delay(tick, 0.016)
|
||||
}
|
||||
$delay(tick, 0.016)
|
||||
```
|
||||
|
||||
While the game runs, query it from a terminal:
|
||||
|
||||
```
|
||||
$ curl -s http://127.0.0.1:9000/discover | jq .targets
|
||||
{
|
||||
"game": ["state", "entities", "entity"]
|
||||
}
|
||||
|
||||
$ curl -s -X POST -d '{"target":"game","name":"state"}' \
|
||||
-H "Content-Type: application/json" \
|
||||
http://127.0.0.1:9000/probe | jq .result.frame
|
||||
7834
|
||||
|
||||
$ curl -s -X POST -d '{"target":"game","name":"entity","args":{"id":1}}' \
|
||||
-H "Content-Type: application/json" \
|
||||
http://127.0.0.1:9000/probe | jq .result
|
||||
{
|
||||
"id": 1,
|
||||
"name": "player",
|
||||
"x": 142,
|
||||
"y": 87,
|
||||
"hp": 100
|
||||
}
|
||||
```
|
||||
|
||||
Probes run inside the actor's normal turn, so the values are always consistent — never a half-updated frame.
|
||||
269
docs/logging.md
Normal file
269
docs/logging.md
Normal file
@@ -0,0 +1,269 @@
|
||||
---
|
||||
title: "Logging"
|
||||
description: "Configurable channel-based logging with sinks"
|
||||
weight: 25
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
Logging in ƿit is channel-based. Any `log.X(value)` call writes to channel `"X"`. Channels are routed to **sinks** — named destinations that format and deliver log output to the console or to files.
|
||||
|
||||
## Channels
|
||||
|
||||
These channels are conventional:
|
||||
|
||||
| Channel | Usage |
|
||||
|---------|-------|
|
||||
| `log.console(msg)` | General output |
|
||||
| `log.error(msg)` | Errors |
|
||||
| `log.warn(msg)` | Compiler diagnostics and warnings |
|
||||
| `log.system(msg)` | Internal system messages |
|
||||
| `log.build(msg)` | Per-file compile/link output |
|
||||
| `log.shop(msg)` | Package management internals |
|
||||
|
||||
Any name works. `log.debug(msg)` creates channel `"debug"`, `log.perf(msg)` creates `"perf"`, and so on.
|
||||
|
||||
```javascript
|
||||
log.console("server started on port 8080")
|
||||
log.error("connection refused")
|
||||
log.debug({query: "SELECT *", rows: 42})
|
||||
```
|
||||
|
||||
Non-text values are JSON-encoded automatically.
|
||||
|
||||
## Default Behavior
|
||||
|
||||
With no configuration, a default sink routes `console` and `error` to the terminal in clean format. The `error` channel includes a stack trace by default:
|
||||
|
||||
```
|
||||
server started on port 8080
|
||||
error: connection refused
|
||||
at handle_request (server.ce:42:3)
|
||||
at main (main.ce:5:1)
|
||||
```
|
||||
|
||||
Clean format prints messages without actor ID or channel prefix. Error messages are prefixed with `error:`. Other formats (`pretty`, `bare`) include actor IDs and are available for debugging. Stack traces are always on for errors unless you explicitly configure a sink without them.
|
||||
|
||||
Channels like `warn`, `build`, and `shop` are not routed to the terminal by default. Enable them with `pit log enable <channel>`.
|
||||
|
||||
## Configuration
|
||||
|
||||
Logging is configured in `.cell/log.toml`. Each `[sink.NAME]` section defines a sink.
|
||||
|
||||
```toml
|
||||
[sink.terminal]
|
||||
type = "console"
|
||||
format = "bare"
|
||||
channels = ["console"]
|
||||
|
||||
[sink.errors]
|
||||
type = "file"
|
||||
path = ".cell/logs/errors.jsonl"
|
||||
channels = ["error"]
|
||||
|
||||
[sink.everything]
|
||||
type = "file"
|
||||
path = ".cell/logs/all.jsonl"
|
||||
channels = ["*"]
|
||||
exclude = ["console"]
|
||||
```
|
||||
|
||||
### Sink fields
|
||||
|
||||
| Field | Values | Description |
|
||||
|-------|--------|-------------|
|
||||
| `type` | `"console"`, `"file"` | Where output goes |
|
||||
| `format` | `"clean"`, `"pretty"`, `"bare"`, `"json"` | How output is formatted |
|
||||
| `channels` | array of names, or `["*"]` | Which channels this sink receives. Quote `'*'` on the CLI to prevent shell glob expansion. |
|
||||
| `exclude` | array of names | Channels to skip (useful with `"*"`) |
|
||||
| `stack` | array of channel names | Channels that capture a stack trace |
|
||||
| `path` | file path | Output file (file sinks only) |
|
||||
|
||||
### Formats
|
||||
|
||||
**clean** — CLI-friendly. No actor ID or channel prefix. Error channel messages are prefixed with `error:`. This is the default format.
|
||||
|
||||
```
|
||||
server started on port 8080
|
||||
error: connection refused
|
||||
```
|
||||
|
||||
**pretty** — human-readable, one line per message. Includes actor ID, channel, source location, and message.
|
||||
|
||||
```
|
||||
[a3f12] [console] main.ce:5 server started
|
||||
```
|
||||
|
||||
**bare** — minimal. Actor ID and message only.
|
||||
|
||||
```
|
||||
[a3f12] server started
|
||||
```
|
||||
|
||||
**json** — structured JSONL (one JSON object per line). Used for file sinks and machine consumption.
|
||||
|
||||
```json
|
||||
{"actor_id":"a3f12...","timestamp":1702656000.5,"channel":"console","event":"server started","source":{"file":"main.ce","line":5,"col":3,"fn":"init"}}
|
||||
```
|
||||
|
||||
## Log Records
|
||||
|
||||
Every log call produces a record:
|
||||
|
||||
```javascript
|
||||
{
|
||||
actor_id: "a3f12...", // full actor GUID
|
||||
timestamp: 1702656000.5, // seconds since epoch
|
||||
channel: "console", // channel name
|
||||
event: "the message", // value passed to log
|
||||
source: {
|
||||
file: "main.ce",
|
||||
line: 5,
|
||||
col: 3,
|
||||
fn: "init"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
File sinks write one JSON-encoded record per line. Console sinks format the record according to their format setting.
|
||||
|
||||
## Stack Traces
|
||||
|
||||
The `error` channel captures stack traces by default. To enable stack traces for other channels, add a `stack` field to a sink — an array of channel names that should include a call stack.
|
||||
|
||||
Via the CLI:
|
||||
|
||||
```bash
|
||||
pit log add terminal console --channels=console,error,debug --stack=error,debug
|
||||
```
|
||||
|
||||
Or in `log.toml`:
|
||||
|
||||
```toml
|
||||
[sink.terminal]
|
||||
type = "console"
|
||||
format = "bare"
|
||||
channels = ["console", "error", "debug"]
|
||||
stack = ["error", "debug"]
|
||||
```
|
||||
|
||||
Only channels listed in `stack` get stack traces. Other channels on the same sink print without one:
|
||||
|
||||
```
|
||||
[a3f12] server started
|
||||
[a3f12] connection failed
|
||||
at handle_request (server.ce:42:3)
|
||||
at process (router.ce:18:5)
|
||||
at main (main.ce:5:1)
|
||||
```
|
||||
|
||||
With JSON format, a `stack` array is added to the record for channels that have stack capture enabled:
|
||||
|
||||
```json
|
||||
{"actor_id":"a3f12...","channel":"error","event":"connection failed","source":{"file":"server.ce","line":42,"col":3,"fn":"handle_request"},"stack":[{"fn":"handle_request","file":"server.ce","line":42,"col":3},{"fn":"process","file":"router.ce","line":18,"col":5},{"fn":"main","file":"main.ce","line":5,"col":1}]}
|
||||
```
|
||||
|
||||
Channels without `stack` configuration produce no stack field. Capturing stacks adds overhead — enable it for debugging, not production.
|
||||
|
||||
## CLI
|
||||
|
||||
The `pit log` command manages sinks and reads log files. See [CLI — pit log](/docs/cli/#pit-log) for the full reference.
|
||||
|
||||
```bash
|
||||
pit log list # show sinks
|
||||
pit log channels # list channels with enabled/disabled status
|
||||
pit log enable warn # enable a channel on the terminal sink
|
||||
pit log disable warn # disable a channel
|
||||
pit log add terminal console --format=clean --channels=console
|
||||
pit log add dump file .cell/logs/dump.jsonl '--channels=*' --exclude=console
|
||||
pit log add debug console --channels=error,debug --stack=error,debug
|
||||
pit log remove terminal
|
||||
pit log read dump --lines=20 --channel=error
|
||||
pit log tail dump
|
||||
```
|
||||
|
||||
### Channel toggling
|
||||
|
||||
The `enable` and `disable` commands modify the terminal sink's channel list without touching other sink configuration. This is the easiest way to opt in to extra output:
|
||||
|
||||
```bash
|
||||
pit log enable warn # see compiler warnings
|
||||
pit log enable build # see per-file compile/link output
|
||||
pit log disable warn # hide warnings again
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Development setup
|
||||
|
||||
Route console output to the terminal with minimal formatting. Send everything else to a structured log file for debugging.
|
||||
|
||||
```toml
|
||||
[sink.terminal]
|
||||
type = "console"
|
||||
format = "bare"
|
||||
channels = ["console"]
|
||||
|
||||
[sink.debug]
|
||||
type = "file"
|
||||
path = ".cell/logs/debug.jsonl"
|
||||
channels = ["*"]
|
||||
exclude = ["console"]
|
||||
```
|
||||
|
||||
```javascript
|
||||
log.console("listening on :8080") // -> terminal: [a3f12] listening on :8080
|
||||
log.error("bad request") // -> debug.jsonl only
|
||||
log.debug({latency: 0.042}) // -> debug.jsonl only
|
||||
```
|
||||
|
||||
### Separate error log
|
||||
|
||||
Keep a dedicated error log alongside a full dump.
|
||||
|
||||
```toml
|
||||
[sink.terminal]
|
||||
type = "console"
|
||||
format = "pretty"
|
||||
channels = ["console", "error", "system"]
|
||||
|
||||
[sink.errors]
|
||||
type = "file"
|
||||
path = ".cell/logs/errors.jsonl"
|
||||
channels = ["error"]
|
||||
|
||||
[sink.all]
|
||||
type = "file"
|
||||
path = ".cell/logs/all.jsonl"
|
||||
channels = ["*"]
|
||||
```
|
||||
|
||||
### JSON console
|
||||
|
||||
Output structured JSON to the console for piping into other tools.
|
||||
|
||||
```toml
|
||||
[sink.json_out]
|
||||
type = "console"
|
||||
format = "json"
|
||||
channels = ["console", "error"]
|
||||
```
|
||||
|
||||
```bash
|
||||
pit run myapp.ce | jq '.event'
|
||||
```
|
||||
|
||||
### Reading logs
|
||||
|
||||
```bash
|
||||
# Last 50 error entries
|
||||
pit log read errors --lines=50
|
||||
|
||||
# Errors since a timestamp
|
||||
pit log read errors --since=1702656000
|
||||
|
||||
# Filter a wildcard sink to one channel
|
||||
pit log read all --channel=debug --lines=10
|
||||
|
||||
# Follow a log file in real time
|
||||
pit log tail all
|
||||
```
|
||||
@@ -5,7 +5,7 @@ weight: 85
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
Nota is a binary message format developed for use in the Procession Protocol. It provides a compact, JSON-like encoding that supports blobs, text, arrays, records, numbers, and symbols.
|
||||
Nota is a binary message format developed for use in the Procession Protocol. It provides a compact, JSON-like encoding that supports blobs, text, arrays, records, numbers, and symbols. Nota is an internal module: `use('internal/nota')`.
|
||||
|
||||
Nota stands for Network Object Transfer Arrangement.
|
||||
|
||||
|
||||
@@ -9,20 +9,21 @@ Packages are the fundamental unit of code organization and sharing in ƿit.
|
||||
|
||||
## Package Structure
|
||||
|
||||
A package is a directory containing a `pit.toml` manifest:
|
||||
A package is a directory containing a `cell.toml` manifest:
|
||||
|
||||
```
|
||||
mypackage/
|
||||
├── pit.toml # package manifest
|
||||
├── cell.toml # package manifest
|
||||
├── main.ce # entry point (optional)
|
||||
├── utils.cm # module
|
||||
├── helper/
|
||||
│ └── math.cm # nested module
|
||||
├── render.c # C extension
|
||||
└── _internal.cm # private module (underscore prefix)
|
||||
└── internal/
|
||||
└── helpers.cm # private module (internal/ only)
|
||||
```
|
||||
|
||||
## pit.toml
|
||||
## cell.toml
|
||||
|
||||
The package manifest declares metadata and dependencies:
|
||||
|
||||
@@ -46,7 +47,7 @@ mylib = "/Users/john/work/mylib"
|
||||
When importing with `use()`, ƿit searches in order:
|
||||
|
||||
1. **Local package** — relative to package root
|
||||
2. **Dependencies** — via aliases in `pit.toml`
|
||||
2. **Dependencies** — via aliases in `cell.toml`
|
||||
3. **Core** — built-in ƿit modules
|
||||
|
||||
```javascript
|
||||
@@ -60,12 +61,12 @@ use('json') // core module
|
||||
|
||||
### Private Modules
|
||||
|
||||
Files starting with underscore are private:
|
||||
Files in the `internal/` directory are private to their package:
|
||||
|
||||
```javascript
|
||||
// _internal.cm is only accessible within the same package
|
||||
use('internal') // OK from same package
|
||||
use('myapp/internal') // Error from other packages
|
||||
// internal/helpers.cm is only accessible within the same package
|
||||
use('internal/helpers') // OK from same package
|
||||
use('myapp/internal/helpers') // Error from other packages
|
||||
```
|
||||
|
||||
## Package Locators
|
||||
@@ -90,10 +91,10 @@ Local packages are symlinked into the shop, making development seamless.
|
||||
|
||||
## The Shop
|
||||
|
||||
ƿit stores all packages in the **shop** at `~/.pit/`:
|
||||
ƿit stores all packages in the **shop** at `~/.cell/`:
|
||||
|
||||
```
|
||||
~/.pit/
|
||||
~/.cell/
|
||||
├── packages/
|
||||
│ ├── core -> gitea.pockle.world/john/cell
|
||||
│ ├── gitea.pockle.world/
|
||||
@@ -104,11 +105,8 @@ Local packages are symlinked into the shop, making development seamless.
|
||||
│ └── john/
|
||||
│ └── work/
|
||||
│ └── mylib -> /Users/john/work/mylib
|
||||
├── lib/
|
||||
│ ├── local.dylib
|
||||
│ └── gitea_pockle_world_john_prosperon.dylib
|
||||
├── build/
|
||||
│ └── <content-addressed cache>
|
||||
│ └── <content-addressed cache (bytecode, dylibs, manifests)>
|
||||
├── cache/
|
||||
│ └── <downloaded zips>
|
||||
├── lock.toml
|
||||
@@ -171,16 +169,16 @@ pit link delete gitea.pockle.world/john/prosperon
|
||||
|
||||
## C Extensions
|
||||
|
||||
C files in a package are compiled into a dynamic library:
|
||||
C files in a package are compiled into per-file dynamic libraries stored in the content-addressed build cache:
|
||||
|
||||
```
|
||||
mypackage/
|
||||
├── pit.toml
|
||||
├── render.c # compiled to mypackage.dylib
|
||||
└── render.cm # optional ƿit wrapper
|
||||
├── cell.toml
|
||||
├── render.c # compiled to ~/.cell/build/<hash>
|
||||
└── physics.c # compiled to ~/.cell/build/<hash>
|
||||
```
|
||||
|
||||
The library is named after the package and placed in `~/.pit/lib/`.
|
||||
Each `.c` file gets its own `.dylib` at a content-addressed path in `~/.cell/build/`. A per-package manifest maps module names to their dylib paths so the runtime can find them — see [Dylib Manifests](/docs/shop/#dylib-manifests). A `.c` file and `.cm` file with the same stem at the same scope is a build error — use distinct names.
|
||||
|
||||
See [Writing C Modules](/docs/c-modules/) for details.
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ The cancel function, when called, should abort the in-progress work.
|
||||
```javascript
|
||||
var fetch_data = function(callback, url) {
|
||||
$contact(function(connection) {
|
||||
$send(connection, {get: url}, function(response) {
|
||||
send(connection, {get: url}, function(response) {
|
||||
callback(response)
|
||||
})
|
||||
}, {host: url, port: 80})
|
||||
@@ -154,11 +154,11 @@ If the requestor does not complete within the time limit, it is cancelled and th
|
||||
|
||||
## Requestors and Actors
|
||||
|
||||
Requestors are particularly useful with actor messaging. Since `$send` is callback-based, it fits naturally:
|
||||
Requestors are particularly useful with actor messaging. Since `send` is callback-based, it fits naturally:
|
||||
|
||||
```javascript
|
||||
var ask_worker = function(callback, task) {
|
||||
$send(worker, task, function(reply) {
|
||||
send(worker, task, function(reply) {
|
||||
callback(reply)
|
||||
})
|
||||
}
|
||||
|
||||
343
docs/semantic-index.md
Normal file
343
docs/semantic-index.md
Normal file
@@ -0,0 +1,343 @@
|
||||
---
|
||||
title: "Semantic Index"
|
||||
description: "Index and query symbols, references, and call sites in source files"
|
||||
weight: 55
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
ƿit includes a semantic indexer that extracts symbols, references, call sites, and imports from source files. The index powers the LSP (find references, rename) and is available as a CLI tool for scripting and debugging.
|
||||
|
||||
## Overview
|
||||
|
||||
The indexer walks the parsed AST without modifying it. It produces a JSON structure that maps every declaration, every reference to that declaration, and every call site in a file.
|
||||
|
||||
```
|
||||
source → tokenize → parse → fold → index
|
||||
↓
|
||||
symbols, references,
|
||||
call sites, imports,
|
||||
exports, reverse refs
|
||||
```
|
||||
|
||||
Two CLI commands expose this:
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `pit index <file>` | Produce the full semantic index as JSON |
|
||||
| `pit explain` | Query the index for a specific symbol or position |
|
||||
|
||||
## pit index
|
||||
|
||||
Index a source file and print the result as JSON.
|
||||
|
||||
```bash
|
||||
pit index <file.ce|file.cm>
|
||||
pit index <file> -o output.json
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
The index contains these sections:
|
||||
|
||||
| Section | Description |
|
||||
|---------|-------------|
|
||||
| `imports` | All `use()` calls with local name, module path, resolved filesystem path, and span |
|
||||
| `symbols` | Every declaration: vars, defs, functions, params |
|
||||
| `references` | Every use of a name, classified as read, write, or call |
|
||||
| `call_sites` | Every function call with callee, args count, and enclosing function |
|
||||
| `exports` | For `.cm` modules, the keys of the top-level `return` record |
|
||||
| `reverse_refs` | Inverted index: name to list of reference spans |
|
||||
|
||||
### Example
|
||||
|
||||
Given a file `graph.ce` with functions `make_node`, `connect`, and `build_graph`:
|
||||
|
||||
```bash
|
||||
pit index graph.ce
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"path": "graph.ce",
|
||||
"is_actor": true,
|
||||
"imports": [
|
||||
{"local_name": "json", "module_path": "json", "resolved_path": ".cell/packages/core/json.cm", "span": {"from_row": 2, "from_col": 0, "to_row": 2, "to_col": 22}}
|
||||
],
|
||||
"symbols": [
|
||||
{
|
||||
"symbol_id": "graph.ce:make_node:fn",
|
||||
"name": "make_node",
|
||||
"kind": "fn",
|
||||
"params": ["name", "kind"],
|
||||
"doc_comment": "// A node in the graph.",
|
||||
"decl_span": {"from_row": 6, "from_col": 0, "to_row": 8, "to_col": 1},
|
||||
"scope_fn_nr": 0
|
||||
}
|
||||
],
|
||||
"references": [
|
||||
{"node_id": 20, "name": "make_node", "ref_kind": "call", "span": {"from_row": 17, "from_col": 13, "to_row": 17, "to_col": 22}}
|
||||
],
|
||||
"call_sites": [
|
||||
{"node_id": 20, "callee": "make_node", "args_count": 2, "span": {"from_row": 17, "from_col": 22, "to_row": 17, "to_col": 40}}
|
||||
],
|
||||
"exports": [],
|
||||
"reverse_refs": {
|
||||
"make_node": [
|
||||
{"node_id": 20, "ref_kind": "call", "span": {"from_row": 17, "from_col": 13, "to_row": 17, "to_col": 22}}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Symbol Kinds
|
||||
|
||||
| Kind | Description |
|
||||
|------|-------------|
|
||||
| `fn` | Function (var or def with function value) |
|
||||
| `var` | Mutable variable |
|
||||
| `def` | Constant |
|
||||
| `param` | Function parameter |
|
||||
|
||||
Each symbol has a `symbol_id` in the format `filename:name:kind` and a `decl_span` with `from_row`, `from_col`, `to_row`, `to_col` (0-based).
|
||||
|
||||
### Reference Kinds
|
||||
|
||||
| Kind | Description |
|
||||
|------|-------------|
|
||||
| `read` | Value is read |
|
||||
| `write` | Value is assigned |
|
||||
| `call` | Used as a function call target |
|
||||
|
||||
### Module Exports
|
||||
|
||||
For `.cm` files, the indexer detects the top-level `return` statement. If it returns a record literal, each key becomes an export linked to its symbol:
|
||||
|
||||
```javascript
|
||||
// math_utils.cm
|
||||
var add = function(a, b) { return a + b }
|
||||
var sub = function(a, b) { return a - b }
|
||||
return {add: add, sub: sub}
|
||||
```
|
||||
|
||||
```bash
|
||||
pit index math_utils.cm
|
||||
```
|
||||
|
||||
The `exports` section will contain:
|
||||
|
||||
```json
|
||||
[
|
||||
{"name": "add", "symbol_id": "math_utils.cm:add:fn"},
|
||||
{"name": "sub", "symbol_id": "math_utils.cm:sub:fn"}
|
||||
]
|
||||
```
|
||||
|
||||
## pit explain
|
||||
|
||||
Query the semantic index for a specific symbol or cursor position. This is the targeted query interface — instead of dumping the full index, it answers a specific question.
|
||||
|
||||
```bash
|
||||
pit explain --span <file>:<line>:<col>
|
||||
pit explain --symbol <name> <file>...
|
||||
```
|
||||
|
||||
### --span: What is at this position?
|
||||
|
||||
Point at a line and column (0-based) to find out what symbol or reference is there.
|
||||
|
||||
```bash
|
||||
pit explain --span demo.ce:6:4
|
||||
```
|
||||
|
||||
If the position lands on a declaration, that symbol is returned along with all its references and call sites. If it lands on a reference, the indexer traces back to the declaration and returns the same information.
|
||||
|
||||
The result includes:
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `symbol` | The resolved declaration (name, kind, params, doc comment, span) |
|
||||
| `reference` | The reference at the cursor, if the cursor was on a reference |
|
||||
| `references` | All references to this symbol across the file |
|
||||
| `call_sites` | All call sites for this symbol |
|
||||
| `imports` | The file's imports (for context) |
|
||||
|
||||
```json
|
||||
{
|
||||
"symbol": {
|
||||
"name": "build_graph",
|
||||
"symbol_id": "demo.ce:build_graph:fn",
|
||||
"kind": "fn",
|
||||
"params": [],
|
||||
"doc_comment": "// Build a sample graph and return it."
|
||||
},
|
||||
"references": [
|
||||
{"node_id": 71, "ref_kind": "call", "span": {"from_row": 39, "from_col": 12, "to_row": 39, "to_col": 23}}
|
||||
],
|
||||
"call_sites": []
|
||||
}
|
||||
```
|
||||
|
||||
### --symbol: Find a symbol by name
|
||||
|
||||
Look up a symbol by name. Pass one file for a focused result, or multiple files (including shell globs) to search across them all:
|
||||
|
||||
```bash
|
||||
pit explain --symbol connect demo.ce
|
||||
pit explain --symbol connect *.ce *.cm
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"symbols": [
|
||||
{
|
||||
"name": "connect",
|
||||
"symbol_id": "demo.ce:connect:fn",
|
||||
"kind": "fn",
|
||||
"params": ["from", "to", "label"],
|
||||
"doc_comment": "// Connect two nodes with a labeled edge."
|
||||
}
|
||||
],
|
||||
"references": [
|
||||
{"node_id": 29, "ref_kind": "call", "span": {"from_row": 21, "from_col": 2, "to_row": 21, "to_col": 9}},
|
||||
{"node_id": 33, "ref_kind": "call", "span": {"from_row": 22, "from_col": 2, "to_row": 22, "to_col": 9}},
|
||||
{"node_id": 37, "ref_kind": "call", "span": {"from_row": 23, "from_col": 2, "to_row": 23, "to_col": 9}}
|
||||
],
|
||||
"call_sites": [
|
||||
{"callee": "connect", "args_count": 3, "span": {"from_row": 21, "from_col": 9, "to_row": 21, "to_col": 29}},
|
||||
{"callee": "connect", "args_count": 3, "span": {"from_row": 22, "from_col": 9, "to_row": 22, "to_col": 31}},
|
||||
{"callee": "connect", "args_count": 3, "span": {"from_row": 23, "from_col": 9, "to_row": 23, "to_col": 29}}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
This tells you: `connect` is a function taking `(from, to, label)`, declared on line 11, and called 3 times inside `build_graph`.
|
||||
|
||||
## Programmatic Use
|
||||
|
||||
The index and explain modules can be used directly from ƿit scripts:
|
||||
|
||||
### Via shop (recommended)
|
||||
|
||||
```javascript
|
||||
var shop = use('internal/shop')
|
||||
var idx = shop.index_file(path)
|
||||
```
|
||||
|
||||
`shop.index_file` runs the full pipeline (tokenize, parse, index, resolve imports) and caches the result.
|
||||
|
||||
### index.cm (direct)
|
||||
|
||||
If you already have a parsed AST and tokens, use `index_ast` directly:
|
||||
|
||||
```javascript
|
||||
var index_mod = use('index')
|
||||
var idx = index_mod.index_ast(ast, tokens, filename)
|
||||
```
|
||||
|
||||
### explain.cm
|
||||
|
||||
```javascript
|
||||
var explain_mod = use('explain')
|
||||
var expl = explain_mod.make(idx)
|
||||
|
||||
// What is at line 10, column 5?
|
||||
var result = expl.at_span(10, 5)
|
||||
|
||||
// Find all symbols named "connect"
|
||||
var result = expl.by_symbol("connect")
|
||||
|
||||
// Get callers and callees of a symbol
|
||||
var chain = expl.call_chain("demo.ce:connect:fn", 2)
|
||||
```
|
||||
|
||||
For cross-file queries:
|
||||
|
||||
```javascript
|
||||
var result = explain_mod.explain_across([idx1, idx2, idx3], "connect")
|
||||
```
|
||||
|
||||
## LSP Integration
|
||||
|
||||
The semantic index powers these LSP features:
|
||||
|
||||
| Feature | LSP Method | Description |
|
||||
|---------|------------|-------------|
|
||||
| Find References | `textDocument/references` | All references to the symbol under the cursor |
|
||||
| Rename | `textDocument/rename` | Rename a symbol and all its references |
|
||||
| Prepare Rename | `textDocument/prepareRename` | Validate that the cursor is on a renameable symbol |
|
||||
| Go to Definition | `textDocument/definition` | Jump to a symbol's declaration (index-backed with AST fallback) |
|
||||
|
||||
These work automatically in any editor with ƿit LSP support. The index is rebuilt on every file change.
|
||||
|
||||
## LLM / AI Assistance
|
||||
|
||||
The semantic index is designed to give LLMs the context they need to read and edit ƿit code accurately. ƿit is not in any training set, so an LLM cannot rely on memorized patterns — it needs structured information about names, scopes, and call relationships. The commands below are the recommended way to provide that.
|
||||
|
||||
### Understand a file before editing
|
||||
|
||||
Before modifying a file, index it to see its structure:
|
||||
|
||||
```bash
|
||||
pit index file.ce
|
||||
```
|
||||
|
||||
This gives the LLM every declaration, every reference, every call site, and the import list with resolved paths. Key things to extract:
|
||||
|
||||
- **`symbols`** — what functions exist, their parameters, and their doc comments. This is enough to understand the file's API without reading every line.
|
||||
- **`imports`** with `resolved_path` — which modules are used, and where they live on disk. The LLM can follow these paths to read dependency source when it needs to understand a called function. Imports without a `resolved_path` are C built-ins (like `json`) with no script source to read.
|
||||
- **`exports`** — for `.cm` modules, what the public API is. This tells the LLM what names other files can access.
|
||||
|
||||
### Investigate a specific symbol
|
||||
|
||||
When the LLM needs to rename, refactor, or understand a specific function:
|
||||
|
||||
```bash
|
||||
pit explain --symbol update analysis.cm
|
||||
```
|
||||
|
||||
This returns the declaration (with doc comment and parameter list), every reference, and every call site. The LLM can use this to:
|
||||
|
||||
- **Rename safely** — the references list has exact spans for every use of the name.
|
||||
- **Understand callers** — `call_sites` shows where and how the function is called, including argument counts.
|
||||
- **Read the doc comment** — often enough to understand intent without reading the function body.
|
||||
|
||||
### Investigate a cursor position
|
||||
|
||||
When the LLM is looking at a specific line and column (e.g., from an error message or a user selection):
|
||||
|
||||
```bash
|
||||
pit explain --span file.ce:17:4
|
||||
```
|
||||
|
||||
This resolves whatever is at that position — declaration or reference — back to the underlying symbol, then returns all references and call sites. Useful for "what is this name?" queries.
|
||||
|
||||
### Search across files
|
||||
|
||||
To find a symbol across multiple files, pass them all:
|
||||
|
||||
```bash
|
||||
pit explain --symbol connect *.ce *.cm
|
||||
pit explain --symbol send server.ce client.ce protocol.cm
|
||||
```
|
||||
|
||||
This indexes each file and searches across all of them. The result merges all matching declarations, references, and call sites. Use this when the LLM needs to understand cross-file usage before making a change that touches multiple files.
|
||||
|
||||
### Import resolution
|
||||
|
||||
Every import in the index includes the original `module_path` (the string passed to `use()`). For script modules, it also includes `resolved_path` — the filesystem path the module resolves to. This lets the LLM follow dependency chains:
|
||||
|
||||
```json
|
||||
{"local_name": "fd", "module_path": "fd", "resolved_path": ".cell/packages/core/fd.cm"}
|
||||
{"local_name": "json", "module_path": "json"}
|
||||
```
|
||||
|
||||
An import without `resolved_path` is a C built-in — no script source to read.
|
||||
|
||||
### Recommended workflow
|
||||
|
||||
1. **Start with `pit index`** on the file to edit. Scan imports and symbols for an overview.
|
||||
2. **Use `pit explain --symbol`** to drill into any function the LLM needs to understand or modify. The doc comment and parameter list are usually sufficient.
|
||||
3. **Follow `resolved_path`** on imports when the LLM needs to understand a dependency — index or read the resolved file.
|
||||
4. **Before renaming**, use `pit explain --symbol` (or `--span`) to get all reference spans, then apply edits to each span.
|
||||
5. **For cross-file changes**, pass all affected files to `pit explain --symbol` to see the full picture before editing.
|
||||
233
docs/shop.md
Normal file
233
docs/shop.md
Normal file
@@ -0,0 +1,233 @@
|
||||
---
|
||||
title: "Shop Architecture"
|
||||
description: "How the shop resolves, compiles, caches, and loads modules"
|
||||
weight: 35
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
The shop is the module resolution and loading engine behind `use()`. It handles finding modules, compiling them, caching the results, and loading C extensions. The shop lives in `internal/shop.cm`.
|
||||
|
||||
## Startup Pipeline
|
||||
|
||||
When `pit` runs a program, startup takes one of two paths:
|
||||
|
||||
### Fast path (warm cache)
|
||||
|
||||
```
|
||||
C runtime → engine.cm (from cache) → shop.cm → user program
|
||||
```
|
||||
|
||||
The C runtime hashes the source of `internal/engine.cm` with BLAKE2 and looks up the hash in the content-addressed cache (`~/.cell/build/<hash>`). On a cache hit, engine.cm loads directly — no bootstrap involved.
|
||||
|
||||
### Cold path (first run or cache cleared)
|
||||
|
||||
```
|
||||
C runtime → bootstrap.cm → (seeds cache) → engine.cm (from cache) → shop.cm → user program
|
||||
```
|
||||
|
||||
On a cache miss, the C runtime loads `boot/bootstrap.cm.mcode` (a pre-compiled seed). Bootstrap compiles engine.cm and the pipeline modules (tokenize, parse, fold, mcode, streamline) from source and caches the results. The C runtime then retries the engine cache lookup, which now succeeds.
|
||||
|
||||
### Engine
|
||||
|
||||
**engine.cm** is self-sufficient. It loads its own compilation pipeline from the content-addressed cache, with fallback to the pre-compiled seeds in `boot/`. It defines `analyze()` (source to AST), `compile_to_blob()` (AST to binary blob), and `use_core()` for loading core modules. It creates the actor runtime and loads shop.cm via `use_core('internal/shop')`.
|
||||
|
||||
### Shop
|
||||
|
||||
**shop.cm** receives its dependencies through the module environment — `analyze`, `run_ast_fn`, `use_cache`, `shop_path`, `runtime_env`, `content_hash`, `cache_path`, and others. It defines `Shop.use()`, which is the function behind every `use()` call in user code.
|
||||
|
||||
### Cache invalidation
|
||||
|
||||
All caching is content-addressed by BLAKE2 hash of the source. When any source file changes, its hash changes and the old cache entry is simply never looked up again. No manual invalidation is needed. To force a full rebuild, delete `~/.cell/build/`.
|
||||
|
||||
## Module Resolution
|
||||
|
||||
When `use('path')` is called from a package context, the shop resolves the module through a multi-layer search. Both the `.cm` script file and C symbol are resolved independently, and the one with the narrowest scope wins.
|
||||
|
||||
### Resolution Order
|
||||
|
||||
For a call like `use('sprite')` from package `myapp`:
|
||||
|
||||
1. **Own package** — `~/.cell/packages/myapp/sprite.cm` and C symbol `js_myapp_sprite_use`
|
||||
2. **Aliased dependencies** — if `myapp/cell.toml` has `renderer = "gitea.pockle.world/john/renderer"`, checks `renderer/sprite.cm` and its C symbols
|
||||
3. **Core** — built-in core modules and internal C symbols
|
||||
|
||||
For calls without a package context (from core modules), only core is searched.
|
||||
|
||||
### Private Modules
|
||||
|
||||
Paths starting with `internal/` are private to their package:
|
||||
|
||||
```javascript
|
||||
use('internal/helpers') // OK from within the same package
|
||||
// Cannot be accessed from other packages
|
||||
```
|
||||
|
||||
### Explicit Package Imports
|
||||
|
||||
Paths containing a dot in the first component are treated as explicit package references:
|
||||
|
||||
```javascript
|
||||
use('gitea.pockle.world/john/renderer/sprite')
|
||||
// Resolves directly to the renderer package's sprite.cm
|
||||
```
|
||||
|
||||
## Compilation and Caching
|
||||
|
||||
Every module goes through a content-addressed caching pipeline. Cache keys are based on the inputs that affect the output artifact, so changing any relevant input automatically invalidates the cache.
|
||||
|
||||
### Cache Hierarchy
|
||||
|
||||
When loading a module, the shop checks (in order):
|
||||
|
||||
1. **In-memory cache** — `use_cache[key]`, checked first on every `use()` call
|
||||
2. **Build-cache dylib** — content-addressed `.dylib` in `~/.cell/build/<hash>`, found via manifest (see [Dylib Manifests](#dylib-manifests))
|
||||
3. **Cached bytecode** — content-addressed in `~/.cell/build/<hash>` (no extension)
|
||||
4. **Internal symbols** — statically linked into the `cell` binary (fat builds)
|
||||
5. **Source compilation** — full pipeline: analyze, mcode, streamline, serialize
|
||||
|
||||
Dylib resolution wins over internal symbols, so a built dylib can hot-patch a fat binary. Results from compilation are cached back to the content-addressed store for future loads.
|
||||
|
||||
Each loading method (except the in-memory cache) can be individually enabled or disabled via `shop.toml` policy flags — see [Shop Configuration](#shop-configuration) below.
|
||||
|
||||
### Content-Addressed Store
|
||||
|
||||
The build cache at `~/.cell/build/` stores all compiled artifacts named by the BLAKE2 hash of their inputs:
|
||||
|
||||
```
|
||||
~/.cell/build/
|
||||
├── a1b2c3d4... # cached bytecode blob, object file, dylib, or manifest (no extension)
|
||||
└── ...
|
||||
```
|
||||
|
||||
Every artifact type uses a unique salt appended to the content before hashing, so collisions between different artifact types are impossible:
|
||||
|
||||
| Salt | Artifact |
|
||||
|------|----------|
|
||||
| `obj` | compiled C object file |
|
||||
| `dylib` | linked dynamic library |
|
||||
| `native` | native-compiled .cm dylib |
|
||||
| `mach` | mach bytecode blob |
|
||||
| `mcode` | mcode IR (JSON) |
|
||||
| `deps` | cached `cc -MM` dependency list |
|
||||
| `fail` | cached compilation failure marker |
|
||||
| `manifest` | package dylib manifest (JSON) |
|
||||
|
||||
This scheme provides automatic cache invalidation: when source changes, its hash changes, and the old cache entry is simply never looked up again.
|
||||
|
||||
### Failure Caching
|
||||
|
||||
When a C file fails to compile (missing SDK headers, syntax errors, etc.), the build system writes a failure marker to the cache using the `fail` salt. On subsequent builds, the failure marker is found and the file is skipped immediately — no time wasted retrying files that can't compile. The failure marker is keyed on the same content as the compilation (command string + source content), so if the source changes or compiler flags change, the failure is automatically invalidated and compilation is retried.
|
||||
|
||||
### Dylib Manifests
|
||||
|
||||
Dylibs live at content-addressed paths (`~/.cell/build/<hash>`) that can only be computed by running the full build pipeline. To allow the runtime to find pre-built dylibs without invoking the build module, `cell build` writes a **manifest** for each package. The manifest is a JSON file mapping each C module to its `{file, symbol, dylib}` entry. The manifest path is itself content-addressed (BLAKE2 hash of the package name + `manifest` salt), so the runtime can compute it from the package name alone.
|
||||
|
||||
At runtime, when `use()` needs a C module from another package, the shop reads the manifest to find the dylib path. This means `cell build` must be run before C modules from packages can be loaded.
|
||||
|
||||
For native `.cm` dylibs, the cache content includes source, target, native mode marker, and sanitize flags, then uses the `native` salt. Changing any of those inputs produces a new cache path automatically.
|
||||
|
||||
### Core Module Caching
|
||||
|
||||
Core modules loaded via `use_core()` in engine.cm follow the same content-addressed pattern. On first use, a module is compiled from source and cached by the BLAKE2 hash of its source content. Subsequent loads with unchanged source hit the cache directly.
|
||||
|
||||
User scripts (`.ce` files) are also cached. The first run compiles and caches; subsequent runs with unchanged source load from cache.
|
||||
|
||||
## C Extension Resolution
|
||||
|
||||
C extensions are resolved alongside script modules. A C module is identified by a symbol name derived from the package and file name:
|
||||
|
||||
```
|
||||
package: gitea.pockle.world/john/prosperon
|
||||
file: sprite.c
|
||||
symbol: js_gitea_pockle_world_john_prosperon_sprite_use
|
||||
```
|
||||
|
||||
### C Resolution Sources
|
||||
|
||||
1. **Build-cache dylibs** — content-addressed dylibs in `~/.cell/build/<hash>`, found via per-package manifests written by `cell build`
|
||||
2. **Internal symbols** — statically linked into the `cell` binary (fat builds)
|
||||
|
||||
Dylibs are checked first at each resolution scope, so a built dylib always wins over a statically linked symbol. This enables hot-patching fat binaries.
|
||||
|
||||
### Name Collisions
|
||||
|
||||
Having both a `.cm` script and a `.c` file with the same stem at the same scope is a **build error**. For example, `render.cm` and `render.c` in the same directory will fail. Use distinct names — e.g., `render.c` for the C implementation and `render_utils.cm` for the script wrapper.
|
||||
|
||||
## Environment Injection
|
||||
|
||||
When a module is loaded, the shop builds an `env` object that becomes the module's set of free variables. This includes:
|
||||
|
||||
- **Runtime functions** — `logical`, `some`, `every`, `starts_with`, `ends_with`, `is_actor`, `log`, `send`, `fallback`, `parallel`, `race`, `sequence`
|
||||
- **Capability injections** — actor intrinsics like `$self`, `$delay`, `$start`, `$receiver`, `$fd`, etc.
|
||||
- **`use` function** — scoped to the module's package context
|
||||
|
||||
The set of injected capabilities is controlled by `script_inject_for()`, which can be tuned per package or file.
|
||||
|
||||
## Shop Configuration
|
||||
|
||||
The shop reads an optional `shop.toml` file from the shop root (`~/.cell/shop.toml`). This file controls which loading methods are permitted through policy flags.
|
||||
|
||||
### Policy Flags
|
||||
|
||||
All flags default to `true`. Set a flag to `false` to disable that loading method.
|
||||
|
||||
```toml
|
||||
[policy]
|
||||
allow_dylib = true # per-file .dylib loading (requires dlopen)
|
||||
allow_static = true # statically linked C symbols (fat builds)
|
||||
allow_mach = true # pre-compiled .mach bytecode (lib/ and build cache)
|
||||
allow_compile = true # on-the-fly source compilation
|
||||
```
|
||||
|
||||
### Example Configurations
|
||||
|
||||
**Production lockdown** — only use pre-compiled artifacts, never compile from source:
|
||||
|
||||
```toml
|
||||
[policy]
|
||||
allow_compile = false
|
||||
```
|
||||
|
||||
**Pure-script mode** — bytecode only, no native code:
|
||||
|
||||
```toml
|
||||
[policy]
|
||||
allow_dylib = false
|
||||
allow_static = false
|
||||
```
|
||||
|
||||
**No dlopen platforms** — static linking and bytecode only:
|
||||
|
||||
```toml
|
||||
[policy]
|
||||
allow_dylib = false
|
||||
```
|
||||
|
||||
If `shop.toml` is missing or has no `[policy]` section, all methods are enabled (default behavior).
|
||||
|
||||
## Shop Directory Layout
|
||||
|
||||
```
|
||||
~/.cell/
|
||||
├── packages/ # installed packages (directories and symlinks)
|
||||
│ └── core -> ... # symlink to the ƿit core
|
||||
├── build/ # content-addressed cache (safe to delete anytime)
|
||||
│ ├── <hash> # cached bytecode, object file, dylib, or manifest
|
||||
│ └── ...
|
||||
├── cache/ # downloaded package zip archives
|
||||
├── lock.toml # installed package versions and commit hashes
|
||||
├── link.toml # local development link overrides
|
||||
└── shop.toml # optional shop configuration and policy flags
|
||||
```
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `internal/bootstrap.cm` | Minimal cache seeder (cold start only) |
|
||||
| `internal/engine.cm` | Self-sufficient entry point: compilation pipeline, actor runtime, `use_core()` |
|
||||
| `internal/shop.cm` | Module resolution, compilation, caching, C extension loading |
|
||||
| `internal/os.c` | OS intrinsics: dylib ops, internal symbol lookup, embedded modules |
|
||||
| `package.cm` | Package directory detection, alias resolution, file listing |
|
||||
| `link.cm` | Development link management (link.toml read/write) |
|
||||
| `boot/*.cm.mcode` | Pre-compiled pipeline seeds (tokenize, parse, fold, mcode, streamline, bootstrap) |
|
||||
3
docs/spec/.pages
Normal file
3
docs/spec/.pages
Normal file
@@ -0,0 +1,3 @@
|
||||
nav:
|
||||
- pipeline.md
|
||||
- mcode.md
|
||||
@@ -241,7 +241,7 @@ source/
|
||||
|
||||
**`cell_runtime.c`** is the single file that defines the native code contract. It should:
|
||||
|
||||
1. Include `quickjs-internal.h` for access to value representation and heap types
|
||||
1. Include `pit_internal.h` for access to value representation and heap types
|
||||
2. Export all `cell_rt_*` functions with C linkage (no `static`)
|
||||
3. Keep each function thin — delegate to existing `JS_*` functions where possible
|
||||
4. Handle GC safety: after any allocation (frame, string, array), callers' frames may have moved
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
---
|
||||
title: "Register VM"
|
||||
description: "Register-based virtual machine (Mach)"
|
||||
description: "Binary encoding of the Mach bytecode interpreter"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Mach VM is a register-based virtual machine using 32-bit instructions. It is modeled after Lua's register VM — operands are register indices rather than stack positions, reducing instruction count and improving performance.
|
||||
The Mach VM is a register-based virtual machine that directly interprets the [Mcode IR](mcode.md) instruction set as compact 32-bit binary bytecode. It is modeled after Lua's register VM — operands are register indices rather than stack positions, reducing instruction count and improving performance.
|
||||
|
||||
The Mach serializer (`mach.c`) converts streamlined mcode JSON into binary instructions. Since the Mach bytecode is a direct encoding of the mcode, the [Mcode IR](mcode.md) reference is the authoritative instruction set documentation.
|
||||
|
||||
## Instruction Formats
|
||||
|
||||
@@ -45,95 +47,12 @@ Used for unconditional jumps with a 24-bit signed offset.
|
||||
|
||||
## Registers
|
||||
|
||||
Each function frame has a fixed number of register slots, determined at compile time. Registers hold:
|
||||
Each function frame has a fixed number of register slots, determined at compile time:
|
||||
|
||||
- **R(0)** — `this` binding
|
||||
- **R(1)..R(arity)** — function arguments
|
||||
- **R(arity+1)..** — local variables and temporaries
|
||||
|
||||
## Instruction Set
|
||||
|
||||
### Loading
|
||||
|
||||
| Opcode | Format | Description |
|
||||
|--------|--------|-------------|
|
||||
| `LOADK` | iABx | `R(A) = K(Bx)` — load from constant pool |
|
||||
| `LOADI` | iAsBx | `R(A) = sBx` — load small integer |
|
||||
| `LOADNULL` | iA | `R(A) = null` |
|
||||
| `LOADTRUE` | iA | `R(A) = true` |
|
||||
| `LOADFALSE` | iA | `R(A) = false` |
|
||||
| `MOVE` | iABC | `R(A) = R(B)` — register copy |
|
||||
|
||||
### Arithmetic
|
||||
|
||||
| Opcode | Format | Description |
|
||||
|--------|--------|-------------|
|
||||
| `ADD` | iABC | `R(A) = R(B) + R(C)` |
|
||||
| `SUB` | iABC | `R(A) = R(B) - R(C)` |
|
||||
| `MUL` | iABC | `R(A) = R(B) * R(C)` |
|
||||
| `DIV` | iABC | `R(A) = R(B) / R(C)` |
|
||||
| `MOD` | iABC | `R(A) = R(B) % R(C)` |
|
||||
| `POW` | iABC | `R(A) = R(B) ^ R(C)` |
|
||||
| `NEG` | iABC | `R(A) = -R(B)` |
|
||||
| `INC` | iABC | `R(A) = R(B) + 1` |
|
||||
| `DEC` | iABC | `R(A) = R(B) - 1` |
|
||||
|
||||
### Comparison
|
||||
|
||||
| Opcode | Format | Description |
|
||||
|--------|--------|-------------|
|
||||
| `EQ` | iABC | `R(A) = R(B) == R(C)` |
|
||||
| `NEQ` | iABC | `R(A) = R(B) != R(C)` |
|
||||
| `LT` | iABC | `R(A) = R(B) < R(C)` |
|
||||
| `LE` | iABC | `R(A) = R(B) <= R(C)` |
|
||||
| `GT` | iABC | `R(A) = R(B) > R(C)` |
|
||||
| `GE` | iABC | `R(A) = R(B) >= R(C)` |
|
||||
|
||||
### Property Access
|
||||
|
||||
| Opcode | Format | Description |
|
||||
|--------|--------|-------------|
|
||||
| `GETFIELD` | iABC | `R(A) = R(B)[K(C)]` — named property |
|
||||
| `SETFIELD` | iABC | `R(A)[K(B)] = R(C)` — set named property |
|
||||
| `GETINDEX` | iABC | `R(A) = R(B)[R(C)]` — computed property |
|
||||
| `SETINDEX` | iABC | `R(A)[R(B)] = R(C)` — set computed property |
|
||||
|
||||
### Variable Resolution
|
||||
|
||||
| Opcode | Format | Description |
|
||||
|--------|--------|-------------|
|
||||
| `GETNAME` | iABx | Unresolved variable (compiler placeholder) |
|
||||
| `GETINTRINSIC` | iABx | Global intrinsic / built-in |
|
||||
| `GETENV` | iABx | Module environment variable |
|
||||
| `GETUP` | iABC | `R(A) = UpFrame(B).slots[C]` — closure upvalue |
|
||||
| `SETUP` | iABC | `UpFrame(A).slots[B] = R(C)` — set closure upvalue |
|
||||
|
||||
### Control Flow
|
||||
|
||||
| Opcode | Format | Description |
|
||||
|--------|--------|-------------|
|
||||
| `JMP` | isJ | Unconditional jump |
|
||||
| `JMPTRUE` | iAsBx | Jump if `R(A)` is true |
|
||||
| `JMPFALSE` | iAsBx | Jump if `R(A)` is false |
|
||||
| `JMPNULL` | iAsBx | Jump if `R(A)` is null |
|
||||
|
||||
### Function Calls
|
||||
|
||||
| Opcode | Format | Description |
|
||||
|--------|--------|-------------|
|
||||
| `CALL` | iABC | Call `R(A)` with `B` args starting at `R(A+1)`, `C`=keep result |
|
||||
| `RETURN` | iA | Return `R(A)` |
|
||||
| `RETNIL` | — | Return null |
|
||||
| `CLOSURE` | iABx | Create closure from function pool entry `Bx` |
|
||||
|
||||
### Object / Array
|
||||
|
||||
| Opcode | Format | Description |
|
||||
|--------|--------|-------------|
|
||||
| `NEWOBJECT` | iA | `R(A) = {}` |
|
||||
| `NEWARRAY` | iABC | `R(A) = array(B)` |
|
||||
| `PUSH` | iABC | Push `R(B)` to array `R(A)` |
|
||||
|
||||
## JSCodeRegister
|
||||
|
||||
The compiled output for a function:
|
||||
@@ -149,8 +68,38 @@ struct JSCodeRegister {
|
||||
uint32_t func_count; // nested function count
|
||||
JSCodeRegister **functions; // nested function table
|
||||
JSValue name; // function name
|
||||
uint16_t disruption_pc; // exception handler offset
|
||||
uint16_t disruption_pc; // disruption handler offset
|
||||
};
|
||||
```
|
||||
|
||||
The constant pool holds all non-immediate values referenced by `LOADK` instructions: strings, large numbers, and other constants.
|
||||
|
||||
### Constant Pool Index Overflow
|
||||
|
||||
Named property instructions (`LOAD_FIELD`, `STORE_FIELD`, `DELETE`) use the iABC format where the constant pool key index occupies an 8-bit field (max 255). When a function references more than 256 unique property names, the serializer automatically falls back to a two-instruction sequence:
|
||||
|
||||
1. `LOADK tmp, key_index` — load the key string into a temporary register (iABx, 16-bit index)
|
||||
2. `LOAD_DYNAMIC` / `STORE_DYNAMIC` / `DELETEINDEX` — use the register-based variant
|
||||
|
||||
This is transparent to the mcode compiler and streamline optimizer.
|
||||
|
||||
## Arithmetic Dispatch
|
||||
|
||||
Arithmetic ops (ADD, SUB, MUL, DIV, MOD, POW) are executed inline without calling the polymorphic `reg_vm_binop()` helper. Since mcode's type guard dispatch guarantees both operands are numbers:
|
||||
|
||||
1. **Int-int fast path**: `JS_VALUE_IS_BOTH_INT` → native integer arithmetic with int32 overflow check. Overflow promotes to float64.
|
||||
2. **Float fallback**: `JS_ToFloat64` → native floating-point operation. Non-finite results produce null.
|
||||
|
||||
DIV and MOD check for zero divisor (→ null). POW uses `pow()` with non-finite handling for finite inputs.
|
||||
|
||||
Comparison ops (EQ through GE) and bitwise ops still use `reg_vm_binop()` for their slow paths, as they handle a wider range of type combinations (string comparisons, null equality, etc.).
|
||||
|
||||
## String Concatenation
|
||||
|
||||
CONCAT has a three-tier dispatch for self-assign patterns (`concat R(A), R(A), R(C)` where dest equals the left operand):
|
||||
|
||||
1. **In-place append**: If `R(A)` is a mutable heap text (S bit clear) with `length + rhs_length <= cap56`, characters are appended directly. Zero allocation, zero GC.
|
||||
2. **Growth allocation** (`JS_ConcatStringGrow`): Allocates a new text with 2x capacity and does **not** stone the result, leaving it mutable for subsequent appends.
|
||||
3. **Exact-fit stoned** (`JS_ConcatString`): Used when dest differs from the left operand (normal non-self-assign concat).
|
||||
|
||||
The `stone_text` instruction (iABC, B=0, C=0) sets the S bit on a mutable heap text in `R(A)`. For non-pointer values or already-stoned text, it is a no-op. This instruction is emitted by the streamline optimizer at escape points; see [Streamline — insert_stone_text](streamline.md#7-insert_stone_text-mutable-text-escape-analysis) and [Stone Memory — Mutable Text](stone.md#mutable-text-concatenation).
|
||||
|
||||
@@ -1,28 +1,345 @@
|
||||
---
|
||||
title: "Mcode IR"
|
||||
description: "JSON-based intermediate representation"
|
||||
description: "Instruction set reference for the JSON-based intermediate representation"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Mcode is a JSON-based intermediate representation that can be interpreted directly. It represents the same operations as the Mach register VM but uses string-based instruction dispatch rather than binary opcodes. Mcode is intended as an intermediate step toward native code compilation.
|
||||
|
||||
## Pipeline
|
||||
Mcode is the intermediate representation at the center of the ƿit compilation pipeline. All source code is lowered to mcode before execution or native compilation. The mcode instruction set is the **authoritative reference** for the operations supported by the ƿit runtime — the Mach VM bytecode is a direct binary encoding of these same instructions.
|
||||
|
||||
```
|
||||
Source → Tokenize → Parse (AST) → Fold → Mcode (JSON) → Streamline → Interpret
|
||||
→ QBE → Native
|
||||
Source → Tokenize → Parse → Fold → Mcode → Streamline → Machine
|
||||
```
|
||||
|
||||
Mcode is produced by `mcode.cm`, which lowers the folded AST to JSON instruction arrays. The streamline optimizer (`streamline.cm`) then eliminates redundant operations. The result can be interpreted by `mcode.c`, or lowered to QBE IL by `qbe_emit.cm` for native compilation. See [Compilation Pipeline](pipeline.md) for the full overview.
|
||||
Mcode is produced by `mcode.cm`, optimized by `streamline.cm`, then either serialized to 32-bit bytecode for the Mach VM (`mach.c`), or lowered to QBE/LLVM IL for native compilation (`qbe_emit.cm`). See [Compilation Pipeline](pipeline.md) for the full overview.
|
||||
|
||||
## JSMCode Structure
|
||||
## Module Structure
|
||||
|
||||
An `.mcode` file is a JSON object representing a compiled module:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | string | Module name (typically the source filename) |
|
||||
| `filename` | string | Source filename |
|
||||
| `data` | object | Constant pool — string and number literals used by instructions |
|
||||
| `main` | function | The top-level function (module body) |
|
||||
| `functions` | array | Nested function definitions (referenced by `function dest, id`) |
|
||||
|
||||
### Function Record
|
||||
|
||||
Each function (both `main` and entries in `functions`) has:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | string | Function name (`"<anonymous>"` for lambdas) |
|
||||
| `filename` | string | Source filename |
|
||||
| `nr_args` | integer | Number of parameters |
|
||||
| `nr_slots` | integer | Total register slots needed (args + locals + temporaries) |
|
||||
| `nr_close_slots` | integer | Number of closure slots captured from parent scope |
|
||||
| `disruption_pc` | integer | Instruction index of the disruption handler (0 if none) |
|
||||
| `instructions` | array | Instruction arrays and label strings |
|
||||
|
||||
Slot 0 is reserved. Slots 1 through `nr_args` hold parameters. Remaining slots up to `nr_slots - 1` are locals and temporaries.
|
||||
|
||||
## Instruction Format
|
||||
|
||||
Each instruction is a JSON array. The first element is the instruction name (string), followed by operands. The last two elements are line and column numbers for source mapping:
|
||||
|
||||
```json
|
||||
["add_int", dest, a, b, line, col]
|
||||
["load_field", dest, obj, "key", line, col]
|
||||
["jump", "label_name"]
|
||||
```
|
||||
|
||||
Operands are register slot numbers (integers), constant values (strings, numbers), or label names (strings).
|
||||
|
||||
## Instruction Reference
|
||||
|
||||
### Loading and Constants
|
||||
|
||||
| Instruction | Operands | Description |
|
||||
|-------------|----------|-------------|
|
||||
| `access` | `dest, name` | Load variable by name (intrinsic or environment) |
|
||||
| `int` | `dest, value` | Load integer constant |
|
||||
| `true` | `dest` | Load boolean `true` |
|
||||
| `false` | `dest` | Load boolean `false` |
|
||||
| `null` | `dest` | Load `null` |
|
||||
| `move` | `dest, src` | Copy register value |
|
||||
| `function` | `dest, id` | Load nested function by index |
|
||||
| `regexp` | `dest, pattern` | Create regexp object |
|
||||
|
||||
### Arithmetic — Integer
|
||||
|
||||
| Instruction | Operands | Description |
|
||||
|-------------|----------|-------------|
|
||||
| `add_int` | `dest, a, b` | `dest = a + b` (integer) |
|
||||
| `sub_int` | `dest, a, b` | `dest = a - b` (integer) |
|
||||
| `mul_int` | `dest, a, b` | `dest = a * b` (integer) |
|
||||
| `div_int` | `dest, a, b` | `dest = a / b` (integer) |
|
||||
| `mod_int` | `dest, a, b` | `dest = a % b` (integer) |
|
||||
| `neg_int` | `dest, src` | `dest = -src` (integer) |
|
||||
|
||||
### Arithmetic — Float
|
||||
|
||||
| Instruction | Operands | Description |
|
||||
|-------------|----------|-------------|
|
||||
| `add_float` | `dest, a, b` | `dest = a + b` (float) |
|
||||
| `sub_float` | `dest, a, b` | `dest = a - b` (float) |
|
||||
| `mul_float` | `dest, a, b` | `dest = a * b` (float) |
|
||||
| `div_float` | `dest, a, b` | `dest = a / b` (float) |
|
||||
| `mod_float` | `dest, a, b` | `dest = a % b` (float) |
|
||||
| `neg_float` | `dest, src` | `dest = -src` (float) |
|
||||
|
||||
### Arithmetic — Generic
|
||||
|
||||
| Instruction | Operands | Description |
|
||||
|-------------|----------|-------------|
|
||||
| `pow` | `dest, a, b` | `dest = a ^ b` (exponentiation) |
|
||||
|
||||
### Text
|
||||
|
||||
| Instruction | Operands | Description |
|
||||
|-------------|----------|-------------|
|
||||
| `concat` | `dest, a, b` | `dest = a ~ b` (text concatenation) |
|
||||
| `stone_text` | `slot` | Stone a mutable text value (see below) |
|
||||
|
||||
The `stone_text` instruction is emitted by the streamline optimizer's escape analysis pass (`insert_stone_text`). It freezes a mutable text value before it escapes its defining slot — for example, before a `move`, `setarg`, `store_field`, `push`, or `put`. The instruction is only inserted when the slot is provably `T_TEXT`; non-text values never need stoning. See [Streamline Optimizer — insert_stone_text](streamline.md#7-insert_stone_text-mutable-text-escape-analysis) for details.
|
||||
|
||||
At the VM level, `stone_text` is a single-operand instruction (iABC with B=0, C=0). If the slot holds a heap text without the S bit set, it sets the S bit. For all other values (integers, booleans, already-stoned text, etc.), it is a no-op.
|
||||
|
||||
### Comparison — Integer
|
||||
|
||||
| Instruction | Operands | Description |
|
||||
|-------------|----------|-------------|
|
||||
| `eq_int` | `dest, a, b` | `dest = a == b` (integer) |
|
||||
| `ne_int` | `dest, a, b` | `dest = a != b` (integer) |
|
||||
| `lt_int` | `dest, a, b` | `dest = a < b` (integer) |
|
||||
| `le_int` | `dest, a, b` | `dest = a <= b` (integer) |
|
||||
| `gt_int` | `dest, a, b` | `dest = a > b` (integer) |
|
||||
| `ge_int` | `dest, a, b` | `dest = a >= b` (integer) |
|
||||
|
||||
### Comparison — Float
|
||||
|
||||
| Instruction | Operands | Description |
|
||||
|-------------|----------|-------------|
|
||||
| `eq_float` | `dest, a, b` | `dest = a == b` (float) |
|
||||
| `ne_float` | `dest, a, b` | `dest = a != b` (float) |
|
||||
| `lt_float` | `dest, a, b` | `dest = a < b` (float) |
|
||||
| `le_float` | `dest, a, b` | `dest = a <= b` (float) |
|
||||
| `gt_float` | `dest, a, b` | `dest = a > b` (float) |
|
||||
| `ge_float` | `dest, a, b` | `dest = a >= b` (float) |
|
||||
|
||||
### Comparison — Text
|
||||
|
||||
| Instruction | Operands | Description |
|
||||
|-------------|----------|-------------|
|
||||
| `eq_text` | `dest, a, b` | `dest = a == b` (text) |
|
||||
| `ne_text` | `dest, a, b` | `dest = a != b` (text) |
|
||||
| `lt_text` | `dest, a, b` | `dest = a < b` (lexicographic) |
|
||||
| `le_text` | `dest, a, b` | `dest = a <= b` (lexicographic) |
|
||||
| `gt_text` | `dest, a, b` | `dest = a > b` (lexicographic) |
|
||||
| `ge_text` | `dest, a, b` | `dest = a >= b` (lexicographic) |
|
||||
|
||||
### Comparison — Boolean
|
||||
|
||||
| Instruction | Operands | Description |
|
||||
|-------------|----------|-------------|
|
||||
| `eq_bool` | `dest, a, b` | `dest = a == b` (boolean) |
|
||||
| `ne_bool` | `dest, a, b` | `dest = a != b` (boolean) |
|
||||
|
||||
### Comparison — Special
|
||||
|
||||
| Instruction | Operands | Description |
|
||||
|-------------|----------|-------------|
|
||||
| `is_identical` | `dest, a, b` | Object identity check (same reference) |
|
||||
| `eq_tol` | `dest, a, b` | Equality with tolerance |
|
||||
| `ne_tol` | `dest, a, b` | Inequality with tolerance |
|
||||
|
||||
### Type Checks
|
||||
|
||||
Inlined from intrinsic function calls. Each sets `dest` to `true` or `false`.
|
||||
|
||||
| Instruction | Operands | Description |
|
||||
|-------------|----------|-------------|
|
||||
| `is_int` | `dest, src` | Check if integer |
|
||||
| `is_num` | `dest, src` | Check if number (integer or float) |
|
||||
| `is_text` | `dest, src` | Check if text |
|
||||
| `is_bool` | `dest, src` | Check if logical |
|
||||
| `is_null` | `dest, src` | Check if null |
|
||||
| `is_array` | `dest, src` | Check if array |
|
||||
| `is_func` | `dest, src` | Check if function |
|
||||
| `is_record` | `dest, src` | Check if record (object) |
|
||||
| `is_stone` | `dest, src` | Check if stone (immutable) |
|
||||
| `is_proxy` | `dest, src` | Check if function proxy (arity 2) |
|
||||
|
||||
### Logical
|
||||
|
||||
| Instruction | Operands | Description |
|
||||
|-------------|----------|-------------|
|
||||
| `not` | `dest, src` | Logical NOT |
|
||||
| `and` | `dest, a, b` | Logical AND |
|
||||
| `or` | `dest, a, b` | Logical OR |
|
||||
|
||||
### Bitwise
|
||||
|
||||
| Instruction | Operands | Description |
|
||||
|-------------|----------|-------------|
|
||||
| `bitand` | `dest, a, b` | Bitwise AND |
|
||||
| `bitor` | `dest, a, b` | Bitwise OR |
|
||||
| `bitxor` | `dest, a, b` | Bitwise XOR |
|
||||
| `bitnot` | `dest, src` | Bitwise NOT |
|
||||
| `shl` | `dest, a, b` | Shift left |
|
||||
| `shr` | `dest, a, b` | Arithmetic shift right |
|
||||
| `ushr` | `dest, a, b` | Unsigned shift right |
|
||||
|
||||
### Property Access
|
||||
|
||||
Memory operations come in typed variants. The compiler selects the appropriate variant based on `type_tag` and `access_kind` annotations from parse and fold.
|
||||
|
||||
| Instruction | Operands | Description |
|
||||
|-------------|----------|-------------|
|
||||
| `load_field` | `dest, obj, key` | Load record property by string key |
|
||||
| `store_field` | `obj, val, key` | Store record property by string key |
|
||||
| `load_index` | `dest, obj, idx` | Load array element by integer index |
|
||||
| `store_index` | `obj, val, idx` | Store array element by integer index |
|
||||
| `load_dynamic` | `dest, obj, key` | Load property (dispatches at runtime) |
|
||||
| `store_dynamic` | `obj, val, key` | Store property (dispatches at runtime) |
|
||||
| `delete` | `obj, key` | Delete property |
|
||||
| `in` | `dest, obj, key` | Check if property exists |
|
||||
| `length` | `dest, src` | Get length of array or text |
|
||||
|
||||
### Object and Array Construction
|
||||
|
||||
| Instruction | Operands | Description |
|
||||
|-------------|----------|-------------|
|
||||
| `record` | `dest` | Create empty record `{}` |
|
||||
| `array` | `dest, n` | Create empty array (elements added via `push`) |
|
||||
| `push` | `arr, val` | Push value to array |
|
||||
| `pop` | `dest, arr` | Pop value from array |
|
||||
|
||||
### Function Calls
|
||||
|
||||
Function calls are decomposed into three instructions:
|
||||
|
||||
| Instruction | Operands | Description |
|
||||
|-------------|----------|-------------|
|
||||
| `frame` | `dest, fn, argc` | Allocate call frame for `fn` with `argc` arguments |
|
||||
| `setarg` | `frame, idx, val` | Set argument `idx` in call frame |
|
||||
| `invoke` | `frame, result` | Execute the call, store result |
|
||||
| `goframe` | `dest, fn, argc` | Allocate frame for async/concurrent call |
|
||||
| `goinvoke` | `frame, result` | Invoke async/concurrent call |
|
||||
|
||||
### Variable Resolution
|
||||
|
||||
| Instruction | Operands | Description |
|
||||
|-------------|----------|-------------|
|
||||
| `access` | `dest, name` | Load variable (intrinsic or module environment) |
|
||||
| `get` | `dest, level, slot` | Get closure variable from parent scope |
|
||||
| `put` | `level, slot, src` | Set closure variable in parent scope |
|
||||
|
||||
### Control Flow
|
||||
|
||||
| Instruction | Operands | Description |
|
||||
|-------------|----------|-------------|
|
||||
| `LABEL` | `name` | Define a named label (not executed) |
|
||||
| `jump` | `label` | Unconditional jump |
|
||||
| `jump_true` | `cond, label` | Jump if `cond` is true |
|
||||
| `jump_false` | `cond, label` | Jump if `cond` is false |
|
||||
| `jump_not_null` | `val, label` | Jump if `val` is not null |
|
||||
| `return` | `src` | Return value from function |
|
||||
| `disrupt` | — | Trigger disruption (error) |
|
||||
|
||||
## Typed Instruction Design
|
||||
|
||||
A key design principle of mcode is that **every type check is an explicit instruction**. Arithmetic and comparison operations come in type-specialized variants (`add_int`, `add_float`, `eq_text`, etc.) rather than a single polymorphic instruction.
|
||||
|
||||
When type information is available from the fold stage, the compiler emits the typed variant directly. When the type is unknown, the compiler emits a type-check/dispatch pattern:
|
||||
|
||||
```json
|
||||
["is_int", check, a]
|
||||
["jump_false", check, "float_path"]
|
||||
["add_int", dest, a, b]
|
||||
["jump", "done"]
|
||||
["LABEL", "float_path"]
|
||||
["add_float", dest, a, b]
|
||||
["LABEL", "done"]
|
||||
```
|
||||
|
||||
The [Streamline Optimizer](streamline.md) eliminates dead branches when types are statically known, collapsing the dispatch to a single typed instruction.
|
||||
|
||||
## Intrinsic Inlining
|
||||
|
||||
The mcode compiler recognizes calls to built-in intrinsic functions and emits direct opcodes instead of the generic frame/setarg/invoke call sequence:
|
||||
|
||||
| Source call | Emitted instruction |
|
||||
|-------------|-------------------|
|
||||
| `is_array(x)` | `is_array dest, src` |
|
||||
| `is_function(x)` | `is_func dest, src` |
|
||||
| `is_object(x)` | `is_record dest, src` |
|
||||
| `is_stone(x)` | `is_stone dest, src` |
|
||||
| `is_integer(x)` | `is_int dest, src` |
|
||||
| `is_text(x)` | `is_text dest, src` |
|
||||
| `is_number(x)` | `is_num dest, src` |
|
||||
| `is_logical(x)` | `is_bool dest, src` |
|
||||
| `is_null(x)` | `is_null dest, src` |
|
||||
| `length(x)` | `length dest, src` |
|
||||
| `push(arr, val)` | `push arr, val` |
|
||||
|
||||
## Function Proxy Decomposition
|
||||
|
||||
When the compiler encounters a method call `obj.method(args)`, it emits a branching pattern to handle ƿit's function proxy protocol. An arity-2 function used as a proxy target receives the method name and argument array instead of a normal method call:
|
||||
|
||||
```json
|
||||
["is_proxy", check, obj]
|
||||
["jump_false", check, "record_path"]
|
||||
|
||||
["access", name_slot, "method"]
|
||||
["array", args_arr, N, arg0, arg1]
|
||||
["null", null_slot]
|
||||
["frame", f, obj, 2]
|
||||
["setarg", f, 0, null_slot]
|
||||
["setarg", f, 1, name_slot]
|
||||
["setarg", f, 2, args_arr]
|
||||
["invoke", f, dest]
|
||||
["jump", "done"]
|
||||
|
||||
["LABEL", "record_path"]
|
||||
["load_field", method, obj, "method"]
|
||||
["frame", f2, method, N]
|
||||
["setarg", f2, 0, obj]
|
||||
["setarg", f2, 1, arg0]
|
||||
["invoke", f2, dest]
|
||||
|
||||
["LABEL", "done"]
|
||||
```
|
||||
|
||||
## Labels and Control Flow
|
||||
|
||||
Control flow uses named labels instead of numeric offsets:
|
||||
|
||||
```json
|
||||
["LABEL", "loop_start"]
|
||||
["add_int", 1, 1, 2]
|
||||
["jump_false", 3, "loop_end"]
|
||||
["jump", "loop_start"]
|
||||
["LABEL", "loop_end"]
|
||||
```
|
||||
|
||||
Labels are collected into a name-to-index map during loading, enabling O(1) jump resolution. The Mach serializer converts label names to numeric offsets in the binary bytecode.
|
||||
|
||||
## Nop Convention
|
||||
|
||||
The streamline optimizer replaces eliminated instructions with nop strings (e.g., `_nop_tc_1`, `_nop_bl_2`). Nop strings are skipped during interpretation and native code emission but preserved in the instruction array to maintain positional stability for jump targets.
|
||||
|
||||
## Internal Structures
|
||||
|
||||
### JSMCode (Mcode Interpreter)
|
||||
|
||||
```c
|
||||
struct JSMCode {
|
||||
uint16_t nr_args; // argument count
|
||||
uint16_t nr_slots; // register count
|
||||
cJSON **instrs; // pre-flattened instruction array
|
||||
cJSON **instrs; // instruction array
|
||||
uint32_t instr_count; // number of instructions
|
||||
|
||||
struct {
|
||||
@@ -37,74 +354,25 @@ struct JSMCode {
|
||||
cJSON *json_root; // keeps JSON alive
|
||||
const char *name; // function name
|
||||
const char *filename; // source file
|
||||
uint16_t disruption_pc; // exception handler offset
|
||||
uint16_t disruption_pc; // disruption handler offset
|
||||
};
|
||||
```
|
||||
|
||||
## Instruction Format
|
||||
### JSCodeRegister (Mach VM Bytecode)
|
||||
|
||||
Each instruction is a JSON array. The first element is the instruction name (string), followed by operands (typically `[op, dest, ...args, line, col]`):
|
||||
|
||||
```json
|
||||
["access", 3, 5, 1, 9]
|
||||
["load_index", 10, 4, 9, 5, 11]
|
||||
["store_dynamic", 4, 11, 12, 6, 3]
|
||||
["frame", 15, 14, 1, 7, 7]
|
||||
["setarg", 15, 0, 16, 7, 7]
|
||||
["invoke", 15, 13, 7, 7]
|
||||
```c
|
||||
struct JSCodeRegister {
|
||||
uint16_t arity; // argument count
|
||||
uint16_t nr_slots; // total register count
|
||||
uint32_t cpool_count; // constant pool size
|
||||
JSValue *cpool; // constant pool
|
||||
uint32_t instr_count; // instruction count
|
||||
MachInstr32 *instructions; // 32-bit instruction array
|
||||
uint32_t func_count; // nested function count
|
||||
JSCodeRegister **functions; // nested function table
|
||||
JSValue name; // function name
|
||||
uint16_t disruption_pc; // disruption handler offset
|
||||
};
|
||||
```
|
||||
|
||||
### Typed Load/Store
|
||||
|
||||
Memory operations come in typed variants for optimization:
|
||||
|
||||
- `load_index dest, obj, idx` — array element by integer index
|
||||
- `load_field dest, obj, key` — record property by string key
|
||||
- `load_dynamic dest, obj, key` — unknown; dispatches at runtime
|
||||
- `store_index obj, val, idx` — array element store
|
||||
- `store_field obj, val, key` — record property store
|
||||
- `store_dynamic obj, val, key` — unknown; dispatches at runtime
|
||||
|
||||
The compiler selects the appropriate variant based on `type_tag` and `access_kind` annotations from parse and fold.
|
||||
|
||||
### Decomposed Calls
|
||||
|
||||
Function calls are split into separate instructions:
|
||||
|
||||
- `frame dest, fn, argc` — allocate call frame
|
||||
- `setarg frame, idx, val` — set argument
|
||||
- `invoke frame, result` — execute the call
|
||||
|
||||
## Labels
|
||||
|
||||
Control flow uses named labels instead of numeric offsets:
|
||||
|
||||
```json
|
||||
["LABEL", "loop_start"]
|
||||
["ADD", 1, 1, 2]
|
||||
["JMPFALSE", 3, "loop_end"]
|
||||
["JMP", "loop_start"]
|
||||
["LABEL", "loop_end"]
|
||||
```
|
||||
|
||||
Labels are collected into a name-to-index map during loading, enabling O(1) jump resolution.
|
||||
|
||||
## Differences from Mach
|
||||
|
||||
| Property | Mcode | Mach |
|
||||
|----------|-------|------|
|
||||
| Instructions | cJSON arrays | 32-bit binary |
|
||||
| Dispatch | String comparison | Switch on opcode byte |
|
||||
| Constants | Inline in JSON | Separate constant pool |
|
||||
| Jump targets | Named labels | Numeric offsets |
|
||||
| Memory | Heap (cJSON nodes) | Off-heap (malloc) |
|
||||
|
||||
## Purpose
|
||||
|
||||
Mcode serves as an inspectable, debuggable intermediate format:
|
||||
|
||||
- **Human-readable** — the JSON representation can be printed and examined
|
||||
- **Language-independent** — any tool that produces the correct JSON can target the ƿit runtime
|
||||
- **Compilation target** — the Mach compiler can consume mcode as input, and future native code generators can work from the same representation
|
||||
|
||||
The cost of string-based dispatch makes mcode slower than the binary Mach VM, so it is primarily useful during development and as a compilation intermediate rather than for production execution.
|
||||
The Mach serializer (`mach.c`) converts the JSON mcode into compact 32-bit instructions with a constant pool. See [Register VM](mach.md) for the binary encoding formats.
|
||||
|
||||
@@ -26,7 +26,7 @@ Every heap-allocated object begins with a 64-bit header word (`objhdr_t`):
|
||||
|
||||
### Flags (bits 3-7)
|
||||
|
||||
- **Bit 3 (S)** — Stone flag. If set, the object is immutable and excluded from GC.
|
||||
- **Bit 3 (S)** — Stone flag. If set, the object is immutable. Stone text in the constant table (ct) is not copied by GC since it lives outside the heap; stone objects on the GC heap are copied normally.
|
||||
- **Bit 4 (P)** — Properties flag.
|
||||
- **Bit 5 (A)** — Array flag.
|
||||
- **Bit 7 (R)** — Reserved.
|
||||
@@ -69,7 +69,9 @@ struct JSText {
|
||||
};
|
||||
```
|
||||
|
||||
Text is stored as UTF-32, with two 32-bit codepoints packed per 64-bit word. When a text object is stoned, the length field is repurposed to cache the hash value (computed via `fash64`), since stoned text is immutable and the hash never changes.
|
||||
Text is stored as UTF-32, with two 32-bit codepoints packed per 64-bit word.
|
||||
|
||||
A mutable text (pretext) uses capacity for the allocated slot count and length for the current codepoint count. When a pretext is stoned, the capacity field is set to the actual length (codepoint count), and the length field is zeroed for use as a lazy hash cache (computed via `fash64` on first use as a key). Since stoned text is immutable, the hash never changes. Stoning is done in-place — no new allocation is needed.
|
||||
|
||||
## Record
|
||||
|
||||
@@ -111,7 +113,7 @@ struct JSFrame {
|
||||
objhdr_t header; // type=6, capacity=slot count
|
||||
JSValue function; // owning function
|
||||
JSValue caller; // parent frame
|
||||
uint32_t return_pc; // return address
|
||||
JSValue address; // return address
|
||||
JSValue slots[]; // [this][args][captured][locals][temps]
|
||||
};
|
||||
```
|
||||
@@ -138,4 +140,4 @@ All objects are aligned to 8 bytes. The total size in bytes for each type:
|
||||
| Record | `8 + 8 + 8 + (capacity + 1) * 16` |
|
||||
| Function | `sizeof(JSFunction)` (fixed) |
|
||||
| Code | `sizeof(JSFunctionBytecode)` (fixed) |
|
||||
| Frame | `8 + 8 + 8 + 4 + capacity * 8` |
|
||||
| Frame | `8 + 8 + 8 + 8 + capacity * 8` |
|
||||
|
||||
@@ -5,14 +5,17 @@ description: "Overview of the compilation stages and optimizations"
|
||||
|
||||
## Overview
|
||||
|
||||
The compilation pipeline transforms source code through several stages, each adding information or lowering the representation toward execution. There are three execution backends: the Mach register VM (default), the Mcode interpreter (debug), and native code via QBE (experimental).
|
||||
The compilation pipeline transforms source code through several stages, each adding information or lowering the representation toward execution. All backends share the same path through mcode and streamline.
|
||||
|
||||
```
|
||||
Source → Tokenize → Parse → Fold → Mach VM (default)
|
||||
→ Mcode → Streamline → Mcode Interpreter
|
||||
→ QBE → Native
|
||||
Source → Tokenize → Parse → Fold → Mcode → Streamline → Machine
|
||||
```
|
||||
|
||||
The final **machine** stage has two targets:
|
||||
|
||||
- **Mach VM** — a register-based bytecode interpreter that directly executes the mcode instruction set as compact 32-bit binary
|
||||
- **Native code** — lowers mcode to QBE or LLVM intermediate language, then compiles to machine code for the target CPU architecture
|
||||
|
||||
## Stages
|
||||
|
||||
### Tokenize (`tokenize.cm`)
|
||||
@@ -24,7 +27,8 @@ Splits source text into tokens. Handles string interpolation by re-tokenizing te
|
||||
Converts tokens into an AST. Also performs semantic analysis:
|
||||
|
||||
- **Scope records**: For each scope (global, function), builds a record mapping variable names to their metadata: `make` (var/def/function/input), `function_nr`, `nr_uses`, `closure` flag, and `level`.
|
||||
- **Type tags**: When the right-hand side of a `def` is a syntactically obvious type, stamps `type_tag` on the scope record entry. Derivable types: `"integer"`, `"number"`, `"text"`, `"array"`, `"record"`, `"function"`, `"logical"`, `"null"`.
|
||||
- **Type tags**: When the right-hand side of a `def` is a syntactically obvious type, stamps `type_tag` on the scope record entry. Derivable types: `"integer"`, `"number"`, `"text"`, `"array"`, `"record"`, `"function"`, `"logical"`. For `def` variables, type tags are also inferred from usage patterns: push (`x[] = v`) implies array, property access (`x.foo = v`) implies record, integer key implies array, text key implies record.
|
||||
- **Type error detection**: For `def` variables with known type tags, provably wrong operations are reported as compile errors: property access on arrays, push on non-arrays, text keys on arrays, integer keys on records. Only `def` variables are checked because `var` can be reassigned.
|
||||
- **Intrinsic resolution**: Names used but not locally bound are recorded in `ast.intrinsics`. Name nodes referencing intrinsics get `intrinsic: true`.
|
||||
- **Access kind**: Subscript (`[`) nodes get `access_kind`: `"index"` for numeric subscripts, `"field"` for string subscripts, omitted otherwise.
|
||||
- **Tail position**: Return statements where the expression is a call get `tail: true`.
|
||||
@@ -37,8 +41,8 @@ Operates on the AST. Performs constant folding and type analysis:
|
||||
- **Constant propagation**: Tracks `def` bindings whose values are known constants.
|
||||
- **Type propagation**: Extends `type_tag` through operations. When both operands of an arithmetic op have known types, the result type is known. Propagates type tags to reference sites.
|
||||
- **Intrinsic specialization**: When an intrinsic call's argument types are known, stamps a `hint` on the call node. For example, `length(x)` where x is a known array gets `hint: "array_length"`. Type checks like `is_array(known_array)` are folded to `true`.
|
||||
- **Purity marking**: Stamps `pure: true` on expressions with no side effects (literals, name references, arithmetic on pure operands).
|
||||
- **Dead code elimination**: Removes unreachable branches when conditions are known constants.
|
||||
- **Purity analysis**: Expressions with no side effects are marked pure (literals, name references, arithmetic on pure operands, calls to pure intrinsics). The pure intrinsic set contains only `is_*` sensory functions — they are the only intrinsics guaranteed to never disrupt regardless of argument types. Other intrinsics like `text`, `number`, and `length` can disrupt on wrong argument types and are excluded.
|
||||
- **Dead code elimination**: Removes unreachable branches when conditions are known constants. Removes unused `var`/`def` declarations with pure initializers. Removes standalone calls to pure intrinsics where the result is discarded.
|
||||
|
||||
### Mcode (`mcode.cm`)
|
||||
|
||||
@@ -47,53 +51,69 @@ Lowers the AST to a JSON-based intermediate representation with explicit operati
|
||||
- **Typed load/store**: Emits `load_index` (array by integer), `load_field` (record by string), or `load_dynamic` (unknown) based on type information from fold.
|
||||
- **Decomposed calls**: Function calls are split into `frame` (create call frame) + `setarg` (set arguments) + `invoke` (execute call).
|
||||
- **Intrinsic access**: Intrinsic functions are loaded via `access` with an intrinsic marker rather than global lookup.
|
||||
- **Intrinsic inlining**: Type-check intrinsics (`is_array`, `is_text`, `is_number`, `is_integer`, `is_logical`, `is_null`, `is_function`, `is_object`, `is_stone`), `length`, and `push` are emitted as direct opcodes instead of frame/setarg/invoke call sequences.
|
||||
- **Disruption handler labels**: When a function has a disruption handler, a label is emitted before the handler code. This allows the streamline optimizer's unreachable code elimination to safely nop dead code after `return` without accidentally eliminating the handler.
|
||||
- **Tail call marking**: When a return statement's expression is a call and the function has no disruption handler, the final `invoke` is renamed to `tail_invoke`. This marks the call site for future tail call optimization. Functions with disruption handlers cannot use TCO because the handler frame must remain on the stack.
|
||||
|
||||
See [Mcode IR](mcode.md) for instruction format details.
|
||||
See [Mcode IR](mcode.md) for the instruction format and complete instruction reference.
|
||||
|
||||
### Streamline (`streamline.cm`)
|
||||
|
||||
Optimizes the Mcode IR. Operates per-function:
|
||||
Optimizes the Mcode IR through a series of independent passes. Operates per-function:
|
||||
|
||||
- **Redundant instruction elimination**: Removes no-op patterns and redundant moves.
|
||||
- **Dead code removal**: Eliminates instructions whose results are never used.
|
||||
- **Type-based narrowing**: When type information is available, narrows `load_dynamic`/`store_dynamic` to typed variants.
|
||||
1. **Backward type inference**: Infers parameter types from how they are used in typed operators (`add_int`, `store_index`, `load_field`, `push`, `pop`, etc.). Immutable `def` parameters keep their inferred type across label join points.
|
||||
2. **Write-type invariance**: Determines which local slots have a consistent write type across all instructions. Slots written by child closures (via `put`) are excluded (forced to unknown).
|
||||
3. **Type-check elimination**: When a slot's type is known, eliminates `is_<type>` + conditional jump pairs. Narrows `load_dynamic`/`store_dynamic` to typed variants.
|
||||
4. **Algebraic simplification**: Rewrites identity operations (add 0, multiply 1, divide 1) and folds same-slot comparisons.
|
||||
5. **Boolean simplification**: Fuses `not` + conditional jump into a single jump with inverted condition.
|
||||
6. **Move elimination**: Removes self-moves (`move a, a`).
|
||||
7. **Unreachable elimination**: Nops dead code after `return` until the next label.
|
||||
8. **Dead jump elimination**: Removes jumps to the immediately following label.
|
||||
9. **Compile-time diagnostics** (optional): When `_warn` is set on the mcode input, emits errors for provably wrong operations (storing named property on array, invoking null, etc.) and warnings for suspicious patterns (named property access on array/text). The engine aborts compilation if any error-severity diagnostics are emitted.
|
||||
|
||||
### QBE Emit (`qbe_emit.cm`)
|
||||
See [Streamline Optimizer](streamline.md) for detailed pass descriptions.
|
||||
|
||||
Lowers optimized Mcode IR to QBE intermediate language for native code compilation. Each Mcode function becomes a QBE function that calls into the cell runtime (`cell_rt_*` functions) for operations that require the runtime (allocation, intrinsic dispatch, etc.).
|
||||
### Machine
|
||||
|
||||
String constants are interned in a data section. Integer constants are NaN-boxed inline.
|
||||
The streamlined mcode is lowered to a machine target for execution.
|
||||
|
||||
### QBE Macros (`qbe.cm`)
|
||||
#### Mach VM (default)
|
||||
|
||||
Provides operation implementations as QBE IL templates. Each arithmetic, comparison, and type operation is defined as a function that emits the corresponding QBE instructions, handling type dispatch (integer, float, text paths) with proper guard checks.
|
||||
|
||||
## Execution Backends
|
||||
|
||||
### Mach VM (default)
|
||||
|
||||
Binary 32-bit register VM. Used for production execution and bootstrapping.
|
||||
The Mach VM is a register-based virtual machine that directly interprets the mcode instruction set as 32-bit binary bytecode. The Mach serializer (`mach.c`) converts streamlined mcode JSON into compact 32-bit instructions with a constant pool. Since the mach bytecode is a direct encoding of the mcode, the [Mcode IR](mcode.md) reference serves as the authoritative instruction set documentation.
|
||||
|
||||
```
|
||||
./cell script.ce
|
||||
pit script.ce
|
||||
```
|
||||
|
||||
### Mcode Interpreter
|
||||
#### Native Code (QBE / LLVM)
|
||||
|
||||
JSON-based interpreter. Used for debugging the compilation pipeline.
|
||||
Lowers the streamlined mcode to QBE or LLVM intermediate language for compilation to native machine code. Each mcode function becomes a native function that calls into the ƿit runtime (`cell_rt_*` functions) for operations that require the runtime (allocation, intrinsic dispatch, etc.).
|
||||
|
||||
String constants are interned in a data section. Integer constants are encoded inline.
|
||||
|
||||
```
|
||||
./cell --mcode script.ce
|
||||
pit --emit-qbe script.ce > output.ssa
|
||||
```
|
||||
|
||||
### QBE Native (experimental)
|
||||
## Boot Seeds
|
||||
|
||||
Generates QBE IL that can be compiled to native code.
|
||||
The `boot/` directory contains pre-compiled mcode IR (JSON) seed files for the pipeline modules:
|
||||
|
||||
```
|
||||
./cell --emit-qbe script.ce > output.ssa
|
||||
boot/tokenize.cm.mcode
|
||||
boot/parse.cm.mcode
|
||||
boot/fold.cm.mcode
|
||||
boot/mcode.cm.mcode
|
||||
boot/streamline.cm.mcode
|
||||
boot/bootstrap.cm.mcode
|
||||
```
|
||||
|
||||
Seeds are used during cold start (empty cache) to compile the pipeline modules from source. The engine's `load_pipeline_module()` hashes the **source file** content — if the source changes, the hash changes, the cache misses, and the module is recompiled from source using the boot seeds. This means:
|
||||
|
||||
- Editing a pipeline module (e.g. `tokenize.cm`) takes effect on the next run automatically
|
||||
- Seeds only need regenerating if the pipeline changes in a way the existing seeds can't compile the new source, or before distribution
|
||||
- Use `pit seed` to regenerate all seeds, and `pit seed --clean` to also clear the build cache
|
||||
|
||||
## Files
|
||||
|
||||
| File | Role |
|
||||
@@ -105,7 +125,17 @@ Generates QBE IL that can be compiled to native code.
|
||||
| `streamline.cm` | Mcode IR optimizer |
|
||||
| `qbe_emit.cm` | Mcode IR → QBE IL emitter |
|
||||
| `qbe.cm` | QBE IL operation templates |
|
||||
| `internal/bootstrap.cm` | Pipeline orchestrator |
|
||||
| `internal/bootstrap.cm` | Cache seeder (cold start only) |
|
||||
| `internal/engine.cm` | Self-sufficient pipeline loader and orchestrator |
|
||||
|
||||
## Debug Tools
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `mcode.ce --pretty` | Print raw Mcode IR before streamlining |
|
||||
| `streamline.ce --types` | Print streamlined IR with type annotations |
|
||||
| `streamline.ce --stats` | Print IR after streamlining with before/after stats |
|
||||
| `streamline.ce --diagnose` | Print compile-time diagnostics (type errors and warnings) |
|
||||
|
||||
## Test Files
|
||||
|
||||
@@ -116,3 +146,6 @@ Generates QBE IL that can be compiled to native code.
|
||||
| `mcode_test.ce` | Typed load/store, decomposed calls |
|
||||
| `streamline_test.ce` | Optimization counts, IR before/after |
|
||||
| `qbe_test.ce` | End-to-end QBE IL generation |
|
||||
| `test_intrinsics.cm` | Inlined intrinsic opcodes (is_array, length, push, etc.) |
|
||||
| `test_backward.cm` | Backward type propagation for parameters |
|
||||
| `tests/compile.cm` | Compile-time diagnostics (type errors and warnings) |
|
||||
|
||||
@@ -77,6 +77,30 @@ Messages between actors are stoned before delivery, ensuring actors never share
|
||||
|
||||
Literal objects and arrays that can be determined at compile time may be allocated directly in stone memory.
|
||||
|
||||
## Mutable Text Concatenation
|
||||
|
||||
String concatenation in a loop (`s = s + "x"`) is optimized to O(n) amortized by leaving concat results **unstoned** with over-allocated capacity. On the next concatenation, if the destination text is mutable (S bit clear) and has enough room, the VM appends in-place with zero allocation.
|
||||
|
||||
### How It Works
|
||||
|
||||
When the VM executes `concat dest, dest, src` (same destination and left operand — a self-assign pattern):
|
||||
|
||||
1. **Inline fast path**: If `dest` holds a heap text, is not stoned, and `length + src_length <= capacity` — append characters in place, update length, done. No allocation, no GC possible.
|
||||
|
||||
2. **Growth path** (`JS_ConcatStringGrow`): Allocate a new text with `capacity = max(new_length * 2, 16)`, copy both operands, and return the result **without stoning** it. The 2x growth factor means a loop of N concatenations does O(log N) allocations totaling O(N) character copies.
|
||||
|
||||
3. **Exact-fit path** (`JS_ConcatString`): When `dest != left` (not self-assign), the existing exact-fit stoned path is used. This is the normal case for expressions like `var c = a + b`.
|
||||
|
||||
### Safety Invariant
|
||||
|
||||
**An unstoned heap text is uniquely referenced by exactly one slot.** This is enforced by the `stone_text` mcode instruction, which the [streamline optimizer](streamline.md#7-insert_stone_text-mutable-text-escape-analysis) inserts before any instruction that would create a second reference to the value (move, store, push, setarg, put). Two VM-level guards cover cases where the compiler cannot prove the type: `get` (closure reads) and `return` (inter-frame returns).
|
||||
|
||||
### Why Over-Allocation Is GC-Safe
|
||||
|
||||
- The copying collector copies based on `cap56` (the object header's capacity field), not `length`. Over-allocated capacity survives GC.
|
||||
- `js_alloc_string` zero-fills the packed data region, so padding beyond `length` is always clean.
|
||||
- String comparisons, hashing, and interning all use `length`, not `cap56`. Extra capacity is invisible to string operations.
|
||||
|
||||
## Relationship to GC
|
||||
|
||||
The Cheney copying collector only operates on the mutable heap. During collection, when the collector encounters a pointer to stone memory (S bit set), it skips it — stone objects are roots that never move. This means stone memory acts as a permanent root set with zero GC overhead.
|
||||
|
||||
483
docs/spec/streamline.md
Normal file
483
docs/spec/streamline.md
Normal file
@@ -0,0 +1,483 @@
|
||||
---
|
||||
title: "Streamline Optimizer"
|
||||
description: "Mcode IR optimization passes"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The streamline optimizer (`streamline.cm`) runs a series of independent passes over the Mcode IR to eliminate redundant operations. Each pass is a standalone function that can be enabled, disabled, or reordered. Passes communicate only through the instruction array they mutate in place, replacing eliminated instructions with nop strings (e.g., `_nop_tc_1`).
|
||||
|
||||
The optimizer runs after `mcode.cm` generates the IR and before the result is lowered to the Mach VM or emitted as QBE IL.
|
||||
|
||||
```
|
||||
Fold (AST) → Mcode (JSON IR) → Streamline → Mach VM / QBE
|
||||
```
|
||||
|
||||
## Type Lattice
|
||||
|
||||
The optimizer tracks a type for each slot in the register file:
|
||||
|
||||
| Type | Meaning |
|
||||
|------|---------|
|
||||
| `unknown` | No type information |
|
||||
| `int` | Integer |
|
||||
| `float` | Floating-point |
|
||||
| `num` | Number (subsumes int and float) |
|
||||
| `text` | String |
|
||||
| `bool` | Logical (true/false) |
|
||||
| `null` | Null value |
|
||||
| `array` | Array |
|
||||
| `record` | Record (object) |
|
||||
| `function` | Function |
|
||||
| `blob` | Binary blob |
|
||||
|
||||
Subsumption: `int` and `float` both satisfy a `num` check.
|
||||
|
||||
## Passes
|
||||
|
||||
### 1. infer_param_types (backward type inference)
|
||||
|
||||
Scans typed operators and generic arithmetic to determine what types their operands must be. For example, `subtract dest, a, b` implies both `a` and `b` are numbers.
|
||||
|
||||
When a parameter slot (1..nr_args) is consistently inferred as a single type, that type is recorded. Since parameters are immutable (`def`), the inferred type holds for the entire function and persists across label join points (loop headers, branch targets).
|
||||
|
||||
Backward inference rules:
|
||||
|
||||
| Operator class | Operand type inferred |
|
||||
|---|---|
|
||||
| `add`, `subtract`, `multiply`, `divide`, `modulo`, `pow`, `negate` | T_NUM |
|
||||
| bitwise ops (`bitand`, `bitor`, `bitxor`, `shl`, `shr`, `ushr`, `bitnot`) | T_INT |
|
||||
| `concat` | T_TEXT |
|
||||
| `not`, `and`, `or` | T_BOOL |
|
||||
| `store_index` (object operand) | T_ARRAY |
|
||||
| `store_index` (index operand) | T_INT |
|
||||
| `store_field` (object operand) | T_RECORD |
|
||||
| `push` (array operand) | T_ARRAY |
|
||||
| `load_index` (object operand) | T_ARRAY |
|
||||
| `load_index` (index operand) | T_INT |
|
||||
| `load_field` (object operand) | T_RECORD |
|
||||
| `pop` (array operand) | T_ARRAY |
|
||||
|
||||
Typed comparison operators (`eq_int`, `lt_float`, `lt_text`, etc.) and typed boolean comparisons (`eq_bool`, `ne_bool`) are excluded from backward inference. These ops always appear inside guard dispatch patterns (`is_type` + `jump_false` + typed_op), where mutually exclusive branches use the same slot with different types. Including them would merge conflicting types (e.g., T_INT from `lt_int` + T_FLOAT from `lt_float` + T_TEXT from `lt_text`) into T_UNKNOWN, losing all type information. Only unconditionally executed ops contribute to backward inference.
|
||||
|
||||
Note: `add` infers T_NUM even though it is polymorphic (numeric addition or text concatenation). When `add` appears in the IR, both operands have already passed a `is_num` guard, so they are guaranteed to be numeric. The text concatenation path uses `concat` instead.
|
||||
|
||||
When a slot appears with conflicting type inferences, the merge widens: INT + FLOAT → NUM, INT + NUM → NUM, FLOAT + NUM → NUM. Incompatible types (e.g., NUM + TEXT) produce `unknown`.
|
||||
|
||||
**Nop prefix:** none (analysis only, does not modify instructions)
|
||||
|
||||
### 2. infer_slot_write_types (slot write-type invariance)
|
||||
|
||||
Scans all instructions to determine which non-parameter slots have a consistent write type. If every instruction that writes to a given slot produces the same type, that type is globally invariant and can safely persist across label join points.
|
||||
|
||||
This analysis is sound because:
|
||||
- `alloc_slot()` in mcode.cm is monotonically increasing — temp slots are never reused
|
||||
- All local variable declarations must be at function body level and initialized — slots are written before any backward jumps to loop headers
|
||||
- `move` is conservatively treated as T_UNKNOWN, avoiding unsound transitive assumptions
|
||||
|
||||
Write type mapping:
|
||||
|
||||
| Instruction class | Write type |
|
||||
|---|---|
|
||||
| `int` | T_INT |
|
||||
| `true`, `false` | T_BOOL |
|
||||
| `null` | T_NULL |
|
||||
| `access` | type of literal value |
|
||||
| `array` | T_ARRAY |
|
||||
| `record` | T_RECORD |
|
||||
| `function` | T_FUNCTION |
|
||||
| `length` | T_INT |
|
||||
| bitwise ops | T_INT |
|
||||
| `concat` | T_TEXT |
|
||||
| `negate` | T_NUM |
|
||||
| `add`, `subtract`, `multiply`, `divide`, `modulo`, `pow` | T_NUM |
|
||||
| bool ops, comparisons, `in` | T_BOOL |
|
||||
| `move`, `load_field`, `load_index`, `load_dynamic`, `pop`, `get` | T_UNKNOWN |
|
||||
| `invoke`, `tail_invoke` | T_UNKNOWN |
|
||||
|
||||
Before filtering, a pre-pass (`mark_closure_writes`) scans all inner functions for `put` instructions (closure variable writes). For each `put`, the pass traverses the parent chain to find the target ancestor function and marks the written slot as `closure_written`. Slots marked as closure-written are forced to T_UNKNOWN regardless of what the local write analysis infers, because the actual runtime write happens in a child function and can produce any type.
|
||||
|
||||
The result is a map of slot→type for slots where all writes agree on a single known type. Parameter slots (1..nr_args) and slot 0 are excluded. Closure-written slots are excluded (forced to unknown).
|
||||
|
||||
Common patterns this enables:
|
||||
|
||||
- **Length variables** (`var len = length(arr)`): written by `length` (T_INT) only → invariant T_INT
|
||||
- **Boolean flags** (`var found = false; ... found = true`): written by `false` and `true` → invariant T_BOOL
|
||||
- **Locally-created containers** (`var arr = []`): written by `array` only → invariant T_ARRAY
|
||||
- **Numeric accumulators** (`var sum = 0; sum = sum - x`): written by `access 0` (T_INT) and `subtract` (T_NUM) → merges to T_NUM
|
||||
|
||||
Note: Loop counters using `+` (`var i = 0; i = i + 1`) may not achieve write-type invariance because the `+` operator emits a guard dispatch with both `concat` (T_TEXT) and `add` (T_NUM) paths writing to the same temp slot, producing T_UNKNOWN. However, when one operand is a known number literal, `mcode.cm` emits a numeric-only path (see "Known-Number Add Shortcut" below), avoiding the text dispatch. Other arithmetic ops (`-`, `*`, `/`, `%`, `**`) always emit a single numeric write path and work cleanly with write-type analysis.
|
||||
|
||||
**Nop prefix:** none (analysis only, does not modify instructions)
|
||||
|
||||
### 3. eliminate_type_checks (type-check + jump elimination)
|
||||
|
||||
Forward pass that tracks the known type of each slot. When a type check (`is_int`, `is_text`, `is_num`, etc.) is followed by a conditional jump, and the slot's type is already known, the check and jump can be eliminated or converted to an unconditional jump.
|
||||
|
||||
Five cases:
|
||||
|
||||
- **Known match** (e.g., `is_int` on a slot known to be `int`): both the check and the conditional jump are eliminated (nop'd).
|
||||
- **Subsumption match** (e.g., `is_num` on a slot known to be `int` or `float`): since `int` and `float` are subtypes of `num`, both the check and jump are eliminated.
|
||||
- **Subsumption partial** (e.g., `is_int` on a slot known to be `num`): the `num` type could be `int` or `float`, so the check must remain. On fallthrough, the slot narrows to the checked subtype (`int`). This is NOT a mismatch — `num` values can pass an `is_int` check.
|
||||
- **Known mismatch** (e.g., `is_text` on a slot known to be `int`): the check is nop'd and the conditional jump is rewritten to an unconditional `jump`.
|
||||
- **Unknown**: the check remains, but on fallthrough, the slot's type is narrowed to the checked type (enabling downstream eliminations).
|
||||
|
||||
This pass also reduces `load_dynamic`/`store_dynamic` to `load_field`/`store_field` or `load_index`/`store_index` when the key slot's type is known.
|
||||
|
||||
At label join points, all type information is reset except for parameter types from backward inference and write-invariant types from slot write-type analysis.
|
||||
|
||||
**Nop prefix:** `_nop_tc_`
|
||||
|
||||
### 4. simplify_algebra (same-slot comparison folding)
|
||||
|
||||
Tracks known constant values. Folds same-slot comparisons:
|
||||
|
||||
| Pattern | Rewrite |
|
||||
|---------|---------|
|
||||
| `eq_* dest, x, x` | `true dest` |
|
||||
| `le_* dest, x, x` | `true dest` |
|
||||
| `ge_* dest, x, x` | `true dest` |
|
||||
| `is_identical dest, x, x` | `true dest` |
|
||||
| `ne_* dest, x, x` | `false dest` |
|
||||
| `lt_* dest, x, x` | `false dest` |
|
||||
| `gt_* dest, x, x` | `false dest` |
|
||||
|
||||
**Nop prefix:** none (rewrites in place, does not create nops)
|
||||
|
||||
### 5. simplify_booleans (not + jump fusion)
|
||||
|
||||
Peephole pass that eliminates unnecessary `not` instructions:
|
||||
|
||||
| Pattern | Rewrite |
|
||||
|---------|---------|
|
||||
| `not d, x; jump_false d, L` | nop; `jump_true x, L` |
|
||||
| `not d, x; jump_true d, L` | nop; `jump_false x, L` |
|
||||
| `not d1, x; not d2, d1` | nop; `move d2, x` |
|
||||
|
||||
This is particularly effective on `if (!cond)` patterns, which the compiler generates as `not; jump_false`. After this pass, they become a single `jump_true`.
|
||||
|
||||
**Nop prefix:** `_nop_bl_`
|
||||
|
||||
### 6. eliminate_moves (self-move elimination)
|
||||
|
||||
Removes `move a, a` instructions where the source and destination are the same slot. These can arise from earlier passes rewriting binary operations into moves.
|
||||
|
||||
**Nop prefix:** `_nop_mv_`
|
||||
|
||||
### 7. insert_stone_text (mutable text escape analysis)
|
||||
|
||||
Inserts `stone_text` instructions before mutable text values escape their defining slot. This pass supports the mutable text concatenation optimization (see [Stone Memory — Mutable Text](stone.md#mutable-text-concatenation)), which leaves `concat` results unstoned with excess capacity so that subsequent `s = s + x` can append in-place.
|
||||
|
||||
The invariant is: **an unstoned heap text is uniquely referenced by exactly one slot.** This pass ensures that whenever a text value is copied or shared (via move, store, push, function argument, closure write, etc.), it is stoned first.
|
||||
|
||||
**Algorithm:**
|
||||
|
||||
1. **Compute liveness.** Build `first_ref[slot]` and `last_ref[slot]` arrays by scanning all instructions. Extend live ranges for backward jumps (loops): if a backward jump targets label L at position `lpos`, every slot referenced between `lpos` and the jump has its `last_ref` extended to the jump position.
|
||||
|
||||
2. **Forward walk with type tracking.** Walk instructions using `track_types` to maintain per-slot types. At each escape point, if the escaping slot is provably `T_TEXT`, insert `stone_text slot` before the instruction.
|
||||
|
||||
3. **Move special case.** For `move dest, src`: only insert `stone_text src` if the source is `T_TEXT` **and** `last_ref[src] > i` (the source slot is still live after the move, meaning both slots alias the same text). If the source is dead after the move, the value transfers uniquely — no stoning needed.
|
||||
|
||||
**Escape points and the slot that gets stoned:**
|
||||
|
||||
| Instruction | Stoned slot | Why it escapes |
|
||||
|---|---|---|
|
||||
| `move` | source (if still live) | Two slots alias the same value |
|
||||
| `store_field` | value | Stored to object property |
|
||||
| `store_index` | value | Stored to array element |
|
||||
| `store_dynamic` | value | Dynamic property store |
|
||||
| `push` | value | Pushed to array |
|
||||
| `setarg` | value | Passed as function argument |
|
||||
| `put` | source | Written to outer closure frame |
|
||||
|
||||
**Not handled by this pass** (handled by VM guards instead):
|
||||
|
||||
| Instruction | Reason |
|
||||
|---|---|
|
||||
| `get` (closure read) | Value arrives from outer frame; type may be T_UNKNOWN at compile time |
|
||||
| `return` | Return value's type may be T_UNKNOWN; VM stones at inter-frame boundary |
|
||||
|
||||
These two cases use runtime `stone_mutable_text` guards in the VM because the streamline pass cannot always prove the slot type across frame boundaries.
|
||||
|
||||
**Nop prefix:** none (inserts instructions, does not create nops)
|
||||
|
||||
### 8. eliminate_unreachable (dead code after return)
|
||||
|
||||
Nops instructions after `return` until the next real label. Only `return` is treated as a terminal instruction; `disrupt` is not, because the disruption handler code immediately follows `disrupt` and must remain reachable.
|
||||
|
||||
The mcode compiler emits a label at disruption handler entry points (see `emit_label(gen_label("disruption"))` in mcode.cm), which provides the label boundary that stops this pass from eliminating handler code.
|
||||
|
||||
**Nop prefix:** `_nop_ur_`
|
||||
|
||||
### 9. eliminate_dead_jumps (jump-to-next-label elimination)
|
||||
|
||||
Removes `jump L` instructions where `L` is the immediately following label (skipping over any intervening nop strings). These are common after other passes eliminate conditional branches, leaving behind jumps that fall through naturally.
|
||||
|
||||
**Nop prefix:** `_nop_dj_`
|
||||
|
||||
### 10. diagnose_function (compile-time diagnostics)
|
||||
|
||||
Optional pass that runs when `_warn` is set on the mcode input. Performs a forward type-tracking scan and emits diagnostics for provably wrong operations. Diagnostics are collected in `ir._diagnostics` as `{severity, file, line, col, message}` records.
|
||||
|
||||
This pass does not modify instructions — it only emits diagnostics.
|
||||
|
||||
**Errors** (compilation is aborted):
|
||||
|
||||
| Pattern | Message |
|
||||
|---------|---------|
|
||||
| `store_field` on T_ARRAY | storing named property on array |
|
||||
| `store_index` on T_RECORD | storing numeric index on record |
|
||||
| `store_field` / `store_index` on T_TEXT | storing property/index on text |
|
||||
| `push` on T_TEXT / T_RECORD | push on text/record |
|
||||
| `invoke` on T_NULL / T_INT / T_FLOAT / T_NUM / T_TEXT / T_BOOL / T_ARRAY | invoking null/number/text/bool/array |
|
||||
| arity mismatch (module imports only) | function expects N arguments, got M |
|
||||
|
||||
**Warnings** (compilation continues):
|
||||
|
||||
| Pattern | Message |
|
||||
|---------|---------|
|
||||
| `load_field` on T_ARRAY | named property access on array |
|
||||
| `load_field` on T_TEXT | named property access on text |
|
||||
| `load_dynamic` with T_TEXT key on T_RECORD | text key on record |
|
||||
| `load_dynamic` with T_RECORD / T_ARRAY / T_BOOL / T_NULL key on T_RECORD | record/array/bool/null key on record |
|
||||
|
||||
The engine (`internal/engine.cm`) prints all diagnostics and aborts compilation if any have severity `"error"`. Warnings are printed but do not block compilation.
|
||||
|
||||
**Nop prefix:** none (diagnostics only, does not modify instructions)
|
||||
|
||||
## Pass Composition
|
||||
|
||||
All passes run in sequence in `optimize_function`:
|
||||
|
||||
```
|
||||
infer_param_types → returns param_types map
|
||||
infer_slot_write_types → returns write_types map
|
||||
eliminate_type_checks → uses param_types + write_types
|
||||
simplify_algebra
|
||||
simplify_booleans
|
||||
eliminate_moves
|
||||
insert_stone_text → escape analysis for mutable text
|
||||
eliminate_unreachable
|
||||
eliminate_dead_jumps
|
||||
diagnose_function → optional, when _warn is set
|
||||
```
|
||||
|
||||
Each pass is independent and can be commented out for testing or benchmarking.
|
||||
|
||||
## Intrinsic Inlining
|
||||
|
||||
Before streamlining, `mcode.cm` recognizes calls to built-in intrinsic functions and emits direct opcodes instead of the generic frame/setarg/invoke call sequence. This reduces a 6-instruction call pattern to a single instruction:
|
||||
|
||||
| Call | Emitted opcode |
|
||||
|------|---------------|
|
||||
| `is_array(x)` | `is_array dest, src` |
|
||||
| `is_function(x)` | `is_func dest, src` |
|
||||
| `is_object(x)` | `is_record dest, src` |
|
||||
| `is_stone(x)` | `is_stone dest, src` |
|
||||
| `is_integer(x)` | `is_int dest, src` |
|
||||
| `is_text(x)` | `is_text dest, src` |
|
||||
| `is_number(x)` | `is_num dest, src` |
|
||||
| `is_logical(x)` | `is_bool dest, src` |
|
||||
| `is_null(x)` | `is_null dest, src` |
|
||||
| `length(x)` | `length dest, src` |
|
||||
| `push(arr, val)` | `push arr, val` |
|
||||
|
||||
These inlined opcodes have corresponding Mach VM implementations in `mach.c`.
|
||||
|
||||
## Unified Arithmetic
|
||||
|
||||
Arithmetic operations use generic opcodes: `add`, `subtract`, `multiply`, `divide`, `modulo`, `pow`, `negate`. There are no type-dispatched variants (e.g., no `add_int`/`add_float`).
|
||||
|
||||
The Mach VM handles arithmetic inline with a two-tier fast path. Since mcode's type guard dispatch guarantees both operands are numbers by the time arithmetic executes, the VM does not need polymorphic dispatch:
|
||||
|
||||
1. **Int-int fast path**: `JS_VALUE_IS_BOTH_INT` → native integer arithmetic with overflow check. If the result fits int32, returns int32; otherwise promotes to float64.
|
||||
2. **Float fallback**: `JS_ToFloat64` both operands → native floating-point arithmetic. Non-finite results (infinity, NaN) produce null.
|
||||
|
||||
Division and modulo additionally check for zero divisor (→ null). Power uses `pow()` with non-finite handling.
|
||||
|
||||
The legacy `reg_vm_binop()` function remains available for comparison operators and any non-mcode bytecode paths, but arithmetic ops no longer call it.
|
||||
|
||||
Bitwise operations (`shl`, `shr`, `ushr`, `bitand`, `bitor`, `bitxor`, `bitnot`) remain integer-only and disrupt if operands are not integers.
|
||||
|
||||
The QBE/native backend maps generic arithmetic to helper calls (`qbe.add`, `qbe.sub`, etc.). The vision for the native path is that with sufficient type inference, the backend can unbox proven-numeric values to raw registers, operate directly, and only rebox at boundaries (returns, calls, stores).
|
||||
|
||||
## Known-Number Add Shortcut
|
||||
|
||||
The `+` operator is the only arithmetic op that is polymorphic at the mcode level — `emit_add_decomposed` in `mcode.cm` emits a guard dispatch that checks for text (→ `concat`) before numeric (→ `add`). This dual dispatch means the temp slot is written by both `concat` (T_TEXT) and `add` (T_NUM), producing T_UNKNOWN in write-type analysis.
|
||||
|
||||
When either operand is a known number literal (e.g., `i + 1`, `x + 0.5`), `emit_add_decomposed` skips the text dispatch entirely and emits `emit_numeric_binop("add")` — a single `is_num` guard + `add` with no `concat` path. This is safe because text concatenation requires both operands to be text; a known number can never participate in concat.
|
||||
|
||||
This optimization eliminates 6-8 instructions from the add block (two `is_text` checks, two conditional jumps, `concat`, `jump`) and produces a clean single-type write path that works with write-type analysis.
|
||||
|
||||
Other arithmetic ops (`subtract`, `multiply`, etc.) always use `emit_numeric_binop` and never have this problem.
|
||||
|
||||
## Target Slot Propagation
|
||||
|
||||
For simple local variable assignments (`i = expr`), the mcode compiler passes the variable's register slot as a `target` to the expression compiler. Binary operations that use `emit_numeric_binop` (subtract, multiply, divide, modulo, pow) can write directly to the target slot instead of allocating a temp and emitting a `move`:
|
||||
|
||||
```
|
||||
// Before: i = i - 1
|
||||
subtract 7, 2, 6 // temp = i - 1
|
||||
move 2, 7 // i = temp
|
||||
|
||||
// After: i = i - 1
|
||||
subtract 2, 2, 6 // i = i - 1 (direct)
|
||||
```
|
||||
|
||||
The `+` operator uses target slot propagation when the target slot equals the left operand (`target == left_slot`), i.e. for self-assign patterns like `s = s + x`. In this case both `concat` and `add` write to the same slot that already holds the left operand, so write-type pollution is acceptable — the value is being updated in place. For other cases (target differs from left operand), `+` still allocates a temp to avoid polluting the target slot's write type with both T_TEXT and T_NUM.
|
||||
|
||||
This enables the VM's in-place append fast path for string concatenation: when `concat dest, dest, src` has the same destination and left operand, the VM can append directly to a mutable text's excess capacity without allocating.
|
||||
|
||||
## Debugging Tools
|
||||
|
||||
CLI tools inspect the IR at different stages:
|
||||
|
||||
- **`cell mcode --pretty`** — prints the raw Mcode IR after `mcode.cm`, before streamlining
|
||||
- **`cell streamline --stats`** — prints the IR after streamlining, with before/after instruction counts
|
||||
- **`cell streamline --types`** — prints the streamlined IR with type annotations on each instruction
|
||||
|
||||
Usage:
|
||||
```
|
||||
cell mcode --pretty <file.ce|file.cm>
|
||||
cell streamline --stats <file.ce|file.cm>
|
||||
cell streamline --types <file.ce|file.cm>
|
||||
```
|
||||
|
||||
## Tail Call Marking
|
||||
|
||||
When a function's return expression is a call (`stmt.tail == true` from the parser) and the function has no disruption handler, mcode.cm renames the final `invoke` instruction to `tail_invoke`. This is semantically identical to `invoke` in the current Mach VM, but marks the call site for future tail call optimization.
|
||||
|
||||
The disruption handler restriction exists because TCO would discard the current frame, but the handler must remain on the stack to catch disruptions from the callee.
|
||||
|
||||
`tail_invoke` is handled by the same passes as `invoke` in streamline (type tracking, algebraic simplification) and executes identically in the VM.
|
||||
|
||||
## Type Propagation Architecture
|
||||
|
||||
Type information flows through three compilation stages, each building on the previous:
|
||||
|
||||
### Stage 1: Parse-time type tags (parse.cm)
|
||||
|
||||
The parser assigns `type_tag` strings to scope variable entries when the type is syntactically obvious:
|
||||
|
||||
- **From initializers**: `def a = []` → `type_tag: "array"`, `def n = 42` → `type_tag: "integer"`, `def r = {}` → `type_tag: "record"`
|
||||
- **From usage patterns** (def only): `def x = null; x[] = v` infers `type_tag: "array"` from the push. `def x = null; x.foo = v` infers `type_tag: "record"` from property access.
|
||||
- **Type error detection** (def only): When a `def` variable has a known type_tag, provably wrong operations are compile errors:
|
||||
- Property access (`.`) on array
|
||||
- Push (`[]`) on non-array
|
||||
- Text key on array
|
||||
- Integer key on record
|
||||
|
||||
Only `def` (constant) variables participate in type inference and error detection. `var` variables can be reassigned, making their initializer type unreliable.
|
||||
|
||||
### Stage 2: Fold-time type propagation (fold.cm)
|
||||
|
||||
The fold pass extends type information through the AST:
|
||||
|
||||
- **Intrinsic folding**: `is_array(known_array)` folds to `true`. `length(known_array)` gets `hint: "array_length"`.
|
||||
- **Purity analysis**: Expressions involving only `is_*` intrinsic calls with pure arguments are considered pure. This enables dead code elimination for unused `var`/`def` bindings with pure initializers, and elimination of standalone pure call statements.
|
||||
- **Dead code**: Unused pure `var`/`def` declarations are removed. Standalone calls to pure intrinsics (where the result is discarded) are removed. Unreachable branches with constant conditions are removed.
|
||||
|
||||
The `pure_intrinsics` set currently contains only `is_*` sensory functions (`is_array`, `is_text`, `is_number`, `is_integer`, `is_function`, `is_logical`, `is_null`, `is_object`, `is_stone`). Other intrinsics like `text`, `number`, and `length` can disrupt on wrong argument types, so they are excluded — removing a call that would disrupt changes observable behavior.
|
||||
|
||||
### Stage 3: Streamline-time type tracking (streamline.cm)
|
||||
|
||||
The streamline optimizer uses a numeric type lattice (`T_INT`, `T_FLOAT`, `T_TEXT`, etc.) for fine-grained per-instruction tracking:
|
||||
|
||||
- **Backward inference** (pass 1): Scans typed operators to infer parameter types. Since parameters are `def` (immutable), inferred types persist across label boundaries.
|
||||
- **Write-type invariance** (pass 2): Scans all instructions to find local slots where every write produces the same type. These invariant types persist across label boundaries alongside parameter types.
|
||||
- **Forward tracking** (pass 3): `track_types` follows instruction execution order, tracking the type of each slot. Known-type operations set their destination type (e.g., `concat` → T_TEXT, `length` → T_INT). Generic arithmetic produces T_UNKNOWN. Type checks on unknown slots narrow the type on fallthrough.
|
||||
- **Type check elimination** (pass 3): When a slot's type is already known, `is_<type>` + conditional jump pairs are eliminated or converted to unconditional jumps.
|
||||
- **Dynamic access narrowing** (pass 3): `load_dynamic`/`store_dynamic` are narrowed to `load_field`/`store_field` or `load_index`/`store_index` when the key type is known.
|
||||
|
||||
Type information resets at label join points (since control flow merges could bring different types), except for parameter types from backward inference and write-invariant types from slot write-type analysis.
|
||||
|
||||
## Future Work
|
||||
|
||||
### Copy Propagation
|
||||
|
||||
A basic-block-local copy propagation pass would replace uses of a copied variable with its source, enabling further move elimination. An implementation was attempted but encountered an unsolved bug where 2-position instruction operand replacement produces incorrect code during self-hosting (the replacement logic for 3-position instructions works correctly). The root cause is not yet understood. See the project memory files for detailed notes.
|
||||
|
||||
### Expanded Purity Analysis
|
||||
|
||||
The current purity set is conservative (only `is_*`). It could be expanded by:
|
||||
|
||||
- **Argument-type-aware purity**: If all arguments to an intrinsic are known to be the correct types (via type_tag or slot_types), the call cannot disrupt and is safe to eliminate. For example, `length(known_array)` is pure but `length(unknown)` is not.
|
||||
- **User function purity**: Analyze user-defined function bodies during pre_scan. A function is pure if its body contains only pure expressions and calls to known-pure functions. This requires fixpoint iteration for mutual recursion.
|
||||
- **Callback-aware purity**: Intrinsics like `filter`, `find`, `reduce`, `some`, `every` are pure if their callback argument is pure.
|
||||
|
||||
### Move Type Resolution in Write-Type Analysis
|
||||
|
||||
Currently, `move` instructions produce T_UNKNOWN in write-type analysis. This prevents type propagation through moves — e.g., a slot written by `access 0` (T_INT) and `move` from an `add` result (T_NUM) merges to T_UNKNOWN instead of T_NUM.
|
||||
|
||||
A two-pass approach would fix this: first compute write types for all non-move instructions, then resolve moves by looking up the source slot's computed type. If the source has a known type, merge it into the destination; if unknown, skip the move (don't poison the destination with T_UNKNOWN).
|
||||
|
||||
This was implemented and tested but causes a bootstrap failure during self-hosting convergence. The root cause is not yet understood — the optimizer modifies its own bytecode, and the move resolution changes the type landscape enough to produce different code on each pass, preventing convergence. Further investigation is needed; the fix is correct in isolation but interacts badly with the self-hosting fixed-point iteration.
|
||||
|
||||
### Target Slot Propagation for Add with Known Numbers
|
||||
|
||||
When the known-number add shortcut applies (one operand is a literal number), the generated code uses `emit_numeric_binop` which has a single write path. Target slot propagation is already enabled for the self-assign case (`i = i + 1`), but when the target differs from the left operand and neither operand is a known number, a temp is still used. Refining the exclusion to check `is_known_number` would enable direct writes for the remaining non-self-assign cases like `j = i + 1`.
|
||||
|
||||
### Forward Type Narrowing from Typed Operations
|
||||
|
||||
With unified arithmetic (generic `add`/`subtract`/`multiply`/`divide`/`modulo`/`negate` instead of typed variants), this approach is no longer applicable. Typed comparisons (`eq_int`, `lt_float`, etc.) still exist and their operands have known types, but these are already handled by backward inference.
|
||||
|
||||
### Guard Hoisting for Parameters
|
||||
|
||||
When a type check on a parameter passes (falls through), the parameter's type could be promoted to `param_types` so it persists across label boundaries. This would allow the first type check on a parameter to prove its type for the entire function. However, this is unsound for polymorphic parameters — if a function is called with different argument types, the first check would wrongly eliminate checks for subsequent types.
|
||||
|
||||
A safe version would require proving that a parameter is monomorphic (called with only one type across all call sites), which requires interprocedural analysis.
|
||||
|
||||
**Note:** For local variables (non-parameters), the write-type invariance analysis (pass 2) achieves a similar effect safely — if every write to a slot produces the same type, that type persists across labels without needing to hoist any guard.
|
||||
|
||||
### Tail Call Optimization
|
||||
|
||||
`tail_invoke` instructions are currently marked but execute identically to `invoke`. Actual TCO would reuse the current call frame instead of creating a new one. This requires:
|
||||
|
||||
- Ensuring argument count matches (or the frame can be resized)
|
||||
- No live locals needed after the call (guaranteed by tail position)
|
||||
- No disruption handler on the current function (already enforced by the marking)
|
||||
- VM support in mach.c to rewrite the frame in place
|
||||
|
||||
### Interprocedural Type Inference
|
||||
|
||||
Currently all type inference is intraprocedural (within a single function). Cross-function analysis could:
|
||||
|
||||
- Infer return types from function bodies
|
||||
- Propagate argument types from call sites to callees
|
||||
- Specialize functions for known argument types (cloning)
|
||||
|
||||
### Strength Reduction
|
||||
|
||||
Common patterns that could be lowered to cheaper operations when operand types are known:
|
||||
|
||||
- `multiply x, 2` with proven-int operands → shift left
|
||||
- `divide x, 2` with proven-int → arithmetic shift right
|
||||
- `modulo x, power_of_2` with proven-int → bitwise and
|
||||
|
||||
### Numeric Unboxing (QBE/native path)
|
||||
|
||||
With unified arithmetic and backward type inference, the native backend can identify regions where numeric values remain in registers without boxing/unboxing:
|
||||
|
||||
1. **Guard once**: When backward inference proves a parameter is T_NUM, emit a single type guard at function entry.
|
||||
2. **Unbox**: Convert the tagged JSValue to a raw double register.
|
||||
3. **Operate**: Use native FP/int instructions directly (no function calls, no tag checks).
|
||||
4. **Rebox**: Convert back to tagged JSValue only at rebox points (function returns, calls, stores to arrays/records).
|
||||
|
||||
This requires inserting `unbox`/`rebox` IR annotations (no-ops in the Mach VM, meaningful only to QBE).
|
||||
|
||||
### Loop-Invariant Code Motion
|
||||
|
||||
Type checks that are invariant across loop iterations (checking a variable that doesn't change in the loop body) could be hoisted above the loop. This would require identifying loop boundaries and proving invariance.
|
||||
|
||||
### Algebraic Identity Optimization
|
||||
|
||||
With unified arithmetic, algebraic identities (x+0→x, x*1→x, x*0→0, x/1→x) require knowing operand values at compile time. Since generic `add`/`multiply` operate on any numeric type, the constant-tracking logic in `simplify_algebra` could be extended to handle these for known-constant slots.
|
||||
|
||||
## Nop Convention
|
||||
|
||||
Eliminated instructions are replaced with strings matching `_nop_<prefix>_<counter>`. The prefix identifies which pass created the nop. Nop strings are:
|
||||
|
||||
- Skipped during interpretation (the VM ignores them)
|
||||
- Skipped during QBE emission
|
||||
- Not counted in instruction statistics
|
||||
- Preserved in the instruction array to maintain positional stability for jump targets
|
||||
239
docs/testing.md
Normal file
239
docs/testing.md
Normal file
@@ -0,0 +1,239 @@
|
||||
---
|
||||
title: "Testing"
|
||||
description: "Writing and running tests in ƿit"
|
||||
weight: 45
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
ƿit has built-in support for writing and running tests. Tests live in the `tests/` directory of a package and are `.cm` modules that return a record of test functions.
|
||||
|
||||
## Writing Tests
|
||||
|
||||
A test file returns a record where each key starting with `test_` is a test function. A test passes if it returns `null` (or nothing). It fails if it returns a text string describing the failure.
|
||||
|
||||
```javascript
|
||||
// tests/math.cm
|
||||
return {
|
||||
test_addition: function() {
|
||||
if (1 + 2 != 3) return "expected 3"
|
||||
},
|
||||
|
||||
test_division: function() {
|
||||
if (10 / 3 != 3.333333333333333333) return "unexpected result"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Test functions take no arguments. Use early returns with a failure message to report errors:
|
||||
|
||||
```javascript
|
||||
test_array_push: function() {
|
||||
var a = [1, 2]
|
||||
a[] = 3
|
||||
if (length(a) != 3) return "expected length 3, got " + text(length(a))
|
||||
if (a[2] != 3) return "expected a[2] to be 3"
|
||||
}
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
pit test # run all tests in current package
|
||||
pit test suite # run a specific test file (tests/suite.cm)
|
||||
pit test tests/math # same, with explicit path
|
||||
pit test all # run all tests in current package
|
||||
pit test package <name> # run all tests in a named package
|
||||
pit test package <name> <test> # run a specific test in a named package
|
||||
pit test package all # run tests from all installed packages
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
```bash
|
||||
pit test suite -g # run GC after each test (useful for detecting leaks)
|
||||
pit test suite --verify # enable IR verification during compilation
|
||||
pit test suite --diff # run each test optimized and unoptimized, compare results
|
||||
```
|
||||
|
||||
`--verify` and `--diff` can be combined:
|
||||
|
||||
```bash
|
||||
pit test suite --verify --diff
|
||||
```
|
||||
|
||||
## IR Verification
|
||||
|
||||
The `--verify` flag enables structural validation of the compiler's intermediate representation after each optimizer pass. This catches bugs like invalid slot references, broken jump targets, and malformed instructions.
|
||||
|
||||
When verification fails, errors are printed with the pass name that introduced them:
|
||||
|
||||
```
|
||||
[verify_ir] slot_bounds: slot 12 out of range 0..9 in instruction add_int
|
||||
[verify_ir] 1 errors after dead_code_elimination
|
||||
```
|
||||
|
||||
IR verification adds overhead and is intended for development, not production use.
|
||||
|
||||
## Differential Testing
|
||||
|
||||
Differential testing runs each test through two paths — with the optimizer enabled and with it disabled — and compares results. Any mismatch between the two indicates an optimizer bug.
|
||||
|
||||
### Inline Mode
|
||||
|
||||
The `--diff` flag on `pit test` runs each test module through both paths during a normal test run:
|
||||
|
||||
```bash
|
||||
pit test suite --diff
|
||||
```
|
||||
|
||||
Output includes a mismatch count at the end:
|
||||
|
||||
```
|
||||
Tests: 493 passed, 0 failed, 493 total
|
||||
Diff mismatches: 0
|
||||
```
|
||||
|
||||
### Standalone Mode
|
||||
|
||||
`pit diff` is a dedicated differential testing tool with detailed mismatch reporting:
|
||||
|
||||
```bash
|
||||
pit diff # diff all test files in current package
|
||||
pit diff suite # diff a specific test file
|
||||
pit diff tests/math # same, with explicit path
|
||||
```
|
||||
|
||||
For each test function, it reports whether the optimized and unoptimized results match:
|
||||
|
||||
```
|
||||
tests/suite.cm: 493 passed, 0 failed
|
||||
----------------------------------------
|
||||
Diff: 493 passed, 0 failed, 493 total
|
||||
```
|
||||
|
||||
When a mismatch is found:
|
||||
|
||||
```
|
||||
tests/suite.cm: 492 passed, 1 failed
|
||||
MISMATCH: test_foo: result mismatch opt=42 noopt=43
|
||||
```
|
||||
|
||||
## ASAN for Native AOT
|
||||
|
||||
When debugging native (`shop.use_native`) crashes, there are two useful sanitizer workflows.
|
||||
|
||||
### 1) AOT-only sanitizer (fastest loop)
|
||||
|
||||
Enable sanitizer flags for generated native modules by creating a marker file:
|
||||
|
||||
```bash
|
||||
touch .cell/asan_aot
|
||||
cell --dev bench --native fibonacci
|
||||
```
|
||||
|
||||
This adds `-fsanitize=address -fno-omit-frame-pointer` to AOT module compilation.
|
||||
|
||||
Disable it with:
|
||||
|
||||
```bash
|
||||
rm -f .cell/asan_aot
|
||||
```
|
||||
|
||||
### 2) Full runtime sanitizer (CLI + runtime + AOT)
|
||||
|
||||
Build an ASAN-instrumented `cell` binary:
|
||||
|
||||
```bash
|
||||
meson setup build-asan -Dbuildtype=debug -Db_sanitize=address
|
||||
CCACHE_DISABLE=1 meson compile -C build-asan
|
||||
ASAN_OPTIONS=abort_on_error=1:detect_leaks=0 ./build-asan/cell --dev bench --native fibonacci
|
||||
```
|
||||
|
||||
This catches bugs crossing the boundary between generated dylibs and runtime helpers.
|
||||
|
||||
If stale native artifacts are suspected after compiler/runtime changes, clear build outputs first:
|
||||
|
||||
```bash
|
||||
cell --dev clean shop --build
|
||||
```
|
||||
|
||||
## Fuzz Testing
|
||||
|
||||
The fuzzer generates random self-checking programs, compiles them, and runs them through both optimized and unoptimized paths. Each generated program contains test functions that validate their own expected results, so failures catch both correctness bugs and optimizer mismatches.
|
||||
|
||||
```bash
|
||||
pit fuzz # 100 iterations, random seed
|
||||
pit fuzz 500 # 500 iterations, random seed
|
||||
pit fuzz --seed 42 # 100 iterations, deterministic seed
|
||||
pit fuzz 1000 --seed 42 # 1000 iterations, deterministic seed
|
||||
```
|
||||
|
||||
The fuzzer generates programs that exercise:
|
||||
|
||||
- Integer and float arithmetic with known expected results
|
||||
- Control flow (if/else, while loops)
|
||||
- Closures and captured variable mutation
|
||||
- Records and property access
|
||||
- Arrays and iteration
|
||||
- Higher-order functions
|
||||
- Disruption handling
|
||||
- Text concatenation
|
||||
|
||||
On failure, the generated source is saved to `tests/fuzz_failures/` for reproduction:
|
||||
|
||||
```
|
||||
Fuzzing: 1000 iterations, starting seed=42
|
||||
FAIL seed=57: diff fuzz_3: opt=10 noopt=11
|
||||
saved to tests/fuzz_failures/seed_57.cm
|
||||
----------------------------------------
|
||||
Fuzz: 999 passed, 1 failed, 1000 total
|
||||
Failures saved to tests/fuzz_failures/
|
||||
```
|
||||
|
||||
Saved failure files are valid `.cm` modules that can be run directly or added to the test suite.
|
||||
|
||||
## Compile-Time Diagnostics Tests
|
||||
|
||||
The `tests/compile.cm` test suite verifies that the type checker catches provably wrong operations at compile time. It works by compiling source snippets through the pipeline with `_warn` enabled and checking that the expected diagnostics are emitted.
|
||||
|
||||
```javascript
|
||||
var shop = use('internal/shop')
|
||||
var streamline = use('streamline')
|
||||
|
||||
function get_diagnostics(src) {
|
||||
fd.slurpwrite(tmpfile, stone(blob(src)))
|
||||
var compiled = shop.mcode_file(tmpfile)
|
||||
compiled._warn = true
|
||||
var optimized = streamline(compiled)
|
||||
if (optimized._diagnostics == null) return []
|
||||
return optimized._diagnostics
|
||||
}
|
||||
```
|
||||
|
||||
The suite covers:
|
||||
- **Store errors**: storing named property on array, numeric index on record, property/index on text, push on text/record
|
||||
- **Invoke errors**: invoking null, number, text
|
||||
- **Warnings**: named property access on array/text, record key on record
|
||||
- **Clean code**: valid operations produce no diagnostics
|
||||
|
||||
Run the compile diagnostics tests with:
|
||||
|
||||
```bash
|
||||
pit test compile
|
||||
```
|
||||
|
||||
## Test File Organization
|
||||
|
||||
Tests live in the `tests/` directory of a package:
|
||||
|
||||
```
|
||||
mypackage/
|
||||
├── pit.toml
|
||||
├── math.cm
|
||||
└── tests/
|
||||
├── suite.cm # main test suite
|
||||
├── math.cm # math-specific tests
|
||||
└── disrupt.cm # disruption tests
|
||||
```
|
||||
|
||||
All `.cm` files under `tests/` are discovered automatically by `pit test`.
|
||||
@@ -5,7 +5,7 @@ weight: 86
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
Wota is a binary message format for local inter-process communication. It is similar to Nota but works at word granularity (64-bit words) rather than byte granularity. Wota arrangements are less compact than Nota but faster to arrange and consume.
|
||||
Wota is a binary message format for local inter-process communication. It is similar to Nota but works at word granularity (64-bit words) rather than byte granularity. Wota arrangements are less compact than Nota but faster to arrange and consume. Wota is an internal module: `use('internal/wota')`.
|
||||
|
||||
Wota stands for Word Object Transfer Arrangement.
|
||||
|
||||
|
||||
13
dump_ir.ce
Normal file
13
dump_ir.ce
Normal file
@@ -0,0 +1,13 @@
|
||||
// cell dump_ir - Dump intermediate representation for a source file
|
||||
|
||||
var shop = use('internal/shop')
|
||||
var json = use('json')
|
||||
|
||||
var optimized = shop.compile_file(args[0])
|
||||
|
||||
var instrs = optimized.main.instructions
|
||||
var i = 0
|
||||
while (i < length(instrs)) {
|
||||
log.compile(text(i) + ': ' + json.encode(instrs[i]))
|
||||
i = i + 1
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
// Document analysis module.
|
||||
// Call make(tokenize_mod, parse_mod) to get an analysis object.
|
||||
// Call make(tokenize_mod, parse_mod, index_mod) to get an analysis object.
|
||||
|
||||
var json = use('json')
|
||||
|
||||
// Create an analysis module bound to the tokenize and parse functions.
|
||||
var make = function(tokenize_mod, parse_mod) {
|
||||
// Create an analysis module bound to the tokenize, parse, and index functions.
|
||||
var make = function(tokenize_mod, parse_mod, index_mod) {
|
||||
|
||||
// Tokenize and parse a document, storing the results.
|
||||
var update = function(docs, uri, params) {
|
||||
@@ -36,13 +36,24 @@ var make = function(tokenize_mod, parse_mod) {
|
||||
}
|
||||
}
|
||||
|
||||
var idx = null
|
||||
var do_index = function() {
|
||||
idx = index_mod.index_ast(ast, (tok_result != null) ? tok_result.tokens : [], uri)
|
||||
} disruption {
|
||||
// indexing failure is non-fatal
|
||||
}
|
||||
if (ast != null && index_mod != null) {
|
||||
do_index()
|
||||
}
|
||||
|
||||
doc = {
|
||||
uri: uri,
|
||||
text: src,
|
||||
version: version,
|
||||
tokens: (tok_result != null) ? tok_result.tokens : [],
|
||||
ast: ast,
|
||||
errors: errors
|
||||
errors: errors,
|
||||
index: idx
|
||||
}
|
||||
docs[uri] = doc
|
||||
return doc
|
||||
|
||||
@@ -13,9 +13,11 @@ var symbols = use('symbols')
|
||||
// These are the same functions the compiler uses internally.
|
||||
var tokenize_mod = use('tokenize')
|
||||
var parse_mod = use('parse')
|
||||
var index_mod = use('index')
|
||||
var explain_mod = use('explain')
|
||||
|
||||
// Create analysis module bound to tokenize/parse
|
||||
var analysis = analysis_make(tokenize_mod, parse_mod)
|
||||
// Create analysis module bound to tokenize/parse/index
|
||||
var analysis = analysis_make(tokenize_mod, parse_mod, index_mod)
|
||||
|
||||
// Document store: URI -> {text, version, ast, tokens, errors}
|
||||
var docs = {}
|
||||
@@ -54,7 +56,9 @@ var handle_initialize = function(id, params) {
|
||||
},
|
||||
hoverProvider: true,
|
||||
definitionProvider: true,
|
||||
documentSymbolProvider: true
|
||||
documentSymbolProvider: true,
|
||||
referencesProvider: true,
|
||||
renameProvider: {prepareProvider: true}
|
||||
},
|
||||
serverInfo: {
|
||||
name: "pit-lsp",
|
||||
@@ -144,6 +148,159 @@ var handle_document_symbol = function(id, params) {
|
||||
protocol.respond(id, result)
|
||||
}
|
||||
|
||||
// Handle textDocument/references request.
|
||||
var handle_references = function(id, params) {
|
||||
var uri = params.textDocument.uri
|
||||
var pos = params.position
|
||||
var doc = docs[uri]
|
||||
var result = []
|
||||
var tok = null
|
||||
var name = null
|
||||
var refs = null
|
||||
var _i = 0
|
||||
var ref = null
|
||||
var expl = null
|
||||
var sym_result = null
|
||||
if (doc != null && doc.index != null) {
|
||||
tok = analysis.token_at(doc, pos.line, pos.character)
|
||||
if (tok != null && tok.kind == "name" && tok.value != null) {
|
||||
name = tok.value
|
||||
refs = doc.index.reverse_refs[name]
|
||||
if (refs != null) {
|
||||
_i = 0
|
||||
while (_i < length(refs)) {
|
||||
ref = refs[_i]
|
||||
if (ref.span != null) {
|
||||
result[] = {
|
||||
uri: uri,
|
||||
range: {
|
||||
start: {line: ref.span.from_row, character: ref.span.from_col},
|
||||
end: {line: ref.span.to_row, character: ref.span.to_col}
|
||||
}
|
||||
}
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
}
|
||||
// Also include the declaration itself if found
|
||||
expl = explain_mod.make(doc.index)
|
||||
sym_result = expl.by_symbol(name)
|
||||
if (sym_result != null && length(sym_result.symbols) > 0) {
|
||||
_i = 0
|
||||
while (_i < length(sym_result.symbols)) {
|
||||
if (sym_result.symbols[_i].decl_span != null) {
|
||||
result[] = {
|
||||
uri: uri,
|
||||
range: {
|
||||
start: {line: sym_result.symbols[_i].decl_span.from_row, character: sym_result.symbols[_i].decl_span.from_col},
|
||||
end: {line: sym_result.symbols[_i].decl_span.to_row, character: sym_result.symbols[_i].decl_span.to_col}
|
||||
}
|
||||
}
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
protocol.respond(id, result)
|
||||
}
|
||||
|
||||
// Handle textDocument/prepareRename request.
|
||||
var handle_prepare_rename = function(id, params) {
|
||||
var uri = params.textDocument.uri
|
||||
var pos = params.position
|
||||
var doc = docs[uri]
|
||||
var tok = null
|
||||
var name = null
|
||||
var result = null
|
||||
var expl = null
|
||||
var sym_result = null
|
||||
if (doc != null) {
|
||||
tok = analysis.token_at(doc, pos.line, pos.character)
|
||||
if (tok != null && tok.kind == "name" && tok.value != null) {
|
||||
name = tok.value
|
||||
// Don't allow renaming intrinsics
|
||||
if (doc.index != null) {
|
||||
expl = explain_mod.make(doc.index)
|
||||
sym_result = expl.by_symbol(name)
|
||||
if (sym_result != null && length(sym_result.symbols) > 0) {
|
||||
result = {
|
||||
range: {
|
||||
start: {line: tok.from_row, character: tok.from_column},
|
||||
end: {line: tok.to_row, character: tok.to_column}
|
||||
},
|
||||
placeholder: name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
protocol.respond(id, result)
|
||||
}
|
||||
|
||||
// Handle textDocument/rename request.
|
||||
var handle_rename = function(id, params) {
|
||||
var uri = params.textDocument.uri
|
||||
var pos = params.position
|
||||
var new_name = params.newName
|
||||
var doc = docs[uri]
|
||||
var tok = null
|
||||
var name = null
|
||||
var edits = []
|
||||
var refs = null
|
||||
var _i = 0
|
||||
var ref = null
|
||||
var expl = null
|
||||
var sym_result = null
|
||||
if (doc != null && doc.index != null) {
|
||||
tok = analysis.token_at(doc, pos.line, pos.character)
|
||||
if (tok != null && tok.kind == "name" && tok.value != null) {
|
||||
name = tok.value
|
||||
expl = explain_mod.make(doc.index)
|
||||
sym_result = expl.by_symbol(name)
|
||||
// Add edit for declaration
|
||||
if (sym_result != null && length(sym_result.symbols) > 0) {
|
||||
_i = 0
|
||||
while (_i < length(sym_result.symbols)) {
|
||||
if (sym_result.symbols[_i].decl_span != null) {
|
||||
edits[] = {
|
||||
range: {
|
||||
start: {line: sym_result.symbols[_i].decl_span.from_row, character: sym_result.symbols[_i].decl_span.from_col},
|
||||
end: {line: sym_result.symbols[_i].decl_span.to_row, character: sym_result.symbols[_i].decl_span.to_col}
|
||||
},
|
||||
newText: new_name
|
||||
}
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
}
|
||||
// Add edits for all references
|
||||
refs = doc.index.reverse_refs[name]
|
||||
if (refs != null) {
|
||||
_i = 0
|
||||
while (_i < length(refs)) {
|
||||
ref = refs[_i]
|
||||
if (ref.span != null) {
|
||||
edits[] = {
|
||||
range: {
|
||||
start: {line: ref.span.from_row, character: ref.span.from_col},
|
||||
end: {line: ref.span.to_row, character: ref.span.to_col}
|
||||
},
|
||||
newText: new_name
|
||||
}
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var changes = {}
|
||||
if (length(edits) > 0) {
|
||||
changes[uri] = edits
|
||||
}
|
||||
protocol.respond(id, {changes: changes})
|
||||
}
|
||||
|
||||
// Dispatch a single message. Wrapped in a function for disruption handling.
|
||||
var dispatch_message = function(msg) {
|
||||
var method = msg.method
|
||||
@@ -167,6 +324,12 @@ var dispatch_message = function(msg) {
|
||||
handle_definition(msg.id, msg.params)
|
||||
} else if (method == "textDocument/documentSymbol") {
|
||||
handle_document_symbol(msg.id, msg.params)
|
||||
} else if (method == "textDocument/references") {
|
||||
handle_references(msg.id, msg.params)
|
||||
} else if (method == "textDocument/prepareRename") {
|
||||
handle_prepare_rename(msg.id, msg.params)
|
||||
} else if (method == "textDocument/rename") {
|
||||
handle_rename(msg.id, msg.params)
|
||||
} else if (method == "shutdown") {
|
||||
protocol.respond(msg.id, null)
|
||||
return "shutdown"
|
||||
|
||||
@@ -91,14 +91,12 @@ var document_symbols = function(doc) {
|
||||
}
|
||||
|
||||
// Find the declaration location of a name at a given position.
|
||||
// Uses the semantic index when available, falls back to AST walk.
|
||||
var definition = function(doc, line, col, token_at) {
|
||||
var tok = token_at(doc, line, col)
|
||||
var ast = doc.ast
|
||||
var name = null
|
||||
var _i = 0
|
||||
var _j = 0
|
||||
var scope = null
|
||||
var v = null
|
||||
var sym = null
|
||||
var decl = null
|
||||
|
||||
if (tok == null || tok.kind != "name" || tok.value == null) {
|
||||
@@ -107,32 +105,18 @@ var definition = function(doc, line, col, token_at) {
|
||||
|
||||
name = tok.value
|
||||
|
||||
if (ast == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Search through scopes for the variable declaration
|
||||
if (ast.scopes != null) {
|
||||
// Use the semantic index if available
|
||||
if (doc.index != null) {
|
||||
_i = 0
|
||||
while (_i < length(ast.scopes)) {
|
||||
scope = ast.scopes[_i]
|
||||
if (scope.vars != null) {
|
||||
_j = 0
|
||||
while (_j < length(scope.vars)) {
|
||||
v = scope.vars[_j]
|
||||
if (v.name == name) {
|
||||
decl = find_declaration(ast.statements, name)
|
||||
if (decl != null) {
|
||||
return {
|
||||
uri: doc.uri,
|
||||
range: {
|
||||
start: {line: decl.from_row, character: decl.from_column},
|
||||
end: {line: decl.to_row, character: decl.to_column}
|
||||
}
|
||||
}
|
||||
}
|
||||
while (_i < length(doc.index.symbols)) {
|
||||
sym = doc.index.symbols[_i]
|
||||
if (sym.name == name && sym.decl_span != null) {
|
||||
return {
|
||||
uri: doc.uri,
|
||||
range: {
|
||||
start: {line: sym.decl_span.from_row, character: sym.decl_span.from_col},
|
||||
end: {line: sym.decl_span.to_row, character: sym.decl_span.to_col}
|
||||
}
|
||||
_j = _j + 1
|
||||
}
|
||||
}
|
||||
_i = _i + 1
|
||||
@@ -140,13 +124,15 @@ var definition = function(doc, line, col, token_at) {
|
||||
}
|
||||
|
||||
// Fallback: walk statements for var/def with this name
|
||||
decl = find_declaration(ast.statements, name)
|
||||
if (decl != null) {
|
||||
return {
|
||||
uri: doc.uri,
|
||||
range: {
|
||||
start: {line: decl.from_row, character: decl.from_column},
|
||||
end: {line: decl.to_row, character: decl.to_column}
|
||||
if (doc.ast != null) {
|
||||
decl = find_declaration(doc.ast.statements, name)
|
||||
if (decl != null) {
|
||||
return {
|
||||
uri: doc.uri,
|
||||
range: {
|
||||
start: {line: decl.from_row, character: decl.from_column},
|
||||
end: {line: decl.to_row, character: decl.to_column}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"vscode-languageserver-protocol": "^3.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.33",
|
||||
"@types/vscode": "^1.75.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
|
||||
@@ -8,15 +8,17 @@ $contact((actor, reason) => {
|
||||
log.console(reason)
|
||||
}
|
||||
}, {
|
||||
address: "108.210.60.32", // NAT server's public IP
|
||||
address: "108.210.60.32",
|
||||
port: 4000,
|
||||
actor: $self
|
||||
})
|
||||
|
||||
$receiver(e => {
|
||||
switch(e.type) {
|
||||
case 'greet':
|
||||
log.console(`hello!`)
|
||||
break
|
||||
var handlers = {
|
||||
greet: function() {
|
||||
log.console(`hello!`)
|
||||
}
|
||||
}
|
||||
|
||||
$receiver(e => {
|
||||
if (handlers[e.type]) handlers[e.type]()
|
||||
})
|
||||
138
explain.ce
Normal file
138
explain.ce
Normal file
@@ -0,0 +1,138 @@
|
||||
// cell explain — Query the semantic index for a source file.
|
||||
//
|
||||
// Usage:
|
||||
// cell explain --span file.ce:10:5 Find symbol at position
|
||||
// cell explain --symbol add_node Find symbol by name
|
||||
// cell explain --symbol add_node file.ce Limit to specific file
|
||||
// cell explain --help Show this help
|
||||
|
||||
var fd = use('fd')
|
||||
var json = use('json')
|
||||
var explain_mod = use('explain')
|
||||
var shop = use('internal/shop')
|
||||
|
||||
var mode = null
|
||||
var span_arg = null
|
||||
var symbol_name = null
|
||||
var files = []
|
||||
var i = 0
|
||||
var parts = null
|
||||
var filename = null
|
||||
var line = null
|
||||
var col = null
|
||||
var idx = null
|
||||
var indexes = []
|
||||
var explain = null
|
||||
var result = null
|
||||
|
||||
for (i = 0; i < length(args); i++) {
|
||||
if (args[i] == '--span') {
|
||||
mode = "span"
|
||||
if (i + 1 < length(args)) {
|
||||
span_arg = args[i + 1]
|
||||
i = i + 1
|
||||
} else {
|
||||
log.error('--span requires file:line:col')
|
||||
$stop()
|
||||
}
|
||||
} else if (args[i] == '--symbol') {
|
||||
mode = "symbol"
|
||||
if (i + 1 < length(args)) {
|
||||
symbol_name = args[i + 1]
|
||||
i = i + 1
|
||||
} else {
|
||||
log.error('--symbol requires a name')
|
||||
$stop()
|
||||
}
|
||||
} else if (args[i] == '--help' || args[i] == '-h') {
|
||||
log.console("Usage: cell explain [options]")
|
||||
log.console("")
|
||||
log.console("Query the semantic index for a source file.")
|
||||
log.console("")
|
||||
log.console("Options:")
|
||||
log.console(" --span file:line:col Find symbol at position")
|
||||
log.console(" --symbol name <file>... Find symbol by name across files")
|
||||
$stop()
|
||||
} else if (!starts_with(args[i], '-')) {
|
||||
files[] = args[i]
|
||||
}
|
||||
}
|
||||
|
||||
if (mode == null) {
|
||||
log.error('Specify --span or --symbol. Use --help for usage.')
|
||||
$stop()
|
||||
}
|
||||
|
||||
if (mode == "span") {
|
||||
parts = array(span_arg, ":")
|
||||
if (length(parts) < 3) {
|
||||
log.error('--span requires file:line:col format')
|
||||
$stop()
|
||||
}
|
||||
|
||||
filename = parts[0]
|
||||
line = number(parts[1])
|
||||
col = number(parts[2])
|
||||
|
||||
if (!fd.is_file(filename)) {
|
||||
log.error('File not found: ' + filename)
|
||||
$stop()
|
||||
}
|
||||
|
||||
idx = shop.index_file(filename)
|
||||
explain = explain_mod.make(idx)
|
||||
result = explain.at_span(line, col)
|
||||
|
||||
if (result == null) {
|
||||
log.console("Nothing found at " + filename + ":" + text(line) + ":" + text(col))
|
||||
} else {
|
||||
log.compile(json.encode(result, true))
|
||||
}
|
||||
}
|
||||
|
||||
if (mode == "symbol") {
|
||||
if (length(files) == 0) {
|
||||
log.error('--symbol requires at least one file argument')
|
||||
$stop()
|
||||
}
|
||||
|
||||
// Validate all files exist.
|
||||
i = 0
|
||||
while (i < length(files)) {
|
||||
if (!fd.is_file(files[i])) {
|
||||
log.error('File not found: ' + files[i])
|
||||
$stop()
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
if (length(files) == 1) {
|
||||
filename = files[0]
|
||||
idx = shop.index_file(filename)
|
||||
explain = explain_mod.make(idx)
|
||||
result = explain.by_symbol(symbol_name)
|
||||
|
||||
if (result == null || length(result.symbols) == 0) {
|
||||
log.console("Symbol '" + symbol_name + "' not found in " + filename)
|
||||
} else {
|
||||
log.compile(json.encode(result, true))
|
||||
}
|
||||
} else if (length(files) > 1) {
|
||||
indexes = []
|
||||
i = 0
|
||||
while (i < length(files)) {
|
||||
idx = shop.index_file(files[i])
|
||||
indexes[] = idx
|
||||
i = i + 1
|
||||
}
|
||||
result = explain_mod.explain_across(indexes, symbol_name)
|
||||
|
||||
if (result == null || length(result.symbols) == 0) {
|
||||
log.console("Symbol '" + symbol_name + "' not found in " + text(length(files)) + " files")
|
||||
} else {
|
||||
log.compile(json.encode(result, true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$stop()
|
||||
238
explain.cm
Normal file
238
explain.cm
Normal file
@@ -0,0 +1,238 @@
|
||||
// explain.cm — Query module over a semantic index.
|
||||
//
|
||||
// Usage:
|
||||
// var explain = use('explain').make(index)
|
||||
// explain.at_span(line, col)
|
||||
// explain.by_symbol(name)
|
||||
// explain.call_chain(symbol_id, depth)
|
||||
|
||||
// Check if a position (line, col) falls inside a span.
|
||||
var span_contains = function(span, line, col) {
|
||||
if (line < span.from_row || line > span.to_row) return false
|
||||
if (line == span.from_row && col < span.from_col) return false
|
||||
if (line == span.to_row && col > span.to_col) return false
|
||||
return true
|
||||
}
|
||||
|
||||
// Create an explain interface bound to a single file index.
|
||||
var make = function(index) {
|
||||
|
||||
// Find symbol or reference at a given line/col position.
|
||||
var at_span = function(line, col) {
|
||||
var _i = 0
|
||||
var sym = null
|
||||
var ref = null
|
||||
var found_sym = null
|
||||
var found_ref = null
|
||||
var result_refs = []
|
||||
var result_calls = []
|
||||
|
||||
// Search symbols for one whose decl_span contains (line, col).
|
||||
_i = 0
|
||||
while (_i < length(index.symbols)) {
|
||||
sym = index.symbols[_i]
|
||||
if (sym.decl_span != null && span_contains(sym.decl_span, line, col)) {
|
||||
found_sym = sym
|
||||
break
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
|
||||
// If no symbol found, search references.
|
||||
if (found_sym == null) {
|
||||
_i = 0
|
||||
while (_i < length(index.references)) {
|
||||
ref = index.references[_i]
|
||||
if (ref.span != null && span_contains(ref.span, line, col)) {
|
||||
found_ref = ref
|
||||
// Look up the symbol this reference points to.
|
||||
if (ref.symbol_id != null) {
|
||||
_i = 0
|
||||
while (_i < length(index.symbols)) {
|
||||
if (index.symbols[_i].symbol_id == ref.symbol_id) {
|
||||
found_sym = index.symbols[_i]
|
||||
break
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
}
|
||||
|
||||
if (found_sym == null && found_ref == null) return null
|
||||
|
||||
// Gather all references to this symbol.
|
||||
if (found_sym != null && index.reverse_refs[found_sym.name] != null) {
|
||||
result_refs = index.reverse_refs[found_sym.name]
|
||||
}
|
||||
|
||||
// Gather call sites.
|
||||
_i = 0
|
||||
while (_i < length(index.call_sites)) {
|
||||
if (found_sym != null) {
|
||||
if (index.call_sites[_i].callee_symbol_id == found_sym.symbol_id ||
|
||||
(index.call_sites[_i].callee_symbol_id == null && index.call_sites[_i].callee == found_sym.name)) {
|
||||
result_calls[] = index.call_sites[_i]
|
||||
}
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
|
||||
return {
|
||||
symbol: found_sym,
|
||||
reference: found_ref,
|
||||
references: result_refs,
|
||||
call_sites: result_calls,
|
||||
imports: index.imports
|
||||
}
|
||||
}
|
||||
|
||||
// Find all symbols matching a name.
|
||||
var by_symbol = function(name) {
|
||||
var _i = 0
|
||||
var matches = []
|
||||
var result_refs = []
|
||||
var result_calls = []
|
||||
|
||||
// Find matching symbols.
|
||||
_i = 0
|
||||
while (_i < length(index.symbols)) {
|
||||
if (index.symbols[_i].name == name) {
|
||||
matches[] = index.symbols[_i]
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
|
||||
// Gather all references to this name.
|
||||
if (index.reverse_refs[name] != null) {
|
||||
result_refs = index.reverse_refs[name]
|
||||
}
|
||||
|
||||
// Gather call sites where this name is the callee.
|
||||
_i = 0
|
||||
while (_i < length(index.call_sites)) {
|
||||
if (index.call_sites[_i].callee == name) {
|
||||
result_calls[] = index.call_sites[_i]
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
|
||||
return {
|
||||
symbols: matches,
|
||||
references: result_refs,
|
||||
call_sites: result_calls
|
||||
}
|
||||
}
|
||||
|
||||
// Build a call chain from/to a symbol.
|
||||
var call_chain = function(symbol_id, depth) {
|
||||
var max_depth = (depth != null) ? depth : 2
|
||||
var callers = []
|
||||
var callees = []
|
||||
var _i = 0
|
||||
var cs = null
|
||||
|
||||
// Callees: calls made FROM this symbol.
|
||||
_i = 0
|
||||
while (_i < length(index.call_sites)) {
|
||||
cs = index.call_sites[_i]
|
||||
if (cs.enclosing == symbol_id) {
|
||||
callees[] = {
|
||||
callee: cs.callee,
|
||||
callee_symbol_id: cs.callee_symbol_id,
|
||||
span: cs.span,
|
||||
args_count: cs.args_count
|
||||
}
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
|
||||
// Callers: calls TO this symbol.
|
||||
_i = 0
|
||||
while (_i < length(index.call_sites)) {
|
||||
cs = index.call_sites[_i]
|
||||
if (cs.callee_symbol_id == symbol_id) {
|
||||
callers[] = {
|
||||
from: cs.enclosing,
|
||||
span: cs.span,
|
||||
args_count: cs.args_count
|
||||
}
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
|
||||
return {
|
||||
symbol_id: symbol_id,
|
||||
callers: callers,
|
||||
callees: callees,
|
||||
depth: max_depth
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
at_span: at_span,
|
||||
by_symbol: by_symbol,
|
||||
call_chain: call_chain
|
||||
}
|
||||
}
|
||||
|
||||
// Search across multiple file indexes.
|
||||
var explain_across = function(indexes, name) {
|
||||
var _i = 0
|
||||
var _j = 0
|
||||
var all_symbols = []
|
||||
var all_refs = []
|
||||
var all_calls = []
|
||||
var idx = null
|
||||
var refs = null
|
||||
|
||||
_i = 0
|
||||
while (_i < length(indexes)) {
|
||||
idx = indexes[_i]
|
||||
|
||||
// Gather symbols.
|
||||
_j = 0
|
||||
while (_j < length(idx.symbols)) {
|
||||
if (idx.symbols[_j].name == name) {
|
||||
all_symbols[] = idx.symbols[_j]
|
||||
}
|
||||
_j = _j + 1
|
||||
}
|
||||
|
||||
// Gather references.
|
||||
refs = idx.reverse_refs[name]
|
||||
if (refs != null) {
|
||||
_j = 0
|
||||
while (_j < length(refs)) {
|
||||
all_refs[] = refs[_j]
|
||||
_j = _j + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Gather call sites.
|
||||
_j = 0
|
||||
while (_j < length(idx.call_sites)) {
|
||||
if (idx.call_sites[_j].callee == name) {
|
||||
all_calls[] = idx.call_sites[_j]
|
||||
}
|
||||
_j = _j + 1
|
||||
}
|
||||
|
||||
_i = _i + 1
|
||||
}
|
||||
|
||||
return {
|
||||
symbols: all_symbols,
|
||||
references: all_refs,
|
||||
call_sites: all_calls
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
make: make,
|
||||
explain_across: explain_across,
|
||||
span_contains: span_contains
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user