264 Commits

Author SHA1 Message Date
John Alanbrook
957b964d9d better disrupt message on fd 2026-02-25 20:38:34 -06:00
John Alanbrook
fa9d2609b1 fix fd bug 2026-02-25 20:25:36 -06:00
John Alanbrook
e38c2f07bf Merge branch 'async_fd' 2026-02-25 17:43:12 -06:00
John Alanbrook
ecc1777b24 async cellfs 2026-02-25 17:43:01 -06:00
John Alanbrook
1cfd5b8133 add hot reload to util 2026-02-25 17:28:11 -06:00
John Alanbrook
9c1141f408 Merge branch 'async_http' 2026-02-25 16:39:16 -06:00
John Alanbrook
696cca530b internal pronto 2026-02-25 16:39:12 -06:00
John Alanbrook
c92a4087a6 Merge branch 'async_fd' 2026-02-25 16:24:20 -06:00
John Alanbrook
01637c49b0 fix sendmessage 2026-02-25 16:24:08 -06:00
John Alanbrook
f9e660ebaa better log 2026-02-25 16:07:39 -06:00
John Alanbrook
4f8fada57d Merge branch 'audit_build' 2026-02-25 16:06:17 -06:00
John Alanbrook
adcaa92bea better logging for compiling 2026-02-25 16:05:37 -06:00
John Alanbrook
fc36707b39 Merge branch 'async_fd' 2026-02-25 16:03:55 -06:00
John Alanbrook
7cb8ce7945 async fd 2026-02-25 16:03:52 -06:00
John Alanbrook
bb7997a751 fix sendmessage 2026-02-25 15:26:20 -06:00
John Alanbrook
327b990442 Merge branch 'master' into async_http 2026-02-25 14:48:46 -06:00
John Alanbrook
51c0a0b306 tls and http 2026-02-25 14:48:37 -06:00
John Alanbrook
8ac82016dd api for sending wota messages direct 2026-02-25 12:45:02 -06:00
John Alanbrook
d0bf757d91 remove dupavlue and freevalue 2026-02-25 09:40:58 -06:00
John Alanbrook
c77f1f8639 move general http business to http and out of the probe cli 2026-02-24 21:06:43 -06:00
John Alanbrook
2b877e6b0c add http.cm and probe 2026-02-24 21:04:03 -06:00
John Alanbrook
3d4c0ec3d3 more comprehensive C suite 2026-02-24 17:51:36 -06:00
John Alanbrook
33d9013409 fix tests; add comprehensive tests for functions and fix bugs in the mach VM regarding them. 2026-02-24 17:41:18 -06:00
John Alanbrook
c2f57d1dae fix tests 2026-02-24 16:55:07 -06:00
John Alanbrook
7bd17c6476 Merge branch 'native_boot' 2026-02-23 19:07:07 -06:00
John Alanbrook
5ac1620b48 Merge branch 'fix_missing_stop' 2026-02-23 19:02:03 -06:00
John Alanbrook
124c9536b4 no more implicit returning in programs and mdouels 2026-02-23 19:01:58 -06:00
John Alanbrook
940807c37a parallel compiling; no more var hoisting; audit reports function hoisting 2026-02-23 18:57:47 -06:00
John Alanbrook
70f560550f rm tests 2026-02-23 18:16:07 -06:00
John Alanbrook
060a494f47 Merge branch 'fix_missing_stop' 2026-02-23 18:09:41 -06:00
John Alanbrook
6812d3edbc enet portal works 2026-02-23 18:09:35 -06:00
John Alanbrook
4da15d2a3e refactor 2026-02-23 18:08:13 -06:00
John Alanbrook
a34566a0c1 further refactor 2026-02-23 17:22:23 -06:00
John Alanbrook
9f7d861932 Merge branch 'audit_c_api' 2026-02-23 16:54:25 -06:00
John Alanbrook
c5536697ff merge cell 2026-02-23 16:54:19 -06:00
John Alanbrook
d066ab03cd correct caching 2026-02-23 13:37:11 -06:00
John Alanbrook
9c1cb43c7d fix repeated loads 2026-02-23 12:39:55 -06:00
John Alanbrook
99fb575c9c Merge branch 'native_boot' 2026-02-23 11:20:41 -06:00
John Alanbrook
193991c532 canonical paths 2026-02-23 11:18:08 -06:00
John Alanbrook
76552c6854 faster boot by refactor qbe_emit 2026-02-23 11:16:13 -06:00
John Alanbrook
f26b6e853d fix string indexing in native 2026-02-23 10:39:49 -06:00
John Alanbrook
94c28f0e17 more native without fallback 2026-02-23 10:20:18 -06:00
John Alanbrook
a18584afd3 Merge branch 'native_boot' 2026-02-23 09:18:31 -06:00
John Alanbrook
3f6cfad7ef fix gc scanning for env on native fns 2026-02-23 09:18:25 -06:00
John Alanbrook
b03edb0d90 Merge branch 'native_boot' 2026-02-22 20:48:19 -06:00
John Alanbrook
62440d3ed6 trace 2026-02-22 20:48:17 -06:00
John Alanbrook
4edc4b7cc5 native boot 2026-02-22 20:47:26 -06:00
John Alanbrook
012b507415 add -e flag 2026-02-22 11:24:27 -06:00
John Alanbrook
ee6398ada9 Merge branch 'master' into fuse_bug 2026-02-22 10:51:07 -06:00
John Alanbrook
173438e8bc add tests 2026-02-22 10:48:09 -06:00
John Alanbrook
7ac5ac63d2 Merge branch 'array_push_bug' 2026-02-22 10:35:00 -06:00
John Alanbrook
a05f180356 fix fuse bug 2026-02-22 10:34:55 -06:00
John Alanbrook
d88692cd30 fix inline issue 2026-02-22 10:31:15 -06:00
John Alanbrook
b0ac5de7e2 more comprehensive vm_suite 2026-02-22 10:08:40 -06:00
John Alanbrook
1d4fc11772 Merge remote-tracking branch 'origin/master' 2026-02-22 09:04:03 -06:00
John Alanbrook
7372b80e07 wary jumps 2026-02-21 20:58:24 -06:00
John Alanbrook
d27047dd82 Merge branch 'optimize_mcode' into fix_aot 2026-02-21 20:42:25 -06:00
John Alanbrook
8e96379377 wary booleans 2026-02-21 20:42:17 -06:00
John Alanbrook
8f415fea80 Merge branch 'optimize_mcode' 2026-02-21 19:45:14 -06:00
John Alanbrook
cec0b99207 correct apply check and add apply opcode 2026-02-21 19:43:05 -06:00
John Alanbrook
2d4645da9c Merge branch 'optimize_mcode' 2026-02-21 19:42:19 -06:00
John Alanbrook
017b63ba80 inline intrinsics 2026-02-21 19:23:53 -06:00
John Alanbrook
99fa86a09c asserts only for frame gets 2026-02-21 19:06:41 -06:00
John Alanbrook
6d6b53009f hash fingerprint for copmile chain 2026-02-21 17:25:14 -06:00
John Alanbrook
cc7fc6b667 fix inlining default params issue 2026-02-21 17:03:00 -06:00
John Alanbrook
bbeb757e40 faster 2026-02-21 16:23:44 -06:00
John Alanbrook
2ac446f7cf inline fns 2026-02-21 15:05:57 -06:00
John Alanbrook
517bd64275 Merge branch 'optimize_mcode' into fix_aot 2026-02-21 14:13:51 -06:00
John Alanbrook
7d0c96f328 inline 2026-02-21 13:44:34 -06:00
John Alanbrook
f7e3c0803c Merge branch 'master' into fix_aot 2026-02-21 13:31:33 -06:00
John Alanbrook
e7ed6bd8b2 Merge branch 'optimize_mcode' 2026-02-21 04:13:29 -06:00
John Alanbrook
700b640cf1 type propagation 2026-02-21 04:13:21 -06:00
John Alanbrook
071aa33153 Merge branch 'optimize_mcode' 2026-02-21 03:38:38 -06:00
John Alanbrook
d041c49972 Merge branch 'optimize_mcode' 2026-02-21 03:38:34 -06:00
John Alanbrook
74e0923629 don't drop type info from add 2026-02-21 03:38:20 -06:00
John Alanbrook
eadad194be doc gc bugs 2026-02-21 03:01:26 -06:00
John Alanbrook
fea76ecac5 remove typed ops 2026-02-21 02:52:17 -06:00
John Alanbrook
81c88f9439 gx fices 2026-02-21 02:37:30 -06:00
John Alanbrook
20c2576fa7 working link 2026-02-21 02:18:42 -06:00
John Alanbrook
ac707bc399 Merge branch 'better_disasm' into optimize_mcode 2026-02-21 01:28:59 -06:00
John Alanbrook
03b5fc1a5e more build tools 2026-02-21 01:21:53 -06:00
John Alanbrook
5caa5d1288 fix merge error 2026-02-21 01:21:26 -06:00
John Alanbrook
3ebd98fc00 Merge branch 'better_disasm' into optimize_mcode 2026-02-21 00:42:34 -06:00
John Alanbrook
ede033f52e disasm 2026-02-21 00:41:47 -06:00
John Alanbrook
f4ad851a3f Merge branch 'master' into optimize_mcode 2026-02-20 23:18:27 -06:00
John Alanbrook
50a2d67c90 fix stone crash 2026-02-20 23:18:19 -06:00
John Alanbrook
5ea0de9fbb crash vm trace 2026-02-20 23:14:43 -06:00
John Alanbrook
34cb19c357 fix gc closure shortening 2026-02-20 23:07:47 -06:00
John Alanbrook
26f63bccee Merge branch 'lexical_this' 2026-02-20 22:02:31 -06:00
John Alanbrook
93aaaa43a1 lexical this 2026-02-20 22:02:26 -06:00
John Alanbrook
1e606095a2 Merge branch 'better_fn_error' 2026-02-20 21:59:29 -06:00
John Alanbrook
652e8a19f0 better fn error 2026-02-20 21:59:17 -06:00
John Alanbrook
bcc9bdac83 Merge branch 'improve_str_concat' into optimize_mcode 2026-02-20 21:55:22 -06:00
John Alanbrook
f46784d884 optimize 2026-02-20 21:55:12 -06:00
John Alanbrook
fca1041e52 str stone; concat 2026-02-20 21:54:19 -06:00
John Alanbrook
e73152bc36 better error when use without tet 2026-02-20 21:49:22 -06:00
John Alanbrook
6f932cd3c3 Merge branch 'master' into optimize_mcode 2026-02-20 21:24:22 -06:00
John Alanbrook
55fa250f12 quicker fn call 2026-02-20 21:24:18 -06:00
John Alanbrook
374069ae99 Merge branch 'imp_audit' 2026-02-20 20:51:34 -06:00
John Alanbrook
3e4f3dff11 closure get type back inference 2026-02-20 20:51:21 -06:00
John Alanbrook
6cac591dc9 move fold 2026-02-20 20:32:28 -06:00
John Alanbrook
f6dab3c081 fix incorrect code elimination 2026-02-20 20:29:09 -06:00
John Alanbrook
a82c13170f Merge branch 'imp_audit' 2026-02-20 20:03:19 -06:00
John Alanbrook
3e7b3e9994 emit warnings for unused vars 2026-02-20 20:03:12 -06:00
John Alanbrook
68ae10bed0 fix resolving c symbols in C 2026-02-20 20:01:26 -06:00
John Alanbrook
cf3c2c9c5f reduce mcode 2026-02-20 19:53:29 -06:00
John Alanbrook
bb8536f6c9 better error print 2026-02-20 19:14:45 -06:00
John Alanbrook
0e86d6f5a1 debug output on build 2026-02-20 19:06:03 -06:00
John Alanbrook
d8df467eae Merge branch 'fix_find' 2026-02-20 18:52:35 -06:00
John Alanbrook
995d900e6e fix find usage 2026-02-20 18:52:02 -06:00
John Alanbrook
eeb2f038a1 fix scheduler memory error on exit 2026-02-20 18:42:21 -06:00
John Alanbrook
ce2e11429f fix build regression 2026-02-20 18:09:19 -06:00
John Alanbrook
55fae8b5d0 Merge branch 'master' into quicken_build 2026-02-20 18:00:18 -06:00
John Alanbrook
c722b9d648 src bsaed dylib 2026-02-20 17:53:25 -06:00
John Alanbrook
680b257a44 fingerprint hash 2026-02-20 17:52:25 -06:00
John Alanbrook
611d538e9f json correct args 2026-02-20 17:04:14 -06:00
John Alanbrook
844ca0b8d5 engine os.print 2026-02-20 16:40:03 -06:00
John Alanbrook
ec404205ca Merge branch 'fix_compile_warnings' 2026-02-20 15:40:36 -06:00
John Alanbrook
9ebe6efe2b regenerated boot files 2026-02-20 15:40:27 -06:00
John Alanbrook
e455159b2d Merge branch 'fix_compile_warnings' 2026-02-20 15:34:50 -06:00
John Alanbrook
5af76bce9b rm print 2026-02-20 15:33:46 -06:00
John Alanbrook
5e413e8d81 Merge branch 'audit_dups' 2026-02-20 15:07:58 -06:00
John Alanbrook
8fe14f9a42 Merge branch 'improve_compile_error' into audit_dups 2026-02-20 15:07:51 -06:00
John Alanbrook
148cf12787 fixed logging and remove 2026-02-20 15:03:58 -06:00
John Alanbrook
9c35e77f3f Merge branch 'improve_fetch' into audit_dups 2026-02-20 15:03:30 -06:00
John Alanbrook
882a3ae8cb centralized ensure dir 2026-02-20 15:02:16 -06:00
John Alanbrook
35d0890242 improved fetch 2026-02-20 15:00:08 -06:00
John Alanbrook
98515e9218 Merge branch 'fix_compile_warnings' 2026-02-20 14:51:53 -06:00
John Alanbrook
11fb213a74 fix gc backtrace 2026-02-20 14:51:42 -06:00
John Alanbrook
4ac92c8a87 reduce dups 2026-02-20 14:44:48 -06:00
John Alanbrook
ed69d53573 Merge branch 'improve_fetch' into audit_dups 2026-02-20 14:43:26 -06:00
John Alanbrook
e4588e43f2 Merge branch 'improve_compile_error' 2026-02-20 14:39:51 -06:00
John Alanbrook
2f41f58521 update docs for compile chain 2026-02-20 14:35:48 -06:00
John Alanbrook
06866bcc0a Merge branch 'fix_compile_warnings' 2026-02-20 14:21:10 -06:00
John Alanbrook
f20fbedeea remove js_malloc from public 2026-02-20 14:21:07 -06:00
John Alanbrook
285395807b core packages now split out 2026-02-20 14:14:07 -06:00
John Alanbrook
601a78b3c7 package resolution 2026-02-20 14:10:24 -06:00
John Alanbrook
ebfc89e072 zero copy blob 2026-02-20 14:06:42 -06:00
John Alanbrook
f0c2486a5c better path resolution 2026-02-20 13:39:26 -06:00
John Alanbrook
c5ad4f0a99 thin out quickjs-internal 2026-02-20 13:08:39 -06:00
John Alanbrook
e6d05abd03 harsher compile error 2026-02-20 12:52:40 -06:00
John Alanbrook
c0aff9e9bf fix compiler warnings 2026-02-20 12:44:18 -06:00
John Alanbrook
8d449e6fc6 better compiler warnings adn errors 2026-02-20 12:40:49 -06:00
John Alanbrook
54e5be0773 update git 2026-02-20 08:48:34 -06:00
John Alanbrook
38f368c6d6 faster startup and fix asan error 2026-02-19 03:47:30 -06:00
John Alanbrook
06ad466b1a import graph 2026-02-19 03:19:24 -06:00
John Alanbrook
65fa37cc03 fix 2026-02-19 03:12:58 -06:00
John Alanbrook
bab4d50b2a shorten frames to closure vars only on gc 2026-02-19 01:55:35 -06:00
John Alanbrook
e7fec94e38 Merge branch 'fix_aot' 2026-02-19 01:38:13 -06:00
John Alanbrook
ab43ab0d2c aot fix 2026-02-19 01:37:54 -06:00
John Alanbrook
a15844af58 Merge branch 'fix_aot' 2026-02-19 01:23:58 -06:00
John Alanbrook
85ef711229 fixes 2026-02-19 01:23:41 -06:00
John Alanbrook
ddfb0b1345 Merge branch 'fix_aot' 2026-02-19 00:47:41 -06:00
John Alanbrook
3f206d80dd jscode 2026-02-19 00:47:34 -06:00
John Alanbrook
3e0dc14318 use globfs 2026-02-19 00:43:06 -06:00
John Alanbrook
19132c1517 jscode 2026-02-19 00:33:16 -06:00
John Alanbrook
e59bfe19f7 Merge branch 'master' into fix_aot 2026-02-18 23:55:17 -06:00
John Alanbrook
e004b2c472 optimize frames; remove trampoline 2026-02-18 22:37:48 -06:00
John Alanbrook
27ca008f18 lower ops directly 2026-02-18 21:18:18 -06:00
John Alanbrook
a05d0e2525 better streamline 2026-02-18 20:56:15 -06:00
John Alanbrook
777474ab4f updated docs for dylib paths 2026-02-18 20:30:54 -06:00
John Alanbrook
621da78de9 faster aot 2026-02-18 20:24:12 -06:00
John Alanbrook
e2c26737f4 Merge branch 'dylib_cache' 2026-02-18 19:42:04 -06:00
John Alanbrook
02eb58772c fix build hangs 2026-02-18 19:41:59 -06:00
John Alanbrook
14a94aff12 Merge branch 'dylib_cache' 2026-02-18 19:27:33 -06:00
John Alanbrook
6bc9dd53a7 better cache handling 2026-02-18 19:27:28 -06:00
John Alanbrook
f7499c4f60 log reentrancy guard 2026-02-18 19:26:06 -06:00
John Alanbrook
fa5c0416fb correct log line blames 2026-02-18 18:47:46 -06:00
John Alanbrook
dc70a15981 add guards for root cycles 2026-02-18 18:27:12 -06:00
John Alanbrook
94fe47b472 Merge branch 'cleanup_thinc' 2026-02-18 18:00:34 -06:00
John Alanbrook
34521e44f1 jstext properly used for oncat 2026-02-18 17:58:36 -06:00
John Alanbrook
81561d426b use unstone jstext for string creation 2026-02-18 17:39:22 -06:00
John Alanbrook
7a4c72025f Merge branch 'master' into fix_aot 2026-02-18 17:03:39 -06:00
John Alanbrook
303f894a70 js helpers for migrating 2026-02-18 16:59:42 -06:00
John Alanbrook
c33c35de87 aot pass all tests 2026-02-18 16:53:33 -06:00
John Alanbrook
417eec2419 Merge branch 'simplify_disruption' 2026-02-18 16:50:09 -06:00
John Alanbrook
c0cd6a61a6 disruption 2026-02-18 16:47:33 -06:00
John Alanbrook
42dc7243f3 fix JS_ToNumber 2026-02-18 14:20:42 -06:00
John Alanbrook
469b7ac478 Merge branch 'master' into fix_aot 2026-02-18 14:16:42 -06:00
John Alanbrook
4872c62704 fix JS_ToNumber 2026-02-18 14:14:44 -06:00
John Alanbrook
91b73f923a Merge branch 'fix_heap_closure' 2026-02-18 12:46:18 -06:00
John Alanbrook
4868a50085 fix compilation error 2026-02-18 12:46:07 -06:00
John Alanbrook
6f8cad9bb2 Merge branch 'improved_log' 2026-02-18 12:22:45 -06:00
John Alanbrook
a1d1e721b6 stack trace in logging toml 2026-02-18 12:22:33 -06:00
John Alanbrook
dc7f933424 add help info to --help 2026-02-18 12:07:17 -06:00
John Alanbrook
36f054d99d Merge branch 'improved_log' 2026-02-18 12:05:10 -06:00
John Alanbrook
037fdbfd2c log stack traces 2026-02-18 12:05:05 -06:00
John Alanbrook
28f5a108d8 improved help 2026-02-18 12:00:46 -06:00
John Alanbrook
d8422ae69b Merge branch 'sem_grab' 2026-02-18 11:00:54 -06:00
John Alanbrook
187d7e9832 correct this handling 2026-02-18 11:00:51 -06:00
John Alanbrook
4aafb3c5e9 Merge branch 'improved_log' 2026-02-18 10:49:33 -06:00
John Alanbrook
4b635228f9 fix build/dl loading; use core from anywhere 2026-02-18 10:49:27 -06:00
John Alanbrook
42f7c270e1 Merge branch 'sem_grab' 2026-02-18 10:49:00 -06:00
John Alanbrook
bd7f9f34ec simplify compilation requestors 2026-02-18 10:46:47 -06:00
John Alanbrook
22c0b421d2 Merge branch 'sem_grab' 2026-02-18 10:35:25 -06:00
John Alanbrook
8be5936c10 better sem analysis 2026-02-18 10:34:47 -06:00
John Alanbrook
bd53089578 doc stop, json, log 2026-02-18 10:18:28 -06:00
John Alanbrook
76c482b84e improved logging 2026-02-18 10:16:01 -06:00
John Alanbrook
b16fa75706 flag used for actor stopping insetad of counter 2026-02-17 17:59:12 -06:00
John Alanbrook
ad419797b4 native function type 2026-02-17 17:40:44 -06:00
John Alanbrook
5ee51198a7 kill actor when abusive 2026-02-17 17:34:25 -06:00
John Alanbrook
2df45b2acb Merge branch 'json_gc_fix' 2026-02-17 15:59:51 -06:00
John Alanbrook
933c63caf8 Merge branch 'root_gc' 2026-02-17 15:59:43 -06:00
John Alanbrook
dc422932d3 Merge branch 'heap_blob' 2026-02-17 15:59:38 -06:00
John Alanbrook
b25285f2e1 Merge branch 'master' into fix_aot 2026-02-17 15:48:54 -06:00
John Alanbrook
b3573dbf26 native flag 2026-02-17 15:48:49 -06:00
John Alanbrook
8a24a69120 fixes to allow native to work - should revert when recursion is fixed 2026-02-17 15:44:17 -06:00
John Alanbrook
56ac53637b heap blobs 2026-02-17 15:41:53 -06:00
John Alanbrook
5415726e33 actors use hdiden symbol now 2026-02-17 14:35:54 -06:00
John Alanbrook
56cb1fb4c6 package now returns C modules 2026-02-17 14:33:06 -06:00
John Alanbrook
278d685c8f Merge branch 'json_gc_fix' 2026-02-17 14:00:28 -06:00
John Alanbrook
51815b66d8 json rooting fix 2026-02-17 14:00:23 -06:00
John Alanbrook
78051e24f3 bench now compares aot 2026-02-17 13:42:36 -06:00
John Alanbrook
c02fbbd9e0 tooling improvements 2026-02-17 13:37:17 -06:00
John Alanbrook
ad26e71ad1 fix push array on itself 2026-02-17 13:27:08 -06:00
John Alanbrook
2e78e7e0b8 Merge branch 'bench_endoders' 2026-02-17 12:36:15 -06:00
John Alanbrook
8f9eb0aaa9 benchmark encoders and speed them up 2026-02-17 12:36:07 -06:00
John Alanbrook
0965aed0ef Merge branch 'fix_actors' 2026-02-17 12:35:26 -06:00
John Alanbrook
1b00fd1f0a Merge branch 'fix_native_suite' 2026-02-17 12:35:20 -06:00
John Alanbrook
3bf63780fd Merge branch 'core_integration' into fix_imports 2026-02-17 12:34:47 -06:00
John Alanbrook
f7f26a1f00 fix building 2026-02-17 12:32:09 -06:00
John Alanbrook
4c9db198db fix string hash bug 2026-02-17 12:26:52 -06:00
John Alanbrook
eff3548c50 bootstrap now uses streamline 2026-02-17 12:23:59 -06:00
John Alanbrook
4fc48fd6f2 Merge branch 'quicken_native' into fix_native_suite 2026-02-17 12:22:42 -06:00
John Alanbrook
3f6388ff4e far smaller assmbly 2026-02-17 11:53:46 -06:00
John Alanbrook
2be2b15a61 update actor doc and add more actor based tests 2026-02-17 11:50:46 -06:00
John Alanbrook
12b6c3544e fix all core script syntax issues 2026-02-17 11:23:12 -06:00
John Alanbrook
570f0cdc83 add qbe config to copmile 2026-02-17 11:17:59 -06:00
John Alanbrook
cc82fcb7d9 Merge branch 'quicken_native' into fix_native_suite 2026-02-17 11:12:59 -06:00
John Alanbrook
5ef3381fff native aot suite passes 2026-02-17 11:12:51 -06:00
John Alanbrook
5fcf765c8d parallel assembly 2026-02-17 10:57:50 -06:00
John Alanbrook
027c1549fc recursive add and install 2026-02-17 10:52:36 -06:00
John Alanbrook
8c408a4b81 qbe in native build 2026-02-17 10:23:47 -06:00
John Alanbrook
2d054fcf21 fix package 2026-02-17 10:11:02 -06:00
John Alanbrook
857f099a68 Merge branch 'cell_lsp' 2026-02-17 09:01:28 -06:00
John Alanbrook
e0b6c69bfe build fix 2026-02-17 09:01:24 -06:00
John Alanbrook
2cef766b0a Merge branch 'gen_dylib' 2026-02-17 08:59:05 -06:00
John Alanbrook
a3ecb0ad05 Merge branch 'fix_actors' 2026-02-17 08:53:57 -06:00
John Alanbrook
2a38292ff7 fix actor working 2026-02-17 08:53:16 -06:00
John Alanbrook
9e42a28d55 aot compile vm_suite 2026-02-17 03:33:21 -06:00
John Alanbrook
4d4d50a905 fix claude.md 2026-02-17 03:10:45 -06:00
John Alanbrook
08515389d2 fix cell toml and add documentation for tools 2026-02-17 02:36:53 -06:00
John Alanbrook
fbdfbc1200 add audit 2026-02-17 01:54:25 -06:00
John Alanbrook
d975214ba6 Merge branch 'master' into cell_lsp 2026-02-17 01:20:11 -06:00
John Alanbrook
3c28dc2c30 fix toml issue / isobject 2026-02-17 01:19:43 -06:00
John Alanbrook
2633fb986f improved semantic indexing 2026-02-17 01:08:10 -06:00
John Alanbrook
400c58e5f2 fix build 2026-02-17 01:04:42 -06:00
John Alanbrook
bd4714a732 Merge branch 'cell_lsp' 2026-02-17 00:28:17 -06:00
John Alanbrook
0ac575db85 fix package bug, improve stack trace 2026-02-17 00:28:10 -06:00
John Alanbrook
41f373981d add docs to website nav 2026-02-17 00:04:55 -06:00
John Alanbrook
c9dad91ea1 fix intrinsics and env 2026-02-16 23:05:00 -06:00
John Alanbrook
63955e45ff Merge branch 'master' into gen_dylib 2026-02-16 22:07:56 -06:00
John Alanbrook
8a19cffe9f Merge branch 'pit_lsp' into fix_libs 2026-02-16 21:53:11 -06:00
John Alanbrook
6315574a45 Merge branch 'fix_imports' into fix_libs 2026-02-16 21:53:00 -06:00
John Alanbrook
2051677679 better errors 2026-02-16 21:52:11 -06:00
John Alanbrook
d398ab8db0 lsp explain and index 2026-02-16 21:50:39 -06:00
John Alanbrook
e7b599e3ac add shop documentation and fix shop remove 2026-02-16 19:55:22 -06:00
John Alanbrook
1f3e53587d log available 2026-02-16 19:51:00 -06:00
John Alanbrook
dce0b5cc89 remove random str for imported 2026-02-16 19:13:37 -06:00
John Alanbrook
ce387d18d5 Merge branch 'fix_toml' into fix_libs 2026-02-16 19:10:53 -06:00
John Alanbrook
c02945e236 document functino nr args 2026-02-16 18:53:53 -06:00
John Alanbrook
8e198d9822 fix toml escape 2026-02-16 18:53:11 -06:00
John Alanbrook
17e35f023f fix building C 2026-02-16 18:47:43 -06:00
John Alanbrook
5a7169654a fixed pipeline module loading; better parse errors for function literals in objects 2026-02-16 18:41:35 -06:00
413 changed files with 208094 additions and 107501 deletions

5
.gitignore vendored
View File

@@ -1,7 +1,12 @@
.git/
.obj/
website/public/
website/site/
website/.hugo_build.lock
.cache
.cell
cell
libcell_runtime*
bin/
build/
*.zip

106
CLAUDE.md
View File

@@ -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.
@@ -107,7 +108,66 @@ var v = a[] // pop: v is 3, a is [1, 2]
- 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)
- Use `JS_FRAME`/`JS_ROOT`/`JS_RETURN` macros for any C function that allocates multiple heap objects. Any `JS_New*`/`JS_SetProperty*` call can trigger GC.
### 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
@@ -118,6 +178,42 @@ 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:

108
Makefile
View File

@@ -1,83 +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
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 .
doit: bootstrap
$(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: cell $(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
# 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. Run cell like ./cell --dev to use a local shop at .cell."
# Clean build artifacts
clean:
rm -rf $(CELL_SHOP)/build build_bootstrap
rm -f cell cell_main libcell_runtime.dylib
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
.PHONY: all install debug install_debug clean

177
add.ce
View File

@@ -3,108 +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 resolved = null
var recursive = false
var cwd = fd.realpath('.')
var parts = null
var cwd = null
var build_target = 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)) {
resolved = fd.realpath(locator)
if (resolved) {
locator = resolved
if (!locator && !recursive) {
log.console("Usage: cell add <locator> [alias]")
return
}
}
// Generate default alias from locator
if (!alias) {
// Use the last component of the locator as alias
parts = array(locator, '/')
alias = parts[length(parts) - 1]
// Remove any version suffix
if (search(alias, '@') != null) {
alias = array(alias, '@')[0]
if (locator)
locator = shop.resolve_locator(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
cwd = fd.realpath('.')
if (!fd.is_file(cwd + '/cell.toml')) {
log.error("Not in a package directory (no cell.toml found)")
$stop()
}
// 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
}
log.console("Adding " + locator + " as '" + alias + "'...")
// 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}`)
// Add to local project's cell.toml
var _add_dep = function() {
pkg.add_dependency(null, locator, alias)
log.console(" Added to cell.toml")
} disruption {
log.error("Failed to update cell.toml")
$stop()
}
_add_dep()
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()
})
// Install to shop
var _install = function() {
shop.get(locator)
shop.extract(locator)
log.console("Added " + text(added) + " package(s)." + (failed > 0 ? " Failed: " + text(failed) + "." : ""))
return
}
// Build scripts
shop.build_package_scripts(locator)
// Single package add
log.console("Adding " + locator + " as '" + alias + "'...")
// Build C code if any
var _build_c = function() {
build_target = build.detect_host_target()
build.build_dynamic(locator, build_target, 'release')
_add_dep = function() {
pkg.add_dependency(null, locator, alias)
log.console(" Added to cell.toml")
} disruption {
// Not all packages have C code
log.error("Failed to update cell.toml")
return
}
_build_c()
_add_dep()
log.console(" Installed to shop")
} disruption {
log.error("Failed to install")
$stop()
_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 + ")")
}
_install()
log.console("Added " + alias + " (" + locator + ")")
run()
$stop()

144
analyze.cm Normal file
View 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

View File

@@ -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,7 +318,6 @@ 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_SetPropertyNumber(js, arr, arr_index++, filename);
@@ -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));
}

149
audit.ce Normal file
View 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()

154
bench.ce
View File

@@ -1,10 +1,12 @@
// 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')
@@ -13,6 +15,25 @@ 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
@@ -394,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)
@@ -406,15 +474,16 @@ 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 bench_mod = null
var use_pkg = null
var benches = []
var native_benches = []
var bench_mod = null
var native_mod = null
var error_result = null
var file_result = {
@@ -423,16 +492,21 @@ function run_benchmarks(package_name, specific_bench) {
}
var _load_file = function() {
use_pkg = package_name ? package_name : fd.realpath('.')
bench_mod = shop.use(mod_path, use_pkg)
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_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) {
@@ -440,18 +514,50 @@ function run_benchmarks(package_name, specific_bench) {
arrfor(benches, function(b) {
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)
}
} disruption {
bench_error = true
@@ -524,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 ===
@@ -536,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`
}
})
})
@@ -553,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`

View File

@@ -1,194 +0,0 @@
// bench_native.ce — compare VM vs native execution speed
//
// Usage:
// cell --dev bench_native.ce <module.cm> [iterations]
//
// Compiles (if needed) and benchmarks a module via both VM and native dylib.
// Reports median/mean timing per benchmark + speedup ratio.
var os = use('os')
var fd = use('fd')
if (length(args) < 1) {
print('usage: cell --dev bench_native.ce <module.cm> [iterations]')
return
}
var file = args[0]
var name = file
if (ends_with(name, '.cm')) {
name = text(name, 0, length(name) - 3)
}
var iterations = 11
if (length(args) > 1) {
iterations = number(args[1])
}
def WARMUP = 3
var safe = replace(replace(name, '/', '_'), '-', '_')
var symbol = 'js_' + safe + '_use'
var dylib_path = './' + file + '.dylib'
// --- Statistics ---
var stat_sort = function(arr) {
return sort(arr)
}
var stat_median = function(arr) {
if (length(arr) == 0) return 0
var sorted = stat_sort(arr)
var mid = floor(length(arr) / 2)
if (length(arr) % 2 == 0) {
return (sorted[mid - 1] + sorted[mid]) / 2
}
return sorted[mid]
}
var stat_mean = function(arr) {
if (length(arr) == 0) return 0
var sum = reduce(arr, function(a, b) { return a + b })
return sum / length(arr)
}
var format_ns = function(ns) {
if (ns < 1000) return text(round(ns)) + 'ns'
if (ns < 1000000) return text(round(ns / 1000 * 100) / 100) + 'us'
if (ns < 1000000000) return text(round(ns / 1000000 * 100) / 100) + 'ms'
return text(round(ns / 1000000000 * 100) / 100) + 's'
}
// --- Collect benchmarks from module ---
var collect_benches = function(mod) {
var benches = []
var keys = null
var i = 0
var k = null
if (is_function(mod)) {
push(benches, {name: 'main', fn: mod})
} else if (is_object(mod)) {
keys = array(mod)
i = 0
while (i < length(keys)) {
k = keys[i]
if (is_function(mod[k])) {
push(benches, {name: k, fn: mod[k]})
}
i = i + 1
}
}
return benches
}
// --- Run one benchmark function ---
var run_bench = function(fn, label) {
var samples = []
var i = 0
var t1 = 0
var t2 = 0
// warmup
i = 0
while (i < WARMUP) {
fn(1)
i = i + 1
}
// collect samples
i = 0
while (i < iterations) {
t1 = os.now()
fn(1)
t2 = os.now()
push(samples, t2 - t1)
i = i + 1
}
return {
label: label,
median: stat_median(samples),
mean: stat_mean(samples)
}
}
// --- Load VM module ---
print('loading VM module: ' + file)
var vm_mod = use(name)
var vm_benches = collect_benches(vm_mod)
if (length(vm_benches) == 0) {
print('no benchmarkable functions found in ' + file)
return
}
// --- Load native module ---
var native_mod = null
var native_benches = []
var has_native = fd.is_file(dylib_path)
var lib = null
if (has_native) {
print('loading native module: ' + dylib_path)
lib = os.dylib_open(dylib_path)
native_mod = os.dylib_symbol(lib, symbol)
native_benches = collect_benches(native_mod)
} else {
print('no ' + dylib_path + ' found -- VM-only benchmarking')
print(' hint: cell --dev compile.ce ' + file)
}
// --- Run benchmarks ---
print('')
print('samples: ' + text(iterations) + ' (warmup: ' + text(WARMUP) + ')')
print('')
var pad = function(s, n) {
var result = s
while (length(result) < n) result = result + ' '
return result
}
var i = 0
var b = null
var vm_result = null
var j = 0
var found = false
var nat_result = null
var speedup = 0
while (i < length(vm_benches)) {
b = vm_benches[i]
vm_result = run_bench(b.fn, 'vm')
print(pad(b.name, 20) + ' VM: ' + pad(format_ns(vm_result.median), 12) + ' (median) ' + format_ns(vm_result.mean) + ' (mean)')
// find matching native bench
j = 0
found = false
while (j < length(native_benches)) {
if (native_benches[j].name == b.name) {
nat_result = run_bench(native_benches[j].fn, 'native')
print(pad('', 20) + ' NT: ' + pad(format_ns(nat_result.median), 12) + ' (median) ' + format_ns(nat_result.mean) + ' (mean)')
if (nat_result.median > 0) {
speedup = vm_result.median / nat_result.median
print(pad('', 20) + ' speedup: ' + text(round(speedup * 100) / 100) + 'x')
}
found = true
}
j = j + 1
}
if (has_native && !found) {
print(pad('', 20) + ' NT: (no matching function)')
}
print('')
i = i + 1
}

405
benches/encoders.cm Normal file
View 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
}
}

302
benches/micro_core.cm Normal file
View 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)
}
}

View File

@@ -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}`);
}

View File

@@ -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()

View File

@@ -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()

View File

@@ -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"

View File

@@ -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("");

View File

@@ -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()

View File

@@ -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++;
}

View File

@@ -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))

View File

@@ -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`);

File diff suppressed because it is too large Load Diff

View File

@@ -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);
}

View File

@@ -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");

View File

@@ -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
View 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()
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

16776
boot/streamline.cm.mcode Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

74
boot_miscompile_bad.cm Normal file
View 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

124
build.ce
View File

@@ -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,70 +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 resolved = null
var lib = null
var results = null
var success = 0
var failed = 0
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')
$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:')
targets = build.list_targets()
for (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)) {
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) {
@@ -86,17 +83,16 @@ 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
@@ -104,9 +100,9 @@ if (target_package) {
// Build single package
log.console('Building ' + target_package + '...')
_build = function() {
lib = build.build_dynamic(target_package, target, buildtype)
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)`)
}
} disruption {
log.error('Build failed')
@@ -116,19 +112,19 @@ if (target_package) {
} else {
// Build all packages
log.console('Building all packages...')
results = build.build_all_dynamic(target, buildtype)
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].library) {
success++
} else if (results[i].error) {
failed++
if (results[i].modules) {
success = success + length(results[i].modules)
}
}
log.console(`Build complete: ${success} libraries built${failed > 0 ? `, ${failed} failed` : ''}`)
}
}
run()
$stop()

994
build.cm

File diff suppressed because it is too large Load Diff

View File

@@ -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++"

416
cellfs.cm
View File

@@ -1,28 +1,24 @@
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') {
_check = function() {
@@ -35,10 +31,10 @@ function mount_exists(mount, path) {
result = mount.handle.stat(path) != null
} disruption {}
_check()
} else {
var full_path = fd.join_paths(mount.source, path)
} else if (mount.type == 'fs') {
full_path = fd.join_paths(mount.source, path)
_check = function() {
var st = fd.stat(full_path)
st = fd.stat(full_path)
result = st.isFile || st.isDirectory
} disruption {}
_check()
@@ -46,11 +42,12 @@ function mount_exists(mount, path) {
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') {
_check = function() {
@@ -63,9 +60,9 @@ function is_directory(path) {
} disruption {}
_check()
} else {
var full_path = fd.join_paths(mount.source, path)
full_path = fd.join_paths(mount.source, path)
_check = function() {
var st = fd.stat(full_path)
st = fd.stat(full_path)
result = st.isDirectory
} disruption {}
_check()
@@ -73,64 +70,63 @@ function is_directory(path) {
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) {
print("Unknown mount point: @" + mount_name); disrupt
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) {
print("File not found in any mount: " + path); disrupt
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,73 +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)
var qop_archive = null
var _try_qop = function() {
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)) {
print("Invalid archive file (not zip or qop): " + source); disrupt
}
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 {
print("Unsupported mount source type: " + source); disrupt
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) { print("File not found: " + path); disrupt }
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) { print("File not found in qop: " + path); disrupt }
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, "@")) {
@@ -213,29 +232,31 @@ function exists(path) {
return res != null
}
// Stat
function stat(path) {
var res = resolve(path, true)
if (!res) { print("File not found: " + path); disrupt }
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) { print("File not found in qop: " + path); disrupt }
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,
@@ -244,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) {
print("Package not found: " + name); disrupt
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') { print("Cannot delete from non-fs mount"); disrupt }
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() {
@@ -305,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)
@@ -361,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) {
@@ -380,7 +425,7 @@ function globfs(globs, dir) {
}, false, true)
return result
}
function check_pos(path) {
var result = false
arrfor(globs, function(g) {
@@ -397,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)
}
@@ -415,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)) {
@@ -438,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
@@ -461,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
View 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()

113
clean.ce
View File

@@ -24,42 +24,42 @@ var clean_fetch = false
var deep = false
var dry_run = false
var i = 0
var resolved = null
var deps = null
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")
$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) {
@@ -76,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)) {
resolved = fd.realpath(scope)
if (resolved) {
scope = resolved
}
}
scope = shop.resolve_locator(scope)
}
var files_to_delete = []
@@ -119,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)
}
}
@@ -220,5 +191,7 @@ if (dry_run) {
log.console("Clean complete: " + text(deleted_count) + " item(s) deleted.")
}
}
}
run()
$stop()

113
clone.ce
View File

@@ -5,115 +5,56 @@ var shop = use('internal/shop')
var link = use('link')
var fd = use('fd')
var http = use('http')
var miniz = use('miniz')
var resolved = null
var cwd = null
var parent = null
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()
}
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, '../')) {
resolved = fd.realpath(target_path)
if (resolved) {
target_path = resolved
} else {
// Path doesn't exist yet, resolve relative to cwd
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
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()
}
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()
}
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()
}
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()
}
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)
var zip_blob = null
var zip = null
var count = 0
var i = 0
var filename = null
var first_slash = null
var rel_path = null
var full_path = null
var dir_path = null
var _clone = function() {
zip_blob = http.fetch(download_url)
// Extract zip to target path
zip = miniz.read(zip_blob)
if (!zip) {
log.console("Error: Failed to read zip archive")
$stop()
}
// Create target directory
fd.mkdir(target_path)
count = zip.count()
for (i = 0; i < count; i++) {
if (zip.is_directory(i)) continue
filename = zip.get_filename(i)
first_slash = search(filename, '/')
if (first_slash == null) continue
if (first_slash + 1 >= length(filename)) continue
rel_path = text(filename, first_slash + 1)
full_path = target_path + '/' + rel_path
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))
}
var zip_blob = http.fetch(download_url)
shop.install_zip(zip_blob, target_path)
log.console("Extracted to " + target_path)
@@ -123,6 +64,8 @@ var _clone = function() {
} disruption {
log.console("Error during clone")
}
_clone()
_clone()
}
run()
$stop()

View File

@@ -1,92 +1,130 @@
// compare_aot.ce — compile a .cm module via both paths and compare results
// compare_aot.ce — compile a .ce/.cm file via both paths and compare results
//
// Usage:
// cell --dev compare_aot.ce <module.cm>
// cell --dev compare_aot.ce <file.ce>
var build = use('build')
var fd_mod = use('fd')
var os = use('os')
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) {
print('usage: cell --dev compare_aot.ce <module.cm>')
log.compile('usage: cell --dev compare_aot.ce <file>')
return
}
var file = args[0]
if (!fd_mod.is_file(file)) {
if (!ends_with(file, '.cm') && fd_mod.is_file(file + '.cm'))
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 {
print('file not found: ' + file)
log.error('file not found: ' + file)
return
}
}
var abs = fd_mod.realpath(file)
// Shared compilation front-end
// 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()
// --- Interpreted (mach VM) ---
print('--- interpreted ---')
var mcode_json = json.encode(optimized)
var mach_blob = mach_compile_mcode_bin(abs, mcode_json)
var result_interp = mach_load(mach_blob, stone({}))
print('result: ' + show(result_interp))
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')
// --- Native (AOT via QBE) ---
print('\n--- native ---')
var dylib_path = build.compile_native(abs, null, null, null)
print('dylib: ' + dylib_path)
var handle = os.dylib_open(dylib_path)
if (!handle) {
print('failed to open dylib')
return
}
// Build env with runtime functions. Must include starts_with etc. because
// the GC can lose global object properties after compaction.
// 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({
logical: logical,
some: some,
every: every,
starts_with: starts_with,
ends_with: ends_with,
log: log,
fallback: fallback,
parallel: parallel,
race: race,
sequence: sequence
sequence: sequence,
use
})
var result_native = os.native_module_load(handle, env)
print('result: ' + show(result_native))
// --- 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 ---
print('\n--- comparison ---')
log.compile('\n--- comparison ---')
var s_interp = show(result_interp)
var s_native = show(result_native)
if (s_interp == s_native) {
print('MATCH')
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 {
print('MISMATCH')
print(' interp: ' + s_interp)
print(' native: ' + s_native)
if (!interp_ok) log.error('interpreted run failed')
if (!native_ok) log.error('native run failed')
}

View File

@@ -10,13 +10,13 @@ var build = use('build')
var fd = use('fd')
if (length(args) < 1) {
print('usage: cell compile <file.cm|file.ce>')
log.compile('usage: cell compile <file.cm|file.ce>')
return
}
var file = args[0]
if (!fd.is_file(file)) {
print('file not found: ' + file)
log.error('file not found: ' + file)
return
}

View File

@@ -1,98 +0,0 @@
// compile_seed.ce — compile a .cm module to native .dylib via QBE (seed mode)
// Usage: ./cell --dev --seed compile_seed <file.cm>
var fd = use("fd")
var os = use("os")
var tokenize = use("tokenize")
var parse = use("parse")
var fold = use("fold")
var mcode = use("mcode")
var streamline = use("streamline")
var qbe_macros = use("qbe")
var qbe_emit = use("qbe_emit")
if (length(args) < 1) {
print("usage: cell --dev --seed compile_seed <file.cm>")
disrupt
}
var file = args[0]
var base = file
if (ends_with(base, ".cm")) {
base = text(base, 0, length(base) - 3)
} else if (ends_with(base, ".ce")) {
base = text(base, 0, length(base) - 3)
}
var safe = replace(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 = file + ".dylib"
var rc = 0
// Step 1: compile to QBE IL
print("compiling " + file + " to QBE IL...")
var src = text(fd.slurp(file))
var result = tokenize(src, file)
var ast = parse(result.tokens, src, file, tokenize)
var folded = fold(ast)
var compiled = mcode(folded)
var optimized = streamline(compiled)
var il = qbe_emit(optimized, qbe_macros)
// Step 2: append wrapper function
var wrapper = `
export function l $${symbol}(l %ctx) {
@entry
%result =l call $cell_rt_module_entry(l %ctx)
ret %result
}
`
il = il + wrapper
// Write IL to file — remove old file first to avoid leftover content
if (fd.is_file(ssa_path)) fd.unlink(ssa_path)
var out_fd = fd.open(ssa_path, 1537, 420)
fd.write(out_fd, il)
fd.close(out_fd)
print("wrote " + ssa_path + " (" + text(length(il)) + " bytes)")
// Step 3: compile QBE IL to assembly
print("qbe compile...")
rc = os.system("qbe -o " + s_path + " " + ssa_path)
if (rc != 0) {
print("qbe compilation failed")
disrupt
}
// Step 4: assemble
print("assemble...")
rc = os.system("cc -c " + s_path + " -o " + o_path)
if (rc != 0) {
print("assembly failed")
disrupt
}
// Step 5: compile runtime stubs
if (!fd.is_file(rt_o_path)) {
print("compile runtime stubs...")
rc = os.system("cc -c source/qbe_helpers.c -o " + rt_o_path + " -fPIC -Isource")
if (rc != 0) {
print("runtime stubs compilation failed")
disrupt
}
}
// Step 6: link dylib
print("link...")
rc = os.system("cc -shared -fPIC -undefined dynamic_lookup " + o_path + " " + rt_o_path + " -o " + dylib_path)
if (rc != 0) {
print("linking failed")
disrupt
}
print("built: " + dylib_path)

40
compile_worker.ce Normal file
View 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)

View File

@@ -1,4 +1,5 @@
#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, js2number(js,argv[0])))

55
diff.ce
View File

@@ -8,12 +8,13 @@ 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('os').analyze
var run_ast_fn = use('os').run_ast_fn
var run_ast_noopt_fn = use('os').run_ast_noopt_fn
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)")
@@ -27,10 +28,7 @@ if (length(_args) > 0) {
target_test = _args[0]
}
function is_valid_package(dir) {
var _dir = dir == null ? '.' : dir
return fd.is_file(_dir + '/cell.toml')
}
var is_valid_package = testlib.is_valid_package
if (!is_valid_package('.')) {
log.console('No cell.toml found in current directory')
@@ -63,47 +61,8 @@ function collect_tests(specific_test) {
return test_files
}
// Deep comparison of two values
function values_equal(a, b) {
var i = 0
var ka = null
var kb = null
if (a == b) return true
if (is_null(a) && is_null(b)) return true
if (is_null(a) || is_null(b)) return false
if (is_array(a) && is_array(b)) {
if (length(a) != length(b)) return false
i = 0
while (i < length(a)) {
if (!values_equal(a[i], b[i])) return false
i = i + 1
}
return true
}
if (is_object(a) && is_object(b)) {
ka = array(a)
kb = array(b)
if (length(ka) != length(kb)) return false
i = 0
while (i < length(ka)) {
if (!values_equal(a[ka[i]], b[ka[i]])) return false
i = i + 1
}
return true
}
return false
}
function describe(val) {
if (is_null(val)) return "null"
if (is_text(val)) return `"${val}"`
if (is_number(val)) return text(val)
if (is_logical(val)) return text(val)
if (is_function(val)) return "<function>"
if (is_array(val)) return `[array length=${text(length(val))}]`
if (is_object(val)) return `{record keys=${text(length(array(val)))}}`
return "<unknown>"
}
var values_equal = testlib.values_equal
var describe = testlib.describe
// Run a single test file through both paths
function diff_test_file(file_path) {

310
diff_ir.ce Normal file
View 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
View 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()

View File

@@ -57,7 +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
@@ -79,7 +81,7 @@ cd cell
make bootstrap
```
The ƿit shop is stored at `~/.pit/`.
The ƿit shop is stored at `~/.cell/`.
## Development

View File

@@ -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
@@ -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()
})
```

View File

@@ -52,10 +52,13 @@ 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
@@ -187,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;
}
)
@@ -226,11 +231,111 @@ var d = vector.dot(1, 0, 0, 1) // 0
C files are automatically compiled when you run:
```bash
pit build
pit update
cell --dev build
```
Each C file is compiled into a per-file dynamic library at `~/.pit/lib/<pkg>/<stem>.dylib`.
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
@@ -244,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`:
@@ -257,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

View File

@@ -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,100 +91,14 @@ pit ls # list files in current project
pit ls <package> # list files in specified package
```
### pit build
### pit audit
Build the current package. Compiles C files into per-file dynamic libraries and installs them to `~/.pit/lib/<pkg>/<stem>.dylib`.
Test-compile all `.ce` and `.cm` scripts in package(s). Continues past failures and reports all errors at the end.
```bash
pit build # build current package
pit build <package> # build specific package
```
### pit test
Run tests. See [Testing](/docs/testing/) for the full guide.
```bash
pit test # run tests in current package
pit test all # run all tests
pit test <package> # run tests in specific package
pit test suite --verify --diff # with IR verification and differential testing
```
### pit link
Manage local package links for development.
```bash
pit link add <canonical> <local_path> # link a package
pit link list # show all links
pit link delete <canonical> # remove a link
pit link clear # remove all links
```
### pit fetch
Fetch package sources without extracting.
```bash
pit fetch <package>
```
### pit upgrade
Upgrade the ƿit installation itself.
```bash
pit upgrade
```
### pit clean
Clean build artifacts.
```bash
pit clean
```
### pit add
Add a dependency to the current package. Updates `cell.toml` and installs the package to the shop.
```bash
pit add gitea.pockle.world/john/prosperon # default alias
pit add gitea.pockle.world/john/prosperon myalias # custom alias
```
### pit clone
Clone a package to a local path and link it for development.
```bash
pit clone gitea.pockle.world/john/prosperon ./prosperon
```
### 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 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 audit # audit all installed packages
pit audit <package> # audit specific package
pit audit . # audit current directory
```
### pit resolve
@@ -220,13 +157,393 @@ pit config actor <name> get <key> # get actor config
pit config actor <name> set <key> <val> # set actor config
```
### pit help
### pit bench
Display help information.
Run benchmarks with statistical analysis. Benchmark files are `.cm` modules in a package's `benches/` directory.
```bash
pit help
pit help <command>
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
Manage local package links for development.
```bash
pit link add <canonical> <local_path> # link a package
pit link list # show all links
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.
```bash
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.
```bash
pit upgrade
```
### pit clean
Clean build artifacts.
```bash
pit clean
```
## Logging
### 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 log list
```
### pit log add
Add a log sink.
```bash
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
@@ -243,15 +560,12 @@ pit install /Users/john/work/mylib
## Configuration
ƿit stores its data in `~/.pit/`:
ƿit stores its data in `~/.cell/`:
```
~/.pit/
~/.cell/
├── packages/ # installed package sources
├── lib/ # installed per-file dylibs and mach (persistent)
│ ├── core/ # core package: .dylib and .mach files
│ └── <pkg>/ # per-package subdirectories
├── build/ # ephemeral build cache (safe to delete)
├── build/ # content-addressed cache (bytecode, dylibs, manifests)
├── cache/ # downloaded archives
├── lock.toml # installed package versions
└── link.toml # local development links

View File

@@ -15,65 +15,60 @@ The compiler runs in stages:
source → tokenize → parse → fold → mcode → streamline → output
```
Each stage has a corresponding dump tool that lets you see its output.
Each stage has a corresponding CLI tool that lets you see its output.
| Stage | Tool | What it shows |
|-------------|-------------------|----------------------------------------|
| fold | `dump_ast.cm` | Folded AST as JSON |
| mcode | `dump_mcode.cm` | Raw mcode IR before optimization |
| streamline | `dump_stream.cm` | Before/after instruction counts + IR |
| streamline | `dump_types.cm` | Optimized IR with type annotations |
| streamline | `streamline.ce` | Full optimized IR as JSON |
| all | `ir_report.ce` | Structured optimizer flight recorder |
| 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
./cell --core . dump_mcode.cm myfile.ce
# see raw mcode IR (pretty-printed)
cell mcode --pretty myfile.ce
# see what the optimizer changed
./cell --core . dump_stream.cm 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 --core . ir_report.ce --full myfile.ce
cell ir_report --full myfile.ce
```
## dump_ast.cm
## fold.ce
Prints the folded AST as JSON. This is the output of the parser and constant folder, before mcode generation.
```bash
./cell --core . dump_ast.cm <file.ce|file.cm>
cell fold <file.ce|file.cm>
```
## dump_mcode.cm
## mcode.ce
Prints the raw mcode IR before any optimization. Shows the instruction array as formatted text with opcode, operands, and program counter.
Prints mcode IR. Default output is JSON; use `--pretty` for human-readable format with opcodes, operands, and program counter.
```bash
./cell --core . dump_mcode.cm <file.ce|file.cm>
```
## dump_stream.cm
Shows a before/after comparison of the optimizer. For each function, prints:
- Instruction count before and after
- Number of eliminated instructions
- The streamlined IR (nops hidden by default)
```bash
./cell --core . dump_stream.cm <file.ce|file.cm>
```
## dump_types.cm
Shows the optimized IR with type annotations. Each instruction is followed by the known types of its slot operands, inferred by walking the instruction stream.
```bash
./cell --core . dump_types.cm <file.ce|file.cm>
cell mcode <file.ce|file.cm> # JSON (default)
cell mcode --pretty <file.ce|file.cm> # human-readable IR
```
## streamline.ce
@@ -81,15 +76,251 @@ Shows the optimized IR with type annotations. Each instruction is followed by th
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 --core . streamline.ce <file.ce|file.cm>
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 --core . ir_report.ce [options] <file.ce|file.cm>
cell ir_report [options] <file.ce|file.cm>
```
### Options
@@ -218,16 +449,16 @@ Properties:
```bash
# what passes changed something?
./cell --core . ir_report.ce --summary myfile.ce | jq 'select(.changed)'
cell ir_report --summary myfile.ce | jq 'select(.changed)'
# list all rewrite rules that fired
./cell --core . ir_report.ce --events myfile.ce | jq 'select(.type == "event") | .rule'
cell ir_report --events myfile.ce | jq 'select(.type == "event") | .rule'
# diff IR before and after optimization
./cell --core . ir_report.ce --ir-all myfile.ce | jq -r 'select(.type == "ir") | .text'
cell ir_report --ir-all myfile.ce | jq -r 'select(.type == "ir") | .text'
# full report for analysis
./cell --core . ir_report.ce --full myfile.ce > report.json
cell ir_report --full myfile.ce > report.json
```
## ir_stats.cm

View File

@@ -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.

View File

@@ -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")
}
```

View File

@@ -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
View 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
View 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
```

View File

@@ -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.

View File

@@ -9,11 +9,11 @@ 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/
@@ -23,7 +23,7 @@ mypackage/
└── helpers.cm # private module (internal/ only)
```
## pit.toml
## cell.toml
The package manifest declares metadata and dependencies:
@@ -47,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
@@ -91,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/
@@ -105,14 +105,8 @@ Local packages are symlinked into the shop, making development seamless.
│ └── john/
│ └── work/
│ └── mylib -> /Users/john/work/mylib
├── lib/
│ ├── core/
│ │ ├── fd.dylib
│ │ └── time.mach
│ └── gitea_pockle_world_john_prosperon/
│ └── sprite.dylib
├── build/
│ └── <content-addressed cache>
│ └── <content-addressed cache (bytecode, dylibs, manifests)>
├── cache/
│ └── <downloaded zips>
├── lock.toml
@@ -175,16 +169,16 @@ pit link delete gitea.pockle.world/john/prosperon
## C Extensions
C files in a package are compiled into per-file dynamic libraries:
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 lib/mypackage/render.dylib
└── physics.c # compiled to lib/mypackage/physics.dylib
├── cell.toml
├── render.c # compiled to ~/.cell/build/<hash>
└── physics.c # compiled to ~/.cell/build/<hash>
```
Each `.c` file gets its own `.dylib` in `~/.pit/lib/<pkg>/`. A `.c` file and `.cm` file with the same stem at the same scope is a build error — use distinct names.
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.

View File

@@ -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
View 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.

View File

@@ -17,7 +17,7 @@ When `pit` runs a program, startup takes one of two paths:
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 (`~/.pit/build/<hash>`). On a cache hit, engine.cm loads directly — no bootstrap involved.
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)
@@ -37,7 +37,7 @@ On a cache miss, the C runtime loads `boot/bootstrap.cm.mcode` (a pre-compiled s
### 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 `~/.pit/build/`.
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
@@ -47,8 +47,8 @@ When `use('path')` is called from a package context, the shop resolves the modul
For a call like `use('sprite')` from package `myapp`:
1. **Own package**`~/.pit/packages/myapp/sprite.cm` and C symbol `js_myapp_sprite_use`
2. **Aliased dependencies** — if `myapp/pit.toml` has `renderer = "gitea.pockle.world/john/renderer"`, checks `renderer/sprite.cm` and its C symbols
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.
@@ -73,38 +73,58 @@ use('gitea.pockle.world/john/renderer/sprite')
## Compilation and Caching
Every module goes through a content-addressed caching pipeline. The cache key is the BLAKE2 hash of the source content, so changing the source automatically invalidates the cache.
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. **Installed dylib**per-file `.dylib` in `~/.pit/lib/<pkg>/<stem>.dylib`
3. **Installed mach** — pre-compiled bytecode in `~/.pit/lib/<pkg>/<stem>.mach`
4. **Cached bytecode** — content-addressed in `~/.pit/build/<hash>` (no extension)
5. **Cached .mcode IR** — JSON IR in `~/.pit/build/<hash>.mcode`
6. **Internal symbols** — statically linked into the `pit` binary (fat builds)
7. **Source compilation** — full pipeline: analyze, mcode, streamline, serialize
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
When both a `.dylib` and `.mach` exist for the same module in `lib/`, the dylib is selected. Dylib resolution also wins over internal symbols, so a dylib in `lib/` can hot-patch a fat binary. Delete the dylib to fall back to mach or static.
Results from steps 5-7 are cached back to the content-addressed store for future loads.
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 `~/.pit/build/` stores ephemeral artifacts named by the BLAKE2 hash of their inputs:
The build cache at `~/.cell/build/` stores all compiled artifacts named by the BLAKE2 hash of their inputs:
```
~/.pit/build/
├── a1b2c3d4... # cached bytecode blob (no extension)
── c9d0e1f2...mcode # cached JSON IR
└── f3a4b5c6... # compiled dylib (checked before copying to lib/)
~/.cell/build/
├── a1b2c3d4... # cached bytecode blob, object file, dylib, or manifest (no extension)
── ...
```
This scheme provides automatic cache invalidation: when source changes, its hash changes, and the old cache entry is simply never looked up again. When building a dylib, the build cache is checked first — if a matching hash exists, it is copied to `lib/` without recompiling.
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
@@ -124,10 +144,10 @@ symbol: js_gitea_pockle_world_john_prosperon_sprite_use
### C Resolution Sources
1. **Installed dylibs**per-file dylibs in `~/.pit/lib/<pkg>/<stem>.dylib` (deterministic paths, no manifests)
2. **Internal symbols** — statically linked into the `pit` binary (fat builds)
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 an installed dylib always wins over a statically linked symbol. This enables hot-patching fat binaries by placing a dylib in `lib/`.
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
@@ -145,7 +165,7 @@ The set of injected capabilities is controlled by `script_inject_for()`, which c
## Shop Configuration
The shop reads an optional `shop.toml` file from the shop root (`~/.pit/shop.toml`). This file controls which loading methods are permitted through policy flags.
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
@@ -188,22 +208,12 @@ If `shop.toml` is missing or has no `[policy]` section, all methods are enabled
## Shop Directory Layout
```
~/.pit/
~/.cell/
├── packages/ # installed packages (directories and symlinks)
│ └── core -> ... # symlink to the ƿit core
├── lib/ # INSTALLED per-file artifacts (persistent, human-readable)
│ ├── core/
│ ├── fd.dylib
│ │ ├── time.mach
│ │ ├── time.dylib
│ │ └── internal/
│ │ └── os.dylib
│ └── gitea_pockle_world_john_prosperon/
│ ├── sprite.dylib
│ └── render.dylib
├── build/ # EPHEMERAL cache (safe to delete anytime)
│ ├── <hash> # cached bytecode or dylib blobs (no extension)
│ └── <hash>.mcode # cached JSON IR
├── 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
@@ -220,4 +230,4 @@ If `shop.toml` is missing or has no `[policy]` section, all methods are enabled
| `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, bootstrap) |
| `boot/*.cm.mcode` | Pre-compiled pipeline seeds (tokenize, parse, fold, mcode, streamline, bootstrap) |

View File

@@ -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

View File

@@ -93,3 +93,13 @@ Arithmetic ops (ADD, SUB, MUL, DIV, MOD, POW) are executed inline without callin
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).

View File

@@ -101,6 +101,11 @@ Operands are register slot numbers (integers), constant values (strings, numbers
| 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
@@ -229,7 +234,6 @@ Function calls are decomposed into three instructions:
| Instruction | Operands | Description |
|-------------|----------|-------------|
| `access` | `dest, name` | Load variable (intrinsic or module environment) |
| `set_var` | `name, src` | Set top-level variable by name |
| `get` | `dest, level, slot` | Get closure variable from parent scope |
| `put` | `level, slot, src` | Set closure variable in parent scope |

View File

@@ -62,12 +62,14 @@ See [Mcode IR](mcode.md) for the instruction format and complete instruction ref
Optimizes the Mcode IR through a series of independent passes. Operates per-function:
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. **Type-check elimination**: When a slot's type is known, eliminates `is_<type>` + conditional jump pairs. Narrows `load_dynamic`/`store_dynamic` to typed variants.
3. **Algebraic simplification**: Rewrites identity operations (add 0, multiply 1, divide 1) and folds same-slot comparisons.
4. **Boolean simplification**: Fuses `not` + conditional jump into a single jump with inverted condition.
5. **Move elimination**: Removes self-moves (`move a, a`).
6. **Unreachable elimination**: Nops dead code after `return` until the next label.
7. **Dead jump elimination**: Removes jumps to the immediately following label.
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.
See [Streamline Optimizer](streamline.md) for detailed pass descriptions.
@@ -93,6 +95,25 @@ String constants are interned in a data section. Integer constants are encoded i
pit --emit-qbe script.ce > output.ssa
```
## Boot Seeds
The `boot/` directory contains pre-compiled mcode IR (JSON) seed files for the pipeline modules:
```
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 |
@@ -111,9 +132,10 @@ pit --emit-qbe script.ce > output.ssa
| File | Purpose |
|------|---------|
| `dump_mcode.cm` | Print raw Mcode IR before streamlining |
| `dump_stream.cm` | Print IR after streamlining with before/after stats |
| `dump_types.cm` | Print streamlined IR with type annotations |
| `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
@@ -126,3 +148,4 @@ pit --emit-qbe script.ce > output.ssa
| `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) |

View File

@@ -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.

View File

@@ -95,7 +95,9 @@ Write type mapping:
| `move`, `load_field`, `load_index`, `load_dynamic`, `pop`, `get` | T_UNKNOWN |
| `invoke`, `tail_invoke` | T_UNKNOWN |
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.
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:
@@ -162,7 +164,44 @@ Removes `move a, a` instructions where the source and destination are the same s
**Nop prefix:** `_nop_mv_`
### 7. eliminate_unreachable (dead code after return)
### 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.
@@ -170,12 +209,42 @@ The mcode compiler emits a label at disruption handler entry points (see `emit_l
**Nop prefix:** `_nop_ur_`
### 8. eliminate_dead_jumps (jump-to-next-label elimination)
### 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`:
@@ -187,8 +256,10 @@ 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.
@@ -253,21 +324,23 @@ move 2, 7 // i = temp
subtract 2, 2, 6 // i = i - 1 (direct)
```
The `+` operator is excluded from target slot propagation when it would use the full text+num dispatch (i.e., when neither operand is a known number), because writing both `concat` and `add` to the variable's slot would pollute its write type. When the known-number shortcut applies, `+` uses `emit_numeric_binop` and would be safe for target propagation, but this is not currently implemented — the exclusion is by operator kind, not by dispatch path.
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
Three dump tools inspect the IR at different stages:
CLI tools inspect the IR at different stages:
- **`dump_mcode.cm`** — prints the raw Mcode IR after `mcode.cm`, before streamlining
- **`dump_stream.cm`** — prints the IR after streamlining, with before/after instruction counts
- **`dump_types.cm`** — prints the streamlined IR with type annotations on each instruction
- **`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 --core . dump_mcode.cm <file.ce|file.cm>
./cell --core . dump_stream.cm <file.ce|file.cm>
./cell --core . dump_types.cm <file.ce|file.cm>
cell mcode --pretty <file.ce|file.cm>
cell streamline --stats <file.ce|file.cm>
cell streamline --types <file.ce|file.cm>
```
## Tail Call Marking
@@ -342,7 +415,7 @@ This was implemented and tested but causes a bootstrap failure during self-hosti
### 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 should be safe in this case, but is currently blocked by the blanket `kind != "+"` exclusion. Refining the exclusion to check whether the shortcut will apply (by testing `is_known_number` on either operand) would enable direct writes for patterns like `i = i + 1`.
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

View File

@@ -118,6 +118,45 @@ When a mismatch is found:
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.
@@ -153,6 +192,36 @@ 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:

View File

@@ -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.

View File

@@ -1,16 +0,0 @@
// dump_ast.cm — pretty-print the folded AST as JSON
//
// Usage: ./cell --core . dump_ast.cm <file.ce|file.cm>
var fd = use("fd")
var json = use("json")
var tokenize = use("tokenize")
var parse = use("parse")
var fold = use("fold")
var filename = args[0]
var src = text(fd.slurp(filename))
var tok = tokenize(src, filename)
var ast = parse(tok.tokens, src, filename, tokenize)
var folded = fold(ast)
print(json.encode(folded))

View File

@@ -1,22 +1,13 @@
var tokenize = use('tokenize')
var parse_mod = use('parse')
var fold = use('fold')
var mcode_mod = use('mcode')
var streamline_mod = use('streamline')
var json = use('json')
var fd = use('fd')
// cell dump_ir - Dump intermediate representation for a source file
var file = args[0]
var src = text(fd.slurp(file))
var tok = tokenize(src, file)
var ast = parse_mod(tok.tokens, src, file, tokenize)
var folded = fold(ast)
var compiled = mcode_mod(folded)
var optimized = streamline_mod(compiled)
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)) {
print(text(i) + ': ' + json.encode(instrs[i]))
log.compile(text(i) + ': ' + json.encode(instrs[i]))
i = i + 1
}

View File

@@ -1,117 +0,0 @@
// dump_mcode.cm — pretty-print mcode IR (before streamlining)
//
// Usage: ./cell --core . dump_mcode.cm <file.ce|file.cm>
var fd = use("fd")
var json = use("json")
var tokenize = use("tokenize")
var parse = use("parse")
var fold = use("fold")
var mcode = use("mcode")
if (length(args) < 1) {
print("usage: cell --core . dump_mcode.cm <file>")
return
}
var filename = args[0]
var src = text(fd.slurp(filename))
var tok = tokenize(src, filename)
var ast = parse(tok.tokens, src, filename, tokenize)
var folded = fold(ast)
var compiled = mcode(folded)
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 json.encode(v)
}
if (is_logical(v)) {
return v ? "true" : "false"
}
return text(v)
}
var dump_function = function(func, name) {
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 i = 0
var pc = 0
var instr = null
var op = null
var n = 0
var parts = null
var j = 0
var operands = null
var pc_str = null
var op_str = null
print(`\n=== ${name} (args=${text(nr_args)}, slots=${text(nr_slots)}, closures=${text(nr_close)}) ===`)
if (instrs == null || length(instrs) == 0) {
print(" (empty)")
return null
}
while (i < length(instrs)) {
instr = instrs[i]
if (is_text(instr)) {
if (!starts_with(instr, "_nop_")) {
print(`${instr}:`)
}
} else if (is_array(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, ", ")
pc_str = pad_right(text(pc), 5)
op_str = pad_right(op, 14)
print(` ${pc_str} ${op_str} ${operands}`)
pc = pc + 1
}
i = i + 1
}
return null
}
var main_name = null
var fi = 0
var func = null
var fname = null
// Dump main
if (compiled.main != null) {
main_name = compiled.name != null ? compiled.name : "<main>"
dump_function(compiled.main, main_name)
}
// Dump sub-functions
if (compiled.functions != null) {
fi = 0
while (fi < length(compiled.functions)) {
func = compiled.functions[fi]
fname = func.name != null ? func.name : `<func_${text(fi)}>`
dump_function(func, `[${text(fi)}] ${fname}`)
fi = fi + 1
}
}

View File

@@ -1,166 +0,0 @@
// dump_stream.cm — show mcode IR before and after streamlining
//
// Usage: ./cell --core . dump_stream.cm <file.ce|file.cm>
var fd = use("fd")
var json = use("json")
var tokenize = use("tokenize")
var parse = use("parse")
var fold = use("fold")
var mcode = use("mcode")
var streamline = use("streamline")
if (length(args) < 1) {
print("usage: cell --core . dump_stream.cm <file>")
return
}
var filename = args[0]
var src = text(fd.slurp(filename))
var tok = tokenize(src, filename)
var ast = parse(tok.tokens, src, filename, tokenize)
var folded = fold(ast)
var compiled = mcode(folded)
// Deep copy IR for before snapshot
var before = json.decode(json.encode(compiled))
var optimized = streamline(compiled)
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 json.encode(v)
}
if (is_logical(v)) {
return v ? "true" : "false"
}
return text(v)
}
var count_stats = function(func) {
var instrs = func.instructions
var total = 0
var nops = 0
var calls = 0
var i = 0
var instr = null
if (instrs == null) {
return {total: 0, nops: 0, real: 0, calls: 0}
}
while (i < length(instrs)) {
instr = instrs[i]
if (is_text(instr)) {
if (starts_with(instr, "_nop_")) {
nops = nops + 1
}
} else if (is_array(instr)) {
total = total + 1
if (instr[0] == "invoke") {
calls = calls + 1
}
}
i = i + 1
}
return {total: total, nops: nops, real: total - nops, calls: calls}
}
var dump_function = function(func, show_nops) {
var instrs = func.instructions
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 pc_str = null
var op_str = null
if (instrs == null || length(instrs) == 0) {
return null
}
while (i < length(instrs)) {
instr = instrs[i]
if (is_text(instr)) {
if (starts_with(instr, "_nop_")) {
if (show_nops) {
print(` ${pad_right(text(pc), 5)} --- nop ---`)
pc = pc + 1
}
} else {
print(`${instr}:`)
}
} else if (is_array(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, ", ")
pc_str = pad_right(text(pc), 5)
op_str = pad_right(op, 14)
print(` ${pc_str} ${op_str} ${operands}`)
pc = pc + 1
}
i = i + 1
}
return null
}
var dump_pair = function(before_func, after_func, name) {
var nr_args = after_func.nr_args != null ? after_func.nr_args : 0
var nr_slots = after_func.nr_slots != null ? after_func.nr_slots : 0
var b_stats = count_stats(before_func)
var a_stats = count_stats(after_func)
var eliminated = a_stats.nops
print(`\n=== ${name} (args=${text(nr_args)}, slots=${text(nr_slots)}) ===`)
print(` before: ${text(b_stats.total)} instructions, ${text(b_stats.calls)} invokes`)
print(` after: ${text(a_stats.real)} instructions (${text(eliminated)} eliminated), ${text(a_stats.calls)} invokes`)
print("\n -- streamlined --")
dump_function(after_func, false)
return null
}
var main_name = null
var fi = 0
var func = null
var bfunc = null
var fname = null
// Dump main
if (optimized.main != null && before.main != null) {
main_name = optimized.name != null ? optimized.name : "<main>"
dump_pair(before.main, optimized.main, main_name)
}
// Dump sub-functions
if (optimized.functions != null && before.functions != null) {
fi = 0
while (fi < length(optimized.functions)) {
func = optimized.functions[fi]
bfunc = before.functions[fi]
fname = func.name != null ? func.name : `<func_${text(fi)}>`
dump_pair(bfunc, func, `[${text(fi)}] ${fname}`)
fi = fi + 1
}
}

View File

@@ -1,237 +0,0 @@
// dump_types.cm — show streamlined IR with type annotations
//
// Usage: ./cell --core . dump_types.cm <file.ce|file.cm>
var fd = use("fd")
var json = use("json")
var tokenize = use("tokenize")
var parse = use("parse")
var fold = use("fold")
var mcode = use("mcode")
var streamline = use("streamline")
if (length(args) < 1) {
print("usage: cell --core . dump_types.cm <file>")
return
}
var filename = args[0]
var src = text(fd.slurp(filename))
var tok = tokenize(src, filename)
var ast = parse(tok.tokens, src, filename, tokenize)
var folded = fold(ast)
var compiled = mcode(folded)
var optimized = streamline(compiled)
// Type constants
def T_UNKNOWN = "unknown"
def T_INT = "int"
def T_FLOAT = "float"
def T_NUM = "num"
def T_TEXT = "text"
def T_BOOL = "bool"
def T_NULL = "null"
def T_ARRAY = "array"
def T_RECORD = "record"
def T_FUNCTION = "function"
def int_result_ops = {
bitnot: true, bitand: true, bitor: true,
bitxor: true, shl: true, shr: true, ushr: true
}
def bool_result_ops = {
eq_int: true, ne_int: true, lt_int: true, gt_int: true,
le_int: true, ge_int: true,
eq_float: true, ne_float: true, lt_float: true, gt_float: true,
le_float: true, ge_float: true,
eq_text: true, ne_text: true, lt_text: true, gt_text: true,
le_text: true, ge_text: true,
eq_bool: true, ne_bool: true,
not: true, and: true, or: true,
is_int: true, is_text: true, is_num: true,
is_bool: true, is_null: true, is_identical: true,
is_array: true, is_func: true, is_record: true, is_stone: true
}
var access_value_type = function(val) {
if (is_number(val)) {
return is_integer(val) ? T_INT : T_FLOAT
}
if (is_text(val)) {
return T_TEXT
}
return T_UNKNOWN
}
var track_types = function(slot_types, instr) {
var op = instr[0]
var src_type = null
if (op == "access") {
slot_types[text(instr[1])] = access_value_type(instr[2])
} else if (op == "int") {
slot_types[text(instr[1])] = T_INT
} else if (op == "true" || op == "false") {
slot_types[text(instr[1])] = T_BOOL
} else if (op == "null") {
slot_types[text(instr[1])] = T_NULL
} else if (op == "move") {
src_type = slot_types[text(instr[2])]
slot_types[text(instr[1])] = src_type != null ? src_type : T_UNKNOWN
} else if (int_result_ops[op] == true) {
slot_types[text(instr[1])] = T_INT
} else if (op == "concat") {
slot_types[text(instr[1])] = T_TEXT
} else if (bool_result_ops[op] == true) {
slot_types[text(instr[1])] = T_BOOL
} else if (op == "typeof") {
slot_types[text(instr[1])] = T_TEXT
} else if (op == "array") {
slot_types[text(instr[1])] = T_ARRAY
} else if (op == "record") {
slot_types[text(instr[1])] = T_RECORD
} else if (op == "function") {
slot_types[text(instr[1])] = T_FUNCTION
} else if (op == "invoke" || op == "tail_invoke") {
slot_types[text(instr[2])] = T_UNKNOWN
} else if (op == "load_field" || op == "load_index" || op == "load_dynamic") {
slot_types[text(instr[1])] = T_UNKNOWN
} else if (op == "pop" || op == "get") {
slot_types[text(instr[1])] = T_UNKNOWN
} else if (op == "length") {
slot_types[text(instr[1])] = T_INT
} else if (op == "add" || op == "subtract" || op == "multiply" ||
op == "divide" || op == "modulo" || op == "pow" || op == "negate") {
slot_types[text(instr[1])] = T_UNKNOWN
}
return null
}
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 json.encode(v)
}
if (is_logical(v)) {
return v ? "true" : "false"
}
return text(v)
}
// Build type annotation string for an instruction
var type_annotation = function(slot_types, instr) {
var n = length(instr)
var parts = []
var j = 1
var v = null
var t = null
while (j < n - 2) {
v = instr[j]
if (is_number(v)) {
t = slot_types[text(v)]
if (t != null && t != T_UNKNOWN) {
push(parts, `s${text(v)}:${t}`)
}
}
j = j + 1
}
if (length(parts) == 0) {
return ""
}
return text(parts, " ")
}
var dump_function_typed = function(func, name) {
var nr_args = func.nr_args != null ? func.nr_args : 0
var nr_slots = func.nr_slots != null ? func.nr_slots : 0
var instrs = func.instructions
var slot_types = {}
var i = 0
var pc = 0
var instr = null
var op = null
var n = 0
var annotation = null
var operand_parts = null
var j = 0
var operands = null
var pc_str = null
var op_str = null
var line = null
print(`\n=== ${name} (args=${text(nr_args)}, slots=${text(nr_slots)}) ===`)
if (instrs == null || length(instrs) == 0) {
print(" (empty)")
return null
}
while (i < length(instrs)) {
instr = instrs[i]
if (is_text(instr)) {
if (starts_with(instr, "_nop_")) {
i = i + 1
continue
}
slot_types = {}
print(`${instr}:`)
} else if (is_array(instr)) {
op = instr[0]
n = length(instr)
annotation = type_annotation(slot_types, instr)
operand_parts = []
j = 1
while (j < n - 2) {
push(operand_parts, fmt_val(instr[j]))
j = j + 1
}
operands = text(operand_parts, ", ")
pc_str = pad_right(text(pc), 5)
op_str = pad_right(op, 14)
line = pad_right(` ${pc_str} ${op_str} ${operands}`, 50)
if (length(annotation) > 0) {
print(`${line} ; ${annotation}`)
} else {
print(line)
}
track_types(slot_types, instr)
pc = pc + 1
}
i = i + 1
}
return null
}
var main_name = null
var fi = 0
var func = null
var fname = null
// Dump main
if (optimized.main != null) {
main_name = optimized.name != null ? optimized.name : "<main>"
dump_function_typed(optimized.main, main_name)
}
// Dump sub-functions
if (optimized.functions != null) {
fi = 0
while (fi < length(optimized.functions)) {
func = optimized.functions[fi]
fname = func.name != null ? func.name : `<func_${text(fi)}>`
dump_function_typed(func, `[${text(fi)}] ${fname}`)
fi = fi + 1
}
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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}
}
}
}
}

View File

@@ -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"
}

View File

@@ -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
View 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
View 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
}

22
fd.cm
View File

@@ -1,5 +1,5 @@
var fd = use('internal/fd')
var wildstar = use('wildstar')
var wildstar = use('internal/wildstar')
function last_pos(str, sep) {
var last = null
@@ -97,4 +97,24 @@ fd.globfs = function(globs, dir) {
return results
}
fd.ensure_dir = function ensure_dir(path) {
if (fd.is_dir(path)) return true
var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
var i = 0
for (i = 0; i < length(parts); i++) {
if (parts[i] == '') continue
current = current + parts[i] + '/'
if (!fd.is_dir(current))
fd.mkdir(current)
}
return true
}
fd.safe_package_path = function safe_package_path(pkg) {
if (pkg && starts_with(pkg, '/'))
return replace(replace(pkg, '/', '_'), '@', '_')
return replace(pkg, '@', '_')
}
return fd

113
fetch.ce
View File

@@ -1,90 +1,49 @@
// cell fetch - Fetch package zips from remote sources
// cell fetch - Sync packages from remote sources
//
// This command ensures that the zip files on disk match what's in the lock file.
// For local packages, this is a no-op.
// For remote packages, downloads the zip if not present or hash mismatch.
// Ensures all packages are fetched, extracted, compiled, and ready to use.
// For local packages, this is a no-op (symlinks only).
//
// Usage:
// cell fetch - Fetch all packages
// cell fetch <package> - Fetch a specific package
// cell fetch - Sync all packages
// cell fetch <package> - Sync a specific package
var shop = use('internal/shop')
// Parse arguments
var target_pkg = null
var i = 0
var packages = null
var count = 0
for (i = 0; i < length(args); i++) {
if (args[i] == '--help' || args[i] == '-h') {
log.console("Usage: cell fetch [package]")
log.console("Fetch package zips from remote sources.")
log.console("")
log.console("Arguments:")
log.console(" package Optional package name to fetch. If omitted, fetches all.")
log.console("")
log.console("This command ensures that the zip files on disk match what's in")
log.console("the lock file. For local packages, this is a no-op.")
$stop()
} else if (!starts_with(args[i], '-')) {
target_pkg = args[i]
var run = function() {
for (i = 0; i < length(args); i++) {
if (args[i] == '--help' || args[i] == '-h') {
log.console("Usage: cell fetch [package]")
log.console("Sync packages from remote sources.")
log.console("")
log.console("Arguments:")
log.console(" package Optional package to sync. If omitted, syncs all.")
return
} else if (!starts_with(args[i], '-')) {
target_pkg = args[i]
}
}
if (target_pkg) {
target_pkg = shop.resolve_locator(target_pkg)
log.console("Syncing " + target_pkg + "...")
shop.sync(target_pkg)
log.console("Done.")
} else {
packages = shop.list_packages()
count = 0
arrfor(packages, function(pkg) {
if (pkg == 'core') return
shop.sync(pkg)
count = count + 1
})
log.console("Synced " + text(count) + " package(s).")
}
}
var all_packages = shop.list_packages()
var lock = shop.load_lock()
var packages_to_fetch = []
if (target_pkg) {
// Fetch specific package
if (find(all_packages, target_pkg) == null) {
log.error("Package not found: " + target_pkg)
$stop()
}
push(packages_to_fetch, target_pkg)
} else {
// Fetch all packages
packages_to_fetch = all_packages
}
var remote_count = 0
arrfor(packages_to_fetch, function(pkg) {
var entry = lock[pkg]
if (pkg != 'core' && (!entry || entry.type != 'local'))
remote_count++
}, null, null)
if (remote_count > 0)
log.console(`Fetching ${text(remote_count)} remote package(s)...`)
var downloaded_count = 0
var cached_count = 0
var fail_count = 0
arrfor(packages_to_fetch, function(pkg) {
// Skip core (handled separately)
if (pkg == 'core') return
var result = shop.fetch(pkg)
if (result.status == 'local') {
// Local packages are just symlinks, nothing to fetch
return
} else if (result.status == 'cached') {
cached_count++
} else if (result.status == 'downloaded') {
log.console(" Downloaded: " + pkg)
downloaded_count++
} else if (result.status == 'error') {
log.error(" Failed: " + pkg + (result.message ? " - " + result.message : ""))
fail_count++
}
}, null, null)
log.console("")
var parts = []
if (downloaded_count > 0) push(parts, `${text(downloaded_count)} downloaded`)
if (cached_count > 0) push(parts, `${text(cached_count)} cached`)
if (fail_count > 0) push(parts, `${text(fail_count)} failed`)
if (length(parts) == 0) push(parts, "nothing to fetch")
log.console("Fetch complete: " + text(parts, ", "))
run()
$stop()

16
fold.ce
View File

@@ -1,13 +1,7 @@
var fd = use("fd")
// cell fold - Run constant folding on a source file
var json = use("json")
var shop = use("internal/shop")
var filename = args[0]
var src = text(fd.slurp(filename))
var tokenize = use("tokenize")
var parse = use("parse")
var fold = use("fold")
var tok_result = tokenize(src, filename)
var ast = parse(tok_result.tokens, src, filename, tokenize)
var folded = fold(ast)
print(json.encode(folded))
var folded = shop.analyze_file(filename)
log.compile(json.encode(folded))

58
fold.cm
View File

@@ -4,6 +4,7 @@
var fold = function(ast) {
var scopes = ast.scopes
var nr_scopes = length(scopes)
ast._diagnostics = []
var type_tag_map = {
array: "array", record: "record", text: "text",
@@ -72,6 +73,7 @@ var fold = function(ast) {
if (k == "record") {
i = 0
while (i < length(expr.list)) {
if (expr.list[i].computed && !is_pure(expr.list[i].left)) return false
if (!is_pure(expr.list[i].right)) return false
i = i + 1
}
@@ -285,6 +287,7 @@ var fold = function(ast) {
if (k == "record") {
i = 0
while (i < length(expr.list)) {
if (expr.list[i].computed) pre_scan_expr_fns(expr.list[i].left)
pre_scan_expr_fns(expr.list[i].right)
i = i + 1
}
@@ -359,6 +362,7 @@ var fold = function(ast) {
var fold_expr = null
var fold_stmt = null
var fold_stmts = null
var fold_fn = null
fold_expr = function(expr, fn_nr) {
if (expr == null) return null
@@ -411,6 +415,9 @@ var fold = function(ast) {
} else if (k == "record") {
i = 0
while (i < length(expr.list)) {
if (expr.list[i].computed) {
expr.list[i].left = fold_expr(expr.list[i].left, fn_nr)
}
expr.list[i].right = fold_expr(expr.list[i].right, fn_nr)
i = i + 1
}
@@ -458,7 +465,7 @@ var fold = function(ast) {
else if (k == "-") result = lv - rv
else if (k == "*") result = lv * rv
else if (k == "/") result = lv / rv
else if (k == "%") result = lv % rv
else if (k == "%") result = lv - (trunc(lv / rv) * rv)
else if (k == "**") result = lv ** rv
if (result == null) return make_null(expr)
return make_number(result, expr)
@@ -586,8 +593,6 @@ var fold = function(ast) {
return expr
}
var fold_fn = null
fold_stmt = function(stmt, fn_nr) {
if (stmt == null) return null
var k = stmt.kind
@@ -701,8 +706,30 @@ var fold = function(ast) {
name = stmt.left.name
if (name != null) {
sv = scope_var(fn_nr, name)
if (sv != null && sv.nr_uses == 0 && is_pure(stmt.right)) {
stmt.dead = true
if (sv != null && sv.nr_uses == 0) {
if (is_pure(stmt.right)) stmt.dead = true
if (stmt.right != null && stmt.right.kind == "(" && stmt.right.expression != null && stmt.right.expression.name == "use") {
push(ast._diagnostics, {
severity: "warning",
line: stmt.left.from_row + 1,
col: stmt.left.from_column + 1,
message: `unused import '${name}'`
})
} else if (stmt.kind == "def") {
push(ast._diagnostics, {
severity: "warning",
line: stmt.left.from_row + 1,
col: stmt.left.from_column + 1,
message: `unused constant '${name}'`
})
} else {
push(ast._diagnostics, {
severity: "warning",
line: stmt.left.from_row + 1,
col: stmt.left.from_column + 1,
message: `unused variable '${name}'`
})
}
}
}
}
@@ -715,6 +742,12 @@ var fold = function(ast) {
sv = scope_var(fn_nr, stmt.name)
if (sv != null && sv.nr_uses == 0) {
stmt.dead = true
push(ast._diagnostics, {
severity: "warning",
line: stmt.from_row + 1,
col: stmt.from_column + 1,
message: `unused function '${stmt.name}'`
})
}
}
if (stmt.dead != true) push(out, stmt)
@@ -831,6 +864,7 @@ var fold = function(ast) {
if (k == "record") {
i = 0
while (i < length(expr.list)) {
if (expr.list[i].computed) walk_expr_for_fns(expr.list[i].left)
walk_expr_for_fns(expr.list[i].right)
i = i + 1
}
@@ -920,6 +954,7 @@ var fold = function(ast) {
if (k == "record") {
i = 0
while (i < length(expr.list)) {
if (expr.list[i].computed) collect_expr_intrinsics(expr.list[i].left)
collect_expr_intrinsics(expr.list[i].right)
i = i + 1
}
@@ -1028,9 +1063,22 @@ var fold = function(ast) {
// Remove dead top-level functions
var live_fns = []
var fn = null
var fn_sv = null
fi = 0
while (fi < length(ast.functions)) {
fn = ast.functions[fi]
if (fn.name != null) {
fn_sv = scope_var(0, fn.name)
if (fn_sv != null && fn_sv.nr_uses == 0) {
fn.dead = true
push(ast._diagnostics, {
severity: "warning",
line: fn.from_row + 1,
col: fn.from_column + 1,
message: `unused function '${fn.name}'`
})
}
}
if (fn.dead != true) {
push(live_fns, fn)
}

48
fuzz.ce
View File

@@ -12,8 +12,9 @@
var fd = use('fd')
var time = use('time')
var json = use('json')
var testlib = use('internal/testlib')
var os_ref = use('os')
var os_ref = use('internal/os')
var analyze = os_ref.analyze
var run_ast_fn = os_ref.run_ast_fn
var run_ast_noopt_fn = os_ref.run_ast_noopt_fn
@@ -54,48 +55,9 @@ if (!run_ast_noopt_fn) {
// Ensure failures directory exists
var failures_dir = "tests/fuzz_failures"
function ensure_dir(path) {
if (fd.is_dir(path)) return
var parts = array(path, '/')
var current = ''
var j = 0
while (j < length(parts)) {
if (parts[j] != '') {
current = current + parts[j] + '/'
if (!fd.is_dir(current)) {
fd.mkdir(current)
}
}
j = j + 1
}
}
// Deep comparison
function values_equal(a, b) {
var j = 0
if (a == b) return true
if (is_null(a) && is_null(b)) return true
if (is_null(a) || is_null(b)) return false
if (is_array(a) && is_array(b)) {
if (length(a) != length(b)) return false
j = 0
while (j < length(a)) {
if (!values_equal(a[j], b[j])) return false
j = j + 1
}
return true
}
return false
}
function describe(val) {
if (is_null(val)) return "null"
if (is_text(val)) return `"${val}"`
if (is_number(val)) return text(val)
if (is_logical(val)) return text(val)
if (is_function(val)) return "<function>"
return "<other>"
}
var ensure_dir = fd.ensure_dir
var values_equal = testlib.values_equal
var describe = testlib.describe
// Run a single fuzz iteration
function run_fuzz(seed_val) {

View File

@@ -1,403 +0,0 @@
# Plan: Complete Copying GC Implementation
## Overview
Remove reference counting (DupValue/FreeValue) entirely and complete the Cheney copying garbage collector. Each JSContext will use bump allocation from a heap block, and when out of memory, request a new heap from JSRuntime's buddy allocator and copy live objects to the new heap.
## Target Architecture (from docs/memory.md)
### Object Types (simplified from current):
**Type 0 - Array**: `{ header, length, elements[] }`
**Type 1 - Blob**: `{ header, length, bits[] }`
**Type 2 - Text**: `{ header, length_or_hash, packed_chars[] }`
**Type 3 - Record**: `{ header, prototype, length, key_value_pairs[] }`
**Type 4 - Function**: `{ header, code_ptr, outer_frame_ptr }` - 3 words only, always stone
**Type 5 - Frame**: `{ header, function_ptr, caller_ptr, ret_addr, args[], closure_vars[], local_vars[], temps[] }`
**Type 6 - Code**: Lives in immutable memory only, never copied
**Type 7 - Forward**: Object has moved; cap56 contains new address
### Key Design Points:
- **JSFunction** is just a pointer to code and a pointer to the frame that created it (3 words)
- **Closure variables live in frames** - when a function returns, its frame is "reduced" to just the closure variables
- **Code objects are immutable** - stored in stone memory, never copied during GC
- **Frame reduction**: When a function returns, `caller` is set to zero, signaling the frame can be shrunk
## Current State (needs refactoring)
1. **Partial Cheney GC exists** at `source/quickjs.c:1844-2030`: `ctx_gc`, `gc_copy_value`, `gc_scan_object`
2. **744 calls to JS_DupValue/JS_FreeValue** scattered throughout (currently undefined, causing compilation errors)
3. **Current JSFunction** is bloated (has kind, name, union of cfunc/bytecode/bound) - needs simplification
4. **Current JSVarRef** is a separate object - should be eliminated, closures live in frames
5. **Bump allocator** in `js_malloc` (line 1495) with `heap_base`/`heap_free`/`heap_end`
6. **Buddy allocator** for memory blocks (lines 1727-1837)
7. **Header offset inconsistency** - some structs have header at offset 0, some at offset 8
## Implementation Steps
### Phase 1: Define No-Op DupValue/FreeValue (To Enable Compilation)
Add these near line 100 in `source/quickjs.c`:
```c
/* Copying GC - no reference counting needed */
#define JS_DupValue(ctx, v) (v)
#define JS_FreeValue(ctx, v) ((void)0)
#define JS_DupValueRT(rt, v) (v)
#define JS_FreeValueRT(rt, v) ((void)0)
```
This makes the code compile while keeping existing call sites (they become no-ops).
### Phase 2: Standardize Object Headers (offset 0)
Remove `JSGCObjectHeader` (ref counting remnant) and put `objhdr_t` at offset 0:
```c
typedef struct JSArray {
objhdr_t hdr; // offset 0
word_t length;
JSValue values[];
} JSArray;
typedef struct JSRecord {
objhdr_t hdr; // offset 0
JSRecord *proto;
word_t length;
slot slots[];
} JSRecord;
typedef struct JSText {
objhdr_t hdr; // offset 0
word_t length; // pretext: length, text: hash
word_t packed[];
} JSText;
typedef struct JSBlob {
objhdr_t hdr; // offset 0
word_t length;
uint8_t bits[];
} JSBlob;
/* Simplified JSFunction per memory.md - 3 words */
typedef struct JSFunction {
objhdr_t hdr; // offset 0, always stone
JSCode *code; // pointer to immutable code object
struct JSFrame *outer; // frame that created this function
} JSFunction;
/* JSFrame per memory.md */
typedef struct JSFrame {
objhdr_t hdr; // offset 0
JSFunction *function; // function being executed
struct JSFrame *caller; // calling frame (NULL = reduced/returned)
word_t ret_addr; // return instruction address
JSValue slots[]; // args, closure vars, locals, temps
} JSFrame;
/* JSCode - always in immutable (stone) memory */
typedef struct JSCode {
objhdr_t hdr; // offset 0, always stone
word_t arity; // max number of inputs
word_t frame_size; // capacity of activation frame
word_t closure_size; // reduced capacity for returned frames
word_t entry_point; // address to begin execution
word_t disruption_point;// address of disruption clause
uint8_t bytecode[]; // actual bytecode
} JSCode;
```
### Phase 3: Complete gc_object_size for All Types
Update `gc_object_size` (line 1850) to read header at offset 0:
```c
static size_t gc_object_size(void *ptr) {
objhdr_t hdr = *(objhdr_t*)ptr; // Header at offset 0
uint8_t type = objhdr_type(hdr);
uint64_t cap = objhdr_cap56(hdr);
switch (type) {
case OBJ_ARRAY:
return sizeof(JSArray) + cap * sizeof(JSValue);
case OBJ_BLOB:
return sizeof(JSBlob) + (cap + 7) / 8; // cap is bits
case OBJ_TEXT:
return sizeof(JSText) + ((cap + 1) / 2) * sizeof(uint64_t);
case OBJ_RECORD:
return sizeof(JSRecord) + (cap + 1) * sizeof(slot); // cap is mask
case OBJ_FUNCTION:
return sizeof(JSFunction); // 3 words
case OBJ_FRAME:
return sizeof(JSFrame) + cap * sizeof(JSValue); // cap is slot count
case OBJ_CODE:
return 0; // Code is never copied (immutable)
default:
return 64; // Conservative fallback
}
}
```
### Phase 4: Complete gc_scan_object for All Types
Update `gc_scan_object` (line 1924):
```c
static void gc_scan_object(JSContext *ctx, void *ptr, uint8_t **to_free, uint8_t *to_end) {
objhdr_t hdr = *(objhdr_t*)ptr;
uint8_t type = objhdr_type(hdr);
switch (type) {
case OBJ_ARRAY: {
JSArray *arr = (JSArray*)ptr;
for (uint32_t i = 0; i < arr->length; i++) {
arr->values[i] = gc_copy_value(ctx, arr->values[i], to_free, to_end);
}
break;
}
case OBJ_RECORD: {
JSRecord *rec = (JSRecord*)ptr;
// Copy prototype
if (rec->proto) {
JSValue proto_val = JS_MKPTR(rec->proto);
proto_val = gc_copy_value(ctx, proto_val, to_free, to_end);
rec->proto = (JSRecord*)JS_VALUE_GET_PTR(proto_val);
}
// Copy table entries
uint32_t mask = objhdr_cap56(rec->hdr);
for (uint32_t i = 1; i <= mask; i++) { // Skip slot 0
JSValue k = rec->slots[i].key;
if (!rec_key_is_empty(k) && !rec_key_is_tomb(k)) {
rec->slots[i].key = gc_copy_value(ctx, k, to_free, to_end);
rec->slots[i].value = gc_copy_value(ctx, rec->slots[i].value, to_free, to_end);
}
}
break;
}
case OBJ_FUNCTION: {
JSFunction *func = (JSFunction*)ptr;
// Code is immutable, don't copy - but outer frame needs copying
if (func->outer) {
JSValue outer_val = JS_MKPTR(func->outer);
outer_val = gc_copy_value(ctx, outer_val, to_free, to_end);
func->outer = (JSFrame*)JS_VALUE_GET_PTR(outer_val);
}
break;
}
case OBJ_FRAME: {
JSFrame *frame = (JSFrame*)ptr;
// Copy function pointer
if (frame->function) {
JSValue func_val = JS_MKPTR(frame->function);
func_val = gc_copy_value(ctx, func_val, to_free, to_end);
frame->function = (JSFunction*)JS_VALUE_GET_PTR(func_val);
}
// Copy caller (unless NULL = reduced frame)
if (frame->caller) {
JSValue caller_val = JS_MKPTR(frame->caller);
caller_val = gc_copy_value(ctx, caller_val, to_free, to_end);
frame->caller = (JSFrame*)JS_VALUE_GET_PTR(caller_val);
}
// Copy all slots (args, closure vars, locals, temps)
uint32_t slot_count = objhdr_cap56(frame->hdr);
for (uint32_t i = 0; i < slot_count; i++) {
frame->slots[i] = gc_copy_value(ctx, frame->slots[i], to_free, to_end);
}
break;
}
case OBJ_TEXT:
case OBJ_BLOB:
case OBJ_CODE:
// No internal references to scan
break;
}
}
```
### Phase 5: Fix gc_copy_value Forwarding
Update `gc_copy_value` (line 1883) for offset 0 headers:
```c
static JSValue gc_copy_value(JSContext *ctx, JSValue v, uint8_t **to_free, uint8_t *to_end) {
if (!JS_IsPtr(v)) return v; // Immediate value
void *ptr = JS_VALUE_GET_PTR(v);
// Stone memory - don't copy (includes Code objects)
objhdr_t hdr = *(objhdr_t*)ptr;
if (objhdr_s(hdr)) return v;
// Check if in current heap
if ((uint8_t*)ptr < ctx->heap_base || (uint8_t*)ptr >= ctx->heap_end)
return v; // External allocation
// Already forwarded?
if (objhdr_type(hdr) == OBJ_FORWARD) {
void *new_ptr = (void*)(uintptr_t)objhdr_cap56(hdr);
return JS_MKPTR(new_ptr);
}
// Copy object to new space
size_t size = gc_object_size(ptr);
void *new_ptr = *to_free;
*to_free += size;
memcpy(new_ptr, ptr, size);
// Leave forwarding pointer in old location
*(objhdr_t*)ptr = objhdr_make((uint64_t)(uintptr_t)new_ptr, OBJ_FORWARD, 0, 0, 0, 0);
return JS_MKPTR(new_ptr);
}
```
### Phase 6: Complete GC Root Tracing
Update `ctx_gc` (line 1966) to trace all roots including JSGCRef:
```c
static int ctx_gc(JSContext *ctx) {
// ... existing setup code ...
// Copy roots: global object, class prototypes, etc. (existing)
ctx->global_obj = gc_copy_value(ctx, ctx->global_obj, &to_free, to_end);
ctx->global_var_obj = gc_copy_value(ctx, ctx->global_var_obj, &to_free, to_end);
// ... other existing root copying ...
// Copy GC root stack (JS_PUSH_VALUE/JS_POP_VALUE)
for (JSGCRef *ref = ctx->top_gc_ref; ref; ref = ref->prev) {
ref->val = gc_copy_value(ctx, ref->val, &to_free, to_end);
}
// Copy GC root list (JS_AddGCRef/JS_DeleteGCRef)
for (JSGCRef *ref = ctx->last_gc_ref; ref; ref = ref->prev) {
ref->val = gc_copy_value(ctx, ref->val, &to_free, to_end);
}
// Copy current exception
ctx->current_exception = gc_copy_value(ctx, ctx->current_exception, &to_free, to_end);
// Cheney scan (existing)
// ...
}
```
### Phase 7: Trigger GC on Allocation Failure
Update `js_malloc` (line 1495):
```c
void *js_malloc(JSContext *ctx, size_t size) {
size = (size + 7) & ~7; // Align to 8 bytes
if ((uint8_t*)ctx->heap_free + size > (uint8_t*)ctx->heap_end) {
if (ctx_gc(ctx) < 0) {
JS_ThrowOutOfMemory(ctx);
return NULL;
}
// Retry after GC
if ((uint8_t*)ctx->heap_free + size > (uint8_t*)ctx->heap_end) {
JS_ThrowOutOfMemory(ctx);
return NULL;
}
}
void *ptr = ctx->heap_free;
ctx->heap_free = (uint8_t*)ctx->heap_free + size;
return ptr;
}
```
### Phase 8: Frame Reduction (for closures)
When a function returns, "reduce" its frame to just closure variables:
```c
static void reduce_frame(JSContext *ctx, JSFrame *frame) {
if (frame->caller == NULL) return; // Already reduced
JSCode *code = frame->function->code;
uint32_t closure_size = code->closure_size;
// Shrink capacity to just closure variables
frame->hdr = objhdr_make(closure_size, OBJ_FRAME, 0, 0, 0, 0);
frame->caller = NULL; // Signal: frame is reduced
}
```
### Phase 9: Remove Unused Reference Counting Code
Delete:
- `gc_decref`, `gc_decref_child` functions
- `gc_scan_incref_child`, `gc_scan_incref_child2` functions
- `JS_GCPhaseEnum`, `gc_phase` fields
- `JSGCObjectHeader` struct (merge into objhdr_t)
- `ref_count` fields from any remaining structs
- `mark_function_children_decref` function
- All `free_*` functions that rely on ref counting
## Files to Modify
1. **source/quickjs.c** - Main implementation:
- Add DupValue/FreeValue no-op macros (~line 100)
- Restructure JSArray, JSBlob, JSText, JSRecord (lines 468-499)
- Simplify JSFunction to 3-word struct (line 1205)
- Add JSFrame as heap object (new)
- Restructure JSCode/JSFunctionBytecode (line 1293)
- Fix gc_object_size (line 1850)
- Fix gc_copy_value (line 1883)
- Complete gc_scan_object (line 1924)
- Update ctx_gc for all roots (line 1966)
- Update js_malloc to trigger GC (line 1495)
- Delete ref counting code throughout
2. **source/quickjs.h** - Public API:
- Remove JSGCObjectHeader
- Update JSValue type checks if needed
- Ensure JS_IsStone works with offset 0 headers
## Execution Order
1. **First**: Add DupValue/FreeValue macros (enables compilation)
2. **Second**: Standardize struct layouts (header at offset 0)
3. **Third**: Fix gc_object_size and gc_copy_value
4. **Fourth**: Complete gc_scan_object for all types
5. **Fifth**: Update ctx_gc with complete root tracing
6. **Sixth**: Wire js_malloc to trigger GC
7. **Seventh**: Add frame reduction for closures
8. **Finally**: Remove ref counting dead code
## Verification
1. **Compile test**: `make` should succeed without errors
2. **Basic test**: Run simple scripts:
```js
var a = [1, 2, 3]
log.console(a[1])
```
3. **Stress test**: Allocate many objects to trigger GC:
```js
for (var i = 0; i < 100000; i++) {
var x = { value: i }
}
log.console("done")
```
4. **Closure test**: Test functions with closures survive GC:
```js
fn make_counter() {
var count = 0
fn inc() { count = count + 1; return count }
return inc
}
var c = make_counter()
log.console(c()) // 1
log.console(c()) // 2
```
5. **GC stress with closures**: Create many closures, trigger GC, verify they still work
## Key Design Decisions (Resolved)
1. **JSCode storage**: Lives in stone (immutable) memory, never copied during GC ✓
2. **Header offset**: Standardized to offset 0 for all heap objects ✓
3. **Closure variables**: Live in JSFrame objects; frames are "reduced" when functions return ✓
4. **JSVarRef**: Eliminated - closures reference their outer frame directly ✓

View File

@@ -15,7 +15,6 @@
var shop = use('internal/shop')
var pkg = use('package')
var link = use('link')
var fd = use('fd')
var json = use('json')
var target_locator = null
@@ -23,41 +22,41 @@ var format = 'tree'
var show_locked = false
var show_world = false
var i = 0
var resolved = null
for (i = 0; i < length(args); i++) {
if (args[i] == '--format' || args[i] == '-f') {
if (i + 1 < length(args)) {
format = args[++i]
if (format != 'tree' && format != 'dot' && format != 'json') {
log.error('Invalid format: ' + format + '. Must be tree, dot, or json')
$stop()
var run = function() {
for (i = 0; i < length(args); i++) {
if (args[i] == '--format' || args[i] == '-f') {
if (i + 1 < length(args)) {
format = args[++i]
if (format != 'tree' && format != 'dot' && format != 'json') {
log.error('Invalid format: ' + format + '. Must be tree, dot, or json')
return
}
} else {
log.error('--format requires a format type')
return
}
} else {
log.error('--format requires a format type')
$stop()
} else if (args[i] == '--resolved') {
show_locked = false
} else if (args[i] == '--locked') {
show_locked = true
} else if (args[i] == '--world') {
show_world = true
} else if (args[i] == '--help' || args[i] == '-h') {
log.console("Usage: cell graph [<locator>] [options]")
log.console("")
log.console("Emit the dependency graph.")
log.console("")
log.console("Options:")
log.console(" --format <fmt> Output format: tree (default), dot, json")
log.console(" --resolved Show resolved view with links applied (default)")
log.console(" --locked Show lock view without links")
log.console(" --world Graph all packages in shop")
return
} else if (!starts_with(args[i], '-')) {
target_locator = args[i]
}
} else if (args[i] == '--resolved') {
show_locked = false
} else if (args[i] == '--locked') {
show_locked = true
} else if (args[i] == '--world') {
show_world = true
} else if (args[i] == '--help' || args[i] == '-h') {
log.console("Usage: cell graph [<locator>] [options]")
log.console("")
log.console("Emit the dependency graph.")
log.console("")
log.console("Options:")
log.console(" --format <fmt> Output format: tree (default), dot, json")
log.console(" --resolved Show resolved view with links applied (default)")
log.console(" --locked Show lock view without links")
log.console(" --world Graph all packages in shop")
$stop()
} else if (!starts_with(args[i], '-')) {
target_locator = args[i]
}
}
var links = show_locked ? {} : link.load()
@@ -127,13 +126,7 @@ if (show_world) {
target_locator = '.'
}
// Resolve local paths
if (target_locator == '.' || starts_with(target_locator, './') || starts_with(target_locator, '../') || fd.is_dir(target_locator)) {
resolved = fd.realpath(target_locator)
if (resolved) {
target_locator = resolved
}
}
target_locator = shop.resolve_locator(target_locator)
push(roots, target_locator)
}
@@ -244,5 +237,7 @@ if (format == 'tree') {
log.console(json.encode(output))
}
}
run()
$stop()

149
help.ce
View File

@@ -1,11 +1,28 @@
// cell help - Display help information for cell commands
var fd = use('fd')
var package = use('package')
var shop = use('internal/shop')
var command = length(args) > 0 ? args[0] : null
var core_dir = shop.get_package_dir('core')
var man_file = null
var stat = null
var content = null
var lines = null
var line = null
var block = null
var ce_path = null
var i = 0
var programs = null
var top_level = []
var descriptions = []
var max_len = 0
var first_line = null
var desc = null
var sep = null
var padding = null
var j = 0
// Display specific command help
if (command) {
@@ -15,55 +32,95 @@ if (command) {
content = text(fd.slurp(man_file))
log.console(content)
} else {
log.error("No help available for command: " + command)
log.console("Run 'cell help' to see available commands.")
ce_path = core_dir + '/' + command + '.ce'
stat = fd.stat(ce_path)
if (stat && stat.isFile) {
content = text(fd.slurp(ce_path))
lines = array(content, '\n')
block = ""
for (i = 0; i < length(lines); i++) {
line = lines[i]
if (starts_with(line, '// ')) {
block = block + text(line, 3, length(line)) + '\n'
} else if (line == '//') {
block = block + '\n'
} else if (starts_with(line, '//')) {
block = block + text(line, 2, length(line)) + '\n'
} else {
i = length(lines)
}
}
if (length(block) > 0) {
log.console(trim(block))
} else {
log.error("No help available for command: " + command)
log.console("Run 'cell help' to see available commands.")
}
} else {
log.error("No help available for command: " + command)
log.console("Run 'cell help' to see available commands.")
}
}
} else {
// Display general help — dynamic listing
programs = sort(package.list_programs('core'))
for (i = 0; i < length(programs); i++) {
if (search(programs[i], '/') == null) {
top_level[] = programs[i]
}
$stop()
}
// Display general help
man_file = 'scripts/man/cell.man'
stat = fd.stat(man_file)
if (stat && stat.isFile) {
content = text(fd.slurp(man_file))
log.console(content)
} else {
// Fallback if man file doesn't exist
log.console("cell - The Cell package manager")
log.console("")
log.console("Usage: cell <command> [arguments]")
log.console("")
log.console("Package Management:")
log.console(" install <locator> Install a package and its dependencies")
log.console(" update [locator] Update packages from remote sources")
log.console(" remove <locator> Remove a package from the shop")
log.console(" add <locator> Add a dependency to current package")
log.console("")
log.console("Building:")
log.console(" build [locator] Build dynamic libraries for packages")
log.console(" clean [scope] Remove build artifacts")
log.console("")
log.console("Linking (Local Development):")
log.console(" link <origin> <target> Link a package to a local path")
log.console(" unlink <origin> Remove a package link")
log.console(" clone <origin> <path> Clone and link a package locally")
log.console("")
log.console("Information:")
log.console(" list [scope] List packages and dependencies")
log.console(" ls [locator] List modules and actors in a package")
log.console(" why <locator> Show reverse dependencies")
log.console(" search <query> Search for packages, modules, or actors")
log.console("")
log.console("Diagnostics:")
log.console(" resolve [locator] Print fully resolved dependency closure")
log.console(" graph [locator] Emit dependency graph (tree, dot, json)")
log.console(" verify [scope] Verify integrity and consistency")
log.console("")
log.console("Other:")
log.console(" help [command] Show help for a command")
log.console(" version Show cell version")
log.console("")
log.console("Run 'cell <command> --help' for more information on a command.")
for (i = 0; i < length(top_level); i++) {
ce_path = core_dir + '/' + top_level[i] + '.ce'
desc = ""
stat = fd.stat(ce_path)
if (stat && stat.isFile) {
content = text(fd.slurp(ce_path))
lines = array(content, '\n')
first_line = length(lines) > 0 ? lines[0] : ""
if (starts_with(first_line, '//')) {
if (starts_with(first_line, '// ')) {
first_line = text(first_line, 3, length(first_line))
} else {
first_line = text(first_line, 2, length(first_line))
}
sep = search(first_line, ' - ')
if (sep != null) {
desc = text(first_line, sep + 3, length(first_line))
} else {
sep = search(first_line, ' — ')
if (sep != null) {
desc = text(first_line, sep + 3, length(first_line))
} else {
desc = first_line
}
}
}
}
descriptions[] = desc
if (length(top_level[i]) > max_len) {
max_len = length(top_level[i])
}
}
log.console("cell - the cell package manager")
log.console("")
log.console("usage: cell <command> [arguments]")
log.console("")
log.console("available commands:")
for (i = 0; i < length(top_level); i++) {
padding = ""
for (j = 0; j < max_len - length(top_level[i]); j++) {
padding = padding + " "
}
if (length(descriptions[i]) > 0) {
log.console(" " + top_level[i] + padding + " " + descriptions[i])
} else {
log.console(" " + top_level[i])
}
}
log.console("")
log.console("Run 'cell help <command>' for more information.")
}
$stop()

742
http.cm Normal file
View File

@@ -0,0 +1,742 @@
var socket = use('socket')
var tls = use('net/tls')
var Blob = use('blob')
def CRLF = "\r\n"
def status_texts = {
"200": "OK", "201": "Created", "204": "No Content",
"301": "Moved Permanently", "302": "Found", "307": "Temporary Redirect",
"400": "Bad Request", "401": "Unauthorized", "403": "Forbidden",
"404": "Not Found", "405": "Method Not Allowed", "500": "Internal Server Error"
}
// ============================================================
// Server (unchanged)
// ============================================================
function serve(port) {
var fd = socket.socket("AF_INET", "SOCK_STREAM")
socket.setsockopt(fd, "SOL_SOCKET", "SO_REUSEADDR", true)
socket.bind(fd, {address: "127.0.0.1", port: port})
socket.listen(fd, 16)
return fd
}
function parse_request(conn_fd) {
var data = socket.recv(conn_fd, 65536)
var raw = text(data)
var hdr_end = search(raw, CRLF + CRLF)
if (hdr_end == null) disrupt
var header_text = text(raw, 0, hdr_end)
var body_text = text(raw, hdr_end + 4)
var lines = array(header_text, CRLF)
var parts = array(lines[0], " ")
var method = parts[0]
var url = parts[1]
var qpos = search(url, "?")
var path = qpos != null ? text(url, 0, qpos) : url
var headers = {}
var i = 1
var colon = null
var key = null
var val = null
while (i < length(lines)) {
colon = search(lines[i], ": ")
if (colon != null) {
key = lower(text(lines[i], 0, colon))
val = text(lines[i], colon + 2)
headers[key] = val
}
i = i + 1
}
var cl = headers["content-length"]
var content_length = null
var remaining = null
var more = null
if (cl != null) content_length = number(cl)
if (content_length != null && length(body_text) < content_length) {
remaining = content_length - length(body_text)
more = socket.recv(conn_fd, remaining)
body_text = body_text + text(more)
}
if (content_length == null || content_length == 0) body_text = null
return {
method: method, path: path, url: url,
headers: headers, body: body_text, _conn: conn_fd
}
}
function accept(server_fd) {
var conn = socket.accept(server_fd)
return parse_request(conn.socket)
}
function on_request(server_fd, handler) {
var _accept = function() {
var conn = socket.accept(server_fd)
var req = null
var _parse = function() {
req = parse_request(conn.socket)
} disruption {
req = null
}
_parse()
if (req != null) handler(req)
socket.on_readable(server_fd, _accept)
}
socket.on_readable(server_fd, _accept)
}
function respond(conn, status, headers, body) {
var st = status_texts[text(status)]
if (st == null) st = "Unknown"
var out = "HTTP/1.1 " + text(status) + " " + st + CRLF
out = out + "Connection: close" + CRLF
var body_str = ""
var keys = null
var i = 0
if (body != null) {
if (is_text(body)) body_str = body
else body_str = text(body)
}
if (headers != null) {
keys = array(headers)
i = 0
while (i < length(keys)) {
out = out + keys[i] + ": " + headers[keys[i]] + CRLF
i = i + 1
}
}
out = out + "Content-Length: " + text(length(body_str)) + CRLF
out = out + CRLF + body_str
socket.send(conn, out)
socket.close(conn)
}
function sse_open(conn, headers) {
var out = "HTTP/1.1 200 OK" + CRLF
out = out + "Content-Type: text/event-stream" + CRLF
out = out + "Cache-Control: no-cache" + CRLF
out = out + "Connection: keep-alive" + CRLF
var keys = null
var i = 0
if (headers != null) {
keys = array(headers)
i = 0
while (i < length(keys)) {
out = out + keys[i] + ": " + headers[keys[i]] + CRLF
i = i + 1
}
}
out = out + CRLF
socket.send(conn, out)
}
function sse_event(conn, event, data) {
var frame = "event: " + event + "\ndata: " + data + "\n\n"
var ok = true
var _send = function() {
socket.send(conn, frame)
} disruption {
ok = false
}
_send()
return ok
}
function sse_close(conn) {
socket.close(conn)
}
// ============================================================
// Blocking client request (kept for compatibility)
// ============================================================
function request(method, url, headers, body) {
var parts = array(url, "/")
var host_port = parts[2]
var path = "/" + text(array(parts, 3, length(parts)), "/")
var hp = array(host_port, ":")
var host = hp[0]
var port = length(hp) > 1 ? number(hp[1]) : 80
var fd = socket.socket("AF_INET", "SOCK_STREAM")
var raw = null
var hdr_end = null
var _do = function() {
socket.connect(fd, {address: host, port: port})
var body_str = ""
if (body != null) {
if (is_text(body)) body_str = body
else body_str = text(body)
}
var keys = null
var i = 0
var req = method + " " + path + " HTTP/1.1" + CRLF
req = req + "Host: " + host_port + CRLF
req = req + "Connection: close" + CRLF
if (headers != null) {
keys = array(headers)
i = 0
while (i < length(keys)) {
req = req + keys[i] + ": " + headers[keys[i]] + CRLF
i = i + 1
}
}
if (length(body_str) > 0) {
req = req + "Content-Length: " + text(length(body_str)) + CRLF
}
req = req + CRLF + body_str
socket.send(fd, req)
raw = text(socket.recv(fd, 65536))
} disruption {
raw = null
}
_do()
socket.close(fd)
if (raw == null) return null
hdr_end = search(raw, CRLF + CRLF)
if (hdr_end == null) return null
var header_text = text(raw, 0, hdr_end)
var lines = array(header_text, CRLF)
var status_parts = array(lines[0], " ")
var status = number(status_parts[1])
var resp_headers = {}
var hi = 1
var colon = null
while (hi < length(lines)) {
colon = search(lines[hi], ": ")
if (colon != null) {
resp_headers[lower(text(lines[hi], 0, colon))] = text(lines[hi], colon + 2)
}
hi = hi + 1
}
return {
status: status,
headers: resp_headers,
body: text(raw, hdr_end + 4)
}
}
// ============================================================
// Requestor-based async fetch
// ============================================================
// parse_url requestor — sync, extract {scheme, host, port, path} from URL
var parse_url = function(callback, value) {
var url = null
var method = "GET"
var req_headers = null
var req_body = null
log.console("value type=" + text(is_text(value)) + " val=" + text(value))
if (is_text(value)) {
url = value
log.console("url after assign=" + text(is_text(url)) + " url=" + text(url))
} else {
url = value.url
if (value.method != null) method = value.method
if (value.headers != null) req_headers = value.headers
if (value.body != null) req_body = value.body
}
// strip scheme
var scheme = "http"
var rest = url
var scheme_end = search(url, "://")
log.console("A: url_type=" + text(is_text(url)) + " scheme_end=" + text(scheme_end))
if (scheme_end != null) {
scheme = lower(text(url, 0, scheme_end))
rest = text(url, scheme_end + 3, length(url))
log.console("B: scheme=" + scheme + " rest=" + rest + " rest_type=" + text(is_text(rest)))
}
// split host from path
var slash = search(rest, "/")
var host_port = rest
var path = "/"
log.console("C: slash=" + text(slash))
if (slash != null) {
host_port = text(rest, 0, slash)
path = text(rest, slash, length(rest))
}
// split host:port
var hp = array(host_port, ":")
var host = hp[0]
var port = null
if (length(hp) > 1) {
port = number(hp[1])
} else {
port = scheme == "https" ? 443 : 80
}
callback({
scheme: scheme, host: host, port: port, path: path,
host_port: host_port, method: method,
req_headers: req_headers, req_body: req_body
})
return null
}
// resolve_dns requestor — blocking getaddrinfo, swappable later
var resolve_dns = function(callback, state) {
var ok = true
var addrs = null
var _resolve = function() {
addrs = socket.getaddrinfo(state.host, text(state.port))
} disruption {
ok = false
}
_resolve()
if (!ok || addrs == null || length(addrs) == 0) {
callback(null, "dns resolution failed for " + state.host)
return null
}
callback(record(state, {address: addrs[0].address}))
return null
}
// open_connection requestor — non-blocking connect + optional TLS
var open_connection = function(callback, state) {
var fd = socket.socket("AF_INET", "SOCK_STREAM")
var cancelled = false
var cancel = function() {
var _close = null
if (!cancelled) {
cancelled = true
_close = function() {
socket.unwatch(fd)
socket.close(fd)
} disruption {}
_close()
}
}
socket.setnonblock(fd)
var finish_connect = function(the_fd) {
var ctx = null
if (state.scheme == "https") {
ctx = tls.wrap(the_fd, state.host)
}
callback(record(state, {fd: the_fd, tls: ctx}))
}
// non-blocking connect — EINPROGRESS is expected
var connect_err = false
var _connect = function() {
socket.connect(fd, {address: state.address, port: state.port})
} disruption {
connect_err = true
}
_connect()
// if connect succeeded immediately (localhost, etc)
var _finish_immediate = null
if (!connect_err && !cancelled) {
_finish_immediate = function() {
finish_connect(fd)
} disruption {
cancel()
callback(null, "connection setup failed")
}
_finish_immediate()
return cancel
}
// wait for connect to complete
socket.on_writable(fd, function() {
if (cancelled) return
var err = socket.getsockopt(fd, "SOL_SOCKET", "SO_ERROR")
if (err != 0) {
cancel()
callback(null, "connect failed (errno " + text(err) + ")")
return
}
var _finish = function() {
finish_connect(fd)
} disruption {
cancel()
callback(null, "connection setup failed")
}
_finish()
})
return cancel
}
// send_request requestor — format + send HTTP/1.1 request
var send_request = function(callback, state) {
var cancelled = false
var cancel = function() {
cancelled = true
}
var _send = function() {
var body_str = ""
var keys = null
var i = 0
if (state.req_body != null) {
if (is_text(state.req_body)) body_str = state.req_body
else body_str = text(state.req_body)
}
var req = state.method + " " + state.path + " HTTP/1.1" + CRLF
req = req + "Host: " + state.host_port + CRLF
req = req + "Connection: close" + CRLF
req = req + "User-Agent: cell/1.0" + CRLF
req = req + "Accept: */*" + CRLF
if (state.req_headers != null) {
keys = array(state.req_headers)
i = 0
while (i < length(keys)) {
req = req + keys[i] + ": " + state.req_headers[keys[i]] + CRLF
i = i + 1
}
}
if (length(body_str) > 0) {
req = req + "Content-Length: " + text(length(body_str)) + CRLF
}
req = req + CRLF + body_str
if (state.tls != null) {
tls.send(state.tls, req)
} else {
socket.send(state.fd, req)
}
} disruption {
if (!cancelled) callback(null, "send request failed")
return cancel
}
_send()
if (!cancelled) callback(state)
return cancel
}
// parse response headers from raw text
function parse_headers(raw) {
var hdr_end = search(raw, CRLF + CRLF)
if (hdr_end == null) return null
var header_text = text(raw, 0, hdr_end)
var lines = array(header_text, CRLF)
var status_parts = array(lines[0], " ")
var status_code = number(status_parts[1])
var headers = {}
var i = 1
var colon = null
while (i < length(lines)) {
colon = search(lines[i], ": ")
if (colon != null) {
headers[lower(text(lines[i], 0, colon))] = text(lines[i], colon + 2)
}
i = i + 1
}
return {
status: status_code, headers: headers,
body_start: hdr_end + 4
}
}
// decode chunked transfer encoding
function decode_chunked(body_text) {
var result = ""
var pos = 0
var chunk_end = null
var chunk_size = null
while (pos < length(body_text)) {
chunk_end = search(text(body_text, pos), CRLF)
if (chunk_end == null) return result
chunk_size = number(text(body_text, pos, pos + chunk_end), 16)
if (chunk_size == null || chunk_size == 0) return result
pos = pos + chunk_end + 2
result = result + text(body_text, pos, pos + chunk_size)
pos = pos + chunk_size + 2
}
return result
}
// receive_response requestor — async incremental receive
var receive_response = function(callback, state) {
var cancelled = false
var buffer = ""
var parsed = null
var content_length = null
var is_chunked = false
var body_complete = false
var cancel = function() {
var _cleanup = null
if (!cancelled) {
cancelled = true
_cleanup = function() {
if (state.tls != null) {
tls.close(state.tls)
} else {
socket.unwatch(state.fd)
socket.close(state.fd)
}
} disruption {}
_cleanup()
}
}
var finish = function() {
if (cancelled) return
var body_text = text(buffer, parsed.body_start)
if (is_chunked) {
body_text = decode_chunked(body_text)
}
// clean up connection
var _cleanup = function() {
if (state.tls != null) {
tls.close(state.tls)
} else {
socket.close(state.fd)
}
} disruption {}
_cleanup()
callback({
status: parsed.status,
headers: parsed.headers,
body: body_text
})
}
var check_complete = function() {
var te = null
var cl = null
var body_text = null
// still waiting for headers
if (parsed == null) {
parsed = parse_headers(buffer)
if (parsed == null) return false
te = parsed.headers["transfer-encoding"]
if (te != null && search(lower(te), "chunked") != null) {
is_chunked = true
}
cl = parsed.headers["content-length"]
if (cl != null) content_length = number(cl)
}
body_text = text(buffer, parsed.body_start)
if (is_chunked) {
// chunked: look for the terminating 0-length chunk
if (search(body_text, CRLF + "0" + CRLF) != null) return true
if (starts_with(body_text, "0" + CRLF)) return true
return false
}
if (content_length != null) {
return length(body_text) >= content_length
}
// connection: close — we read until EOF (handled by recv returning 0 bytes)
return false
}
var on_data = function() {
if (cancelled) return
var chunk = null
var got_data = false
var eof = false
var _recv = function() {
if (state.tls != null) {
chunk = tls.recv(state.tls, 16384)
} else {
chunk = socket.recv(state.fd, 16384)
}
} disruption {
// recv error — treat as EOF
eof = true
}
_recv()
var chunk_text = null
if (!eof && chunk != null) {
stone(chunk)
chunk_text = text(chunk)
if (length(chunk_text) > 0) {
buffer = buffer + chunk_text
got_data = true
} else {
eof = true
}
}
if (got_data && check_complete()) {
finish()
return
}
if (eof) {
// connection closed — if we have headers, deliver what we have
if (parsed != null || parse_headers(buffer) != null) {
if (parsed == null) parsed = parse_headers(buffer)
finish()
} else {
cancel()
callback(null, "connection closed before headers received")
}
return
}
// re-register for more data (one-shot watches)
if (!cancelled) {
if (state.tls != null) {
tls.on_readable(state.tls, on_data)
} else {
socket.on_readable(state.fd, on_data)
}
}
}
// start reading
if (state.tls != null) {
tls.on_readable(state.tls, on_data)
} else {
socket.on_readable(state.fd, on_data)
}
return cancel
}
// ============================================================
// fetch — synchronous HTTP(S) GET, returns response body (stoned blob)
// ============================================================
var fetch = function(url) {
var scheme = "http"
var rest = url
var scheme_end = search(url, "://")
var slash = null
var host_port = null
var path = "/"
var hp = null
var host = null
var port = null
var fd = null
var ctx = null
var buf = Blob()
var raw_text = null
var hdr_end = null
var header_text = null
var body_start_bits = null
var body = null
var addrs = null
var address = null
var ok = true
if (scheme_end != null) {
scheme = lower(text(url, 0, scheme_end))
rest = text(url, scheme_end + 3, length(url))
}
slash = search(rest, "/")
host_port = rest
if (slash != null) {
host_port = text(rest, 0, slash)
path = text(rest, slash, length(rest))
}
hp = array(host_port, ":")
host = hp[0]
port = length(hp) > 1 ? number(hp[1]) : (scheme == "https" ? 443 : 80)
addrs = socket.getaddrinfo(host, text(port))
if (addrs == null || length(addrs) == 0) return null
address = addrs[0].address
fd = socket.socket("AF_INET", "SOCK_STREAM")
var _do = function() {
var req = null
var chunk = null
socket.connect(fd, {address: address, port: port})
if (scheme == "https") ctx = tls.wrap(fd, host)
req = "GET " + path + " HTTP/1.1" + CRLF
req = req + "Host: " + host_port + CRLF
req = req + "Connection: close" + CRLF
req = req + "User-Agent: cell/1.0" + CRLF
req = req + "Accept: */*" + CRLF + CRLF
if (ctx != null) tls.send(ctx, req)
else socket.send(fd, req)
while (true) {
if (ctx != null) chunk = tls.recv(ctx, 16384)
else chunk = socket.recv(fd, 16384)
if (chunk == null) break
stone(chunk)
if (length(chunk) == 0) break
buf.write_blob(chunk)
}
} disruption {
ok = false
}
_do()
var _cleanup = function() {
if (ctx != null) tls.close(ctx)
else socket.close(fd)
} disruption {}
_cleanup()
if (!ok) return null
stone(buf)
raw_text = text(buf)
hdr_end = search(raw_text, CRLF + CRLF)
if (hdr_end == null) return null
header_text = text(raw_text, 0, hdr_end)
if (search(lower(header_text), "transfer-encoding: chunked") != null)
return decode_chunked(text(raw_text, hdr_end + 4))
// Headers are ASCII so char offset = byte offset
body_start_bits = (hdr_end + 4) * 8
body = buf.read_blob(body_start_bits, length(buf))
stone(body)
return body
}
// ============================================================
// fetch_requestor — async requestor pipeline for fetch
// ============================================================
var fetch_requestor = sequence([
parse_url,
resolve_dns,
open_connection,
send_request,
receive_response
])
function close(fd) {
socket.close(fd)
}
return {
// server
serve: serve, accept: accept, on_request: on_request,
respond: respond, close: close,
sse_open: sse_open, sse_event: sse_event, sse_close: sse_close,
// client
fetch: fetch,
fetch_requestor: fetch_requestor,
request: request
}

189
imports.ce Normal file
View File

@@ -0,0 +1,189 @@
// cell imports [<file>] - Trace module-level imports
//
// Usage:
// cell imports Trace imports for current package entry
// cell imports <file.ce> Trace imports starting from a .ce or .cm file
// cell imports <file.cm> Trace imports starting from a .cm file
//
// Options:
// --flat Flat list instead of tree
// --packages Only show unique packages, not individual modules
var shop = use('internal/shop')
var pkg = use('package')
var fd = use('fd')
var target = null
var flat_mode = false
var packages_only = false
var i = 0
var pkg_dir = null
var config = null
var target_path = null
for (i = 0; i < length(args); i++) {
if (args[i] == '--flat') {
flat_mode = true
} else if (args[i] == '--packages') {
packages_only = true
} else if (args[i] == '--help' || args[i] == '-h') {
log.console("Usage: cell imports [<file>] [options]")
log.console("")
log.console("Trace module-level imports across packages.")
log.console("")
log.console("Options:")
log.console(" --flat Flat list instead of tree")
log.console(" --packages Only show unique packages, not individual modules")
$stop()
} else if (!starts_with(args[i], '-')) {
target = args[i]
}
}
// Resolve target file
if (target) {
if (!ends_with(target, '.ce') && !ends_with(target, '.cm'))
target = target + '.ce'
if (fd.is_file(target))
target_path = fd.realpath(target)
else {
pkg_dir = pkg.find_package_dir('.')
if (pkg_dir)
target_path = pkg_dir + '/' + target
}
} else {
pkg_dir = pkg.find_package_dir('.')
if (pkg_dir) {
config = pkg.load_config(null)
if (config.entry)
target_path = pkg_dir + '/' + config.entry
}
}
if (!target_path || !fd.is_file(target_path)) {
log.error('Could not find file: ' + (target || '(no target specified)'))
$stop()
}
// Collect all imports recursively
var visited = {}
var all_imports = []
var all_packages = {}
function trace_imports(file_path, depth) {
if (visited[file_path]) return
visited[file_path] = true
var fi = shop.file_info(file_path)
var file_pkg = fi.package || '(local)'
var idx = null
var j = 0
var imp = null
var mod_path = null
var resolved = null
var imp_pkg = null
var imp_type = null
var rinfo = null
all_packages[file_pkg] = true
var _trace = function() {
idx = shop.index_file(file_path)
if (!idx || !idx.imports) return
j = 0
while (j < length(idx.imports)) {
imp = idx.imports[j]
mod_path = imp.module_path
resolved = null
imp_pkg = '?'
imp_type = 'unresolved'
// Use the full resolver that checks .cm files, C symbols, and aliases
rinfo = shop.resolve_import_info(mod_path, file_pkg)
if (rinfo) {
resolved = rinfo.resolved_path
imp_pkg = rinfo.package || '?'
imp_type = rinfo.type
}
all_packages[imp_pkg] = true
push(all_imports, {
from: file_path,
from_pkg: file_pkg,
module_path: mod_path,
resolved_path: resolved,
package: imp_pkg,
type: imp_type,
depth: depth
})
// Recurse into resolved scripts
if (resolved && (ends_with(resolved, '.cm') || ends_with(resolved, '.ce'))) {
trace_imports(resolved, depth + 1)
}
j = j + 1
}
} disruption {
// File might fail to parse/index
}
_trace()
}
trace_imports(target_path, 0)
// Output results
var fi2 = null
if (packages_only) {
log.console("Packages used by " + target_path + ":")
log.console("")
arrfor(array(all_packages), function(p) {
log.console(" " + p)
})
} else if (flat_mode) {
log.console("Imports from " + target_path + ":")
log.console("")
arrfor(all_imports, function(imp) {
var suffix = ''
if (imp.type == 'native') suffix = ' [native]'
if (imp.type == 'unresolved') suffix = ' [unresolved]'
log.console(" " + imp.module_path + " -> " + imp.package + suffix)
})
} else {
// Tree output
function print_tree(file_path, prefix, is_last) {
var children = filter(all_imports, function(imp) {
return imp.from == file_path
})
var j = 0
var imp = null
var last = false
var connector = null
var suffix = null
var child_prefix = null
while (j < length(children)) {
imp = children[j]
last = (j == length(children) - 1)
connector = last ? "\\-- " : "|-- "
suffix = " (" + imp.package + ")"
if (imp.type == 'native') suffix = suffix + " [native]"
if (imp.type == 'unresolved') suffix = suffix + " [unresolved]"
log.console(prefix + connector + imp.module_path + suffix)
if (imp.resolved_path) {
child_prefix = prefix + (last ? " " : "| ")
print_tree(imp.resolved_path, child_prefix, last)
}
j = j + 1
}
}
fi2 = shop.file_info(target_path)
log.console(target_path + " (" + (fi2.package || 'local') + ")")
print_tree(target_path, "", true)
}
$stop()

58
index.ce Normal file
View File

@@ -0,0 +1,58 @@
// cell index <file> — Build semantic index for a source file.
//
// Usage:
// cell index <file.ce|file.cm> Index one file, output JSON to stdout
// cell index <file> -o <output.json> Index one file, write to file
// cell index --help Show this help
var fd = use('fd')
var json = use('json')
var shop = use('internal/shop')
var filename = null
var output_path = null
var i = 0
for (i = 0; i < length(args); i++) {
if (args[i] == '-o' || args[i] == '--output') {
if (i + 1 < length(args)) {
output_path = args[i + 1]
i = i + 1
} else {
log.error('-o requires a file path')
$stop()
}
} else if (args[i] == '--help' || args[i] == '-h') {
log.console("Usage: cell index <file.ce|file.cm> [options]")
log.console("")
log.console("Build a semantic index for a source file.")
log.console("")
log.console("Options:")
log.console(" -o <path> Write output to file instead of stdout")
$stop()
} else if (!starts_with(args[i], '-')) {
filename = args[i]
}
}
if (filename == null) {
log.error('No file specified. Usage: cell index <file>')
$stop()
}
if (!fd.is_file(filename)) {
log.error('File not found: ' + filename)
$stop()
}
var idx = shop.index_file(filename)
var out = json.encode(idx, true)
if (output_path != null) {
fd.slurpwrite(output_path, out)
log.console('Wrote index to ' + output_path)
} else {
log.compile(out)
}
$stop()

648
index.cm Normal file
View File

@@ -0,0 +1,648 @@
// index.cm — Core semantic indexing module.
// Walks AST output from parse (+ optional fold) to build a semantic index.
//
// Entry point:
// index_ast(ast, tokens, filename) — index a pre-parsed AST
var make_span = function(node) {
return {
from_row: node.from_row,
from_col: node.from_column,
to_row: node.to_row,
to_col: node.to_column
}
}
// Index an already-parsed AST. Tokens are optional (used for doc comments).
var index_ast = function(ast, tokens, filename) {
var is_actor = ends_with(filename, ".ce")
var imports = []
var symbols = []
var references = []
var call_sites = []
var exports_list = []
var intrinsic_refs = []
var node_counter = 0
var fn_map = {}
var _i = 0
var _j = 0
var fn = null
var sym_id = null
var params_list = null
var scope = null
var keys = null
var key = null
var entry = null
var reverse = {}
// Build function_nr -> {name, outer, from_row} map from ast.functions.
if (ast.functions != null) {
_i = 0
while (_i < length(ast.functions)) {
fn = ast.functions[_i]
fn_map[text(fn.function_nr)] = {
name: fn.name,
outer: fn.outer,
from_row: fn.from_row
}
_i = _i + 1
}
}
// Walk scope chain upward by `lvl` levels from func_nr.
var resolve_scope_nr = function(func_nr, lvl) {
var current = func_nr
var remaining = lvl
var info = null
if (remaining == null || remaining < 0) return null
while (remaining > 0 && current != null) {
info = fn_map[text(current)]
if (info != null) {
current = info.outer
} else {
return null
}
remaining = remaining - 1
}
return current
}
// Resolve a name node to its symbol_id using scope chain.
var resolve_symbol_id = function(name_node) {
var decl_fn_nr = resolve_scope_nr(name_node.function_nr, name_node.level)
var _si = 0
var s = null
var e = null
var kind_str = null
if (decl_fn_nr == null) return null
if (ast.scopes == null) return null
_si = 0
while (_si < length(ast.scopes)) {
s = ast.scopes[_si]
if (s.function_nr == decl_fn_nr) {
e = s[name_node.name]
if (e != null) {
kind_str = e.make
if (kind_str == "function") kind_str = "fn"
if (kind_str == "input") kind_str = "param"
return filename + ":" + name_node.name + ":" + kind_str
}
}
_si = _si + 1
}
return null
}
// Get enclosing symbol id for a function_nr.
var get_enclosing = function(func_nr) {
var info = fn_map[text(func_nr)]
if (info == null || func_nr == 0) return null
if (info.name != null) return filename + ":" + info.name + ":fn"
return null
}
// Find doc comment in tokens immediately before target_row.
var find_doc_comment = function(target_row) {
var _ti = 0
var tok = null
var lines = []
var line_nr = null
if (tokens == null) return null
_ti = 0
while (_ti < length(tokens)) {
tok = tokens[_ti]
if (tok.kind == "comment" && tok.from_row >= target_row - 10 && tok.from_row < target_row) {
lines[] = tok.value
}
if (tok.from_row >= target_row) break
_ti = _ti + 1
}
if (length(lines) > 0) return text(lines, "\n")
return null
}
// Allocate a monotonic node id.
var next_id = function() {
node_counter = node_counter + 1
return node_counter
}
// Forward declarations for mutual recursion.
var walk_expr = null
var walk_stmts = null
var walk_stmt = null
// Walk an expression node, collecting references and call sites.
walk_expr = function(node, enclosing, is_lhs) {
var nid = 0
var ref_kind = null
var callee_name = null
var callee_sym = null
var arg_count = 0
var _ai = 0
var enc = null
var param_name = null
if (node == null) return
nid = next_id()
// this keyword
if (node.kind == "this") {
references[] = {
node_id: nid,
name: "this",
symbol_id: null,
span: make_span(node),
enclosing: enclosing,
ref_kind: "read"
}
return
}
// Capture intrinsic refs with positions (intrinsics lack function_nr).
if (node.kind == "name" && node.name != null && node.intrinsic == true) {
intrinsic_refs[] = {
node_id: nid,
name: node.name,
span: make_span(node),
enclosing: enclosing
}
}
// Name reference — has function_nr when it's a true variable reference.
if (node.kind == "name" && node.name != null && node.function_nr != null) {
if (node.intrinsic != true) {
ref_kind = is_lhs ? "write" : "read"
references[] = {
node_id: nid,
name: node.name,
symbol_id: resolve_symbol_id(node),
span: make_span(node),
enclosing: enclosing,
ref_kind: ref_kind
}
}
}
// Call expression.
if (node.kind == "(") {
callee_name = null
callee_sym = null
arg_count = (node.list != null) ? length(node.list) : 0
if (node.expression != null) {
if (node.expression.kind == "name") {
callee_name = node.expression.name
if (node.expression.intrinsic != true && node.expression.function_nr != null) {
callee_sym = resolve_symbol_id(node.expression)
}
} else if (node.expression.kind == ".") {
if (node.expression.left != null && node.expression.left.kind == "name") {
callee_name = node.expression.left.name
}
if (is_text(node.expression.right)) {
callee_name = (callee_name != null ? callee_name + "." : "") + node.expression.right
} else if (node.expression.right != null && node.expression.right.name != null) {
callee_name = (callee_name != null ? callee_name + "." : "") + node.expression.right.name
}
}
}
if (callee_name != "use") {
call_sites[] = {
node_id: nid,
callee: callee_name,
callee_symbol_id: callee_sym,
span: make_span(node),
enclosing: enclosing,
args_count: arg_count
}
}
// Also record the callee name as a "call" reference.
if (node.expression != null && node.expression.kind == "name" &&
node.expression.function_nr != null && node.expression.intrinsic != true) {
references[] = {
node_id: nid,
name: node.expression.name,
symbol_id: resolve_symbol_id(node.expression),
span: make_span(node.expression),
enclosing: enclosing,
ref_kind: "call"
}
}
// Capture intrinsic callee refs (e.g., print, length).
if (node.expression != null && node.expression.kind == "name" &&
node.expression.intrinsic == true && node.expression.name != null) {
intrinsic_refs[] = {
node_id: nid,
name: node.expression.name,
span: make_span(node.expression),
enclosing: enclosing
}
}
// Walk callee expression (skip name — already recorded above).
if (node.expression != null && node.expression.kind != "name") {
walk_expr(node.expression, enclosing, false)
}
// Walk arguments.
if (node.list != null) {
_ai = 0
while (_ai < length(node.list)) {
walk_expr(node.list[_ai], enclosing, false)
_ai = _ai + 1
}
}
return
}
// Function / arrow function expression — walk body.
if (node.kind == "function" || node.kind == "arrow function") {
enc = enclosing
if (node.function_nr != null) {
if (node.name != null) {
enc = filename + ":" + node.name + ":fn"
} else {
enc = filename + ":anon_" + text(node.function_nr) + ":fn"
}
}
// Record params as symbols.
if (node.list != null) {
_ai = 0
while (_ai < length(node.list)) {
param_name = node.list[_ai].name
if (param_name != null) {
symbols[] = {
symbol_id: filename + ":" + param_name + ":param",
name: param_name,
kind: "param",
decl_span: make_span(node.list[_ai]),
doc_comment: null,
scope_fn_nr: node.function_nr,
params: null
}
}
_ai = _ai + 1
}
}
walk_stmts(node.statements, enc)
walk_stmts(node.disruption, enc)
return
}
// Assignment operators — left side is a write.
if (node.kind == "=" || node.kind == "+=" || node.kind == "-=" ||
node.kind == "*=" || node.kind == "/=" || node.kind == "%=") {
walk_expr(node.left, enclosing, true)
walk_expr(node.right, enclosing, false)
return
}
// Property access — only walk left (right is property name, not a ref).
if (node.kind == ".") {
walk_expr(node.left, enclosing, false)
return
}
// Index access.
if (node.kind == "[") {
walk_expr(node.left, enclosing, false)
walk_expr(node.right, enclosing, false)
return
}
// Array literal.
if (node.kind == "array" && node.list != null) {
_ai = 0
while (_ai < length(node.list)) {
walk_expr(node.list[_ai], enclosing, false)
_ai = _ai + 1
}
return
}
// Record literal — only walk values, not keys.
if (node.kind == "record" && node.list != null) {
_ai = 0
while (_ai < length(node.list)) {
if (node.list[_ai] != null) {
walk_expr(node.list[_ai].right, enclosing, false)
}
_ai = _ai + 1
}
return
}
// Template literal.
if (node.kind == "template" && node.list != null) {
_ai = 0
while (_ai < length(node.list)) {
walk_expr(node.list[_ai], enclosing, false)
_ai = _ai + 1
}
return
}
// Prefix/postfix increment/decrement — treat as write.
if (node.kind == "++" || node.kind == "--") {
walk_expr(node.expression, enclosing, true)
return
}
// Ternary.
if (node.kind == "?" || node.kind == "then") {
walk_expr(node.expression, enclosing, false)
walk_expr(node.then, enclosing, false)
walk_expr(node.else, enclosing, false)
return
}
// Generic fallthrough: walk left, right, expression.
if (node.left != null) walk_expr(node.left, enclosing, is_lhs)
if (node.right != null) walk_expr(node.right, enclosing, false)
if (node.expression != null) walk_expr(node.expression, enclosing, false)
}
// Walk an array of statements.
walk_stmts = function(stmts, enclosing) {
var _wi = 0
if (stmts == null) return
_wi = 0
while (_wi < length(stmts)) {
walk_stmt(stmts[_wi], enclosing)
_wi = _wi + 1
}
}
// Walk a single statement.
walk_stmt = function(stmt, enclosing) {
var sym_kind = null
var s_id = null
var p_list = null
var _di = 0
var local_name = null
if (stmt == null) return
// Variable/constant declaration.
if (stmt.kind == "var" || stmt.kind == "def") {
if (stmt.left != null && stmt.left.name != null) {
sym_kind = stmt.kind
p_list = null
// Check if RHS is a function expression.
if (stmt.right != null && (stmt.right.kind == "function" || stmt.right.kind == "arrow function")) {
sym_kind = "fn"
p_list = []
if (stmt.right.list != null) {
_di = 0
while (_di < length(stmt.right.list)) {
if (stmt.right.list[_di].name != null) {
p_list[] = stmt.right.list[_di].name
}
_di = _di + 1
}
}
}
s_id = filename + ":" + stmt.left.name + ":" + sym_kind
symbols[] = {
symbol_id: s_id,
name: stmt.left.name,
kind: sym_kind,
decl_span: make_span(stmt),
doc_comment: find_doc_comment(stmt.from_row),
scope_fn_nr: 0,
params: p_list
}
// Check for import: var x = use('path').
if (stmt.right != null && stmt.right.kind == "(" &&
stmt.right.expression != null && stmt.right.expression.name == "use" &&
stmt.right.list != null && length(stmt.right.list) > 0 &&
stmt.right.list[0].kind == "text") {
imports[] = {
local_name: stmt.left.name,
module_path: stmt.right.list[0].value,
span: make_span(stmt)
}
}
}
walk_expr(stmt.right, enclosing, false)
return
}
// Multiple declarations (var_list).
if (stmt.kind == "var_list" && stmt.list != null) {
_di = 0
while (_di < length(stmt.list)) {
walk_stmt(stmt.list[_di], enclosing)
_di = _di + 1
}
return
}
// Expression statement.
if (stmt.kind == "call") {
// Check for bare use() as expression statement.
if (stmt.expression != null && stmt.expression.kind == "(" &&
stmt.expression.expression != null && stmt.expression.expression.name == "use" &&
stmt.expression.list != null && length(stmt.expression.list) > 0 &&
stmt.expression.list[0].kind == "text") {
imports[] = {
local_name: null,
module_path: stmt.expression.list[0].value,
span: make_span(stmt)
}
}
walk_expr(stmt.expression, enclosing, false)
return
}
// If statement.
if (stmt.kind == "if") {
walk_expr(stmt.expression, enclosing, false)
walk_stmts(stmt.then, enclosing)
if (stmt.else != null) {
walk_stmts(stmt.else, enclosing)
}
// else-if chain.
if (stmt.list != null) {
walk_stmts(stmt.list, enclosing)
}
return
}
// While loop.
if (stmt.kind == "while") {
walk_expr(stmt.expression, enclosing, false)
walk_stmts(stmt.statements, enclosing)
return
}
// For loop.
if (stmt.kind == "for") {
walk_expr(stmt.init, enclosing, false)
walk_expr(stmt.test, enclosing, false)
walk_expr(stmt.update, enclosing, false)
walk_stmts(stmt.statements, enclosing)
return
}
// Do-while loop.
if (stmt.kind == "do") {
walk_stmts(stmt.statements, enclosing)
walk_expr(stmt.expression, enclosing, false)
return
}
// Return statement.
if (stmt.kind == "return") {
walk_expr(stmt.expression, enclosing, false)
return
}
// Disrupt.
if (stmt.kind == "disrupt") {
walk_expr(stmt.expression, enclosing, false)
return
}
// Block.
if (stmt.kind == "block") {
walk_stmts(stmt.statements, enclosing)
return
}
// Fallthrough: walk any sub-nodes.
walk_expr(stmt.expression, enclosing, false)
walk_expr(stmt.left, enclosing, false)
walk_expr(stmt.right, enclosing, false)
walk_stmts(stmt.statements, enclosing)
}
// --- 1. Process named functions from ast.functions ---
if (ast.functions != null) {
_i = 0
while (_i < length(ast.functions)) {
fn = ast.functions[_i]
sym_id = filename + ":" + (fn.name != null ? fn.name : "anon_" + text(fn.function_nr)) + ":fn"
params_list = []
if (fn.list != null) {
_j = 0
while (_j < length(fn.list)) {
if (fn.list[_j].name != null) {
params_list[] = fn.list[_j].name
}
_j = _j + 1
}
}
symbols[] = {
symbol_id: sym_id,
name: fn.name,
kind: "fn",
decl_span: make_span(fn),
doc_comment: find_doc_comment(fn.from_row),
scope_fn_nr: fn.outer != null ? fn.outer : 0,
params: params_list
}
// Record params as symbols.
if (fn.list != null) {
_j = 0
while (_j < length(fn.list)) {
if (fn.list[_j].name != null) {
symbols[] = {
symbol_id: filename + ":" + fn.list[_j].name + ":param",
name: fn.list[_j].name,
kind: "param",
decl_span: make_span(fn.list[_j]),
doc_comment: null,
scope_fn_nr: fn.function_nr,
params: null
}
}
_j = _j + 1
}
}
// Walk function body.
walk_stmts(fn.statements, sym_id)
walk_stmts(fn.disruption, sym_id)
_i = _i + 1
}
}
// --- 2. Walk top-level statements ---
walk_stmts(ast.statements, null)
// --- 3. Detect exports for .cm modules ---
if (!is_actor && ast.statements != null) {
_i = length(ast.statements) - 1
while (_i >= 0) {
if (ast.statements[_i].kind == "return" && ast.statements[_i].expression != null) {
// Check if the return expression is a record literal with key-value pairs.
if (ast.statements[_i].expression.list != null) {
_j = 0
while (_j < length(ast.statements[_i].expression.list)) {
entry = ast.statements[_i].expression.list[_j]
if (entry != null && entry.left != null && entry.left.name != null) {
// Link the export to a symbol if the value is a name reference.
sym_id = null
if (entry.right != null && entry.right.kind == "name" && entry.right.function_nr != null) {
sym_id = resolve_symbol_id(entry.right)
}
exports_list[] = {
name: entry.left.name,
symbol_id: sym_id
}
}
_j = _j + 1
}
}
break
}
_i = _i - 1
}
}
// --- 4. Build reverse refs ---
_i = 0
while (_i < length(references)) {
key = references[_i].name
if (reverse[key] == null) {
reverse[key] = []
}
reverse[key][] = {
node_id: references[_i].node_id,
span: references[_i].span,
enclosing: references[_i].enclosing,
ref_kind: references[_i].ref_kind
}
_i = _i + 1
}
return {
version: 1,
path: filename,
is_actor: is_actor,
imports: imports,
symbols: symbols,
references: references,
intrinsic_refs: intrinsic_refs,
call_sites: call_sites,
exports: exports_list,
reverse_refs: reverse
}
}
return {
index_ast: index_ast
}

View File

@@ -1,4 +1,4 @@
// cell install <locator> - Install a package to the shop
// cell install <locator> - Install a package and its dependencies
//
// Usage:
// cell install <locator> Install a package and its dependencies
@@ -6,187 +6,113 @@
//
// Options:
// --target <triple> Build for target platform
// --refresh Refresh floating refs before locking
// --dry-run Show what would be installed
// -r Recursively find and install all packages in directory
var shop = use('internal/shop')
var build = use('build')
var pkg = use('package')
var fd = use('fd')
if (length(args) < 1) {
log.console("Usage: cell install <locator> [options]")
log.console("")
log.console("Options:")
log.console(" --target <triple> Build for target platform")
log.console(" --refresh Refresh floating refs before locking")
log.console(" --dry-run Show what would be installed")
$stop()
}
var locator = null
var target_triple = null
var refresh = false
var dry_run = false
var recursive = false
var i = 0
var resolved = null
var locators = null
var cwd = fd.realpath('.')
var lock = null
var installed = 0
var failed = 0
for (i = 0; i < length(args); i++) {
if (args[i] == '--target' || args[i] == '-t') {
if (i + 1 < length(args)) {
target_triple = args[++i]
} else {
log.error('--target requires a triple')
$stop()
var run = function() {
for (i = 0; i < length(args); i++) {
if (args[i] == '--target' || args[i] == '-t') {
if (i + 1 < length(args)) {
target_triple = args[++i]
} else {
log.error('--target requires a triple')
return
}
} else if (args[i] == '--dry-run') {
dry_run = true
} else if (args[i] == '-r') {
recursive = true
} else if (args[i] == '--help' || args[i] == '-h') {
log.console("Usage: cell install <locator> [options]")
log.console("")
log.console("Install a package and its dependencies.")
log.console("")
log.console("Options:")
log.console(" --target <triple> Build for target platform")
log.console(" --dry-run Show what would be installed")
log.console(" -r Recursively find and install all packages in directory")
return
} else if (!starts_with(args[i], '-')) {
locator = args[i]
}
} else if (args[i] == '--refresh') {
refresh = true
} else if (args[i] == '--dry-run') {
dry_run = true
} else if (args[i] == '--help' || args[i] == '-h') {
}
if (!locator && !recursive) {
log.console("Usage: cell install <locator> [options]")
log.console("")
log.console("Install a package and its dependencies to the shop.")
log.console("")
log.console("Options:")
log.console(" --target <triple> Build for target platform")
log.console(" --refresh Refresh floating refs before locking")
log.console(" --dry-run Show what would be installed")
$stop()
} else if (!starts_with(args[i], '-')) {
locator = args[i]
}
}
if (!locator) {
log.console("Usage: cell install <locator>")
$stop()
}
// Resolve relative paths to absolute paths
// Local paths like '.' or '../foo' need to be converted to absolute paths
if (locator == '.' || starts_with(locator, './') || starts_with(locator, '../') || fd.is_dir(locator)) {
resolved = fd.realpath(locator)
if (resolved) {
locator = resolved
}
}
// Default target
if (!target_triple) {
target_triple = build.detect_host_target()
}
log.console("Installing " + locator + "...")
// Gather all packages that will be installed
var packages_to_install = []
var skipped_packages = []
var visited = {}
function gather_packages(pkg_locator) {
var lock = null
var update_result = null
var deps = null
if (visited[pkg_locator]) return
visited[pkg_locator] = true
// Check if this is a local path that doesn't exist
if (starts_with(pkg_locator, '/') && !fd.is_dir(pkg_locator)) {
push(skipped_packages, pkg_locator)
log.console(" Skipping missing local package: " + pkg_locator)
return
}
push(packages_to_install, pkg_locator)
if (locator)
locator = shop.resolve_locator(locator)
// Try to read dependencies
var _gather = function() {
// For packages not yet extracted, we need to update and extract first to read deps
lock = shop.load_lock()
if (!lock[pkg_locator]) {
if (!dry_run) {
update_result = shop.update(pkg_locator)
if (update_result) {
shop.extract(pkg_locator)
} else {
// Update failed - package might not be fetchable
log.console("Warning: Could not fetch " + pkg_locator)
return
}
}
} else {
// Package is in lock, ensure it's extracted
if (!dry_run) {
shop.extract(pkg_locator)
}
}
deps = pkg.dependencies(pkg_locator)
if (deps) {
arrfor(array(deps), function(alias) {
var dep_locator = deps[alias]
gather_packages(dep_locator)
})
}
} disruption {
// Package might not have dependencies or cell.toml issue
if (!dry_run) {
log.console(`Warning: Could not read dependencies for ${pkg_locator}`)
}
// Recursive mode: find all packages in directory and install each
if (recursive) {
if (!locator) locator = '.'
locator = shop.resolve_locator(locator)
if (!fd.is_dir(locator)) {
log.error(`${locator} is not a directory`)
return
}
_gather()
}
// Gather all packages
gather_packages(locator)
if (dry_run) {
log.console("Would install:")
arrfor(packages_to_install, function(p) {
var lock = shop.load_lock()
var exists = lock[p] != null
log.console(" " + p + (exists ? " (already installed)" : ""))
locators = filter(pkg.find_packages(locator), function(p) {
return p != cwd
})
if (length(skipped_packages) > 0) {
log.console("")
log.console("Would skip (missing local paths):")
arrfor(skipped_packages, function(p) {
log.console(" " + p)
if (length(locators) == 0) {
log.console("No packages found in " + locator)
return
}
log.console(`Found ${text(length(locators))} package(s) in ${locator}`)
if (dry_run) {
log.console("Would install:")
arrfor(locators, function(loc) {
lock = shop.load_lock()
log.console(" " + loc + (lock[loc] ? " (already installed)" : ""))
})
} else {
installed = 0
failed = 0
arrfor(locators, function(loc) {
log.console(" Installing " + loc + "...")
var _inst = function() {
shop.sync(loc, {target: target_triple})
installed = installed + 1
} disruption {
failed = failed + 1
log.console(` Warning: Failed to install ${loc}`)
}
_inst()
})
log.console("Installed " + text(installed) + " package(s)." + (failed > 0 ? " Failed: " + text(failed) + "." : ""))
}
$stop()
return
}
// Install each package
function install_package(pkg_locator) {
// Update lock entry
shop.update(pkg_locator)
// Extract/symlink the package
shop.extract(pkg_locator)
// Build scripts
shop.build_package_scripts(pkg_locator)
// Build C code
var _build_c = function() {
build.build_dynamic(pkg_locator, target_triple, 'release')
} disruption {
// Not all packages have C code
// Single package install with dependencies
if (dry_run) {
log.console("Would install: " + locator + " (and dependencies)")
return
}
_build_c()
}
arrfor(packages_to_install, function(p) {
log.console(" Installing " + p + "...")
install_package(p)
})
var summary = "Installed " + text(length(packages_to_install)) + " package(s)."
if (length(skipped_packages) > 0) {
summary += " Skipped " + text(length(skipped_packages)) + " missing local path(s)."
log.console("Installing " + locator + "...")
shop.sync_with_deps(locator, {refresh: true, target: target_triple})
log.console("Done.")
}
log.console(summary)
run()
$stop()

View File

@@ -1,6 +1,6 @@
// Minimal bootstrap — seeds the content-addressed cache
// Only runs on cold start (C runtime couldn't find engine in cache)
// Hidden vars: os, core_path, shop_path
// Hidden vars: os, core_path, shop_path, native_mode (optional)
var load_internal = os.load_internal
function use_embed(name) {
return load_internal("js_core_" + name + "_use")
@@ -8,10 +8,12 @@ function use_embed(name) {
var fd = use_embed('internal_fd')
var json_mod = use_embed('json')
var crypto = use_embed('crypto')
var crypto = use_embed('internal_crypto')
function content_hash(content) {
return text(crypto.blake2(content), 'h')
var data = content
if (!is_blob(data)) data = stone(blob(text(data)))
return text(crypto.blake2(data), 'h')
}
function cache_path(hash) {
@@ -26,13 +28,13 @@ function ensure_build_dir() {
return dir
}
// Load seed pipeline from boot/ (tokenize, parse, mcode only)
// Load seed pipeline from boot/
function boot_load(name) {
var mcode_path = core_path + '/boot/' + name + '.cm.mcode'
var mcode_blob = null
var mach_blob = null
if (!fd.is_file(mcode_path)) {
print("error: missing seed: " + name + "\n")
os.print("error: missing seed: " + name + "\n")
disrupt
}
mcode_blob = fd.slurp(mcode_path)
@@ -44,6 +46,7 @@ var tokenize_mod = boot_load("tokenize")
var parse_mod = boot_load("parse")
var fold_mod = boot_load("fold")
var mcode_mod = boot_load("mcode")
var streamline_mod = boot_load("streamline")
function analyze(src, filename) {
var tok_result = tokenize_mod(src, filename)
@@ -57,9 +60,9 @@ function analyze(src, filename) {
e = ast.errors[_i]
msg = e.message
if (e.line != null && e.column != null)
print(`${filename}:${text(e.line)}:${text(e.column)}: error: ${msg}`)
os.print(`${filename}:${text(e.line)}:${text(e.column)}: error: ${msg}\n`)
else
print(`${filename}: error: ${msg}`)
os.print(`${filename}: error: ${msg}\n`)
_i = _i + 1
}
disrupt
@@ -77,7 +80,7 @@ function compile_and_cache(name, source_path) {
var mach_blob = null
if (cached && fd.is_file(cached)) return
ast = analyze(text(source_blob), source_path)
compiled = mcode_mod(ast)
compiled = streamline_mod(mcode_mod(ast))
mcode_json = json_mod.encode(compiled)
mach_blob = mach_compile_mcode_bin(name, mcode_json)
if (cached) {
@@ -86,20 +89,190 @@ function compile_and_cache(name, source_path) {
}
}
// Seed the cache with everything engine needs
var seed_files = [
{name: "tokenize", path: "tokenize.cm"},
{name: "parse", path: "parse.cm"},
{name: "fold", path: "fold.cm"},
{name: "mcode", path: "mcode.cm"},
{name: "streamline", path: "streamline.cm"},
{name: "engine", path: "internal/engine.cm"}
]
var _i = 0
var entry = null
while (_i < length(seed_files)) {
entry = seed_files[_i]
compile_and_cache(entry.name, core_path + '/' + entry.path)
_i = _i + 1
// --- Native compilation support ---
function detect_host_target() {
var platform = os.platform()
var arch = os.arch ? os.arch() : 'arm64'
if (platform == 'macOS' || platform == 'darwin')
return arch == 'x86_64' ? 'macos_x86_64' : 'macos_arm64'
if (platform == 'Linux' || platform == 'linux')
return arch == 'x86_64' ? 'linux' : 'linux_arm64'
if (platform == 'Windows' || platform == 'windows')
return 'windows'
return null
}
function detect_cc() {
var platform = os.platform()
if (platform == 'macOS') return 'clang'
return 'cc'
}
// Compute native dylib cache path matching build.cm's scheme:
// cache_path(native_cache_content(src, target, ''), SALT_NATIVE)
function native_dylib_cache_path(src, target) {
var native_key = src + '\n' + target + '\nnative\n'
var full_key = native_key + '\nnative'
return cache_path(content_hash(full_key))
}
// Compile a module to a native dylib and cache it
var _qbe_mod = null
var _qbe_emit_mod = null
var _host_target = null
var _cc = null
var _is_darwin = false
var _rt_compiled = false
function compile_native_cached(name, source_path) {
var source_blob = fd.slurp(source_path)
var src = text(source_blob)
var dylib_path = native_dylib_cache_path(src, _host_target)
var ast = null
var compiled = null
var il_parts = null
var helpers_il = null
var all_fns = null
var full_il = null
var asm_text = null
var tmp = null
var rc = null
var rt_o = null
var qbe_rt_path = null
var link_cmd = null
if (dylib_path && fd.is_file(dylib_path)) {
os.print("bootstrap: native cache hit: " + name + "\n")
return
}
var t0 = null
var t1 = null
os.print("bootstrap: compiling native: " + name + "\n")
t0 = os.now()
ast = analyze(src, source_path)
compiled = streamline_mod(mcode_mod(ast))
t1 = os.now()
os.print(" [" + name + "] pipeline (tok+parse+fold+mcode+streamline): " + text((t1 - t0) / 1000000) + "ms\n")
t0 = os.now()
il_parts = _qbe_emit_mod(compiled, _qbe_mod, null)
t1 = os.now()
os.print(" [" + name + "] qbe_emit: " + text((t1 - t0) / 1000000) + "ms\n")
helpers_il = (il_parts.helpers && length(il_parts.helpers) > 0)
? text(il_parts.helpers, "\n") : ""
all_fns = text(il_parts.functions, "\n")
full_il = il_parts.data + "\n\n" + helpers_il + "\n\n" + all_fns
t0 = os.now()
asm_text = os.qbe(full_il)
t1 = os.now()
os.print(" [" + name + "] os.qbe (QBE compile): " + text((t1 - t0) / 1000000) + "ms\n")
tmp = '/tmp/cell_boot_' + name
fd.slurpwrite(tmp + '.s', stone(blob(asm_text)))
t0 = os.now()
rc = os.system(_cc + ' -c ' + tmp + '.s -o ' + tmp + '.o')
t1 = os.now()
os.print(" [" + name + "] clang -c: " + text((t1 - t0) / 1000000) + "ms\n")
if (rc != 0) {
os.print("error: assembly failed for " + name + "\n")
disrupt
}
// Compile QBE runtime stubs (once)
rt_o = '/tmp/cell_qbe_rt.o'
if (!_rt_compiled && !fd.is_file(rt_o)) {
qbe_rt_path = core_path + '/src/qbe_rt.c'
rc = os.system(_cc + ' -c ' + qbe_rt_path + ' -o ' + rt_o + ' -fPIC')
if (rc != 0) {
os.print("error: qbe_rt compilation failed\n")
disrupt
}
_rt_compiled = true
}
// Link dylib
ensure_build_dir()
link_cmd = _cc + ' -shared -fPIC'
if (_is_darwin)
link_cmd = link_cmd + ' -undefined dynamic_lookup'
link_cmd = link_cmd + ' ' + tmp + '.o ' + rt_o + ' -o ' + dylib_path
t0 = os.now()
rc = os.system(link_cmd)
t1 = os.now()
os.print(" [" + name + "] clang -shared (link): " + text((t1 - t0) / 1000000) + "ms\n")
if (rc != 0) {
os.print("error: linking failed for " + name + "\n")
disrupt
}
}
// --- Main bootstrap logic ---
// Check if native_mode was passed from C runtime
var _native = false
var _check_nm = function() {
if (native_mode) _native = true
} disruption {}
_check_nm()
var _targets = null
var _ti = 0
var _te = null
if (_native) {
// Native path: compile everything to native dylibs
_qbe_mod = boot_load("qbe")
_qbe_emit_mod = boot_load("qbe_emit")
_host_target = detect_host_target()
_cc = detect_cc()
_is_darwin = os.platform() == 'macOS'
if (!_host_target) {
os.print("error: could not detect host target for native compilation\n")
disrupt
}
// Also seed bytecode cache for engine (so non-native path still works)
compile_and_cache("engine", core_path + '/internal/engine.cm')
// Compile pipeline modules + qbe/qbe_emit + engine to native dylibs
_targets = [
{name: "tokenize", path: "tokenize.cm"},
{name: "parse", path: "parse.cm"},
{name: "fold", path: "fold.cm"},
{name: "mcode", path: "mcode.cm"},
{name: "streamline", path: "streamline.cm"},
{name: "qbe", path: "qbe.cm"},
{name: "qbe_emit", path: "qbe_emit.cm"},
{name: "engine", path: "internal/engine.cm"}
]
_ti = 0
while (_ti < length(_targets)) {
_te = _targets[_ti]
compile_native_cached(_te.name, core_path + '/' + _te.path)
_ti = _ti + 1
}
} else {
// Bytecode path: seed cache with everything engine needs
_targets = [
{name: "tokenize", path: "tokenize.cm"},
{name: "parse", path: "parse.cm"},
{name: "fold", path: "fold.cm"},
{name: "mcode", path: "mcode.cm"},
{name: "streamline", path: "streamline.cm"},
{name: "engine", path: "internal/engine.cm"}
]
_ti = 0
while (_ti < length(_targets)) {
_te = _targets[_ti]
compile_and_cache(_te.name, core_path + '/' + _te.path)
_ti = _ti + 1
}
}
print("bootstrap: cache seeded\n")

View File

@@ -56,12 +56,12 @@
static void *get_blob_check_bits(JSContext *js, JSValue val, size_t expected_bits, const char *name) {
size_t bits;
void* result = js_get_blob_data_bits(js, &bits, val);
if (result == -1) {
if (result == (void *)-1) {
return NULL; // Exception already thrown by js_get_blob_data_bits
}
if (bits != expected_bits) {
JS_ThrowTypeError(js, "%s: expected %zu bits, got %zu", name, expected_bits, bits);
JS_RaiseDisrupt(js, "%s: expected %zu bits, got %zu", name, expected_bits, bits);
return NULL;
}
return result;
@@ -70,7 +70,7 @@ static void *get_blob_check_bits(JSContext *js, JSValue val, size_t expected_bit
// Helper to get any blob data (checking it is a stoned blob)
static void *get_blob_any(JSContext *js, JSValue val, size_t *out_bits, const char *name) {
void *result = js_get_blob_data_bits(js, out_bits, val);
if (result == -1)
if (result == (void *)-1)
return NULL;
return result;
}
@@ -79,7 +79,7 @@ static void *get_blob_any(JSContext *js, JSValue val, size_t *out_bits, const ch
JSValue js_crypto_shared(JSContext *js, JSValue self, int argc, JSValue *argv)
{
if (argc < 2) {
return JS_ThrowTypeError(js, "crypto.shared: expected public_key, private_key");
return JS_RaiseDisrupt(js, "crypto.shared: expected public_key, private_key");
}
uint8_t *pub = get_blob_check_bits(js, argv[0], 256, "crypto.shared public_key");
@@ -97,7 +97,7 @@ JSValue js_crypto_shared(JSContext *js, JSValue self, int argc, JSValue *argv)
JSValue js_crypto_blake2(JSContext *js, JSValue self, int argc, JSValue *argv)
{
if (argc < 1)
return JS_ThrowTypeError(js, "crypto.blake2: expected data blob");
return JS_RaiseDisrupt(js, "crypto.blake2: expected data blob");
size_t data_bits;
uint8_t *data = get_blob_any(js, argv[0], &data_bits, "crypto.blake2 data");
@@ -108,7 +108,7 @@ JSValue js_crypto_blake2(JSContext *js, JSValue self, int argc, JSValue *argv)
if (JS_ToInt32(js, &hash_len, argv[1]))
return JS_EXCEPTION;
if (hash_len < 1 || hash_len > 64)
return JS_ThrowRangeError(js, "crypto.blake2: hash length must be between 1 and 64 bytes");
return JS_RaiseDisrupt(js, "crypto.blake2: hash length must be between 1 and 64 bytes");
}
uint8_t hash[64];
@@ -119,7 +119,7 @@ JSValue js_crypto_blake2(JSContext *js, JSValue self, int argc, JSValue *argv)
}
JSValue js_crypto_sign(JSContext *js, JSValue self, int argc, JSValue *argv) {
if (argc < 2) return JS_ThrowTypeError(js, "crypto.sign: expected secret_key, message");
if (argc < 2) return JS_RaiseDisrupt(js, "crypto.sign: expected secret_key, message");
uint8_t *sk = get_blob_check_bits(js, argv[0], 512, "crypto.sign secret_key");
if (!sk) return JS_EXCEPTION;
@@ -135,7 +135,7 @@ JSValue js_crypto_sign(JSContext *js, JSValue self, int argc, JSValue *argv) {
}
JSValue js_crypto_verify(JSContext *js, JSValue self, int argc, JSValue *argv) {
if (argc < 3) return JS_ThrowTypeError(js, "crypto.verify: expected signature, public_key, message");
if (argc < 3) return JS_RaiseDisrupt(js, "crypto.verify: expected signature, public_key, message");
uint8_t *sig = get_blob_check_bits(js, argv[0], 512, "crypto.verify signature");
if (!sig) return JS_EXCEPTION;
@@ -152,7 +152,7 @@ JSValue js_crypto_verify(JSContext *js, JSValue self, int argc, JSValue *argv) {
}
JSValue js_crypto_lock(JSContext *js, JSValue self, int argc, JSValue *argv) {
if (argc < 3) return JS_ThrowTypeError(js, "crypto.lock: expected key, nonce, message, [ad]");
if (argc < 3) return JS_RaiseDisrupt(js, "crypto.lock: expected key, nonce, message, [ad]");
uint8_t *key = get_blob_check_bits(js, argv[0], 256, "crypto.lock key");
if (!key) return JS_EXCEPTION;
@@ -176,7 +176,7 @@ JSValue js_crypto_lock(JSContext *js, JSValue self, int argc, JSValue *argv) {
size_t out_len = msg_len + 16;
uint8_t *out = malloc(out_len);
if (!out) return JS_ThrowOutOfMemory(js);
if (!out) return JS_RaiseOOM(js);
// Output: [Ciphertext (msg_len)] [MAC (16)]
crypto_aead_lock(out, out + msg_len, key, nonce, ad, ad_len, msg, msg_len);
@@ -187,7 +187,7 @@ JSValue js_crypto_lock(JSContext *js, JSValue self, int argc, JSValue *argv) {
}
JSValue js_crypto_unlock(JSContext *js, JSValue self, int argc, JSValue *argv) {
if (argc < 3) return JS_ThrowTypeError(js, "crypto.unlock: expected key, nonce, ciphertext, [ad]");
if (argc < 3) return JS_RaiseDisrupt(js, "crypto.unlock: expected key, nonce, ciphertext, [ad]");
uint8_t *key = get_blob_check_bits(js, argv[0], 256, "crypto.unlock key");
if (!key) return JS_EXCEPTION;
@@ -200,7 +200,7 @@ JSValue js_crypto_unlock(JSContext *js, JSValue self, int argc, JSValue *argv) {
if (!cipher) return JS_EXCEPTION;
size_t cipher_len = (cipher_bits + 7) / 8;
if (cipher_len < 16) return JS_ThrowTypeError(js, "crypto.unlock: ciphertext too short (min 16 bytes)");
if (cipher_len < 16) return JS_RaiseDisrupt(js, "crypto.unlock: ciphertext too short (min 16 bytes)");
size_t msg_len = cipher_len - 16;
@@ -214,7 +214,7 @@ JSValue js_crypto_unlock(JSContext *js, JSValue self, int argc, JSValue *argv) {
}
uint8_t *out = malloc(msg_len > 0 ? msg_len : 1);
if (!out) return JS_ThrowOutOfMemory(js);
if (!out) return JS_RaiseOOM(js);
// MAC is at cipher + msg_len
const uint8_t *mac = cipher + msg_len;
@@ -238,7 +238,7 @@ static const JSCFunctionListEntry js_crypto_funcs[] = {
JS_CFUNC_DEF("unlock", 3, js_crypto_unlock),
};
JSValue js_core_crypto_use(JSContext *js)
JSValue js_core_internal_crypto_use(JSContext *js)
{
JS_FRAME(js);
JS_ROOT(mod, JS_NewObject(js));

View File

@@ -14,28 +14,22 @@ static void js_enet_host_finalizer(JSRuntime *rt, JSValue val)
if (host) enet_host_destroy(host);
}
//static void js_enet_peer_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func)
//{
// ENetPeer *peer = JS_GetOpaque(val, enet_peer_class_id);
// JS_MarkValue(rt, *(JSValue*)peer->data, mark_func);
//}
static void js_enet_peer_finalizer(JSRuntime *rt, JSValue val)
{
ENetPeer *peer = JS_GetOpaque(val, enet_peer_class_id);
JS_FreeValueRT(rt, *(JSValue*)peer->data);
free(peer->data);
if (peer && peer->data) {
free(peer->data);
}
}
// Initialize the ENet library. Must be called before using any ENet functionality.
static JSValue js_enet_initialize(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (enet_initialize() != 0) return JS_ThrowInternalError(ctx, "Error initializing ENet");
if (enet_initialize() != 0) return JS_RaiseDisrupt(ctx, "Error initializing ENet");
return JS_NULL;
}
// Deinitialize the ENet library, cleaning up all resources. Call this when you no longer
// need any ENet functionality.
// Deinitialize the ENet library, cleaning up all resources.
static JSValue js_enet_deinitialize(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
enet_deinitialize();
@@ -43,14 +37,7 @@ static JSValue js_enet_deinitialize(JSContext *ctx, JSValueConst this_val, int a
}
// Create an ENet host for either a client-like unbound host or a server bound to a specific
// address and port:
//
// - If no argument is provided, creates an unbound "client-like" host with default settings
// (maximum 32 peers, 2 channels, unlimited bandwidth).
// - If you pass an "ip:port" string (e.g. "127.0.0.1:7777"), it creates a server bound to
// that address. The server supports up to 32 peers, 2 channels, and unlimited bandwidth.
//
// Throws an error if host creation fails for any reason.
// address and port.
static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetHost *host;
@@ -64,14 +51,13 @@ static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, int ar
if (argc < 1 || !JS_IsRecord(argv[0])) {
host = enet_host_create(NULL, peer_count, channel_limit, incoming_bandwidth, outgoing_bandwidth);
if (!host) return JS_ThrowInternalError(ctx, "Failed to create ENet client host");
if (!host) return JS_RaiseDisrupt(ctx, "Failed to create ENet client host");
goto wrap;
}
JSValue config_obj = argv[0];
JSValue addr_val = JS_GetPropertyStr(ctx, config_obj, "address");
const char *addr_str = JS_IsText(addr_val) ? JS_ToCString(ctx, addr_val) : NULL;
JS_FreeValue(ctx, addr_val);
if (!addr_str)
send = NULL;
@@ -79,7 +65,6 @@ static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, int ar
JSValue port_val = JS_GetPropertyStr(ctx, config_obj, "port");
int32_t port32 = 0;
JS_ToInt32(ctx, &port32, port_val);
JS_FreeValue(ctx, port_val);
if (strcmp(addr_str, "any") == 0)
address.host = ENET_HOST_ANY;
@@ -89,7 +74,7 @@ static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, int ar
int err = enet_address_set_host_ip(&address, addr_str);
if (err != 0) {
JS_FreeCString(ctx, addr_str);
return JS_ThrowInternalError(ctx, "Failed to set host IP from '%s'. Error: %d", addr_str, err);
return JS_RaiseDisrupt(ctx, "Failed to set host IP from '%s'. Error: %d", addr_str, err);
}
}
address.port = (enet_uint16)port32;
@@ -98,18 +83,15 @@ static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, int ar
JSValue chan_val = JS_GetPropertyStr(ctx, config_obj, "channels");
JS_ToUint32(ctx, &channel_limit, chan_val);
JS_FreeValue(ctx, chan_val);
JSValue in_bw_val = JS_GetPropertyStr(ctx, config_obj, "incoming_bandwidth");
JS_ToUint32(ctx, &incoming_bandwidth, in_bw_val);
JS_FreeValue(ctx, in_bw_val);
JSValue out_bw_val = JS_GetPropertyStr(ctx, config_obj, "outgoing_bandwidth");
JS_ToUint32(ctx, &outgoing_bandwidth, out_bw_val);
JS_FreeValue(ctx, out_bw_val);
host = enet_host_create(send, peer_count, channel_limit, incoming_bandwidth, outgoing_bandwidth);
if (!host) return JS_ThrowInternalError(ctx, "Failed to create ENet host");
if (!host) return JS_RaiseDisrupt(ctx, "Failed to create ENet host");
wrap:
obj = JS_NewObjectClass(ctx, enet_host_id);
@@ -129,79 +111,84 @@ static JSValue peer_get_value(JSContext *ctx, ENetPeer *peer)
*(JSValue*)peer->data = JS_NewObjectClass(ctx, enet_peer_class_id);
JS_SetOpaque(*(JSValue*)peer->data, peer);
}
return JS_DupValue(ctx, *(JSValue*)peer->data);
return *(JSValue*)peer->data;
}
// Poll for and process any available network events (connect, receive, disconnect, or none)
// from this host, calling the provided callback for each event. This function loops until
// no more events are available in the current timeframe.
//
// :param callback: A function called once for each available event, receiving an event
// object as its single argument.
// :param timeout: (optional) Timeout in milliseconds. Defaults to 0 (non-blocking).
// :return: None
// Poll for and process any available network events from this host,
// calling the provided callback for each event.
static JSValue js_enet_host_service(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
if (!host) return JS_EXCEPTION;
if (argc < 1 || !JS_IsFunction(argv[0])) return JS_ThrowTypeError(ctx, "Expected a callback function as first argument");
JSValue callback = JS_DupValue(ctx, argv[0]);
if (argc < 1 || !JS_IsFunction(argv[0]))
return JS_RaiseDisrupt(ctx, "Expected a callback function as first argument");
double secs;
JS_ToFloat64(ctx, &secs, argv[1]);
enet_uint32 timeout_ms = 0;
if (argc >= 2 && !JS_IsNull(argv[1])) {
double secs = 0;
JS_ToFloat64(ctx, &secs, argv[1]);
if (secs > 0) timeout_ms = (enet_uint32)(secs * 1000.0);
}
JS_FRAME(ctx);
JSGCRef event_ref = { .val = JS_NULL, .prev = NULL };
JS_PushGCRef(ctx, &event_ref);
ENetEvent event;
while (enet_host_service(host, &event, secs*1000.0f) > 0) {
JSValue event_obj = JS_NewObject(ctx);
JS_SetPropertyStr(ctx, event_obj, "peer", peer_get_value(ctx, event.peer));
while (enet_host_service(host, &event, timeout_ms) > 0) {
event_ref.val = JS_NewObject(ctx);
JSValue peer_val = peer_get_value(ctx, event.peer);
JS_SetPropertyStr(ctx, event_ref.val, "peer", peer_val);
switch (event.type) {
case ENET_EVENT_TYPE_CONNECT:
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "connect"));
case ENET_EVENT_TYPE_CONNECT: {
JSValue type_str = JS_NewString(ctx, "connect");
JS_SetPropertyStr(ctx, event_ref.val, "type", type_str);
break;
case ENET_EVENT_TYPE_RECEIVE:
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "receive"));
JS_SetPropertyStr(ctx, event_obj, "channelID", JS_NewInt32(ctx, event.channelID));
// Pass raw data as string or ArrayBuffer
}
case ENET_EVENT_TYPE_RECEIVE: {
JSValue type_str = JS_NewString(ctx, "receive");
JS_SetPropertyStr(ctx, event_ref.val, "type", type_str);
JS_SetPropertyStr(ctx, event_ref.val, "channelID", JS_NewInt32(ctx, event.channelID));
if (event.packet->dataLength > 0) {
JSValue data_val = js_new_blob_stoned_copy(ctx, event.packet->data, event.packet->dataLength);
JS_SetPropertyStr(ctx, event_obj, "data", data_val);
JS_SetPropertyStr(ctx, event_ref.val, "data", data_val);
}
enet_packet_destroy(event.packet);
break;
case ENET_EVENT_TYPE_DISCONNECT:
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "disconnect"));
}
case ENET_EVENT_TYPE_DISCONNECT: {
JSValue type_str = JS_NewString(ctx, "disconnect");
JS_SetPropertyStr(ctx, event_ref.val, "type", type_str);
break;
case ENET_EVENT_TYPE_DISCONNECT_TIMEOUT:
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "disconnect_timeout"));
}
case ENET_EVENT_TYPE_DISCONNECT_TIMEOUT: {
JSValue type_str = JS_NewString(ctx, "disconnect_timeout");
JS_SetPropertyStr(ctx, event_ref.val, "type", type_str);
break;
case ENET_EVENT_TYPE_NONE:
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "none"));
}
case ENET_EVENT_TYPE_NONE: {
JSValue type_str = JS_NewString(ctx, "none");
JS_SetPropertyStr(ctx, event_ref.val, "type", type_str);
break;
}
}
// TODO: raise exception?
JS_FreeValue(ctx, event_obj);
JS_Call(ctx, argv[0], JS_NULL, 1, &event_ref.val);
}
JS_FreeValue(ctx, callback);
return JS_NULL;
JS_RETURN_NULL();
}
// Initiate a connection from this host to a remote server. Throws an error if the
// connection cannot be started.
//
// :param hostname: The hostname or IP address of the remote server (e.g. "example.com" or "127.0.0.1").
// :param port: The port number to connect to.
// :return: An ENetPeer object representing the connection.
// Initiate a connection from this host to a remote server.
static JSValue js_enet_host_connect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
if (!host) return JS_EXCEPTION;
if (argc < 2) return JS_ThrowTypeError(ctx, "Expected 2 arguments: hostname, port");
if (argc < 2) return JS_RaiseDisrupt(ctx, "Expected 2 arguments: hostname, port");
const char *hostname = JS_ToCString(ctx, argv[0]);
if (!hostname) return JS_EXCEPTION;
@@ -214,14 +201,12 @@ static JSValue js_enet_host_connect(JSContext *ctx, JSValueConst this_val, int a
address.port = port;
ENetPeer *peer = enet_host_connect(host, &address, 2, 0);
if (!peer) return JS_ThrowInternalError(ctx, "No available peers for initiating an ENet connection");
if (!peer) return JS_RaiseDisrupt(ctx, "No available peers for initiating an ENet connection");
return peer_get_value(ctx, peer);
}
// Flush all pending outgoing packets for this host immediately.
//
// :return: None
static JSValue js_enet_host_flush(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
@@ -230,16 +215,13 @@ static JSValue js_enet_host_flush(JSContext *ctx, JSValueConst this_val, int arg
return JS_NULL;
}
// Broadcast a string or ArrayBuffer to all connected peers on channel 0.
//
// :param data: A string or ArrayBuffer to broadcast to all peers.
// :return: None
// Broadcast a string or blob to all connected peers on channel 0.
static JSValue js_enet_host_broadcast(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
if (!host) return JS_EXCEPTION;
if (argc < 1) return JS_ThrowTypeError(ctx, "Expected a string or ArrayBuffer to broadcast");
if (argc < 1) return JS_RaiseDisrupt(ctx, "Expected a string or blob to broadcast");
const char *data_str = NULL;
size_t data_len = 0;
@@ -248,45 +230,40 @@ static JSValue js_enet_host_broadcast(JSContext *ctx, JSValueConst this_val, int
if (JS_IsText(argv[0])) {
data_str = JS_ToCStringLen(ctx, &data_len, argv[0]);
if (!data_str) return JS_EXCEPTION;
} else if (js_is_blob(ctx,argv[0])) {
} else if (js_is_blob(ctx, argv[0])) {
buf = js_get_blob_data(ctx, &data_len, argv[0]);
if (!buf) return JS_EXCEPTION;
} else {
return JS_ThrowTypeError(ctx, "broadcast() only accepts a string or ArrayBuffer");
return JS_RaiseDisrupt(ctx, "broadcast() only accepts a string or blob");
}
ENetPacket *packet = enet_packet_create(data_str ? (const void*)data_str : (const void*)buf, data_len, ENET_PACKET_FLAG_RELIABLE);
if (data_str) JS_FreeCString(ctx, data_str);
if (!packet) return JS_ThrowInternalError(ctx, "Failed to create ENet packet");
if (!packet) return JS_RaiseDisrupt(ctx, "Failed to create ENet packet");
enet_host_broadcast(host, 0, packet);
return JS_NULL;
}
static JSValue js_enet_host_get_port(JSContext *js, JSValueConst self, int argc, JSValueConst *argv)
// Host property getters
static JSValue js_enet_host_get_port(JSContext *js, JSValueConst self)
{
ENetHost *host = JS_GetOpaque(self, enet_host_id);
if (!host) return JS_EXCEPTION;
return JS_NewInt32(js, host->address.port);
}
static JSValue js_enet_host_get_address(JSContext *js, JSValueConst self, int argc, JSValueConst *argv)
static JSValue js_enet_host_get_address(JSContext *js, JSValueConst self)
{
ENetHost *me = JS_GetOpaque(self, enet_host_id);
if (!me) return JS_EXCEPTION;
char ip_str[128];
if (enet_address_get_host_ip(&me->address, ip_str, sizeof(ip_str)) != 0)
return JS_NULL;
return JS_NewString(js, ip_str);
}
// Peer-level operations
// Request a graceful disconnection from this peer. The connection will close after
// pending data is sent.
//
// :return: None
static JSValue js_enet_peer_disconnect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
@@ -295,16 +272,12 @@ static JSValue js_enet_peer_disconnect(JSContext *ctx, JSValueConst this_val, in
return JS_NULL;
}
// Send a string or ArrayBuffer to this peer on channel 0.
//
// :param data: A string or ArrayBuffer to send.
// :return: None
static JSValue js_enet_peer_send(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_EXCEPTION;
if (argc < 1) return JS_ThrowTypeError(ctx, "Expected a string or ArrayBuffer to send");
if (argc < 1) return JS_RaiseDisrupt(ctx, "Expected a string or blob to send");
const char *data_str = NULL;
size_t data_len = 0;
@@ -313,24 +286,21 @@ static JSValue js_enet_peer_send(JSContext *ctx, JSValueConst this_val, int argc
if (JS_IsText(argv[0])) {
data_str = JS_ToCStringLen(ctx, &data_len, argv[0]);
if (!data_str) return JS_EXCEPTION;
} else if (js_is_blob(ctx,argv[0])) {
} else if (js_is_blob(ctx, argv[0])) {
buf = js_get_blob_data(ctx, &data_len, argv[0]);
if (!buf) return JS_EXCEPTION;
} else {
return JS_ThrowTypeError(ctx, "send() only accepts a string or ArrayBuffer");
return JS_RaiseDisrupt(ctx, "send() only accepts a string or blob");
}
ENetPacket *packet = enet_packet_create(data_str ? (const void*)data_str : (const void*)buf, data_len, ENET_PACKET_FLAG_RELIABLE);
if (data_str) JS_FreeCString(ctx, data_str);
if (!packet) return JS_ThrowInternalError(ctx, "Failed to create ENet packet");
if (!packet) return JS_RaiseDisrupt(ctx, "Failed to create ENet packet");
if (enet_peer_send(peer, 0, packet) < 0) return JS_ThrowInternalError(ctx, "Unable to send packet");
if (enet_peer_send(peer, 0, packet) < 0) return JS_RaiseDisrupt(ctx, "Unable to send packet");
return JS_NULL;
}
// Immediately terminate the connection to this peer, discarding any pending data.
//
// :return: None
static JSValue js_enet_peer_disconnect_now(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
@@ -339,9 +309,6 @@ static JSValue js_enet_peer_disconnect_now(JSContext *ctx, JSValueConst this_val
return JS_NULL;
}
// Request a disconnection from this peer after all queued packets are sent.
//
// :return: None
static JSValue js_enet_peer_disconnect_later(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
@@ -350,9 +317,6 @@ static JSValue js_enet_peer_disconnect_later(JSContext *ctx, JSValueConst this_v
return JS_NULL;
}
// Reset this peer's connection, immediately dropping it and clearing its internal state.
//
// :return: None
static JSValue js_enet_peer_reset(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
@@ -361,9 +325,6 @@ static JSValue js_enet_peer_reset(JSContext *ctx, JSValueConst this_val, int arg
return JS_NULL;
}
// Send a ping request to this peer to measure latency.
//
// :return: None
static JSValue js_enet_peer_ping(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
@@ -372,13 +333,6 @@ static JSValue js_enet_peer_ping(JSContext *ctx, JSValueConst this_val, int argc
return JS_NULL;
}
// Configure the throttling behavior for this peer, controlling how ENet adjusts its sending
// rate based on packet loss or congestion.
//
// :param interval: The interval (ms) between throttle adjustments.
// :param acceleration: The factor to increase sending speed when conditions improve.
// :param deceleration: The factor to decrease sending speed when conditions worsen.
// :return: None
static JSValue js_enet_peer_throttle_configure(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
@@ -386,7 +340,7 @@ static JSValue js_enet_peer_throttle_configure(JSContext *ctx, JSValueConst this
int interval, acceleration, deceleration;
if (argc < 3 || JS_ToInt32(ctx, &interval, argv[0]) || JS_ToInt32(ctx, &acceleration, argv[1]) || JS_ToInt32(ctx, &deceleration, argv[2]))
return JS_ThrowTypeError(ctx, "Expected 3 int arguments: interval, acceleration, deceleration");
return JS_RaiseDisrupt(ctx, "Expected 3 int arguments: interval, acceleration, deceleration");
enet_peer_throttle_configure(peer, interval, acceleration, deceleration);
return JS_NULL;
@@ -399,7 +353,7 @@ static JSValue js_enet_peer_timeout(JSContext *ctx, JSValueConst this_val, int a
int timeout_limit, timeout_min, timeout_max;
if (argc < 3 || JS_ToInt32(ctx, &timeout_limit, argv[0]) || JS_ToInt32(ctx, &timeout_min, argv[1]) || JS_ToInt32(ctx, &timeout_max, argv[2]))
return JS_ThrowTypeError(ctx, "Expected 3 integer arguments: timeout_limit, timeout_min, timeout_max");
return JS_RaiseDisrupt(ctx, "Expected 3 integer arguments: timeout_limit, timeout_min, timeout_max");
enet_peer_timeout(peer, timeout_limit, timeout_min, timeout_max);
return JS_NULL;
@@ -414,12 +368,10 @@ static JSClassDef enet_host = {
static JSClassDef enet_peer_class = {
"ENetPeer",
.finalizer = js_enet_peer_finalizer,
// .gc_mark = js_enet_peer_mark
};
JSValue js_enet_resolve_hostname(JSContext *js, JSValue self, int argc, JSValue *argv)
static JSValue js_enet_resolve_hostname(JSContext *js, JSValue self, int argc, JSValue *argv)
{
// TODO: implement
const char *hostname = JS_ToCString(js, argv[0]);
JS_FreeCString(js, hostname);
return JS_NULL;
@@ -437,18 +389,19 @@ static const JSCFunctionListEntry js_enet_host_funcs[] = {
JS_CFUNC_DEF("connect", 2, js_enet_host_connect),
JS_CFUNC_DEF("flush", 0, js_enet_host_flush),
JS_CFUNC_DEF("broadcast", 1, js_enet_host_broadcast),
// JS_CGETSET_DEF("port", js_enet_host_get_port, NULL),
// JS_CGETSET_DEF("address", js_enet_host_get_address, NULL),
JS_CFUNC0_DEF("port", js_enet_host_get_port),
JS_CFUNC0_DEF("address", js_enet_host_get_address),
};
static JSValue js_enet_peer_get_rtt(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
// Peer property getters (zero-arg methods)
static JSValue js_enet_peer_get_rtt(JSContext *ctx, JSValueConst this_val)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_EXCEPTION;
return JS_NewInt32(ctx, peer->roundTripTime);
}
static JSValue js_enet_peer_get_incoming_bandwidth(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
static JSValue js_enet_peer_get_incoming_bandwidth(JSContext *ctx, JSValueConst this_val)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_EXCEPTION;
@@ -456,7 +409,7 @@ static JSValue js_enet_peer_get_incoming_bandwidth(JSContext *ctx, JSValueConst
return JS_NewInt32(ctx, peer->incomingBandwidth);
}
static JSValue js_enet_peer_get_outgoing_bandwidth(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
static JSValue js_enet_peer_get_outgoing_bandwidth(JSContext *ctx, JSValueConst this_val)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_EXCEPTION;
@@ -464,14 +417,14 @@ static JSValue js_enet_peer_get_outgoing_bandwidth(JSContext *ctx, JSValueConst
return JS_NewInt32(ctx, peer->outgoingBandwidth);
}
static JSValue js_enet_peer_get_last_send_time(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
static JSValue js_enet_peer_get_last_send_time(JSContext *ctx, JSValueConst this_val)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_EXCEPTION;
return JS_NewInt32(ctx, peer->lastSendTime);
}
static JSValue js_enet_peer_get_last_receive_time(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
static JSValue js_enet_peer_get_last_receive_time(JSContext *ctx, JSValueConst this_val)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_EXCEPTION;
@@ -530,16 +483,17 @@ static JSValue js_enet_peer_get_reliable_data_in_transit(JSContext *ctx, JSValue
static JSValue js_enet_peer_get_port(JSContext *js, JSValueConst self)
{
ENetPeer *peer = JS_GetOpaque(self, enet_peer_class_id);
if (!peer) return JS_EXCEPTION;
return JS_NewUint32(js, peer->address.port);
}
static JSValue js_enet_peer_get_address(JSContext *js, JSValueConst self)
{
ENetPeer *peer = JS_GetOpaque(self, enet_peer_class_id);
if (!peer) return JS_EXCEPTION;
char ip_str[128];
if (enet_address_get_host_ip(&peer->address, ip_str, sizeof(ip_str)) != 0)
return JS_NULL;
return JS_NewString(js, ip_str);
}
@@ -552,24 +506,26 @@ static const JSCFunctionListEntry js_enet_peer_funcs[] = {
JS_CFUNC_DEF("ping", 0, js_enet_peer_ping),
JS_CFUNC_DEF("throttle_configure", 3, js_enet_peer_throttle_configure),
JS_CFUNC_DEF("timeout", 3, js_enet_peer_timeout),
// JS_CGETSET_DEF("rtt", js_enet_peer_get_rtt, NULL),
// JS_CGETSET_DEF("incoming_bandwidth", js_enet_peer_get_incoming_bandwidth, NULL),
// JS_CGETSET_DEF("outgoing_bandwidth", js_enet_peer_get_outgoing_bandwidth, NULL),
// JS_CGETSET_DEF("last_send_time", js_enet_peer_get_last_send_time, NULL),
// JS_CGETSET_DEF("last_receive_time", js_enet_peer_get_last_receive_time, NULL),
// JS_CGETSET_DEF("mtu", js_enet_peer_get_mtu, NULL),
// JS_CGETSET_DEF("outgoing_data_total", js_enet_peer_get_outgoing_data_total, NULL),
// JS_CGETSET_DEF("incoming_data_total", js_enet_peer_get_incoming_data_total, NULL),
// JS_CGETSET_DEF("rtt_variance", js_enet_peer_get_rtt_variance, NULL),
// JS_CGETSET_DEF("packet_loss", js_enet_peer_get_packet_loss, NULL),
// JS_CGETSET_DEF("state", js_enet_peer_get_state, NULL),
// JS_CGETSET_DEF("reliable_data_in_transit", js_enet_peer_get_reliable_data_in_transit, NULL),
// JS_CGETSET_DEF("port", js_enet_peer_get_port, NULL),
// JS_CGETSET_DEF("address", js_enet_peer_get_address, NULL),
JS_CFUNC0_DEF("rtt", js_enet_peer_get_rtt),
JS_CFUNC0_DEF("incoming_bandwidth", js_enet_peer_get_incoming_bandwidth),
JS_CFUNC0_DEF("outgoing_bandwidth", js_enet_peer_get_outgoing_bandwidth),
JS_CFUNC0_DEF("last_send_time", js_enet_peer_get_last_send_time),
JS_CFUNC0_DEF("last_receive_time", js_enet_peer_get_last_receive_time),
JS_CFUNC0_DEF("mtu", js_enet_peer_get_mtu),
JS_CFUNC0_DEF("outgoing_data_total", js_enet_peer_get_outgoing_data_total),
JS_CFUNC0_DEF("incoming_data_total", js_enet_peer_get_incoming_data_total),
JS_CFUNC0_DEF("rtt_variance", js_enet_peer_get_rtt_variance),
JS_CFUNC0_DEF("packet_loss", js_enet_peer_get_packet_loss),
JS_CFUNC0_DEF("state", js_enet_peer_get_state),
JS_CFUNC0_DEF("reliable_data_in_transit", js_enet_peer_get_reliable_data_in_transit),
JS_CFUNC0_DEF("port", js_enet_peer_get_port),
JS_CFUNC0_DEF("address", js_enet_peer_get_address),
};
JSValue js_core_enet_use(JSContext *ctx)
JSValue js_core_internal_enet_use(JSContext *ctx)
{
enet_initialize();
JS_FRAME(ctx);
JS_NewClassID(&enet_host_id);

Some files were not shown because too many files have changed in this diff Show More