90 Commits

Author SHA1 Message Date
John Alanbrook
4b7cde9400 progress on aot 2026-02-16 21:58:45 -06:00
John Alanbrook
a1ee7dd458 better json pretty print 2026-02-16 17:00:06 -06:00
John Alanbrook
9dbe699033 better make 2026-02-16 01:45:00 -06:00
John Alanbrook
f809cb05f0 Merge branch 'fix_core_scripts' into quicken_mcode 2026-02-16 01:43:08 -06:00
John Alanbrook
788ea98651 bootstrap init 2026-02-16 01:36:36 -06:00
John Alanbrook
433ce8a86e update actors 2026-02-16 01:35:07 -06:00
John Alanbrook
cd6e357b6e Merge branch 'quicken_mcode' into gen_dylib 2026-02-16 00:35:40 -06:00
John Alanbrook
f4f56ed470 run dylibs 2026-02-16 00:35:23 -06:00
John Alanbrook
ff61ab1f50 better streamline 2026-02-16 00:34:49 -06:00
John Alanbrook
46c345d34e cache invalidation 2026-02-16 00:04:30 -06:00
John Alanbrook
dc440587ff pretty json 2026-02-15 22:55:11 -06:00
John Alanbrook
8f92870141 correct syntax errors in core scripts 2026-02-15 22:23:04 -06:00
John Alanbrook
7fc4a205f6 go reuses frames 2026-02-15 19:45:17 -06:00
John Alanbrook
23b201bdd7 dynamic dispatch 2026-02-15 17:51:07 -06:00
John Alanbrook
913ec9afb1 Merge branch 'audit_gc' into fix_slots 2026-02-15 15:44:28 -06:00
John Alanbrook
56de0ce803 fix infinite loop in shop 2026-02-15 15:41:09 -06:00
John Alanbrook
96bbb9e4c8 idompent 2026-02-15 14:58:46 -06:00
John Alanbrook
ebd624b772 fixing gc bugs; nearly idempotent 2026-02-15 13:14:26 -06:00
John Alanbrook
7de20b39da more detail on broken pipeline and vm suit tests 2026-02-15 11:51:23 -06:00
John Alanbrook
ee646db394 failsafe boot mode 2026-02-15 11:44:33 -06:00
John Alanbrook
ff80e0d30d Merge branch 'fix_gc' into pitweb 2026-02-15 10:04:54 -06:00
John Alanbrook
d9f41db891 fix syntax errors in build 2026-02-15 09:29:07 -06:00
John Alanbrook
860632e0fa update cli docs and fix cli scripts with new syntax 2026-02-14 22:24:32 -06:00
John Alanbrook
dcc9659e6b Merge branch 'runtime_rework' into fix_gc 2026-02-14 22:11:31 -06:00
John Alanbrook
2f7f2233b8 compiling 2026-02-14 22:08:55 -06:00
John Alanbrook
eee06009b9 no more special case for core C 2026-02-14 22:00:12 -06:00
John Alanbrook
a765872017 remove if/else dispatch from compile chain 2026-02-14 17:57:48 -06:00
John Alanbrook
a93218e1ff faster streamline 2026-02-14 17:14:43 -06:00
John Alanbrook
f2c4fa2f2b remove redundant check 2026-02-14 16:49:16 -06:00
John Alanbrook
5fe05c60d3 faster gc 2026-02-14 16:46:11 -06:00
John Alanbrook
e75596ce30 respsect array and object length requests 2026-02-14 15:42:19 -06:00
John Alanbrook
86609c27f8 correct sections 2026-02-14 15:13:18 -06:00
John Alanbrook
356c51bde3 better array allocation 2026-02-14 14:44:00 -06:00
John Alanbrook
89421e11a4 pull out prettify mcode 2026-02-14 14:14:34 -06:00
John Alanbrook
e5fc04fecd faster mach compile 2026-02-14 14:02:15 -06:00
John Alanbrook
8ec56e85fa shop audit 2026-02-14 14:00:27 -06:00
John Alanbrook
f49ca530bb fix delete gc bug 2026-02-13 21:52:37 -06:00
John Alanbrook
83263379bd ocaml style rooting macros 2026-02-13 20:46:31 -06:00
John Alanbrook
e80e615634 fix array gc bug; new gc error chasing 2026-02-13 16:58:42 -06:00
John Alanbrook
c1430fd59b Merge branch 'fix_gc' into runtime_rework 2026-02-13 15:42:37 -06:00
John Alanbrook
db73eb4eeb Merge branch 'mcode_streamline' into runtime_rework 2026-02-13 15:42:20 -06:00
John Alanbrook
f2556c5622 proper shop caching 2026-02-13 09:04:25 -06:00
John Alanbrook
291304f75d new way to track actor bad memory access 2026-02-13 09:03:33 -06:00
John Alanbrook
3795533554 clean up bytecode 2026-02-13 09:03:00 -06:00
John Alanbrook
d26a96bc62 cached bootstrap 2026-02-13 08:11:35 -06:00
John Alanbrook
0acaabd5fa merge add 2026-02-13 08:09:12 -06:00
John Alanbrook
1ba060668e growable buddy memory runtime 2026-02-13 07:59:52 -06:00
John Alanbrook
77fa058135 mach loading 2026-02-13 07:26:49 -06:00
John Alanbrook
f7e2ff13b5 guard hoisting 2026-02-13 06:32:58 -06:00
John Alanbrook
36fd0a35f9 Merge branch 'fix_gc' into mcode_streamline 2026-02-13 05:59:11 -06:00
John Alanbrook
77c02bf9bf simplify text 2026-02-13 05:59:01 -06:00
John Alanbrook
f251691146 Merge branch 'mach_memory' into mcode_streamline 2026-02-13 05:58:21 -06:00
John Alanbrook
e9ea6ec299 Merge branch 'runtime_rework' into mach_memory 2026-02-13 05:54:28 -06:00
John Alanbrook
bf5fdbc688 backward inference 2026-02-13 05:39:25 -06:00
John Alanbrook
b960d03eeb immediate ascii for string path 2026-02-13 05:35:11 -06:00
John Alanbrook
b4d42fb83d stone pool renamed to constant pool - more appropriate 2026-02-13 05:17:22 -06:00
John Alanbrook
0a680a0cd3 gc print 2026-02-13 05:03:45 -06:00
John Alanbrook
9f0fd84f4f fix growing gc 2026-02-13 04:33:32 -06:00
John Alanbrook
cb9d6e0c0e mmap for poison heap 2026-02-13 04:03:36 -06:00
John Alanbrook
4f18a0b524 tco 2026-02-13 03:57:18 -06:00
John Alanbrook
f296a0c10d fix segv 2026-02-13 03:08:27 -06:00
John Alanbrook
1df6553577 Merge branch 'runtime_rework' into mcode_streamline 2026-02-13 02:52:54 -06:00
John Alanbrook
30a9cfee79 simplify gc model 2026-02-13 02:33:25 -06:00
John Alanbrook
6fff96d9d9 lower intrinsics in mcode 2026-02-13 02:31:16 -06:00
John Alanbrook
4a50d0587d guards in mcode 2026-02-13 02:30:41 -06:00
John Alanbrook
e346348eb5 Merge branch 'fix_gc' into mcode_streamline 2026-02-12 19:15:13 -06:00
John Alanbrook
ff560973f3 Merge branch 'fix_gc' into runtime_rework 2026-02-12 18:57:44 -06:00
John Alanbrook
de4b3079d4 organize 2026-02-12 18:53:06 -06:00
John Alanbrook
29227e655b Merge branch 'pretty_mcode' into mcode_streamline 2026-02-12 18:48:17 -06:00
John Alanbrook
588e88373e Merge branch 'fix_ternary' into pretty_mcode 2026-02-12 18:46:04 -06:00
John Alanbrook
9aca365771 Merge branch 'runtime_rework' into pretty_mcode 2026-02-12 18:44:56 -06:00
John Alanbrook
c56d4d5c3c some cleanup 2026-02-12 18:44:09 -06:00
John Alanbrook
c1e101b24f benchmarks 2026-02-12 18:41:15 -06:00
John Alanbrook
9f0dfbc6a2 fix ternary operator in object literals 2026-02-12 18:33:43 -06:00
John Alanbrook
5c9403a43b compiler optimization output 2026-02-12 18:27:19 -06:00
John Alanbrook
89e34ba71d comprehensive testing for regression analysis 2026-02-12 18:15:03 -06:00
John Alanbrook
73bfa8d7b1 rm some functions 2026-02-12 18:08:56 -06:00
John Alanbrook
4aedb8b0c5 Merge branch 'cli_audit' into ir_artifact 2026-02-12 17:20:45 -06:00
John Alanbrook
ec072f3b63 Merge branch 'runtime_rework' into ir_artifact 2026-02-12 17:18:23 -06:00
John Alanbrook
65755d9c0c fix using old mach 2026-02-12 17:17:12 -06:00
John Alanbrook
19524b3a53 faster json decode 2026-02-12 17:06:48 -06:00
John Alanbrook
f901332c5b clean up cli 2026-02-12 16:45:10 -06:00
John Alanbrook
add136c140 Merge branch 'pretty_mcode' into runtime_rework 2026-02-12 16:36:58 -06:00
John Alanbrook
c1a99dfd4c mcode looks better 2026-02-12 16:36:53 -06:00
John Alanbrook
7b46c6e947 update docs 2026-02-12 16:34:45 -06:00
John Alanbrook
1efb0b1bc9 run with mcode 2026-02-12 16:14:46 -06:00
John Alanbrook
0ba2783b48 Merge branch 'bytecode_cleanup' into mach 2026-02-12 14:08:45 -06:00
John Alanbrook
6de542f0d0 Merge branch 'mach_suite_fix' into bytecode_cleanup 2026-02-12 12:32:06 -06:00
John Alanbrook
6ba4727119 rm call 2026-02-12 11:58:29 -06:00
John Alanbrook
900db912a5 streamline mcode 2026-02-12 09:43:13 -06:00
152 changed files with 108042 additions and 6281 deletions

View File

@@ -103,6 +103,11 @@ var v = a[] // pop: v is 3, a is [1, 2]
- Most files don't have headers; files in a package are not shared between packages
- No undefined in C API: use `JS_IsNull` and `JS_NULL` only
- A C file with correct macros (`CELL_USE_FUNCS` etc) is loaded as a module by its name (e.g., `png.c` in a package → `use('<package>/png')`)
- C symbol naming: `js_<pkg>_<file>_use` (e.g., `js_core_math_radians_use` for `core/math/radians`)
- Core is the `core` package — its symbols follow the same `js_core_<name>_use` pattern as all other packages
- Package directories should contain only source files (no `.mach`/`.mcode` alongside source)
- Build cache files in `build/` are bare hashes (no extensions)
- 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.
## Project Layout
@@ -113,6 +118,19 @@ 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)
## Testing
After any C runtime changes, run all three test suites before considering the work done:
```
make # rebuild
./cell --dev vm_suite # VM-level tests (641 tests)
./cell --dev test suite # language-level tests (493 tests)
./cell --dev fuzz # fuzzer (100 iterations)
```
All three must pass with 0 failures.
## Documentation
The `docs/` folder is the single source of truth. The website at `website/` mounts it via Hugo. Key files:

View File

@@ -5,15 +5,11 @@
# or manually build with meson once.
#
# The cell shop is at ~/.cell and core scripts are installed to ~/.cell/core
#
# See BUILDING.md for details on the bootstrap process and .mach files.
CELL_SHOP = $(HOME)/.cell
CELL_CORE_PACKAGE = $(CELL_SHOP)/packages/core
# .cm sources that compile to .mach bytecode
MACH_SOURCES = tokenize.cm parse.cm fold.cm mcode.cm \
internal/bootstrap.cm internal/engine.cm
doit: bootstrap
maker: install
@@ -22,7 +18,7 @@ makecell:
cp cell /opt/homebrew/bin/
# Install core: symlink this directory to ~/.cell/core
install: bootstrap .mach.stamp $(CELL_SHOP)
install: cell $(CELL_SHOP)
@echo "Linking cell core to $(CELL_CORE_PACKAGE)"
rm -rf $(CELL_CORE_PACKAGE)
ln -s $(PWD) $(CELL_CORE_PACKAGE)
@@ -46,16 +42,6 @@ libcell_runtime.dylib: $(CELL_SHOP)/build/dynamic
cell_main: source/main.c libcell_runtime.dylib
cc -o cell_main source/main.c -L. -lcell_runtime -Wl,-rpath,@loader_path -Wl,-rpath,/opt/homebrew/lib
# Regenerate .mach bytecode when any .cm source changes
.mach.stamp: $(MACH_SOURCES)
./cell --core . regen.cm
@touch $@
# Force-regenerate all .mach bytecode files
regen:
./cell --core . regen.cm
@touch .mach.stamp
# Create the cell shop directories
$(CELL_SHOP):
mkdir -p $(CELL_SHOP)
@@ -78,13 +64,12 @@ bootstrap:
meson compile -C build_bootstrap
cp build_bootstrap/cell .
cp build_bootstrap/libcell_runtime.dylib .
@echo "Bootstrap complete. Cell shop initialized at $(CELL_SHOP)"
@echo "Now run 'make' to rebuild with cell itself."
@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 .mach.stamp
rm -f cell cell_main libcell_runtime.dylib
# Ensure dynamic build directory exists
$(CELL_SHOP)/build/dynamic: $(CELL_SHOP)
@@ -95,4 +80,4 @@ meson:
meson setup build_dbg -Dbuildtype=debugoptimized
meson install -C build_dbg
.PHONY: cell static bootstrap clean meson install regen
.PHONY: cell static bootstrap clean meson install

33
add.ce
View File

@@ -13,6 +13,10 @@ var fd = use('fd')
var locator = null
var alias = null
var resolved = null
var parts = null
var cwd = null
var build_target = null
array(args, function(arg) {
if (arg == '--help' || arg == '-h') {
@@ -41,7 +45,7 @@ if (!locator) {
// Resolve relative paths to absolute paths
if (locator == '.' || starts_with(locator, './') || starts_with(locator, '../') || fd.is_dir(locator)) {
var resolved = fd.realpath(locator)
resolved = fd.realpath(locator)
if (resolved) {
locator = resolved
}
@@ -50,7 +54,7 @@ if (locator == '.' || starts_with(locator, './') || starts_with(locator, '../')
// Generate default alias from locator
if (!alias) {
// Use the last component of the locator as alias
var parts = array(locator, '/')
parts = array(locator, '/')
alias = parts[length(parts) - 1]
// Remove any version suffix
if (search(alias, '@') != null) {
@@ -59,7 +63,7 @@ if (!alias) {
}
// Check we're in a package directory
var cwd = fd.realpath('.')
cwd = fd.realpath('.')
if (!fd.is_file(cwd + '/cell.toml')) {
log.error("Not in a package directory (no cell.toml found)")
$stop()
@@ -68,16 +72,17 @@ if (!fd.is_file(cwd + '/cell.toml')) {
log.console("Adding " + locator + " as '" + alias + "'...")
// Add to local project's cell.toml
try {
var _add_dep = function() {
pkg.add_dependency(null, locator, alias)
log.console(" Added to cell.toml")
} catch (e) {
log.error("Failed to update cell.toml: " + e)
} disruption {
log.error("Failed to update cell.toml")
$stop()
}
_add_dep()
// Install to shop
try {
var _install = function() {
shop.get(locator)
shop.extract(locator)
@@ -85,18 +90,20 @@ try {
shop.build_package_scripts(locator)
// Build C code if any
try {
var target = build.detect_host_target()
build.build_dynamic(locator, target, 'release')
} catch (e) {
var _build_c = function() {
build_target = build.detect_host_target()
build.build_dynamic(locator, build_target, 'release')
} disruption {
// Not all packages have C code
}
_build_c()
log.console(" Installed to shop")
} catch (e) {
log.error("Failed to install: " + e)
} disruption {
log.error("Failed to install")
$stop()
}
_install()
log.console("Added " + alias + " (" + locator + ")")

View File

@@ -379,21 +379,23 @@ static const JSCFunctionListEntry js_reader_funcs[] = {
JS_CFUNC_DEF("count", 0, js_reader_count),
};
JSValue js_miniz_use(JSContext *js)
JSValue js_core_miniz_use(JSContext *js)
{
JS_FRAME(js);
JS_NewClassID(&js_reader_class_id);
JS_NewClass(js, js_reader_class_id, &js_reader_class);
JSValue reader_proto = JS_NewObject(js);
JS_SetPropertyFunctionList(js, reader_proto, js_reader_funcs, sizeof(js_reader_funcs) / sizeof(JSCFunctionListEntry));
JS_SetClassProto(js, js_reader_class_id, reader_proto);
JS_ROOT(reader_proto, JS_NewObject(js));
JS_SetPropertyFunctionList(js, reader_proto.val, js_reader_funcs, sizeof(js_reader_funcs) / sizeof(JSCFunctionListEntry));
JS_SetClassProto(js, js_reader_class_id, reader_proto.val);
JS_NewClassID(&js_writer_class_id);
JS_NewClass(js, js_writer_class_id, &js_writer_class);
JSValue writer_proto = JS_NewObject(js);
JS_SetPropertyFunctionList(js, writer_proto, js_writer_funcs, sizeof(js_writer_funcs) / sizeof(JSCFunctionListEntry));
JS_SetClassProto(js, js_writer_class_id, writer_proto);
JSValue export = JS_NewObject(js);
JS_SetPropertyFunctionList(js, export, js_miniz_funcs, sizeof(js_miniz_funcs)/sizeof(JSCFunctionListEntry));
return export;
JS_ROOT(writer_proto, JS_NewObject(js));
JS_SetPropertyFunctionList(js, writer_proto.val, js_writer_funcs, sizeof(js_writer_funcs) / sizeof(JSCFunctionListEntry));
JS_SetClassProto(js, js_writer_class_id, writer_proto.val);
JS_ROOT(export, JS_NewObject(js));
JS_SetPropertyFunctionList(js, export.val, js_miniz_funcs, sizeof(js_miniz_funcs)/sizeof(JSCFunctionListEntry));
JS_RETURN(export.val);
}

167
bench.ce
View File

@@ -8,7 +8,7 @@ var os = use('os')
var testlib = use('internal/testlib')
var math = use('math/radians')
if (!args) args = []
var _args = args == null ? [] : args
var target_pkg = null // null = current package
var target_bench = null // null = all benchmarks, otherwise specific bench file
@@ -55,14 +55,19 @@ function stddev(arr, mean_val) {
function percentile(arr, p) {
if (length(arr) == 0) return 0
var sorted = sort(arr)
var idx = floor(arr) * p / 100
var idx = floor(length(arr) * p / 100)
if (idx >= length(arr)) idx = length(arr) - 1
return sorted[idx]
}
// Parse arguments similar to test.ce
function parse_args() {
if (length(args) == 0) {
var name = null
var lock = null
var resolved = null
var bench_path = null
if (length(_args) == 0) {
if (!testlib.is_valid_package('.')) {
log.console('No cell.toml found in current directory')
return false
@@ -71,7 +76,7 @@ function parse_args() {
return true
}
if (args[0] == 'all') {
if (_args[0] == 'all') {
if (!testlib.is_valid_package('.')) {
log.console('No cell.toml found in current directory')
return false
@@ -80,28 +85,28 @@ function parse_args() {
return true
}
if (args[0] == 'package') {
if (length(args) < 2) {
if (_args[0] == 'package') {
if (length(_args) < 2) {
log.console('Usage: cell bench package <name> [bench]')
log.console(' cell bench package all')
return false
}
if (args[1] == 'all') {
if (_args[1] == 'all') {
all_pkgs = true
log.console('Benchmarking all packages...')
return true
}
var name = args[1]
var lock = shop.load_lock()
name = _args[1]
lock = shop.load_lock()
if (lock[name]) {
target_pkg = name
} else if (starts_with(name, '/') && testlib.is_valid_package(name)) {
target_pkg = name
} else {
if (testlib.is_valid_package('.')) {
var resolved = pkg.alias_to_package(null, name)
resolved = pkg.alias_to_package(null, name)
if (resolved) {
target_pkg = resolved
} else {
@@ -114,8 +119,8 @@ function parse_args() {
}
}
if (length(args) >= 3) {
target_bench = args[2]
if (length(_args) >= 3) {
target_bench = _args[2]
}
log.console(`Benchmarking package: ${target_pkg}`)
@@ -123,7 +128,7 @@ function parse_args() {
}
// cell bench benches/suite or cell bench <path>
var bench_path = args[0]
bench_path = _args[0]
// Normalize path - add benches/ prefix if not present
if (!starts_with(bench_path, 'benches/') && !starts_with(bench_path, '/')) {
@@ -160,12 +165,15 @@ function collect_benches(package_name, specific_bench) {
var files = pkg.list_files(package_name)
var bench_files = []
arrfor(files, function(f) {
var bench_name = null
var match_name = null
var match_base = null
if (starts_with(f, "benches/") && ends_with(f, ".cm")) {
if (specific_bench) {
var bench_name = text(f, 0, -3)
var match_name = specific_bench
bench_name = text(f, 0, -3)
match_name = specific_bench
if (!starts_with(match_name, 'benches/')) match_name = 'benches/' + match_name
var match_base = ends_with(match_name, '.cm') ? text(match_name, 0, -3) : match_name
match_base = ends_with(match_name, '.cm') ? text(match_name, 0, -3) : match_name
if (bench_name != match_base) return
}
push(bench_files, f)
@@ -180,24 +188,25 @@ function calibrate_batch_size(bench_fn, is_batch) {
var n = MIN_BATCH_SIZE
var dt = 0
var start = 0
var new_n = 0
var calc = 0
var target_n = 0
// Find a batch size that takes at least MIN_SAMPLE_NS
while (n < MAX_BATCH_SIZE) {
// Ensure n is a valid number before calling
if (!is_number(n) || n < 1) {
n = 1
break
}
var start = os.now()
start = os.now()
bench_fn(n)
dt = os.now() - start
if (dt >= MIN_SAMPLE_NS) break
// Double the batch size
var new_n = n * 2
// Check if multiplication produced a valid number
new_n = n * 2
if (!is_number(new_n) || new_n > MAX_BATCH_SIZE) {
n = MAX_BATCH_SIZE
break
@@ -207,10 +216,9 @@ function calibrate_batch_size(bench_fn, is_batch) {
// Adjust to target sample duration
if (dt > 0 && dt < TARGET_SAMPLE_NS && is_number(n) && is_number(dt)) {
var calc = n * TARGET_SAMPLE_NS / dt
calc = n * TARGET_SAMPLE_NS / dt
if (is_number(calc) && calc > 0) {
var target_n = floor(calc)
// Check if floor returned a valid number
target_n = floor(calc)
if (is_number(target_n) && target_n > 0) {
if (target_n > MAX_BATCH_SIZE) target_n = MAX_BATCH_SIZE
if (target_n < MIN_BATCH_SIZE) target_n = MIN_BATCH_SIZE
@@ -219,7 +227,6 @@ function calibrate_batch_size(bench_fn, is_batch) {
}
}
// Safety check - ensure we always return a valid batch size
if (!is_number(n) || n < 1) {
n = 1
}
@@ -230,72 +237,70 @@ function calibrate_batch_size(bench_fn, is_batch) {
// Run a single benchmark function
function run_single_bench(bench_fn, bench_name) {
var timings_per_op = []
// Detect benchmark format:
// 1. Object with { setup, run, teardown } - structured format
// 2. Function that accepts (n) - batch format
// 3. Function that accepts () - legacy format
var is_structured = is_object(bench_fn) && bench_fn.run
var is_batch = false
var batch_size = 1
var setup_fn = null
var run_fn = null
var teardown_fn = null
var calibrate_fn = null
var _detect = null
var i = 0
var state = null
var start = 0
var duration = 0
var ns_per_op = 0
if (is_structured) {
setup_fn = bench_fn.setup || function() { return null }
run_fn = bench_fn.run
teardown_fn = bench_fn.teardown || function(state) {}
teardown_fn = bench_fn.teardown || function(s) {}
// Check if run function accepts batch size
try {
_detect = function() {
var test_state = setup_fn()
run_fn(1, test_state)
is_batch = true
if (teardown_fn) teardown_fn(test_state)
} catch (e) {
} disruption {
is_batch = false
}
_detect()
// Create wrapper for calibration
var calibrate_fn = function(n) {
var state = setup_fn()
run_fn(n, state)
if (teardown_fn) teardown_fn(state)
calibrate_fn = function(n) {
var s = setup_fn()
run_fn(n, s)
if (teardown_fn) teardown_fn(s)
}
batch_size = calibrate_batch_size(calibrate_fn, is_batch)
// Safety check for structured benchmarks
if (!is_number(batch_size) || batch_size < 1) {
batch_size = 1
}
} else {
// Simple function format
try {
_detect = function() {
bench_fn(1)
is_batch = true
} catch (e) {
} disruption {
is_batch = false
}
_detect()
batch_size = calibrate_batch_size(bench_fn, is_batch)
}
// Safety check - ensure batch_size is valid
if (!batch_size || batch_size < 1) {
batch_size = 1
}
// Warmup phase
for (var i = 0; i < WARMUP_BATCHES; i++) {
// Ensure batch_size is valid before warmup
for (i = 0; i < WARMUP_BATCHES; i++) {
if (!is_number(batch_size) || batch_size < 1) {
var type_str = is_null(batch_size) ? 'null' : is_number(batch_size) ? 'number' : is_text(batch_size) ? 'text' : is_object(batch_size) ? 'object' : is_array(batch_size) ? 'array' : is_function(batch_size) ? 'function' : is_logical(batch_size) ? 'logical' : 'unknown'
log.console(`WARNING: batch_size became ${type_str} = ${batch_size}, resetting to 1`)
batch_size = 1
}
if (is_structured) {
var state = setup_fn()
state = setup_fn()
if (is_batch) {
run_fn(batch_size, state)
} else {
@@ -312,35 +317,34 @@ function run_single_bench(bench_fn, bench_name) {
}
// Measurement phase - collect SAMPLES timing samples
for (var i = 0; i < SAMPLES; i++) {
// Double-check batch_size is valid (should never happen, but defensive)
for (i = 0; i < SAMPLES; i++) {
if (!is_number(batch_size) || batch_size < 1) {
batch_size = 1
}
if (is_structured) {
var state = setup_fn()
var start = os.now()
state = setup_fn()
start = os.now()
if (is_batch) {
run_fn(batch_size, state)
} else {
run_fn(state)
}
var duration = os.now() - start
duration = os.now() - start
if (teardown_fn) teardown_fn(state)
var ns_per_op = is_batch ? duration / batch_size : duration
ns_per_op = is_batch ? duration / batch_size : duration
push(timings_per_op, ns_per_op)
} else {
var start = os.now()
start = os.now()
if (is_batch) {
bench_fn(batch_size)
} else {
bench_fn()
}
var duration = os.now() - start
duration = os.now() - start
var ns_per_op = is_batch ? duration / batch_size : duration
ns_per_op = is_batch ? duration / batch_size : duration
push(timings_per_op, ns_per_op)
}
}
@@ -354,7 +358,6 @@ function run_single_bench(bench_fn, bench_name) {
var p95_ns = percentile(timings_per_op, 95)
var p99_ns = percentile(timings_per_op, 99)
// Calculate ops/s from median
var ops_per_sec = 0
if (median_ns > 0) {
ops_per_sec = floor(1000000000 / median_ns)
@@ -408,18 +411,21 @@ function run_benchmarks(package_name, specific_bench) {
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 error_result = null
var file_result = {
name: f,
benchmarks: []
}
try {
var bench_mod
var use_pkg = package_name ? package_name : fd.realpath('.')
var _load_file = function() {
use_pkg = package_name ? package_name : fd.realpath('.')
bench_mod = shop.use(mod_path, use_pkg)
var benches = []
if (is_function(bench_mod)) {
push(benches, {name: 'main', fn: bench_mod})
} else if (is_object(bench_mod)) {
@@ -432,8 +438,11 @@ function run_benchmarks(package_name, specific_bench) {
if (length(benches) > 0) {
log.console(` ${f}`)
arrfor(benches, function(b) {
try {
var result = run_single_bench(b.fn, b.name)
var bench_error = false
var result = null
var _run_bench = function() {
result = run_single_bench(b.fn, b.name)
result.package = pkg_result.package
push(file_result.benchmarks, result)
pkg_result.total++
@@ -444,25 +453,32 @@ function run_benchmarks(package_name, specific_bench) {
if (result.batch_size > 1) {
log.console(` batch: ${result.batch_size} samples: ${result.samples}`)
}
} catch (e) {
log.console(` ERROR ${b.name}: ${e}`)
log.error(e)
var error_result = {
} disruption {
bench_error = true
}
_run_bench()
if (bench_error) {
log.console(` ERROR ${b.name}`)
error_result = {
package: pkg_result.package,
name: b.name,
error: e.toString()
error: "benchmark disrupted"
}
push(file_result.benchmarks, error_result)
pkg_result.total++
}
})
}
} catch (e) {
log.console(` Error loading ${f}: ${e}`)
var error_result = {
} disruption {
load_error = true
}
_load_file()
if (load_error) {
log.console(` Error loading ${f}`)
error_result = {
package: pkg_result.package,
name: "load_module",
error: `Error loading module: ${e}`
error: "error loading module"
}
push(file_result.benchmarks, error_result)
pkg_result.total++
@@ -478,15 +494,16 @@ function run_benchmarks(package_name, specific_bench) {
// Run all benchmarks
var all_results = []
var packages = null
if (all_pkgs) {
if (testlib.is_valid_package('.')) {
push(all_results, run_benchmarks(null, null))
}
var packages = shop.list_packages()
arrfor(packages, function(pkg) {
push(all_results, run_benchmarks(pkg, null))
packages = shop.list_packages()
arrfor(packages, function(p) {
push(all_results, run_benchmarks(p, null))
})
} else {
push(all_results, run_benchmarks(target_pkg, target_bench))

194
bench_native.ce Normal file
View File

@@ -0,0 +1,194 @@
// 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
}

232
benches/actor_patterns.cm Normal file
View File

@@ -0,0 +1,232 @@
// actor_patterns.cm — Actor concurrency benchmarks
// Message passing, fan-out/fan-in, mailbox throughput.
// These use structured benchmarks with setup/run/teardown.
// Note: actor benchmarks are measured differently from pure compute.
// Each iteration sends messages and waits for results, so they're
// inherently slower but test real concurrency costs.
// Simple ping-pong: two actors sending messages back and forth
// Since we can't create real actors from a module, we simulate
// the message-passing patterns with function call overhead that
// mirrors what the actor dispatch does.
// Simulate message dispatch overhead
function make_mailbox() {
return {
queue: [],
delivered: 0
}
}
function send(mailbox, msg) {
push(mailbox.queue, msg)
return null
}
function receive(mailbox) {
if (length(mailbox.queue) == 0) return null
mailbox.delivered++
return pop(mailbox.queue)
}
function drain(mailbox) {
var count = 0
while (length(mailbox.queue) > 0) {
pop(mailbox.queue)
count++
}
return count
}
// Ping-pong: simulate two actors exchanging messages
function ping_pong(rounds) {
var box_a = make_mailbox()
var box_b = make_mailbox()
var i = 0
var msg = null
send(box_a, {type: "ping", val: 0})
for (i = 0; i < rounds; i++) {
// A receives and sends to B
msg = receive(box_a)
if (msg) {
send(box_b, {type: "pong", val: msg.val + 1})
}
// B receives and sends to A
msg = receive(box_b)
if (msg) {
send(box_a, {type: "ping", val: msg.val + 1})
}
}
return box_a.delivered + box_b.delivered
}
// Fan-out: one sender, N receivers
function fan_out(n_receivers, messages_per) {
var receivers = []
var i = 0
var j = 0
for (i = 0; i < n_receivers; i++) {
push(receivers, make_mailbox())
}
// Send messages to all receivers
for (j = 0; j < messages_per; j++) {
for (i = 0; i < n_receivers; i++) {
send(receivers[i], {seq: j, data: j * 17})
}
}
// All receivers drain
var total = 0
for (i = 0; i < n_receivers; i++) {
total += drain(receivers[i])
}
return total
}
// Fan-in: N senders, one receiver
function fan_in(n_senders, messages_per) {
var inbox = make_mailbox()
var i = 0
var j = 0
// Each sender sends messages
for (i = 0; i < n_senders; i++) {
for (j = 0; j < messages_per; j++) {
send(inbox, {sender: i, seq: j, data: i * 100 + j})
}
}
// Receiver processes all
var total = 0
var msg = null
msg = receive(inbox)
while (msg) {
total += msg.data
msg = receive(inbox)
}
return total
}
// Pipeline: chain of processors
function pipeline(stages, items) {
var boxes = []
var i = 0
var j = 0
var msg = null
for (i = 0; i <= stages; i++) {
push(boxes, make_mailbox())
}
// Feed input
for (i = 0; i < items; i++) {
send(boxes[0], {val: i})
}
// Process each stage
for (j = 0; j < stages; j++) {
msg = receive(boxes[j])
while (msg) {
send(boxes[j + 1], {val: msg.val * 2 + 1})
msg = receive(boxes[j])
}
}
// Drain output
var total = 0
msg = receive(boxes[stages])
while (msg) {
total += msg.val
msg = receive(boxes[stages])
}
return total
}
// Request-response pattern (simulate RPC)
function request_response(n_requests) {
var client_box = make_mailbox()
var server_box = make_mailbox()
var i = 0
var req = null
var resp = null
var total = 0
for (i = 0; i < n_requests; i++) {
// Client sends request
send(server_box, {id: i, payload: i * 3, reply_to: client_box})
// Server processes
req = receive(server_box)
if (req) {
send(req.reply_to, {id: req.id, result: req.payload * 2 + 1})
}
// Client receives response
resp = receive(client_box)
if (resp) {
total += resp.result
}
}
return total
}
return {
// Ping-pong: 10K rounds
ping_pong_10k: function(n) {
var i = 0
var x = 0
for (i = 0; i < n; i++) {
x += ping_pong(10000)
}
return x
},
// Fan-out: 100 receivers, 100 messages each
fan_out_100x100: function(n) {
var i = 0
var x = 0
for (i = 0; i < n; i++) {
x += fan_out(100, 100)
}
return x
},
// Fan-in: 100 senders, 100 messages each
fan_in_100x100: function(n) {
var i = 0
var x = 0
for (i = 0; i < n; i++) {
x += fan_in(100, 100)
}
return x
},
// Pipeline: 10 stages, 1000 items
pipeline_10x1k: function(n) {
var i = 0
var x = 0
for (i = 0; i < n; i++) {
x += pipeline(10, 1000)
}
return x
},
// Request-response: 5K requests
rpc_5k: function(n) {
var i = 0
var x = 0
for (i = 0; i < n; i++) {
x += request_response(5000)
}
return x
}
}

141
benches/cli_tool.cm Normal file
View File

@@ -0,0 +1,141 @@
// cli_tool.cm — CLI tool simulation (macro benchmark)
// Parse args + process data + transform + format output.
// Simulates a realistic small utility program.
var json = use('json')
// Generate fake records
function generate_records(n) {
var records = []
var x = 42
var i = 0
var status_vals = ["active", "inactive", "pending", "archived"]
var dept_vals = ["eng", "sales", "ops", "hr", "marketing"]
for (i = 0; i < n; i++) {
x = ((x * 1103515245 + 12345) & 0x7FFFFFFF) | 0
push(records, {
id: i + 1,
name: `user_${i}`,
score: (x % 1000) / 10,
status: status_vals[i % 4],
department: dept_vals[i % 5]
})
}
return records
}
// Filter records by field value
function filter_records(records, field, value) {
var result = []
var i = 0
for (i = 0; i < length(records); i++) {
if (records[i][field] == value) {
push(result, records[i])
}
}
return result
}
// Group by a field
function group_by(records, field) {
var groups = {}
var i = 0
var key = null
for (i = 0; i < length(records); i++) {
key = records[i][field]
if (!key) key = "unknown"
if (!groups[key]) groups[key] = []
push(groups[key], records[i])
}
return groups
}
// Aggregate: compute stats per group
function aggregate(groups) {
var keys = array(groups)
var result = []
var i = 0
var j = 0
var grp = null
var total = 0
var mn = 0
var mx = 0
for (i = 0; i < length(keys); i++) {
grp = groups[keys[i]]
total = 0
mn = 999999
mx = 0
for (j = 0; j < length(grp); j++) {
total += grp[j].score
if (grp[j].score < mn) mn = grp[j].score
if (grp[j].score > mx) mx = grp[j].score
}
push(result, {
group: keys[i],
count: length(grp),
average: total / length(grp),
low: mn,
high: mx
})
}
return result
}
// Full pipeline: load → filter → sort → group → aggregate → encode
function run_pipeline(n_records) {
// Generate data
var records = generate_records(n_records)
// Filter to active records
var filtered = filter_records(records, "status", "active")
// Sort by score
filtered = sort(filtered, "score")
// Limit to first 50
if (length(filtered) > 50) {
filtered = array(filtered, 0, 50)
}
// Group and aggregate
var groups = group_by(filtered, "department")
var stats = aggregate(groups)
stats = sort(stats, "average")
// Encode as JSON
var output = json.encode(stats)
return length(output)
}
return {
// Small dataset (100 records)
cli_pipeline_100: function(n) {
var i = 0
var x = 0
for (i = 0; i < n; i++) {
x += run_pipeline(100)
}
return x
},
// Medium dataset (1000 records)
cli_pipeline_1k: function(n) {
var i = 0
var x = 0
for (i = 0; i < n; i++) {
x += run_pipeline(1000)
}
return x
},
// Large dataset (10K records)
cli_pipeline_10k: function(n) {
var i = 0
var x = 0
for (i = 0; i < n; i++) {
x += run_pipeline(10000)
}
return x
}
}

162
benches/deltablue.cm Normal file
View File

@@ -0,0 +1,162 @@
// deltablue.cm — Constraint solver kernel (DeltaBlue-inspired)
// Dynamic dispatch, pointer chasing, object-heavy workload.
def REQUIRED = 0
def STRONG = 1
def NORMAL = 2
def WEAK = 3
def WEAKEST = 4
function make_variable(name, value) {
return {
name: name,
value: value,
constraints: [],
determined_by: null,
stay: true,
mark: 0
}
}
function make_constraint(strength, variables, satisfy_fn) {
return {
strength: strength,
variables: variables,
satisfy: satisfy_fn,
output: null
}
}
// Constraint propagation: simple forward solver
function propagate(vars, constraints) {
var changed = true
var passes = 0
var max_passes = length(constraints) * 3
var i = 0
var c = null
var old_val = 0
while (changed && passes < max_passes) {
changed = false
passes++
for (i = 0; i < length(constraints); i++) {
c = constraints[i]
old_val = c.output ? c.output.value : null
c.satisfy(c)
if (c.output && c.output.value != old_val) {
changed = true
}
}
}
return passes
}
// Build a chain of equality constraints: v[i] = v[i-1] + 1
function build_chain(n) {
var vars = []
var constraints = []
var i = 0
for (i = 0; i < n; i++) {
push(vars, make_variable(`v${i}`, 0))
}
// Set first variable
vars[0].value = 1
var c = null
for (i = 1; i < n; i++) {
c = make_constraint(NORMAL, [vars[i - 1], vars[i]], function(self) {
self.variables[1].value = self.variables[0].value + 1
self.output = self.variables[1]
})
push(constraints, c)
push(vars[i].constraints, c)
}
return {vars: vars, constraints: constraints}
}
// Build a projection: pairs of variables with scaling constraints
function build_projection(n) {
var src = []
var dst = []
var constraints = []
var i = 0
for (i = 0; i < n; i++) {
push(src, make_variable(`src${i}`, i * 10))
push(dst, make_variable(`dst${i}`, 0))
}
var scale_c = null
for (i = 0; i < n; i++) {
scale_c = make_constraint(STRONG, [src[i], dst[i]], function(self) {
self.variables[1].value = self.variables[0].value * 2 + 1
self.output = self.variables[1]
})
push(constraints, scale_c)
push(dst[i].constraints, scale_c)
}
return {src: src, dst: dst, constraints: constraints}
}
// Edit constraint: change a source, re-propagate
function run_edits(system, edits) {
var i = 0
var total_passes = 0
for (i = 0; i < edits; i++) {
system.vars[0].value = i
total_passes += propagate(system.vars, system.constraints)
}
return total_passes
}
return {
// Chain of 100 variables, propagate
chain_100: function(n) {
var i = 0
var chain = null
var x = 0
for (i = 0; i < n; i++) {
chain = build_chain(100)
x += propagate(chain.vars, chain.constraints)
}
return x
},
// Chain of 500 variables, propagate
chain_500: function(n) {
var i = 0
var chain = null
var x = 0
for (i = 0; i < n; i++) {
chain = build_chain(500)
x += propagate(chain.vars, chain.constraints)
}
return x
},
// Projection of 100 pairs
projection_100: function(n) {
var i = 0
var proj = null
var x = 0
for (i = 0; i < n; i++) {
proj = build_projection(100)
x += propagate(proj.src, proj.constraints)
}
return x
},
// Edit and re-propagate (incremental update)
chain_edit_100: function(n) {
var chain = build_chain(100)
var i = 0
var x = 0
for (i = 0; i < n; i++) {
chain.vars[0].value = i
x += propagate(chain.vars, chain.constraints)
}
return x
}
}

126
benches/fibonacci.cm Normal file
View File

@@ -0,0 +1,126 @@
// fibonacci.cm — Fibonacci variants kernel
// Tests recursion overhead, memoization patterns, iteration vs recursion.
// Naive recursive (exponential) — measures call overhead
function fib_naive(n) {
if (n <= 1) return n
return fib_naive(n - 1) + fib_naive(n - 2)
}
// Iterative (linear)
function fib_iter(n) {
var a = 0
var b = 1
var i = 0
var tmp = 0
for (i = 0; i < n; i++) {
tmp = a + b
a = b
b = tmp
}
return a
}
// Memoized recursive (tests object property lookup + recursion)
function make_memo_fib() {
var cache = {}
var fib = function(n) {
var key = text(n)
if (cache[key]) return cache[key]
var result = null
if (n <= 1) {
result = n
} else {
result = fib(n - 1) + fib(n - 2)
}
cache[key] = result
return result
}
return fib
}
// CPS (continuation passing style) — tests closure creation
function fib_cps(n, cont) {
if (n <= 1) return cont(n)
return fib_cps(n - 1, function(a) {
return fib_cps(n - 2, function(b) {
return cont(a + b)
})
})
}
// Matrix exponentiation style (accumulator)
function fib_matrix(n) {
var a = 1
var b = 0
var c = 0
var d = 1
var ta = 0
var tb = 0
var m = n
while (m > 0) {
if (m % 2 == 1) {
ta = a * d + b * c // wrong but stresses numeric ops
tb = b * d + a * c
a = ta
b = tb
}
ta = c * c + d * d
tb = d * (2 * c + d)
c = ta
d = tb
m = floor(m / 2)
}
return b
}
return {
fib_naive_25: function(n) {
var i = 0
var x = 0
for (i = 0; i < n; i++) x += fib_naive(25)
return x
},
fib_naive_30: function(n) {
var i = 0
var x = 0
for (i = 0; i < n; i++) x += fib_naive(30)
return x
},
fib_iter_80: function(n) {
var i = 0
var x = 0
for (i = 0; i < n; i++) x += fib_iter(80)
return x
},
fib_memo_100: function(n) {
var i = 0
var x = 0
var fib = null
for (i = 0; i < n; i++) {
fib = make_memo_fib()
x += fib(100)
}
return x
},
fib_cps_20: function(n) {
var i = 0
var x = 0
var identity = function(v) { return v }
for (i = 0; i < n; i++) {
x += fib_cps(20, identity)
}
return x
},
fib_matrix_80: function(n) {
var i = 0
var x = 0
for (i = 0; i < n; i++) x += fib_matrix(80)
return x
}
}

159
benches/hash_workload.cm Normal file
View File

@@ -0,0 +1,159 @@
// hash_workload.cm — Hash-heavy / word-count / map-reduce kernel
// Stresses record (object) creation, property access, and string handling.
function make_words(count) {
// Generate a repeating word list to simulate text processing
var base_words = [
"the", "quick", "brown", "fox", "jumps", "over", "lazy", "dog",
"and", "cat", "sat", "on", "mat", "with", "hat", "bat",
"alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta",
"hello", "world", "foo", "bar", "baz", "qux", "quux", "corge"
]
var words = []
var i = 0
for (i = 0; i < count; i++) {
push(words, base_words[i % length(base_words)])
}
return words
}
// Word frequency count
function word_count(words) {
var freq = {}
var i = 0
var w = null
for (i = 0; i < length(words); i++) {
w = words[i]
if (freq[w]) {
freq[w] = freq[w] + 1
} else {
freq[w] = 1
}
}
return freq
}
// Find top-N words by frequency
function top_n(freq, n) {
var keys = array(freq)
var pairs = []
var i = 0
for (i = 0; i < length(keys); i++) {
push(pairs, {word: keys[i], count: freq[keys[i]]})
}
var sorted = sort(pairs, "count")
// Return last N (highest counts)
var result = []
var start = length(sorted) - n
if (start < 0) start = 0
for (i = start; i < length(sorted); i++) {
push(result, sorted[i])
}
return result
}
// Histogram: group words by length
function group_by_length(words) {
var groups = {}
var i = 0
var w = null
var k = null
for (i = 0; i < length(words); i++) {
w = words[i]
k = text(length(w))
if (!groups[k]) groups[k] = []
push(groups[k], w)
}
return groups
}
// Simple hash table with chaining (stress property access patterns)
function hash_table_ops(n) {
var table = {}
var i = 0
var k = null
var collisions = 0
// Insert phase
for (i = 0; i < n; i++) {
k = `key_${i % 512}`
if (table[k]) collisions++
table[k] = i
}
// Lookup phase
var found = 0
for (i = 0; i < n; i++) {
k = `key_${i % 512}`
if (table[k]) found++
}
// Delete phase
var deleted = 0
for (i = 0; i < n; i += 3) {
k = `key_${i % 512}`
if (table[k]) {
delete table[k]
deleted++
}
}
return found - deleted + collisions
}
var words_1k = make_words(1000)
var words_10k = make_words(10000)
return {
// Word count on 1K words
wordcount_1k: function(n) {
var i = 0
var freq = null
for (i = 0; i < n; i++) {
freq = word_count(words_1k)
}
return freq
},
// Word count on 10K words
wordcount_10k: function(n) {
var i = 0
var freq = null
for (i = 0; i < n; i++) {
freq = word_count(words_10k)
}
return freq
},
// Word count + top-10 extraction
wordcount_top10: function(n) {
var i = 0
var freq = null
var top = null
for (i = 0; i < n; i++) {
freq = word_count(words_10k)
top = top_n(freq, 10)
}
return top
},
// Group words by length
group_by_len: function(n) {
var i = 0
var groups = null
for (i = 0; i < n; i++) {
groups = group_by_length(words_10k)
}
return groups
},
// Hash table insert/lookup/delete
hash_table: function(n) {
var i = 0
var x = 0
for (i = 0; i < n; i++) {
x += hash_table_ops(2048)
}
return x
}
}

167
benches/json_walk.cm Normal file
View File

@@ -0,0 +1,167 @@
// json_walk.cm — JSON parse + walk + serialize kernel
// Stresses strings, records, arrays, and recursive traversal.
var json = use('json')
function make_nested_object(depth, breadth) {
var obj = {}
var i = 0
var k = null
if (depth <= 0) {
for (i = 0; i < breadth; i++) {
k = `key_${i}`
obj[k] = i * 3.14
}
return obj
}
for (i = 0; i < breadth; i++) {
k = `node_${i}`
obj[k] = make_nested_object(depth - 1, breadth)
}
obj.value = depth
obj.name = `level_${depth}`
return obj
}
function make_array_data(size) {
var arr = []
var i = 0
for (i = 0; i < size; i++) {
push(arr, {
id: i,
name: `item_${i}`,
active: i % 2 == 0,
score: i * 1.5,
tags: [`tag_${i % 5}`, `tag_${(i + 1) % 5}`]
})
}
return arr
}
// Walk an object tree, counting nodes
function walk_count(obj) {
var count = 1
var keys = null
var i = 0
var v = null
if (is_object(obj)) {
keys = array(obj)
for (i = 0; i < length(keys); i++) {
v = obj[keys[i]]
if (is_object(v) || is_array(v)) {
count += walk_count(v)
}
}
} else if (is_array(obj)) {
for (i = 0; i < length(obj); i++) {
v = obj[i]
if (is_object(v) || is_array(v)) {
count += walk_count(v)
}
}
}
return count
}
// Walk and extract all numbers
function walk_sum(obj) {
var sum = 0
var keys = null
var i = 0
var v = null
if (is_object(obj)) {
keys = array(obj)
for (i = 0; i < length(keys); i++) {
v = obj[keys[i]]
if (is_number(v)) {
sum += v
} else if (is_object(v) || is_array(v)) {
sum += walk_sum(v)
}
}
} else if (is_array(obj)) {
for (i = 0; i < length(obj); i++) {
v = obj[i]
if (is_number(v)) {
sum += v
} else if (is_object(v) || is_array(v)) {
sum += walk_sum(v)
}
}
}
return sum
}
// Pre-build test data strings
var nested_obj = make_nested_object(3, 4)
var nested_json = json.encode(nested_obj)
var array_data = make_array_data(200)
var array_json = json.encode(array_data)
return {
// Parse nested JSON
json_parse_nested: function(n) {
var i = 0
var obj = null
for (i = 0; i < n; i++) {
obj = json.decode(nested_json)
}
return obj
},
// Parse array-of-records JSON
json_parse_array: function(n) {
var i = 0
var arr = null
for (i = 0; i < n; i++) {
arr = json.decode(array_json)
}
return arr
},
// Encode nested object to JSON
json_encode_nested: function(n) {
var i = 0
var s = null
for (i = 0; i < n; i++) {
s = json.encode(nested_obj)
}
return s
},
// Encode array to JSON
json_encode_array: function(n) {
var i = 0
var s = null
for (i = 0; i < n; i++) {
s = json.encode(array_data)
}
return s
},
// Parse + walk + count
json_roundtrip_walk: function(n) {
var i = 0
var obj = null
var count = 0
for (i = 0; i < n; i++) {
obj = json.decode(nested_json)
count += walk_count(obj)
}
return count
},
// Parse + sum all numbers + re-encode
json_roundtrip_full: function(n) {
var i = 0
var obj = null
var sum = 0
var out = null
for (i = 0; i < n; i++) {
obj = json.decode(array_json)
sum += walk_sum(obj)
out = json.encode(obj)
}
return sum
}
}

View File

@@ -1,24 +1,24 @@
// micro_ops.bench.ce (or .cm depending on your convention)
// micro_ops.cm — microbenchmarks for core operations
// Note: We use a function-local sink in each benchmark to avoid cross-contamination
function blackhole(sink, x) {
// Prevent dead-code elimination
return (sink + (x | 0)) | 0
}
function make_obj_xy(x, y) {
return { x, y }
return {x: x, y: y}
}
function make_obj_yx(x, y) {
// Different insertion order to force a different shape in many engines
return { y, x }
// Different insertion order to force a different shape
return {y: y, x: x}
}
function make_shapes(n) {
var out = []
for (var i = 0; i < n; i++) {
var o = { a: i }
var i = 0
var o = null
for (i = 0; i < n; i++) {
o = {a: i}
o[`p${i}`] = i
push(out, o)
}
@@ -27,13 +27,15 @@ function make_shapes(n) {
function make_packed_array(n) {
var a = []
for (var i = 0; i < n; i++) push(a, i)
var i = 0
for (i = 0; i < n; i++) push(a, i)
return a
}
function make_holey_array(n) {
var a = []
for (var i = 0; i < n; i += 2) a[i] = i
var i = 0
for (i = 0; i < n; i += 2) a[i] = i
return a
}
@@ -41,7 +43,8 @@ return {
// 0) Baseline loop cost
loop_empty: function(n) {
var sink = 0
for (var i = 0; i < n; i++) {}
var i = 0
for (i = 0; i < n; i++) {}
return blackhole(sink, n)
},
@@ -49,35 +52,40 @@ return {
i32_add: function(n) {
var sink = 0
var x = 1
for (var i = 0; i < n; i++) x = (x + 3) | 0
var i = 0
for (i = 0; i < n; i++) x = (x + 3) | 0
return blackhole(sink, x)
},
f64_add: function(n) {
var sink = 0
var x = 1.0
for (var i = 0; i < n; i++) x = x + 3.14159
var i = 0
for (i = 0; i < n; i++) x = x + 3.14159
return blackhole(sink, x | 0)
},
mixed_add: function(n) {
var sink = 0
var x = 1
for (var i = 0; i < n; i++) x = x + 0.25
var i = 0
for (i = 0; i < n; i++) x = x + 0.25
return blackhole(sink, x | 0)
},
bit_ops: function(n) {
var sink = 0
var x = 0x12345678
for (var i = 0; i < n; i++) x = ((x << 5) ^ (x >>> 3)) | 0
var i = 0
for (i = 0; i < n; i++) x = ((x << 5) ^ (x >>> 3)) | 0
return blackhole(sink, x)
},
overflow_path: function(n) {
var sink = 0
var x = 0x70000000
for (var i = 0; i < n; i++) x = (x + 0x10000000) | 0
var i = 0
for (i = 0; i < n; i++) x = (x + 0x10000000) | 0
return blackhole(sink, x)
},
@@ -85,7 +93,8 @@ return {
branch_predictable: function(n) {
var sink = 0
var x = 0
for (var i = 0; i < n; i++) {
var i = 0
for (i = 0; i < n; i++) {
if ((i & 7) != 0) x++
else x += 2
}
@@ -95,7 +104,8 @@ return {
branch_alternating: function(n) {
var sink = 0
var x = 0
for (var i = 0; i < n; i++) {
var i = 0
for (i = 0; i < n; i++) {
if ((i & 1) == 0) x++
else x += 2
}
@@ -105,29 +115,47 @@ return {
// 3) Calls
call_direct: function(n) {
var sink = 0
function f(a) { return (a + 1) | 0 }
var f = function(a) { return (a + 1) | 0 }
var x = 0
for (var i = 0; i < n; i++) x = f(x)
var i = 0
for (i = 0; i < n; i++) x = f(x)
return blackhole(sink, x)
},
call_indirect: function(n) {
var sink = 0
function f(a) { return (a + 1) | 0 }
var f = function(a) { return (a + 1) | 0 }
var g = f
var x = 0
for (var i = 0; i < n; i++) x = g(x)
var i = 0
for (i = 0; i < n; i++) x = g(x)
return blackhole(sink, x)
},
call_closure: function(n) {
var sink = 0
function make_adder(k) {
var make_adder = function(k) {
return function(a) { return (a + k) | 0 }
}
var add3 = make_adder(3)
var x = 0
for (var i = 0; i < n; i++) x = add3(x)
var i = 0
for (i = 0; i < n; i++) x = add3(x)
return blackhole(sink, x)
},
call_multi_arity: function(n) {
var sink = 0
var f0 = function() { return 1 }
var f1 = function(a) { return a + 1 }
var f2 = function(a, b) { return a + b }
var f3 = function(a, b, c) { return a + b + c }
var f4 = function(a, b, c, d) { return a + b + c + d }
var x = 0
var i = 0
for (i = 0; i < n; i++) {
x = (x + f0() + f1(i) + f2(i, 1) + f3(i, 1, 2) + f4(i, 1, 2, 3)) | 0
}
return blackhole(sink, x)
},
@@ -136,7 +164,8 @@ return {
var sink = 0
var o = make_obj_xy(1, 2)
var x = 0
for (var i = 0; i < n; i++) x = (x + o.x) | 0
var i = 0
for (i = 0; i < n; i++) x = (x + o.x) | 0
return blackhole(sink, x)
},
@@ -145,20 +174,38 @@ return {
var a = make_obj_xy(1, 2)
var b = make_obj_yx(1, 2)
var x = 0
for (var i = 0; i < n; i++) {
var o = (i & 1) == 0 ? a : b
var i = 0
var o = null
for (i = 0; i < n; i++) {
o = (i & 1) == 0 ? a : b
x = (x + o.x) | 0
}
return blackhole(sink, x)
},
prop_read_poly_4: function(n) {
var sink = 0
var shapes = [
{x: 1, y: 2},
{y: 2, x: 1},
{x: 1, z: 3, y: 2},
{w: 0, x: 1, y: 2}
]
var x = 0
var i = 0
for (i = 0; i < n; i++) {
x = (x + shapes[i & 3].x) | 0
}
return blackhole(sink, x)
},
prop_read_mega: function(n) {
var sink = 0
var objs = make_shapes(32)
var x = 0
for (var i = 0; i < n; i++) {
var o = objs[i & 31]
x = (x + o.a) | 0
var i = 0
for (i = 0; i < n; i++) {
x = (x + objs[i & 31].a) | 0
}
return blackhole(sink, x)
},
@@ -166,7 +213,8 @@ return {
prop_write_mono: function(n) {
var sink = 0
var o = make_obj_xy(1, 2)
for (var i = 0; i < n; i++) o.x = (o.x + 1) | 0
var i = 0
for (i = 0; i < n; i++) o.x = (o.x + 1) | 0
return blackhole(sink, o.x)
},
@@ -175,14 +223,16 @@ return {
var sink = 0
var a = make_packed_array(1024)
var x = 0
for (var i = 0; i < n; i++) x = (x + a[i & 1023]) | 0
var i = 0
for (i = 0; i < n; i++) x = (x + a[i & 1023]) | 0
return blackhole(sink, x)
},
array_write_packed: function(n) {
var sink = 0
var a = make_packed_array(1024)
for (var i = 0; i < n; i++) a[i & 1023] = i
var i = 0
for (i = 0; i < n; i++) a[i & 1023] = i
return blackhole(sink, a[17] | 0)
},
@@ -190,9 +240,10 @@ return {
var sink = 0
var a = make_holey_array(2048)
var x = 0
for (var i = 0; i < n; i++) {
var v = a[(i & 2047)]
// If "missing" is a special value in your language, this stresses that path too
var i = 0
var v = null
for (i = 0; i < n; i++) {
v = a[(i & 2047)]
if (v) x = (x + v) | 0
}
return blackhole(sink, x)
@@ -201,21 +252,97 @@ return {
array_push_steady: function(n) {
var sink = 0
var x = 0
for (var j = 0; j < n; j++) {
var a = []
for (var i = 0; i < 256; i++) push(a, i)
var j = 0
var i = 0
var a = null
for (j = 0; j < n; j++) {
a = []
for (i = 0; i < 256; i++) push(a, i)
x = (x + length(a)) | 0
}
return blackhole(sink, x)
},
array_push_pop: function(n) {
var sink = 0
var a = []
var x = 0
var i = 0
var v = 0
for (i = 0; i < n; i++) {
push(a, i)
if (length(a) > 64) {
v = pop(a)
x = (x + v) | 0
}
}
return blackhole(sink, x)
},
array_indexed_sum: function(n) {
var sink = 0
var a = make_packed_array(1024)
var x = 0
var j = 0
var i = 0
for (j = 0; j < n; j++) {
x = 0
for (i = 0; i < 1024; i++) {
x = (x + a[i]) | 0
}
}
return blackhole(sink, x)
},
// 6) Strings
string_concat_small: function(n) {
var sink = 0
var x = 0
for (var j = 0; j < n; j++) {
var s = ""
for (var i = 0; i < 16; i++) s = s + "x"
var j = 0
var i = 0
var s = null
for (j = 0; j < n; j++) {
s = ""
for (i = 0; i < 16; i++) s = s + "x"
x = (x + length(s)) | 0
}
return blackhole(sink, x)
},
string_concat_medium: function(n) {
var sink = 0
var x = 0
var j = 0
var i = 0
var s = null
for (j = 0; j < n; j++) {
s = ""
for (i = 0; i < 100; i++) s = s + "abcdefghij"
x = (x + length(s)) | 0
}
return blackhole(sink, x)
},
string_interpolation: function(n) {
var sink = 0
var x = 0
var i = 0
var s = null
for (i = 0; i < n; i++) {
s = `item_${i}_value_${i * 2}`
x = (x + length(s)) | 0
}
return blackhole(sink, x)
},
string_slice: function(n) {
var sink = 0
var base = "the quick brown fox jumps over the lazy dog"
var x = 0
var i = 0
var s = null
for (i = 0; i < n; i++) {
s = text(base, i % 10, i % 10 + 10)
x = (x + length(s)) | 0
}
return blackhole(sink, x)
@@ -225,8 +352,10 @@ return {
alloc_tiny_objects: function(n) {
var sink = 0
var x = 0
for (var i = 0; i < n; i++) {
var o = { a: i, b: i + 1, c: i + 2 }
var i = 0
var o = null
for (i = 0; i < n; i++) {
o = {a: i, b: i + 1, c: i + 2}
x = (x + o.b) | 0
}
return blackhole(sink, x)
@@ -235,9 +364,12 @@ return {
alloc_linked_list: function(n) {
var sink = 0
var head = null
for (var i = 0; i < n; i++) head = { v: i, next: head }
var i = 0
var x = 0
var p = head
var p = null
for (i = 0; i < n; i++) head = {v: i, next: head}
x = 0
p = head
while (p) {
x = (x + p.v) | 0
p = p.next
@@ -245,18 +377,118 @@ return {
return blackhole(sink, x)
},
// 8) meme-specific (adapt these to your exact semantics)
meme_clone_read: function(n) {
// If meme(obj) clones like Object.create / prototypal clone, this hits it hard.
// Replace with your exact meme call form.
alloc_arrays: function(n) {
var sink = 0
var base = { x: 1, y: 2 }
var x = 0
for (var i = 0; i < n; i++) {
var o = meme(base)
var i = 0
var a = null
for (i = 0; i < n; i++) {
a = [i, i + 1, i + 2, i + 3]
x = (x + a[2]) | 0
}
return blackhole(sink, x)
},
alloc_short_lived: function(n) {
var sink = 0
var x = 0
var i = 0
var o = null
// Allocate objects that immediately become garbage
for (i = 0; i < n; i++) {
o = {val: i, data: {inner: i + 1}}
x = (x + o.data.inner) | 0
}
return blackhole(sink, x)
},
alloc_long_lived_pressure: function(n) {
var sink = 0
var store = []
var x = 0
var i = 0
var o = null
// Keep first 1024 objects alive, churn the rest
for (i = 0; i < n; i++) {
o = {val: i, data: i * 2}
if (i < 1024) {
push(store, o)
}
x = (x + o.data) | 0
}
return blackhole(sink, x)
},
// 8) Meme (prototype clone)
meme_clone_read: function(n) {
var sink = 0
var base = {x: 1, y: 2}
var x = 0
var i = 0
var o = null
for (i = 0; i < n; i++) {
o = meme(base)
x = (x + o.x) | 0
}
return blackhole(sink, x)
},
// 9) Guard / type check paths
guard_hot_number: function(n) {
// Monomorphic number path — guards should hoist
var sink = 0
var x = 1
var i = 0
for (i = 0; i < n; i++) x = x + 1
return blackhole(sink, x | 0)
},
guard_mixed_types: function(n) {
// Alternating number/text — guards must stay
var sink = 0
var vals = [1, "a", 2, "b", 3, "c", 4, "d"]
var x = 0
var i = 0
for (i = 0; i < n; i++) {
if (is_number(vals[i & 7])) x = (x + vals[i & 7]) | 0
}
return blackhole(sink, x)
},
// 10) Reduce / higher-order
reduce_sum: function(n) {
var sink = 0
var a = make_packed_array(256)
var x = 0
var i = 0
for (i = 0; i < n; i++) {
x = (x + reduce(a, function(acc, v) { return acc + v }, 0)) | 0
}
return blackhole(sink, x)
},
filter_evens: function(n) {
var sink = 0
var a = make_packed_array(256)
var x = 0
var i = 0
for (i = 0; i < n; i++) {
x = (x + length(filter(a, function(v) { return v % 2 == 0 }))) | 0
}
return blackhole(sink, x)
},
arrfor_sum: function(n) {
var sink = 0
var a = make_packed_array(256)
var x = 0
var i = 0
var sum = 0
for (i = 0; i < n; i++) {
sum = 0
arrfor(a, function(v) { sum += v })
x = (x + sum) | 0
}
return blackhole(sink, x)
}
}

249
benches/module_load.cm Normal file
View File

@@ -0,0 +1,249 @@
// module_load.cm — Module loading simulation (macro benchmark)
// Simulates parsing many small modules, linking, and running.
// Tests the "build scenario" pattern.
var json = use('json')
// Simulate a small module: parse token stream + build AST + evaluate
function tokenize(src) {
var tokens = []
var i = 0
var ch = null
var chars = array(src)
var buf = ""
for (i = 0; i < length(chars); i++) {
ch = chars[i]
if (ch == " " || ch == "\n" || ch == "\t") {
if (length(buf) > 0) {
push(tokens, buf)
buf = ""
}
} else if (ch == "(" || ch == ")" || ch == "+" || ch == "-"
|| ch == "*" || ch == "=" || ch == ";" || ch == ",") {
if (length(buf) > 0) {
push(tokens, buf)
buf = ""
}
push(tokens, ch)
} else {
buf = buf + ch
}
}
if (length(buf) > 0) push(tokens, buf)
return tokens
}
// Build a simple AST from tokens
function parse_tokens(tokens) {
var ast = []
var i = 0
var tok = null
var node = null
for (i = 0; i < length(tokens); i++) {
tok = tokens[i]
if (tok == "var" || tok == "def") {
node = {type: "decl", kind: tok, name: null, value: null}
i++
if (i < length(tokens)) node.name = tokens[i]
i++ // skip =
i++
if (i < length(tokens)) node.value = tokens[i]
push(ast, node)
} else if (tok == "return") {
node = {type: "return", value: null}
i++
if (i < length(tokens)) node.value = tokens[i]
push(ast, node)
} else if (tok == "function") {
node = {type: "func", name: null, body: []}
i++
if (i < length(tokens)) node.name = tokens[i]
// Skip to matching )
while (i < length(tokens) && tokens[i] != ")") i++
push(ast, node)
} else {
push(ast, {type: "expr", value: tok})
}
}
return ast
}
// Evaluate: simple symbol table + resolution
function evaluate(ast, env) {
var result = null
var i = 0
var node = null
for (i = 0; i < length(ast); i++) {
node = ast[i]
if (node.type == "decl") {
env[node.name] = node.value
} else if (node.type == "return") {
result = node.value
if (env[result]) result = env[result]
} else if (node.type == "func") {
env[node.name] = node
}
}
return result
}
// Generate fake module source code
function generate_module(id, dep_count) {
var src = ""
var i = 0
src = src + "var _id = " + text(id) + ";\n"
for (i = 0; i < dep_count; i++) {
src = src + "var dep" + text(i) + " = use(mod_" + text(i) + ");\n"
}
src = src + "var x = " + text(id * 17) + ";\n"
src = src + "var y = " + text(id * 31) + ";\n"
src = src + "function compute(a, b) { return a + b; }\n"
src = src + "var result = compute(x, y);\n"
src = src + "return result;\n"
return src
}
// Simulate loading N modules with dependency chains
function simulate_build(n_modules, deps_per_module) {
var modules = []
var loaded = {}
var i = 0
var j = 0
var src = null
var tokens = null
var ast = null
var env = null
var result = null
var total_tokens = 0
var total_nodes = 0
// Generate all module sources
for (i = 0; i < n_modules; i++) {
src = generate_module(i, deps_per_module)
push(modules, src)
}
// "Load" each module: tokenize → parse → evaluate
for (i = 0; i < n_modules; i++) {
tokens = tokenize(modules[i])
total_tokens += length(tokens)
ast = parse_tokens(tokens)
total_nodes += length(ast)
env = {}
// Resolve dependencies
for (j = 0; j < deps_per_module; j++) {
if (j < i) {
env["dep" + text(j)] = loaded["mod_" + text(j)]
}
}
result = evaluate(ast, env)
loaded["mod_" + text(i)] = result
}
return {
modules: n_modules,
total_tokens: total_tokens,
total_nodes: total_nodes,
last_result: result
}
}
// Dependency graph analysis (topological sort simulation)
function topo_sort(n_modules, deps_per_module) {
// Build adjacency list
var adj = {}
var in_degree = {}
var i = 0
var j = 0
var name = null
var dep = null
for (i = 0; i < n_modules; i++) {
name = "mod_" + text(i)
adj[name] = []
in_degree[name] = 0
}
for (i = 0; i < n_modules; i++) {
name = "mod_" + text(i)
for (j = 0; j < deps_per_module; j++) {
if (j < i) {
dep = "mod_" + text(j)
push(adj[dep], name)
in_degree[name] = in_degree[name] + 1
}
}
}
// Kahn's algorithm
var queue = []
var keys = array(in_degree)
for (i = 0; i < length(keys); i++) {
if (in_degree[keys[i]] == 0) push(queue, keys[i])
}
var order = []
var current = null
var neighbors = null
var qi = 0
while (qi < length(queue)) {
current = queue[qi]
qi++
push(order, current)
neighbors = adj[current]
if (neighbors) {
for (i = 0; i < length(neighbors); i++) {
in_degree[neighbors[i]] = in_degree[neighbors[i]] - 1
if (in_degree[neighbors[i]] == 0) push(queue, neighbors[i])
}
}
}
return order
}
return {
// Small build: 50 modules, 3 deps each
build_50: function(n) {
var i = 0
var result = null
for (i = 0; i < n; i++) {
result = simulate_build(50, 3)
}
return result
},
// Medium build: 200 modules, 5 deps each
build_200: function(n) {
var i = 0
var result = null
for (i = 0; i < n; i++) {
result = simulate_build(200, 5)
}
return result
},
// Large build: 500 modules, 5 deps each
build_500: function(n) {
var i = 0
var result = null
for (i = 0; i < n; i++) {
result = simulate_build(500, 5)
}
return result
},
// Topo sort of 500 module dependency graph
topo_sort_500: function(n) {
var i = 0
var order = null
for (i = 0; i < n; i++) {
order = topo_sort(500, 5)
}
return order
}
}

160
benches/nbody.cm Normal file
View File

@@ -0,0 +1,160 @@
// nbody.cm — N-body gravitational simulation kernel
// Pure numeric + allocation workload. Classic VM benchmark.
var math = use('math/radians')
def PI = 3.141592653589793
def SOLAR_MASS = 4 * PI * PI
def DAYS_PER_YEAR = 365.24
function make_system() {
// Sun + 4 Jovian planets
var sun = {x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0, mass: SOLAR_MASS}
var jupiter = {
x: 4.84143144246472090,
y: -1.16032004402742839,
z: -0.103622044471123109,
vx: 0.00166007664274403694 * DAYS_PER_YEAR,
vy: 0.00769901118419740425 * DAYS_PER_YEAR,
vz: -0.0000690460016972063023 * DAYS_PER_YEAR,
mass: 0.000954791938424326609 * SOLAR_MASS
}
var saturn = {
x: 8.34336671824457987,
y: 4.12479856412430479,
z: -0.403523417114321381,
vx: -0.00276742510726862411 * DAYS_PER_YEAR,
vy: 0.00499852801234917238 * DAYS_PER_YEAR,
vz: 0.0000230417297573763929 * DAYS_PER_YEAR,
mass: 0.000285885980666130812 * SOLAR_MASS
}
var uranus = {
x: 12.8943695621391310,
y: -15.1111514016986312,
z: -0.223307578892655734,
vx: 0.00296460137564761618 * DAYS_PER_YEAR,
vy: 0.00237847173959480950 * DAYS_PER_YEAR,
vz: -0.0000296589568540237556 * DAYS_PER_YEAR,
mass: 0.0000436624404335156298 * SOLAR_MASS
}
var neptune = {
x: 15.3796971148509165,
y: -25.9193146099879641,
z: 0.179258772950371181,
vx: 0.00268067772490389322 * DAYS_PER_YEAR,
vy: 0.00162824170038242295 * DAYS_PER_YEAR,
vz: -0.0000951592254519715870 * DAYS_PER_YEAR,
mass: 0.0000515138902046611451 * SOLAR_MASS
}
var bodies = [sun, jupiter, saturn, uranus, neptune]
// Offset momentum
var px = 0
var py = 0
var pz = 0
var i = 0
for (i = 0; i < length(bodies); i++) {
px += bodies[i].vx * bodies[i].mass
py += bodies[i].vy * bodies[i].mass
pz += bodies[i].vz * bodies[i].mass
}
sun.vx = -px / SOLAR_MASS
sun.vy = -py / SOLAR_MASS
sun.vz = -pz / SOLAR_MASS
return bodies
}
function advance(bodies, dt) {
var n = length(bodies)
var i = 0
var j = 0
var bi = null
var bj = null
var dx = 0
var dy = 0
var dz = 0
var dist_sq = 0
var dist = 0
var mag = 0
for (i = 0; i < n; i++) {
bi = bodies[i]
for (j = i + 1; j < n; j++) {
bj = bodies[j]
dx = bi.x - bj.x
dy = bi.y - bj.y
dz = bi.z - bj.z
dist_sq = dx * dx + dy * dy + dz * dz
dist = math.sqrt(dist_sq)
mag = dt / (dist_sq * dist)
bi.vx -= dx * bj.mass * mag
bi.vy -= dy * bj.mass * mag
bi.vz -= dz * bj.mass * mag
bj.vx += dx * bi.mass * mag
bj.vy += dy * bi.mass * mag
bj.vz += dz * bi.mass * mag
}
}
for (i = 0; i < n; i++) {
bi = bodies[i]
bi.x += dt * bi.vx
bi.y += dt * bi.vy
bi.z += dt * bi.vz
}
}
function energy(bodies) {
var e = 0
var n = length(bodies)
var i = 0
var j = 0
var bi = null
var bj = null
var dx = 0
var dy = 0
var dz = 0
for (i = 0; i < n; i++) {
bi = bodies[i]
e += 0.5 * bi.mass * (bi.vx * bi.vx + bi.vy * bi.vy + bi.vz * bi.vz)
for (j = i + 1; j < n; j++) {
bj = bodies[j]
dx = bi.x - bj.x
dy = bi.y - bj.y
dz = bi.z - bj.z
e -= (bi.mass * bj.mass) / math.sqrt(dx * dx + dy * dy + dz * dz)
}
}
return e
}
return {
nbody_1k: function(n) {
var i = 0
var j = 0
var bodies = null
for (i = 0; i < n; i++) {
bodies = make_system()
for (j = 0; j < 1000; j++) advance(bodies, 0.01)
energy(bodies)
}
},
nbody_10k: function(n) {
var i = 0
var j = 0
var bodies = null
for (i = 0; i < n; i++) {
bodies = make_system()
for (j = 0; j < 10000; j++) advance(bodies, 0.01)
energy(bodies)
}
}
}

154
benches/ray_tracer.cm Normal file
View File

@@ -0,0 +1,154 @@
// ray_tracer.cm — Simple ray tracer kernel
// Control flow + numeric + allocation. Classic VM benchmark.
var math = use('math/radians')
function vec(x, y, z) {
return {x: x, y: y, z: z}
}
function vadd(a, b) {
return {x: a.x + b.x, y: a.y + b.y, z: a.z + b.z}
}
function vsub(a, b) {
return {x: a.x - b.x, y: a.y - b.y, z: a.z - b.z}
}
function vmul(v, s) {
return {x: v.x * s, y: v.y * s, z: v.z * s}
}
function vdot(a, b) {
return a.x * b.x + a.y * b.y + a.z * b.z
}
function vnorm(v) {
var len = math.sqrt(vdot(v, v))
if (len == 0) return vec(0, 0, 0)
return vmul(v, 1 / len)
}
function make_sphere(center, radius, color) {
return {
center: center,
radius: radius,
color: color
}
}
function intersect_sphere(origin, dir, sphere) {
var oc = vsub(origin, sphere.center)
var b = vdot(oc, dir)
var c = vdot(oc, oc) - sphere.radius * sphere.radius
var disc = b * b - c
if (disc < 0) return -1
var sq = math.sqrt(disc)
var t1 = -b - sq
var t2 = -b + sq
if (t1 > 0.001) return t1
if (t2 > 0.001) return t2
return -1
}
function make_scene() {
var spheres = [
make_sphere(vec(0, -1, 5), 1, vec(1, 0, 0)),
make_sphere(vec(2, 0, 6), 1, vec(0, 1, 0)),
make_sphere(vec(-2, 0, 4), 1, vec(0, 0, 1)),
make_sphere(vec(0, 1, 4.5), 0.5, vec(1, 1, 0)),
make_sphere(vec(1, -0.5, 3), 0.3, vec(1, 0, 1)),
make_sphere(vec(0, -101, 5), 100, vec(0.5, 0.5, 0.5))
]
var light = vnorm(vec(1, 1, -1))
return {spheres: spheres, light: light}
}
function trace(origin, dir, scene) {
var closest_t = 999999
var closest_sphere = null
var i = 0
var t = 0
for (i = 0; i < length(scene.spheres); i++) {
t = intersect_sphere(origin, dir, scene.spheres[i])
if (t > 0 && t < closest_t) {
closest_t = t
closest_sphere = scene.spheres[i]
}
}
if (!closest_sphere) return vec(0.2, 0.3, 0.5) // sky color
var hit = vadd(origin, vmul(dir, closest_t))
var normal = vnorm(vsub(hit, closest_sphere.center))
var diffuse = vdot(normal, scene.light)
if (diffuse < 0) diffuse = 0
// Shadow check
var shadow_origin = vadd(hit, vmul(normal, 0.001))
var in_shadow = false
for (i = 0; i < length(scene.spheres); i++) {
if (scene.spheres[i] != closest_sphere) {
t = intersect_sphere(shadow_origin, scene.light, scene.spheres[i])
if (t > 0) {
in_shadow = true
break
}
}
}
var ambient = 0.15
var intensity = in_shadow ? ambient : ambient + diffuse * 0.85
return vmul(closest_sphere.color, intensity)
}
function render(width, height, scene) {
var aspect = width / height
var fov = 1.0
var total_r = 0
var total_g = 0
var total_b = 0
var y = 0
var x = 0
var u = 0
var v = 0
var dir = null
var color = null
var origin = vec(0, 0, 0)
for (y = 0; y < height; y++) {
for (x = 0; x < width; x++) {
u = (2 * (x + 0.5) / width - 1) * aspect * fov
v = (1 - 2 * (y + 0.5) / height) * fov
dir = vnorm(vec(u, v, 1))
color = trace(origin, dir, scene)
total_r += color.x
total_g += color.y
total_b += color.z
}
}
return {r: total_r, g: total_g, b: total_b}
}
var scene = make_scene()
return {
raytrace_32x32: function(n) {
var i = 0
var result = null
for (i = 0; i < n; i++) {
result = render(32, 32, scene)
}
return result
},
raytrace_64x64: function(n) {
var i = 0
var result = null
for (i = 0; i < n; i++) {
result = render(64, 64, scene)
}
return result
}
}

251
benches/richards.cm Normal file
View File

@@ -0,0 +1,251 @@
// richards.cm — Richards benchmark (scheduler simulation)
// Object-ish workload: dynamic dispatch, state machines, queuing.
def IDLE = 0
def WORKER = 1
def HANDLER_A = 2
def HANDLER_B = 3
def DEVICE_A = 4
def DEVICE_B = 5
def NUM_TASKS = 6
def TASK_RUNNING = 0
def TASK_WAITING = 1
def TASK_HELD = 2
def TASK_SUSPENDED = 3
function make_packet(link, id, kind) {
return {link: link, id: id, kind: kind, datum: 0, data: array(4, 0)}
}
function scheduler() {
var tasks = array(NUM_TASKS, null)
var current = null
var queue_count = 0
var hold_count = 0
var v1 = 0
var v2 = 0
var w_id = HANDLER_A
var w_datum = 0
var h_a_queue = null
var h_a_count = 0
var h_b_queue = null
var h_b_count = 0
var dev_a_pkt = null
var dev_b_pkt = null
var find_next = function() {
var best = null
var i = 0
for (i = 0; i < NUM_TASKS; i++) {
if (tasks[i] && tasks[i].state == TASK_RUNNING) {
if (!best || tasks[i].priority > best.priority) {
best = tasks[i]
}
}
}
return best
}
var hold_self = function() {
hold_count++
if (current) current.state = TASK_HELD
return find_next()
}
var release = function(id) {
var t = tasks[id]
if (!t) return find_next()
if (t.state == TASK_HELD) t.state = TASK_RUNNING
if (t.priority > (current ? current.priority : -1)) return t
return current
}
var queue_packet = function(pkt) {
var t = tasks[pkt.id]
var p = null
if (!t) return find_next()
queue_count++
pkt.link = null
pkt.id = current ? current.id : 0
if (!t.queue) {
t.queue = pkt
t.state = TASK_RUNNING
if (t.priority > (current ? current.priority : -1)) return t
} else {
p = t.queue
while (p.link) p = p.link
p.link = pkt
}
return current
}
// Idle task
tasks[IDLE] = {id: IDLE, priority: 0, queue: null, state: TASK_RUNNING,
hold_count: 0, queue_count: 0,
fn: function(pkt) {
v1--
if (v1 == 0) return hold_self()
if ((v2 & 1) == 0) {
v2 = v2 >> 1
return release(DEVICE_A)
}
v2 = (v2 >> 1) ^ 0xD008
return release(DEVICE_B)
}
}
// Worker task
tasks[WORKER] = {id: WORKER, priority: 1000, queue: null, state: TASK_SUSPENDED,
hold_count: 0, queue_count: 0,
fn: function(pkt) {
var i = 0
if (!pkt) return hold_self()
w_id = (w_id == HANDLER_A) ? HANDLER_B : HANDLER_A
pkt.id = w_id
pkt.datum = 0
for (i = 0; i < 4; i++) {
w_datum++
if (w_datum > 26) w_datum = 1
pkt.data[i] = 65 + w_datum
}
return queue_packet(pkt)
}
}
// Handler A
tasks[HANDLER_A] = {id: HANDLER_A, priority: 2000, queue: null, state: TASK_SUSPENDED,
hold_count: 0, queue_count: 0,
fn: function(pkt) {
var p = null
if (pkt) { h_a_queue = pkt; h_a_count++ }
if (h_a_queue) {
p = h_a_queue
h_a_queue = p.link
if (h_a_count < 3) return queue_packet(p)
return release(DEVICE_A)
}
return hold_self()
}
}
// Handler B
tasks[HANDLER_B] = {id: HANDLER_B, priority: 3000, queue: null, state: TASK_SUSPENDED,
hold_count: 0, queue_count: 0,
fn: function(pkt) {
var p = null
if (pkt) { h_b_queue = pkt; h_b_count++ }
if (h_b_queue) {
p = h_b_queue
h_b_queue = p.link
if (h_b_count < 3) return queue_packet(p)
return release(DEVICE_B)
}
return hold_self()
}
}
// Device A
tasks[DEVICE_A] = {id: DEVICE_A, priority: 4000, queue: null, state: TASK_SUSPENDED,
hold_count: 0, queue_count: 0,
fn: function(pkt) {
var p = null
if (pkt) { dev_a_pkt = pkt; return hold_self() }
if (dev_a_pkt) {
p = dev_a_pkt
dev_a_pkt = null
return queue_packet(p)
}
return hold_self()
}
}
// Device B
tasks[DEVICE_B] = {id: DEVICE_B, priority: 5000, queue: null, state: TASK_SUSPENDED,
hold_count: 0, queue_count: 0,
fn: function(pkt) {
var p = null
if (pkt) { dev_b_pkt = pkt; return hold_self() }
if (dev_b_pkt) {
p = dev_b_pkt
dev_b_pkt = null
return queue_packet(p)
}
return hold_self()
}
}
var run = function(iterations) {
var i = 0
var pkt1 = null
var pkt2 = null
var steps = 0
var pkt = null
var next = null
v1 = iterations
v2 = 0xBEEF
queue_count = 0
hold_count = 0
w_id = HANDLER_A
w_datum = 0
h_a_queue = null
h_a_count = 0
h_b_queue = null
h_b_count = 0
dev_a_pkt = null
dev_b_pkt = null
for (i = 0; i < NUM_TASKS; i++) {
if (tasks[i]) {
tasks[i].state = (i == IDLE) ? TASK_RUNNING : TASK_SUSPENDED
tasks[i].queue = null
}
}
pkt1 = make_packet(null, WORKER, 1)
pkt2 = make_packet(pkt1, WORKER, 1)
tasks[WORKER].queue = pkt2
tasks[WORKER].state = TASK_RUNNING
current = find_next()
while (current && steps < iterations * 10) {
pkt = current.queue
if (pkt) {
current.queue = pkt.link
current.queue_count++
}
next = current.fn(pkt)
if (next) current = next
else current = find_next()
steps++
}
return {queue_count: queue_count, hold_count: hold_count, steps: steps}
}
return {run: run}
}
return {
richards_100: function(n) {
var i = 0
var s = null
var result = null
for (i = 0; i < n; i++) {
s = scheduler()
result = s.run(100)
}
return result
},
richards_1k: function(n) {
var i = 0
var s = null
var result = null
for (i = 0; i < n; i++) {
s = scheduler()
result = s.run(1000)
}
return result
}
}

180
benches/sorting.cm Normal file
View File

@@ -0,0 +1,180 @@
// sorting.cm — Sorting and searching kernel
// Array manipulation, comparison-heavy, allocation patterns.
function make_random_array(n, seed) {
var a = []
var x = seed
var i = 0
for (i = 0; i < n; i++) {
x = ((x * 1103515245 + 12345) & 0x7FFFFFFF) | 0
push(a, x % 10000)
}
return a
}
function make_descending(n) {
var a = []
var i = 0
for (i = n - 1; i >= 0; i--) push(a, i)
return a
}
// Manual quicksort (tests recursion + array mutation)
function qsort(arr, lo, hi) {
var i = lo
var j = hi
var pivot = arr[floor((lo + hi) / 2)]
var tmp = 0
if (lo >= hi) return null
while (i <= j) {
while (arr[i] < pivot) i++
while (arr[j] > pivot) j--
if (i <= j) {
tmp = arr[i]
arr[i] = arr[j]
arr[j] = tmp
i++
j--
}
}
if (lo < j) qsort(arr, lo, j)
if (i < hi) qsort(arr, i, hi)
return null
}
// Merge sort (tests allocation + array creation)
function msort(arr) {
var n = length(arr)
if (n <= 1) return arr
var mid = floor(n / 2)
var left = msort(array(arr, 0, mid))
var right = msort(array(arr, mid, n))
return merge(left, right)
}
function merge(a, b) {
var result = []
var i = 0
var j = 0
while (i < length(a) && j < length(b)) {
if (a[i] <= b[j]) {
push(result, a[i])
i++
} else {
push(result, b[j])
j++
}
}
while (i < length(a)) {
push(result, a[i])
i++
}
while (j < length(b)) {
push(result, b[j])
j++
}
return result
}
// Binary search
function bsearch(arr, target) {
var lo = 0
var hi = length(arr) - 1
var mid = 0
while (lo <= hi) {
mid = floor((lo + hi) / 2)
if (arr[mid] == target) return mid
if (arr[mid] < target) lo = mid + 1
else hi = mid - 1
}
return -1
}
// Sort objects by field
function sort_records(n) {
var records = []
var x = 42
var i = 0
for (i = 0; i < n; i++) {
x = ((x * 1103515245 + 12345) & 0x7FFFFFFF) | 0
push(records, {id: i, score: x % 10000, name: `item_${i}`})
}
return sort(records, "score")
}
return {
// Quicksort 1K random integers
qsort_1k: function(n) {
var i = 0
var a = null
for (i = 0; i < n; i++) {
a = make_random_array(1000, i)
qsort(a, 0, length(a) - 1)
}
return a
},
// Quicksort 10K random integers
qsort_10k: function(n) {
var i = 0
var a = null
for (i = 0; i < n; i++) {
a = make_random_array(10000, i)
qsort(a, 0, length(a) - 1)
}
return a
},
// Merge sort 1K (allocation heavy)
msort_1k: function(n) {
var i = 0
var result = null
for (i = 0; i < n; i++) {
result = msort(make_random_array(1000, i))
}
return result
},
// Built-in sort 1K
builtin_sort_1k: function(n) {
var i = 0
var result = null
for (i = 0; i < n; i++) {
result = sort(make_random_array(1000, i))
}
return result
},
// Sort worst case (descending → ascending)
sort_worst_case: function(n) {
var i = 0
var a = null
for (i = 0; i < n; i++) {
a = make_descending(1000)
qsort(a, 0, length(a) - 1)
}
return a
},
// Binary search in sorted array
bsearch_1k: function(n) {
var sorted = make_random_array(1000, 42)
sorted = sort(sorted)
var found = 0
var i = 0
for (i = 0; i < n; i++) {
if (bsearch(sorted, sorted[i % 1000]) >= 0) found++
}
return found
},
// Sort records by field
sort_records_500: function(n) {
var i = 0
var result = null
for (i = 0; i < n; i++) {
result = sort_records(500)
}
return result
}
}

82
benches/spectral_norm.cm Normal file
View File

@@ -0,0 +1,82 @@
// spectral_norm.cm — Spectral norm kernel
// Pure numeric, dense array access, mathematical computation.
var math = use('math/radians')
function eval_a(i, j) {
return 1.0 / ((i + j) * (i + j + 1) / 2 + i + 1)
}
function eval_a_times_u(n, u, au) {
var i = 0
var j = 0
var sum = 0
for (i = 0; i < n; i++) {
sum = 0
for (j = 0; j < n; j++) {
sum += eval_a(i, j) * u[j]
}
au[i] = sum
}
}
function eval_at_times_u(n, u, atu) {
var i = 0
var j = 0
var sum = 0
for (i = 0; i < n; i++) {
sum = 0
for (j = 0; j < n; j++) {
sum += eval_a(j, i) * u[j]
}
atu[i] = sum
}
}
function eval_ata_times_u(n, u, atau) {
var v = array(n, 0)
eval_a_times_u(n, u, v)
eval_at_times_u(n, v, atau)
}
function spectral_norm(n) {
var u = array(n, 1)
var v = array(n, 0)
var i = 0
var vbv = 0
var vv = 0
for (i = 0; i < 10; i++) {
eval_ata_times_u(n, u, v)
eval_ata_times_u(n, v, u)
}
vbv = 0
vv = 0
for (i = 0; i < n; i++) {
vbv += u[i] * v[i]
vv += v[i] * v[i]
}
return math.sqrt(vbv / vv)
}
return {
spectral_100: function(n) {
var i = 0
var result = 0
for (i = 0; i < n; i++) {
result = spectral_norm(100)
}
return result
},
spectral_200: function(n) {
var i = 0
var result = 0
for (i = 0; i < n; i++) {
result = spectral_norm(200)
}
return result
}
}

View File

@@ -0,0 +1,188 @@
// string_processing.cm — String-heavy kernel
// Concat, split, search, replace, interning path stress.
function make_lorem(paragraphs) {
var base = "Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat"
var result = ""
var i = 0
for (i = 0; i < paragraphs; i++) {
if (i > 0) result = result + " "
result = result + base
}
return result
}
// Build a lookup table from text
function build_index(txt) {
var words = array(txt, " ")
var index = {}
var i = 0
var w = null
for (i = 0; i < length(words); i++) {
w = words[i]
if (!index[w]) {
index[w] = []
}
push(index[w], i)
}
return index
}
// Levenshtein-like distance (simplified)
function edit_distance(a, b) {
var la = length(a)
var lb = length(b)
if (la == 0) return lb
if (lb == 0) return la
// Use flat array for 2 rows of DP matrix
var prev = array(lb + 1, 0)
var curr = array(lb + 1, 0)
var i = 0
var j = 0
var cost = 0
var del = 0
var ins = 0
var sub = 0
var tmp = null
var ca = array(a)
var cb = array(b)
for (j = 0; j <= lb; j++) prev[j] = j
for (i = 1; i <= la; i++) {
curr[0] = i
for (j = 1; j <= lb; j++) {
cost = ca[i - 1] == cb[j - 1] ? 0 : 1
del = prev[j] + 1
ins = curr[j - 1] + 1
sub = prev[j - 1] + cost
curr[j] = del
if (ins < curr[j]) curr[j] = ins
if (sub < curr[j]) curr[j] = sub
}
tmp = prev
prev = curr
curr = tmp
}
return prev[lb]
}
var lorem_5 = make_lorem(5)
var lorem_20 = make_lorem(20)
return {
// Split text into words and count
string_split_count: function(n) {
var i = 0
var words = null
var count = 0
for (i = 0; i < n; i++) {
words = array(lorem_5, " ")
count += length(words)
}
return count
},
// Build word index (split + hash + array ops)
string_index_build: function(n) {
var i = 0
var idx = null
for (i = 0; i < n; i++) {
idx = build_index(lorem_5)
}
return idx
},
// Search for substrings
string_search: function(n) {
var targets = ["dolor", "minim", "quis", "magna", "ipsum"]
var i = 0
var j = 0
var count = 0
for (i = 0; i < n; i++) {
for (j = 0; j < length(targets); j++) {
if (search(lorem_20, targets[j])) count++
}
}
return count
},
// Replace operations
string_replace: function(n) {
var i = 0
var result = null
for (i = 0; i < n; i++) {
result = replace(lorem_5, "dolor", "DOLOR")
result = replace(result, "ipsum", "IPSUM")
result = replace(result, "amet", "AMET")
}
return result
},
// String concatenation builder
string_builder: function(n) {
var i = 0
var j = 0
var s = null
var total = 0
for (i = 0; i < n; i++) {
s = ""
for (j = 0; j < 50; j++) {
s = s + "key=" + text(j) + "&value=" + text(j * 17) + "&"
}
total += length(s)
}
return total
},
// Edit distance (DP + array + string ops)
edit_distance: function(n) {
var words = ["kitten", "sitting", "saturday", "sunday", "intention", "execution"]
var i = 0
var j = 0
var total = 0
for (i = 0; i < n; i++) {
for (j = 0; j < length(words) - 1; j++) {
total += edit_distance(words[j], words[j + 1])
}
}
return total
},
// Upper/lower/trim chain
string_transforms: function(n) {
var src = " Hello World "
var i = 0
var x = 0
var result = null
for (i = 0; i < n; i++) {
result = trim(src)
result = upper(result)
result = lower(result)
x += length(result)
}
return x
},
// Starts_with / ends_with (interning path)
string_prefix_suffix: function(n) {
var strs = [
"application/json",
"text/html",
"image/png",
"application/xml",
"text/plain"
]
var i = 0
var j = 0
var count = 0
for (i = 0; i < n; i++) {
for (j = 0; j < length(strs); j++) {
if (starts_with(strs[j], "application/")) count++
if (ends_with(strs[j], "/json")) count++
if (starts_with(strs[j], "text/")) count++
}
}
return count
}
}

137
benches/tree_ops.cm Normal file
View File

@@ -0,0 +1,137 @@
// tree_ops.cm — Tree data structure operations kernel
// Pointer chasing, recursion, allocation patterns.
// Binary tree: create, walk, transform, check
function make_tree(depth) {
if (depth <= 0) return {val: 1, left: null, right: null}
return {
val: depth,
left: make_tree(depth - 1),
right: make_tree(depth - 1)
}
}
function tree_check(node) {
if (!node) return 0
if (!node.left) return node.val
return node.val + tree_check(node.left) - tree_check(node.right)
}
function tree_sum(node) {
if (!node) return 0
return node.val + tree_sum(node.left) + tree_sum(node.right)
}
function tree_depth(node) {
if (!node) return 0
var l = tree_depth(node.left)
var r = tree_depth(node.right)
return 1 + (l > r ? l : r)
}
function tree_count(node) {
if (!node) return 0
return 1 + tree_count(node.left) + tree_count(node.right)
}
// Transform tree: map values
function tree_map(node, fn) {
if (!node) return null
return {
val: fn(node.val),
left: tree_map(node.left, fn),
right: tree_map(node.right, fn)
}
}
// Flatten tree to array (in-order)
function tree_flatten(node, result) {
if (!node) return null
tree_flatten(node.left, result)
push(result, node.val)
tree_flatten(node.right, result)
return null
}
// Build sorted tree from array (balanced)
function build_balanced(arr, lo, hi) {
if (lo > hi) return null
var mid = floor((lo + hi) / 2)
return {
val: arr[mid],
left: build_balanced(arr, lo, mid - 1),
right: build_balanced(arr, mid + 1, hi)
}
}
// Find a value in BST
function bst_find(node, val) {
if (!node) return false
if (val == node.val) return true
if (val < node.val) return bst_find(node.left, val)
return bst_find(node.right, val)
}
return {
// Binary tree create + check (allocation heavy)
tree_create_check: function(n) {
var i = 0
var t = null
var x = 0
for (i = 0; i < n; i++) {
t = make_tree(10)
x += tree_check(t)
}
return x
},
// Deep tree traversals
tree_traversal: function(n) {
var t = make_tree(12)
var x = 0
var i = 0
for (i = 0; i < n; i++) {
x += tree_sum(t) + tree_depth(t) + tree_count(t)
}
return x
},
// Tree map (create new tree from old)
tree_transform: function(n) {
var t = make_tree(10)
var i = 0
var mapped = null
for (i = 0; i < n; i++) {
mapped = tree_map(t, function(v) { return v * 2 + 1 })
}
return mapped
},
// Flatten + rebuild (array <-> tree conversion)
tree_flatten_rebuild: function(n) {
var t = make_tree(10)
var i = 0
var flat = null
var rebuilt = null
for (i = 0; i < n; i++) {
flat = []
tree_flatten(t, flat)
rebuilt = build_balanced(flat, 0, length(flat) - 1)
}
return rebuilt
},
// BST search (pointer chasing)
bst_search: function(n) {
// Build a balanced BST of 1024 elements
var data = []
var i = 0
for (i = 0; i < 1024; i++) push(data, i)
var bst = build_balanced(data, 0, 1023)
var found = 0
for (i = 0; i < n; i++) {
if (bst_find(bst, i % 1024)) found++
}
return found
}
}

6200
boot/bootstrap.cm.mcode Normal file

File diff suppressed because it is too large Load Diff

21056
boot/fold.cm.mcode Normal file

File diff suppressed because it is too large Load Diff

24810
boot/mcode.cm.mcode Normal file

File diff suppressed because it is too large Load Diff

29874
boot/parse.cm.mcode Normal file

File diff suppressed because it is too large Load Diff

10436
boot/tokenize.cm.mcode Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +0,0 @@
// bootstrap.ce — regenerate .mach bytecode files consumed by the mach engine
// usage: cell bootstrap.ce
var fd = use("fd")
var json = use("json")
var tokenize = use("tokenize")
var parse = use("parse")
var fold = use("fold")
var files = [
{src: "tokenize.cm", name: "tokenize", out: "tokenize.mach"},
{src: "parse.cm", name: "parse", out: "parse.mach"},
{src: "fold.cm", name: "fold", out: "fold.mach"},
{src: "mcode.cm", name: "mcode", out: "mcode.mach"},
{src: "internal/bootstrap.cm", name: "bootstrap", out: "internal/bootstrap.mach"},
{src: "internal/engine.cm", name: "engine", out: "internal/engine.mach"}
]
var i = 0
var entry = null
var src = null
var tok_result = null
var ast = null
var folded = null
var ast_json = null
var bytecode = null
var f = null
while (i < length(files)) {
entry = files[i]
src = text(fd.slurp(entry.src))
tok_result = tokenize(src, entry.src)
ast = parse(tok_result.tokens, src, entry.src, tokenize)
folded = fold(ast)
ast_json = json.encode(folded)
bytecode = mach_compile_ast(entry.name, ast_json)
f = fd.open(entry.out, "w")
fd.write(f, bytecode)
fd.close(f)
print(`wrote ${entry.out}`)
i = i + 1
}

View File

@@ -17,8 +17,16 @@ var target_package = null
var buildtype = 'release'
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 (var i = 0; i < length(args); i++) {
for (i = 0; i < length(args); i++) {
if (args[i] == '-t' || args[i] == '--target') {
if (i + 1 < length(args)) {
target = args[++i]
@@ -51,8 +59,8 @@ for (var i = 0; i < length(args); i++) {
dry_run = true
} else if (args[i] == '--list-targets') {
log.console('Available targets:')
var targets = build.list_targets()
for (var t = 0; t < length(targets); t++) {
targets = build.list_targets()
for (t = 0; t < length(targets); t++) {
log.console(' ' + targets[t])
}
$stop()
@@ -65,7 +73,7 @@ for (var i = 0; i < length(args); i++) {
// Resolve local paths to absolute paths
if (target_package) {
if (target_package == '.' || starts_with(target_package, './') || starts_with(target_package, '../') || fd.is_dir(target_package)) {
var resolved = fd.realpath(target_package)
resolved = fd.realpath(target_package)
if (resolved) {
target_package = resolved
}
@@ -91,33 +99,35 @@ arrfor(packages, function(package) {
shop.extract(package)
})
var _build = null
if (target_package) {
// Build single package
log.console('Building ' + target_package + '...')
try {
var lib = build.build_dynamic(target_package, target, buildtype)
_build = function() {
lib = build.build_dynamic(target_package, target, buildtype)
if (lib) {
log.console('Built: ' + lib)
}
} catch (e) {
log.error('Build failed: ' + e)
} disruption {
log.error('Build failed')
$stop()
}
_build()
} else {
// Build all packages
log.console('Building all packages...')
var results = build.build_all_dynamic(target, buildtype)
var success = 0
var failed = 0
for (var i = 0; i < length(results); i++) {
results = build.build_all_dynamic(target, buildtype)
success = 0
failed = 0
for (i = 0; i < length(results); i++) {
if (results[i].library) {
success++
} else if (results[i].error) {
failed++
}
}
log.console(`Build complete: ${success} libraries built${failed > 0 ? `, ${failed} failed` : ''}`)
}

480
build.cm
View File

@@ -85,7 +85,8 @@ function ensure_dir(path) {
if (fd.stat(path).isDirectory) return
var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
for (var i = 0; i < length(parts); i++) {
var i = 0
for (i = 0; i < length(parts); i++) {
if (parts[i] == '') continue
current += parts[i] + '/'
if (!fd.stat(current).isDirectory) fd.mkdir(current)
@@ -100,12 +101,13 @@ Build.ensure_dir = ensure_dir
// Compile a single C file for a package
// Returns the object file path (content-addressed in .cell/build)
Build.compile_file = function(pkg, file, target, buildtype = 'release') {
Build.compile_file = function(pkg, file, target, buildtype) {
var _buildtype = buildtype || 'release'
var pkg_dir = shop.get_package_dir(pkg)
var src_path = pkg_dir + '/' + file
if (!fd.is_file(src_path)) {
throw Error('Source file not found: ' + src_path)
print('Source file not found: ' + src_path); disrupt
}
// Get flags (with sigil replacement)
@@ -120,11 +122,11 @@ Build.compile_file = function(pkg, file, target, buildtype = 'release') {
var cmd_parts = [cc, '-c', '-fPIC']
// Add buildtype-specific flags
if (buildtype == 'release') {
if (_buildtype == 'release') {
cmd_parts = array(cmd_parts, ['-O3', '-DNDEBUG'])
} else if (buildtype == 'debug') {
} else if (_buildtype == 'debug') {
cmd_parts = array(cmd_parts, ['-O2', '-g'])
} else if (buildtype == 'minsize') {
} else if (_buildtype == 'minsize') {
cmd_parts = array(cmd_parts, ['-Os', '-DNDEBUG'])
}
@@ -133,10 +135,11 @@ Build.compile_file = function(pkg, file, target, buildtype = 'release') {
// Add package CFLAGS (resolve relative -I paths)
arrfor(cflags, function(flag) {
if (starts_with(flag, '-I') && !starts_with(flag, '-I/')) {
flag = '-I"' + pkg_dir + '/' + text(flag, 2) + '"'
var f = flag
if (starts_with(f, '-I') && !starts_with(f, '-I/')) {
f = '-I"' + pkg_dir + '/' + text(f, 2) + '"'
}
push(cmd_parts, flag)
push(cmd_parts, f)
})
// Add target CFLAGS
@@ -167,7 +170,7 @@ Build.compile_file = function(pkg, file, target, buildtype = 'release') {
log.console('Compiling ' + file)
var ret = os.system(full_cmd)
if (ret != 0) {
throw Error('Compilation failed: ' + file)
print('Compilation failed: ' + file); disrupt
}
return obj_path
@@ -175,12 +178,14 @@ Build.compile_file = function(pkg, file, target, buildtype = 'release') {
// Build all C files for a package
// Returns array of object file paths
Build.build_package = function(pkg, target = Build.detect_host_target(), exclude_main, buildtype = 'release') {
var c_files = pkg_tools.get_c_files(pkg, target, exclude_main)
Build.build_package = function(pkg, target, exclude_main, buildtype) {
var _target = target || Build.detect_host_target()
var _buildtype = buildtype || 'release'
var c_files = pkg_tools.get_c_files(pkg, _target, exclude_main)
var objects = []
arrfor(c_files, function(file) {
var obj = Build.compile_file(pkg, file, target, buildtype)
var obj = Build.compile_file(pkg, file, _target, _buildtype)
push(objects, obj)
})
@@ -192,14 +197,14 @@ Build.build_package = function(pkg, target = Build.detect_host_target(), exclude
// ============================================================================
// Compute link key from all inputs that affect the dylib output
function compute_link_key(objects, ldflags, target_ldflags, target, cc) {
function compute_link_key(objects, ldflags, target_ldflags, opts) {
// Sort objects for deterministic hash
var sorted_objects = sort(objects)
// Build a string representing all link inputs
var parts = []
push(parts, 'target:' + target)
push(parts, 'cc:' + cc)
push(parts, 'target:' + opts.target)
push(parts, 'cc:' + opts.cc)
arrfor(sorted_objects, function(obj) {
// Object paths are content-addressed, so the path itself is the hash
push(parts, 'obj:' + obj)
@@ -214,74 +219,46 @@ function compute_link_key(objects, ldflags, target_ldflags, target, cc) {
return content_hash(text(parts, '\n'))
}
// Build a dynamic library for a package
// Output goes to .cell/lib/<package_name>.<ext>
// Dynamic libraries do NOT link against core; undefined symbols are resolved at dlopen time
// Uses content-addressed store + symlink for caching
Build.build_dynamic = function(pkg, target = Build.detect_host_target(), buildtype = 'release') {
var objects = Build.build_package(pkg, target, true, buildtype) // exclude main.c
// Build a per-module dynamic library for a single C file
// Returns the content-addressed dylib path in .cell/build/<hash>.<target>.dylib
Build.build_module_dylib = function(pkg, file, target, buildtype) {
var _target = target || Build.detect_host_target()
var _buildtype = buildtype || 'release'
var obj = Build.compile_file(pkg, file, _target, _buildtype)
if (length(objects) == 0) {
log.console('No C files in ' + pkg)
return null
}
var lib_dir = shop.get_lib_dir()
var store_dir = lib_dir + '/store'
ensure_dir(lib_dir)
ensure_dir(store_dir)
var lib_name = shop.lib_name_for_package(pkg)
var dylib_ext = toolchains[target].system == 'windows' ? '.dll' : (toolchains[target].system == 'darwin' ? '.dylib' : '.so')
var stable_path = lib_dir + '/' + lib_name + dylib_ext
// Get link flags (with sigil replacement)
var ldflags = replace_sigils_array(pkg_tools.get_flags(pkg, 'LDFLAGS', target))
var target_ldflags = toolchains[target].c_link_args || []
var cc = toolchains[target].cpp || toolchains[target].c
var pkg_dir = shop.get_package_dir(pkg)
var tc = toolchains[_target]
var dylib_ext = tc.system == 'windows' ? '.dll' : (tc.system == 'darwin' ? '.dylib' : '.so')
var cc = tc.cpp || tc.c
var local_dir = get_local_dir()
var tc = toolchains[target]
var pkg_dir = shop.get_package_dir(pkg)
// Resolve relative -L paths in ldflags for hash computation
// Get link flags
var ldflags = replace_sigils_array(pkg_tools.get_flags(pkg, 'LDFLAGS', _target))
var target_ldflags = tc.c_link_args || []
var resolved_ldflags = []
arrfor(ldflags, function(flag) {
if (starts_with(flag, '-L') && !starts_with(flag, '-L/')) {
flag = '-L"' + pkg_dir + '/' + text(flag, 2) + '"'
var f = flag
if (starts_with(f, '-L') && !starts_with(f, '-L/')) {
f = '-L"' + pkg_dir + '/' + text(f, 2) + '"'
}
push(resolved_ldflags, flag)
push(resolved_ldflags, f)
})
// Compute link key
var link_key = compute_link_key(objects, resolved_ldflags, target_ldflags, target, cc)
var store_path = store_dir + '/' + lib_name + '-' + link_key + dylib_ext
// Content-addressed output: hash of (object + link flags + target)
var link_key = compute_link_key([obj], resolved_ldflags, target_ldflags, {target: _target, cc: cc})
var build_dir = get_build_dir()
ensure_dir(build_dir)
var dylib_path = build_dir + '/' + link_key + '.' + _target + dylib_ext
// Check if already linked in store
if (fd.is_file(store_path)) {
// Ensure symlink points to the store file
if (fd.is_link(stable_path)) {
var current_target = fd.readlink(stable_path)
if (current_target == store_path) {
// Already up to date
return stable_path
}
fd.unlink(stable_path)
} else if (fd.is_file(stable_path)) {
fd.unlink(stable_path)
}
fd.symlink(store_path, stable_path)
return stable_path
}
if (fd.is_file(dylib_path))
return dylib_path
// Build link command
var cmd_parts = [cc, '-shared', '-fPIC']
// Platform-specific flags for undefined symbols (resolved at dlopen) and size optimization
if (tc.system == 'darwin') {
cmd_parts = array(cmd_parts, [
'-undefined', 'dynamic_lookup',
'-Wl,-dead_strip',
'-Wl,-install_name,' + stable_path,
'-Wl,-rpath,@loader_path/../local',
'-Wl,-rpath,' + local_dir
])
@@ -293,41 +270,53 @@ Build.build_dynamic = function(pkg, target = Build.detect_host_target(), buildty
'-Wl,-rpath,' + local_dir
])
} else if (tc.system == 'windows') {
// Windows DLLs: use --allow-shlib-undefined for mingw
push(cmd_parts, '-Wl,--allow-shlib-undefined')
}
// Add .cell/local to library search path
push(cmd_parts, '-L"' + local_dir + '"')
arrfor(objects, function(obj) {
push(cmd_parts, '"' + obj + '"')
})
// Do NOT link against core library - symbols resolved at dlopen time
push(cmd_parts, '"' + obj + '"')
cmd_parts = array(cmd_parts, resolved_ldflags)
cmd_parts = array(cmd_parts, target_ldflags)
push(cmd_parts, '-o')
push(cmd_parts, '"' + store_path + '"')
push(cmd_parts, '"' + dylib_path + '"')
var cmd_str = text(cmd_parts, ' ')
log.console('Linking ' + lib_name + dylib_ext)
log.console('Linking module ' + file + ' -> ' + fd.basename(dylib_path))
var ret = os.system(cmd_str)
if (ret != 0) {
throw Error('Linking failed: ' + pkg)
print('Linking failed: ' + file); disrupt
}
// Update symlink to point to the new store file
if (fd.is_link(stable_path)) {
fd.unlink(stable_path)
} else if (fd.is_file(stable_path)) {
fd.unlink(stable_path)
// Install to deterministic lib/<pkg>/<stem>.dylib
var file_stem = file
var install_dir = shop.get_lib_dir() + '/' + shop.lib_name_for_package(pkg)
var stem_dir = fd.dirname(file_stem)
if (stem_dir && stem_dir != '.') {
install_dir = install_dir + '/' + stem_dir
}
fd.symlink(store_path, stable_path)
ensure_dir(install_dir)
var install_path = shop.get_lib_dir() + '/' + shop.lib_name_for_package(pkg) + '/' + file_stem + dylib_ext
fd.slurpwrite(install_path, fd.slurp(dylib_path))
return stable_path
return dylib_path
}
// Build a dynamic library for a package (one dylib per C file)
// Returns array of {file, symbol, dylib} for each module
// Also writes a manifest mapping symbols to dylib paths
Build.build_dynamic = function(pkg, target, buildtype) {
var _target = target || Build.detect_host_target()
var _buildtype = buildtype || 'release'
var c_files = pkg_tools.get_c_files(pkg, _target, true)
var results = []
arrfor(c_files, function(file) {
var sym_name = shop.c_symbol_for_file(pkg, file)
var dylib = Build.build_module_dylib(pkg, file, _target, _buildtype)
push(results, {file: file, symbol: sym_name, dylib: dylib})
})
return results
}
// ============================================================================
@@ -337,7 +326,9 @@ Build.build_dynamic = function(pkg, target = Build.detect_host_target(), buildty
// Build a static binary from multiple packages
// packages: array of package names
// output: output binary path
Build.build_static = function(packages, target = Build.detect_host_target(), output, buildtype = 'release') {
Build.build_static = function(packages, target, output, buildtype) {
var _target = target || Build.detect_host_target()
var _buildtype = buildtype || 'release'
var all_objects = []
var all_ldflags = []
var seen_flags = {}
@@ -347,14 +338,14 @@ Build.build_static = function(packages, target = Build.detect_host_target(), out
var is_core = (pkg == 'core')
// For core, include main.c; for others, exclude it
var objects = Build.build_package(pkg, target, !is_core, buildtype)
var objects = Build.build_package(pkg, _target, !is_core, _buildtype)
arrfor(objects, function(obj) {
push(all_objects, obj)
})
// Collect LDFLAGS (with sigil replacement)
var ldflags = replace_sigils_array(pkg_tools.get_flags(pkg, 'LDFLAGS', target))
var ldflags = replace_sigils_array(pkg_tools.get_flags(pkg, 'LDFLAGS', _target))
var pkg_dir = shop.get_package_dir(pkg)
// Deduplicate based on the entire LDFLAGS string for this package
@@ -362,28 +353,29 @@ Build.build_static = function(packages, target = Build.detect_host_target(), out
if (!seen_flags[ldflags_key]) {
seen_flags[ldflags_key] = true
arrfor(ldflags, function(flag) {
// Resolve relative -L paths
if (starts_with(flag, '-L') && !starts_with(flag, '-L/')) {
flag = '-L"' + pkg_dir + '/' + text(flag, 2) + '"'
var f = flag
if (starts_with(f, '-L') && !starts_with(f, '-L/')) {
f = '-L"' + pkg_dir + '/' + text(f, 2) + '"'
}
push(all_ldflags, flag)
push(all_ldflags, f)
})
}
})
if (length(all_objects) == 0) {
throw Error('No object files to link')
print('No object files to link'); disrupt
}
// Link
var cc = toolchains[target].c
var target_ldflags = toolchains[target].c_link_args || []
var exe_ext = toolchains[target].system == 'windows' ? '.exe' : ''
var cc = toolchains[_target].c
var target_ldflags = toolchains[_target].c_link_args || []
var exe_ext = toolchains[_target].system == 'windows' ? '.exe' : ''
if (!ends_with(output, exe_ext) && exe_ext) {
output = output + exe_ext
var out_path = output
if (!ends_with(out_path, exe_ext) && exe_ext) {
out_path = out_path + exe_ext
}
var cmd_parts = [cc]
arrfor(all_objects, function(obj) {
@@ -398,17 +390,240 @@ Build.build_static = function(packages, target = Build.detect_host_target(), out
push(cmd_parts, flag)
})
push(cmd_parts, '-o', '"' + output + '"')
push(cmd_parts, '-o', '"' + out_path + '"')
var cmd_str = text(cmd_parts, ' ')
log.console('Linking ' + output)
log.console('Linking ' + out_path)
var ret = os.system(cmd_str)
if (ret != 0) {
throw Error('Linking failed with command: ' + cmd_str)
print('Linking failed: ' + cmd_str); disrupt
}
log.console('Built ' + output)
log.console('Built ' + out_path)
return out_path
}
// ============================================================================
// Native .cm compilation (source → mcode → QBE IL → .o → .dylib)
// ============================================================================
// Post-process QBE IL: insert dead labels after ret/jmp (QBE requirement)
function qbe_insert_dead_labels(il_text) {
var lines = array(il_text, "\n")
var result = []
var dead_id = 0
var need_label = false
var i = 0
var line = null
var trimmed = null
while (i < length(lines)) {
line = lines[i]
trimmed = trim(line)
if (need_label && !starts_with(trimmed, '@') && !starts_with(trimmed, '}') && length(trimmed) > 0) {
push(result, "@_dead_" + text(dead_id))
dead_id = dead_id + 1
need_label = false
}
if (starts_with(trimmed, '@') || starts_with(trimmed, '}') || length(trimmed) == 0) {
need_label = false
}
if (starts_with(trimmed, 'ret ') || starts_with(trimmed, 'jmp ')) {
need_label = true
}
push(result, line)
i = i + 1
}
return text(result, "\n")
}
// Compile a .cm source file to a native .dylib via QBE
// Returns the content-addressed dylib path
Build.compile_native = function(src_path, target, buildtype, pkg) {
var _target = target || Build.detect_host_target()
var _buildtype = buildtype || 'release'
var qbe_rt_path = null
var native_stem = null
var native_install_dir = null
var native_install_path = null
if (!fd.is_file(src_path)) {
print('Source file not found: ' + src_path); disrupt
}
var tc = toolchains[_target]
var dylib_ext = tc.system == 'windows' ? '.dll' : (tc.system == 'darwin' ? '.dylib' : '.so')
var cc = tc.c
// Step 1: Read source and compile through pipeline
var content = fd.slurp(src_path)
var src = text(content)
var tokenize = use('tokenize')
var parse = use('parse')
var fold = use('fold')
var mcode_mod = use('mcode')
var streamline_mod = use('streamline')
var qbe_macros = use('qbe')
var qbe_emit = use('qbe_emit')
var tok_result = tokenize(src, src_path)
var ast = parse(tok_result.tokens, src, src_path, tokenize)
var folded = fold(ast)
var compiled = mcode_mod(folded)
var optimized = streamline_mod(compiled)
// Step 2: Generate QBE IL
var sym_name = null
if (pkg) {
sym_name = shop.c_symbol_for_file(pkg, fd.basename(src_path))
}
var il = qbe_emit(optimized, qbe_macros, sym_name)
// Step 3: Post-process (insert dead labels)
il = qbe_insert_dead_labels(il)
// Content hash for cache key
var hash = content_hash(src + '\n' + _target + '\nnative')
var build_dir = get_build_dir()
ensure_dir(build_dir)
var dylib_path = build_dir + '/' + hash + '.' + _target + dylib_ext
if (fd.is_file(dylib_path))
return dylib_path
// Step 4: Write QBE IL to temp file
var tmp = '/tmp/cell_native_' + hash
var ssa_path = tmp + '.ssa'
var s_path = tmp + '.s'
var o_path = tmp + '.o'
var rt_o_path = '/tmp/cell_qbe_rt.o'
fd.slurpwrite(ssa_path, stone(blob(il)))
// Step 5: QBE compile to assembly
var rc = os.system('qbe -o ' + s_path + ' ' + ssa_path)
if (rc != 0) {
print('QBE compilation failed for: ' + src_path); disrupt
}
// Step 6: Assemble
rc = os.system(cc + ' -c ' + s_path + ' -o ' + o_path)
if (rc != 0) {
print('Assembly failed for: ' + src_path); disrupt
}
// Step 7: Compile QBE runtime stubs if needed
if (!fd.is_file(rt_o_path)) {
qbe_rt_path = shop.get_package_dir('core') + '/qbe_rt.c'
rc = os.system(cc + ' -c ' + qbe_rt_path + ' -o ' + rt_o_path + ' -fPIC')
if (rc != 0) {
print('QBE runtime stubs compilation failed'); disrupt
}
}
// Step 8: Link dylib
var link_cmd = cc + ' -shared -fPIC'
if (tc.system == 'darwin') {
link_cmd = link_cmd + ' -undefined dynamic_lookup'
} else if (tc.system == 'linux') {
link_cmd = link_cmd + ' -Wl,--allow-shlib-undefined'
}
link_cmd = link_cmd + ' ' + o_path + ' ' + rt_o_path + ' -o ' + dylib_path
rc = os.system(link_cmd)
if (rc != 0) {
print('Linking native dylib failed for: ' + src_path); disrupt
}
log.console('Built native: ' + fd.basename(dylib_path))
// Install to deterministic lib/<pkg>/<stem>.dylib
if (pkg) {
native_stem = fd.basename(src_path)
native_install_dir = shop.get_lib_dir() + '/' + shop.lib_name_for_package(pkg)
ensure_dir(native_install_dir)
native_install_path = native_install_dir + '/' + native_stem + dylib_ext
fd.slurpwrite(native_install_path, fd.slurp(dylib_path))
}
return dylib_path
}
// ============================================================================
// Module table generation (for static builds)
// ============================================================================
// Compile a .cm module to mach bytecode blob
// Returns the raw mach bytes as a blob
Build.compile_cm_to_mach = function(src_path) {
if (!fd.is_file(src_path)) {
print('Source file not found: ' + src_path); disrupt
}
var src = text(fd.slurp(src_path))
var tokenize = use('tokenize')
var parse = use('parse')
var fold = use('fold')
var mcode_mod = use('mcode')
var streamline_mod = use('streamline')
var json = use('json')
var tok_result = tokenize(src, src_path)
var ast = parse(tok_result.tokens, src, src_path, tokenize)
var folded = fold(ast)
var compiled = mcode_mod(folded)
var optimized = streamline_mod(compiled)
return mach_compile_mcode_bin(src_path, json.encode(optimized))
}
// Generate a module_table.c file that embeds mach bytecode for .cm modules
// modules: array of {name, src_path} — name is the module name, src_path is the .cm file
// output: path to write the generated .c file
Build.generate_module_table = function(modules, output) {
var lines = []
var json = use('json')
push(lines, '/* Generated module table — do not edit */')
push(lines, '#include <stddef.h>')
push(lines, '#include <string.h>')
push(lines, '')
push(lines, 'struct cell_embedded_entry {')
push(lines, ' const char *name;')
push(lines, ' const unsigned char *data;')
push(lines, ' size_t size;')
push(lines, '};')
push(lines, '')
var entries = []
arrfor(modules, function(mod) {
var safe = replace(replace(replace(mod.name, '/', '_'), '.', '_'), '-', '_')
var mach = Build.compile_cm_to_mach(mod.src_path)
var bytes = array(mach)
var hex = []
arrfor(bytes, function(b) {
push(hex, '0x' + text(b, 'h2'))
})
push(lines, 'static const unsigned char mod_' + safe + '_data[] = {')
push(lines, ' ' + text(hex, ', '))
push(lines, '};')
push(lines, '')
push(entries, safe)
log.console('Embedded: ' + mod.name + ' (' + text(length(bytes)) + ' bytes)')
})
// Lookup function
push(lines, 'const struct cell_embedded_entry *cell_embedded_module_lookup(const char *name) {')
arrfor(modules, function(mod, i) {
var safe = entries[i]
push(lines, ' if (strcmp(name, "' + mod.name + '") == 0) {')
push(lines, ' static const struct cell_embedded_entry e = {"' + mod.name + '", mod_' + safe + '_data, sizeof(mod_' + safe + '_data)};')
push(lines, ' return &e;')
push(lines, ' }')
})
push(lines, ' return (void *)0;')
push(lines, '}')
var c_text = text(lines, '\n')
fd.slurpwrite(output, stone(blob(c_text)))
log.console('Generated ' + output)
return output
}
@@ -417,38 +632,27 @@ Build.build_static = function(packages, target = Build.detect_host_target(), out
// ============================================================================
// Build dynamic libraries for all installed packages
Build.build_all_dynamic = function(target, buildtype = 'release') {
target = target || Build.detect_host_target()
Build.build_all_dynamic = function(target, buildtype) {
var _target = target || Build.detect_host_target()
var _buildtype = buildtype || 'release'
var packages = shop.list_packages()
var results = []
var core_mods = null
// Build core first
if (find(packages, 'core') != null) {
try {
var lib = Build.build_dynamic('core', target, buildtype)
push(results, { package: 'core', library: lib })
} catch (e) {
log.error('Failed to build core: ' + text(e))
push(results, { package: 'core', error: e })
}
if (find(packages, function(p) { return p == 'core' }) != null) {
core_mods = Build.build_dynamic('core', _target, _buildtype)
push(results, {package: 'core', modules: core_mods})
}
// Build other packages
arrfor(packages, function(pkg) {
if (pkg == 'core') return
try {
var lib = Build.build_dynamic(pkg, target, buildtype)
push(results, { package: pkg, library: lib })
} catch (e) {
log.error('Failed to build ' + pkg + ': ')
log.console(e.message)
log.console(e.stack)
push(results, { package: pkg, error: e })
}
var pkg_mods = Build.build_dynamic(pkg, _target, _buildtype)
push(results, {package: pkg, modules: pkg_mods})
})
return results
}

View File

@@ -22,55 +22,55 @@ function normalize_path(path) {
// Check if a file exists in a specific mount
function mount_exists(mount, path) {
var result = false
var _check = null
if (mount.type == 'zip') {
try {
_check = function() {
mount.handle.mod(path)
return true
} catch (e) {
return false
}
result = true
} disruption {}
_check()
} else if (mount.type == 'qop') {
try {
return mount.handle.stat(path) != null
} catch (e) {
return false
}
} else { // fs
_check = function() {
result = mount.handle.stat(path) != null
} disruption {}
_check()
} else {
var full_path = fd.join_paths(mount.source, path)
try {
_check = function() {
var st = fd.stat(full_path)
return st.isFile || st.isDirectory
} catch (e) {
return false
}
result = st.isFile || st.isDirectory
} disruption {}
_check()
}
return result
}
// Check if a path refers to a directory in a specific mount
function is_directory(path) {
var res = resolve(path)
var mount = res.mount
var result = false
var _check = null
if (mount.type == 'zip') {
try {
return mount.handle.is_directory(path);
} catch (e) {
return false;
}
_check = function() {
result = mount.handle.is_directory(path)
} disruption {}
_check()
} else if (mount.type == 'qop') {
try {
return mount.handle.is_directory(path);
} catch (e) {
return false;
}
} else { // fs
_check = function() {
result = mount.handle.is_directory(path)
} disruption {}
_check()
} else {
var full_path = fd.join_paths(mount.source, path)
try {
_check = function() {
var st = fd.stat(full_path)
return st.isDirectory
} catch (e) {
return false
}
result = st.isDirectory
} disruption {}
_check()
}
return result
}
// Resolve a path to a specific mount and relative path
@@ -102,7 +102,7 @@ function resolve(path, must_exist) {
}, false, true)
if (!mount) {
throw Error("Unknown mount point: @" + mount_name)
print("Unknown mount point: @" + mount_name); disrupt
}
return { mount: mount, path: rel_path }
@@ -122,7 +122,7 @@ function resolve(path, must_exist) {
}
if (must_exist) {
throw Error("File not found in any mount: " + path)
print("File not found in any mount: " + path); disrupt
}
}
@@ -144,12 +144,11 @@ function mount(source, name) {
} else if (st.isFile) {
var blob = fd.slurp(source)
// Try QOP first (it's likely faster to fail?) or Zip?
// QOP open checks magic.
var qop_archive = null
try {
qop_archive = qop.open(blob)
} catch(e) {}
var _try_qop = function() {
qop_archive = qop.open(blob)
} disruption {}
_try_qop()
if (qop_archive) {
mount_info.type = 'qop'
@@ -158,7 +157,7 @@ function mount(source, name) {
} else {
var zip = miniz.read(blob)
if (!is_object(zip) || !is_function(zip.count)) {
throw Error("Invalid archive file (not zip or qop): " + source)
print("Invalid archive file (not zip or qop): " + source); disrupt
}
mount_info.type = 'zip'
@@ -166,7 +165,7 @@ function mount(source, name) {
mount_info.zip_blob = blob // keep blob alive
}
} else {
throw Error("Unsupported mount source type: " + source)
print("Unsupported mount source type: " + source); disrupt
}
push(mounts, mount_info)
@@ -182,13 +181,13 @@ function unmount(name_or_source) {
// Read file
function slurp(path) {
var res = resolve(path, true)
if (!res) throw Error("File not found: " + path)
if (!res) { print("File not found: " + path); disrupt }
if (res.mount.type == 'zip') {
return res.mount.handle.slurp(res.path)
} else if (res.mount.type == 'qop') {
var data = res.mount.handle.read(res.path)
if (!data) throw Error("File not found in qop: " + path)
if (!data) { print("File not found in qop: " + path); disrupt }
return data
} else {
var full_path = fd.join_paths(res.mount.source, res.path)
@@ -217,8 +216,8 @@ function exists(path) {
// Stat
function stat(path) {
var res = resolve(path, true)
if (!res) throw Error("File not found: " + path)
if (!res) { print("File not found: " + path); disrupt }
if (res.mount.type == 'zip') {
var mod = res.mount.handle.mod(res.path)
return {
@@ -228,7 +227,7 @@ function stat(path) {
}
} else if (res.mount.type == 'qop') {
var s = res.mount.handle.stat(res.path)
if (!s) throw Error("File not found in qop: " + path)
if (!s) { print("File not found in qop: " + path); disrupt }
return {
filesize: s.size,
modtime: s.modtime,
@@ -261,7 +260,7 @@ function mount_package(name) {
var dir = shop.get_package_dir(name)
if (!dir) {
throw Error("Package not found: " + name)
print("Package not found: " + name); disrupt
}
mount(dir, name)
@@ -275,7 +274,7 @@ function match(str, pattern) {
function rm(path) {
var res = resolve(path, true)
if (res.mount.type != 'fs') throw Error("Cannot delete from non-fs mount")
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)

View File

@@ -23,8 +23,11 @@ var clean_build = false
var clean_fetch = false
var deep = false
var dry_run = false
var i = 0
var resolved = null
var deps = null
for (var i = 0; i < length(args); i++) {
for (i = 0; i < length(args); i++) {
if (args[i] == '--build') {
clean_build = true
} else if (args[i] == '--fetch') {
@@ -74,7 +77,7 @@ var is_world_scope = (scope == 'world')
if (!is_shop_scope && !is_world_scope) {
if (scope == '.' || starts_with(scope, './') || starts_with(scope, '../') || fd.is_dir(scope)) {
var resolved = fd.realpath(scope)
resolved = fd.realpath(scope)
if (resolved) {
scope = resolved
}
@@ -86,6 +89,7 @@ var dirs_to_delete = []
// Gather packages to clean
var packages_to_clean = []
var _gather = null
if (is_shop_scope) {
packages_to_clean = shop.list_packages()
@@ -97,14 +101,15 @@ if (is_shop_scope) {
push(packages_to_clean, scope)
if (deep) {
try {
var deps = pkg.gather_dependencies(scope)
_gather = function() {
deps = pkg.gather_dependencies(scope)
arrfor(deps, function(dep) {
push(packages_to_clean, dep)
})
} catch (e) {
} disruption {
// Skip if can't read dependencies
}
_gather()
}
}
@@ -168,6 +173,7 @@ if (clean_fetch) {
}
// Execute or report
var deleted_count = 0
if (dry_run) {
log.console("Would delete:")
if (length(files_to_delete) == 0 && length(dirs_to_delete) == 0) {
@@ -181,20 +187,19 @@ if (dry_run) {
})
}
} else {
var deleted_count = 0
arrfor(files_to_delete, function(f) {
try {
var _del = function() {
fd.unlink(f)
log.console("Deleted: " + f)
deleted_count++
} catch (e) {
log.error("Failed to delete " + f + ": " + e)
} disruption {
log.error("Failed to delete " + f)
}
_del()
})
arrfor(dirs_to_delete, function(d) {
try {
var _del = function() {
if (fd.is_link(d)) {
fd.unlink(d)
} else {
@@ -202,9 +207,10 @@ if (dry_run) {
}
log.console("Deleted: " + d)
deleted_count++
} catch (e) {
log.error("Failed to delete " + d + ": " + e)
} disruption {
log.error("Failed to delete " + d)
}
_del()
})
if (deleted_count == 0) {

View File

@@ -7,11 +7,14 @@ 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()
return
}
var origin = args[0]
@@ -19,19 +22,19 @@ var target_path = args[1]
// Resolve target path to absolute
if (target_path == '.' || starts_with(target_path, './') || starts_with(target_path, '../')) {
var resolved = fd.realpath(target_path)
resolved = fd.realpath(target_path)
if (resolved) {
target_path = resolved
} else {
// Path doesn't exist yet, resolve relative to cwd
var cwd = fd.realpath('.')
cwd = fd.realpath('.')
if (target_path == '.') {
target_path = cwd
} else if (starts_with(target_path, './')) {
target_path = cwd + text(target_path, 1)
} else if (starts_with(target_path, '../')) {
// Go up one directory from cwd
var parent = fd.dirname(cwd)
parent = fd.dirname(cwd)
target_path = parent + text(target_path, 2)
}
}
@@ -41,7 +44,6 @@ if (target_path == '.' || starts_with(target_path, './') || starts_with(target_p
if (fd.is_dir(target_path)) {
log.console("Error: " + target_path + " already exists")
$stop()
return
}
log.console("Cloning " + origin + " to " + target_path + "...")
@@ -51,7 +53,6 @@ var info = shop.resolve_package_info(origin)
if (!info || info == 'local') {
log.console("Error: " + origin + " is not a remote package")
$stop()
return
}
// Update to get the commit hash
@@ -59,7 +60,6 @@ var update_result = shop.update(origin)
if (!update_result) {
log.console("Error: Could not fetch " + origin)
$stop()
return
}
// Fetch and extract to the target path
@@ -68,54 +68,61 @@ var entry = lock[origin]
if (!entry || !entry.commit) {
log.console("Error: No commit found for " + origin)
$stop()
return
}
var download_url = shop.get_download_url(origin, entry.commit)
log.console("Downloading from " + download_url)
try {
var zip_blob = http.fetch(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
var zip = miniz.read(zip_blob)
zip = miniz.read(zip_blob)
if (!zip) {
log.console("Error: Failed to read zip archive")
$stop()
return
}
// Create target directory
fd.mkdir(target_path)
var count = zip.count()
for (var i = 0; i < count; i++) {
count = zip.count()
for (i = 0; i < count; i++) {
if (zip.is_directory(i)) continue
var filename = zip.get_filename(i)
var first_slash = search(filename, '/')
filename = zip.get_filename(i)
first_slash = search(filename, '/')
if (first_slash == null) continue
if (first_slash + 1 >= length(filename)) continue
var rel_path = text(filename, first_slash + 1)
var full_path = target_path + '/' + rel_path
var dir_path = fd.dirname(full_path)
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))
}
log.console("Extracted to " + target_path)
// Link the origin to the cloned path
link.add(origin, target_path, shop)
log.console("Linked " + origin + " -> " + target_path)
} catch (e) {
log.console("Error: " + e.message)
if (e.stack) log.console(e.stack)
} disruption {
log.console("Error during clone")
}
_clone()
$stop()

92
compare_aot.ce Normal file
View File

@@ -0,0 +1,92 @@
// compare_aot.ce — compile a .cm module via both paths and compare results
//
// Usage:
// cell --dev compare_aot.ce <module.cm>
var build = use('build')
var fd_mod = use('fd')
var os = use('os')
var json = use('json')
var show = function(v) {
return json.encode(v)
}
if (length(args) < 1) {
print('usage: cell --dev compare_aot.ce <module.cm>')
return
}
var file = args[0]
if (!fd_mod.is_file(file)) {
if (!ends_with(file, '.cm') && fd_mod.is_file(file + '.cm'))
file = file + '.cm'
else {
print('file not found: ' + file)
return
}
}
var abs = fd_mod.realpath(file)
// Shared compilation front-end
var tokenize = use('tokenize')
var parse_mod = use('parse')
var fold = use('fold')
var mcode_mod = use('mcode')
var streamline_mod = use('streamline')
var src = text(fd_mod.slurp(abs))
var tok = tokenize(src, abs)
var ast = parse_mod(tok.tokens, src, abs, tokenize)
var folded = fold(ast)
var compiled = mcode_mod(folded)
var optimized = streamline_mod(compiled)
// --- 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))
// --- 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.
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
})
var result_native = os.native_module_load(handle, env)
print('result: ' + show(result_native))
// --- Comparison ---
print('\n--- comparison ---')
var s_interp = show(result_interp)
var s_native = show(result_native)
if (s_interp == s_native) {
print('MATCH')
} else {
print('MISMATCH')
print(' interp: ' + s_interp)
print(' native: ' + s_native)
}

View File

@@ -1,102 +1,27 @@
// compile.ce — compile a .cm module to native .dylib via QBE
// compile.ce — compile a .cm or .ce file to native .dylib via QBE
//
// Usage:
// cell --core . compile.ce <file.cm>
// cell compile <file.cm|file.ce>
//
// Produces <file>.dylib in the current directory.
// Installs the dylib to .cell/lib/<pkg>/<stem>.dylib
var shop = use('internal/shop')
var build = use('build')
var fd = use('fd')
var os = use('os')
if (length(args) < 1) {
print('usage: cell --core . compile.ce <file.cm>')
print('usage: cell compile <file.cm|file.ce>')
return
}
var file = args[0]
var base = file
if (ends_with(base, '.cm')) {
base = text(base, 0, length(base) - 3)
}
var safe = replace(replace(base, '/', '_'), '-', '_')
var symbol = 'js_' + safe + '_use'
var tmp = '/tmp/qbe_' + safe
var ssa_path = tmp + '.ssa'
var s_path = tmp + '.s'
var o_path = tmp + '.o'
var rt_o_path = '/tmp/qbe_rt.o'
var dylib_path = base + '.dylib'
var cwd = fd.getcwd()
var rc = 0
// Step 1: emit QBE IL
print('emit qbe...')
rc = os.system('cd ' + cwd + ' && ./cell --core . --emit-qbe ' + file + ' > ' + ssa_path)
if (rc != 0) {
print('failed to emit qbe il')
if (!fd.is_file(file)) {
print('file not found: ' + file)
return
}
// Step 2: post-process — insert dead labels after ret/jmp, append wrapper
// Use awk via shell to avoid blob/slurpwrite issues with long strings
print('post-process...')
var awk_cmd = `awk '
need_label && /^[[:space:]]*[^@}]/ && NF > 0 {
print "@_dead_" dead_id; dead_id++; need_label=0
}
/^@/ || /^}/ || NF==0 { need_label=0 }
/^[[:space:]]*ret / || /^[[:space:]]*jmp / { need_label=1; print; next }
{ print }
' ` + ssa_path + ` > ` + tmp + `_fixed.ssa`
rc = os.system(awk_cmd)
if (rc != 0) {
print('post-process failed')
return
}
var abs = fd.realpath(file)
var file_info = shop.file_info(abs)
var pkg = file_info.package
// Append wrapper function — called as symbol(ctx) by os.dylib_symbol.
// Delegates to cell_rt_module_entry which heap-allocates a frame
// (so closures survive) and calls cell_main.
var wrapper_cmd = `printf '\nexport function l $` + symbol + `(l %%ctx) {\n@entry\n %%result =l call $cell_rt_module_entry(l %%ctx)\n ret %%result\n}\n' >> ` + tmp + `_fixed.ssa`
rc = os.system(wrapper_cmd)
if (rc != 0) {
print('wrapper append failed')
return
}
// Step 3: compile QBE IL to assembly
print('qbe compile...')
rc = os.system('~/.local/bin/qbe -o ' + s_path + ' ' + tmp + '_fixed.ssa')
if (rc != 0) {
print('qbe compilation failed')
return
}
// Step 4: assemble
print('assemble...')
rc = os.system('cc -c ' + s_path + ' -o ' + o_path)
if (rc != 0) {
print('assembly failed')
return
}
// Step 5: compile runtime stubs (cached — skip if already built)
if (!fd.is_file(rt_o_path)) {
print('compile runtime stubs...')
rc = os.system('cc -c ' + cwd + '/qbe_rt.c -o ' + rt_o_path + ' -fPIC')
if (rc != 0) {
print('runtime stubs compilation failed')
return
}
}
// Step 6: link dylib
print('link...')
rc = os.system('cc -shared -fPIC -undefined dynamic_lookup ' + o_path + ' ' + rt_o_path + ' -o ' + cwd + '/' + dylib_path)
if (rc != 0) {
print('linking failed')
return
}
print('built: ' + dylib_path)
build.compile_native(abs, null, null, pkg)

98
compile_seed.ce Normal file
View File

@@ -0,0 +1,98 @@
// 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)

261
config.ce
View File

@@ -47,8 +47,10 @@ function get_nested(obj, path) {
// Set a value in nested object using path
function set_nested(obj, path, value) {
var current = obj
for (var i = 0; i < length(path) - 1; i++) {
var segment = path[i]
var i = 0
var segment = null
for (i = 0; i < length(path) - 1; i++) {
segment = path[i]
if (is_null(current[segment]) || !is_object(current[segment])) {
current[segment] = {}
}
@@ -59,15 +61,17 @@ function set_nested(obj, path, value) {
// Parse value string into appropriate type
function parse_value(str) {
var num_str = null
var n = null
// Boolean
if (str == 'true') return true
if (str == 'false') return false
// Number (including underscores)
var num_str = replace(str, /_/g, '')
if (/^-?\d+$/.test(num_str)) return parseInt(num_str)
if (/^-?\d*\.\d+$/.test(num_str)) return parseFloat(num_str)
// Number
num_str = replace(str, /_/g, '')
n = number(num_str)
if (n != null) return n
// String
return str
}
@@ -75,22 +79,19 @@ function parse_value(str) {
// Format value for display
function format_value(val) {
if (is_text(val)) return '"' + val + '"'
if (is_number(val) && val >= 1000) {
// Add underscores to large numbers
return replace(val.toString(), /\B(?=(\d{3})+(?!\d))/g, '_')
}
return text(val)
}
// Print configuration tree recursively
function print_config(obj, prefix = '') {
function print_config(obj, pfx) {
var p = pfx || ''
arrfor(array(obj), function(key) {
var val = obj[key]
var full_key = prefix ? prefix + '.' + key : key
var full_key = p ? p + '.' + key : key
if (is_object(val))
print_config(val, full_key)
else
else if (!is_null(val))
log.console(full_key + ' = ' + format_value(val))
})
}
@@ -99,151 +100,123 @@ function print_config(obj, prefix = '') {
if (length(args) == 0) {
print_help()
$stop()
return
}
var config = pkg.load_config()
if (!config) {
log.error("Failed to load cell.toml")
$stop()
return
}
var command = args[0]
var key
var path
var value
var key = null
var path = null
var value = null
var value_str = null
var valid_system_keys = null
var actor_name = null
var actor_cmd = null
switch (command) {
case 'help':
case '-h':
case '--help':
print_help()
break
case 'list':
log.console("# Cell Configuration")
log.console("")
print_config(config)
break
case 'get':
if (length(args) < 2) {
log.error("Usage: cell config get <key>")
if (command == 'help' || command == '-h' || command == '--help') {
print_help()
} else if (command == 'list') {
log.console("# Cell Configuration")
log.console("")
print_config(config)
} else if (command == 'get') {
if (length(args) < 2) {
log.error("Usage: cell config get <key>")
$stop()
}
key = args[1]
path = parse_key(key)
value = get_nested(config, path)
if (value == null) {
log.error("Key not found: " + key)
} else if (is_object(value)) {
print_config(value, key)
} else {
log.console(key + ' = ' + format_value(value))
}
} else if (command == 'set') {
if (length(args) < 3) {
log.error("Usage: cell config set <key> <value>")
$stop()
}
key = args[1]
value_str = args[2]
path = parse_key(key)
value = parse_value(value_str)
if (path[0] == 'system') {
valid_system_keys = [
'ar_timer', 'actor_memory', 'net_service',
'reply_timeout', 'actor_max', 'stack_max'
]
if (find(valid_system_keys, path[1]) == null) {
log.error("Invalid system key. Valid keys: " + text(valid_system_keys, ', '))
$stop()
return
}
key = args[1]
path = parse_key(key)
value = get_nested(config, path)
if (value == null) {
log.error("Key not found: " + key)
} else if (isa(value, object)) {
// Print all nested values
print_config(value, key)
}
set_nested(config, path, value)
pkg.save_config(config)
log.console("Set " + key + " = " + format_value(value))
} else if (command == 'actor') {
if (length(args) < 3) {
log.error("Usage: cell config actor <name> <command> [options]")
$stop()
}
actor_name = args[1]
actor_cmd = args[2]
config.actors = config.actors || {}
config.actors[actor_name] = config.actors[actor_name] || {}
if (actor_cmd == 'list') {
if (length(array(config.actors[actor_name])) == 0) {
log.console("No configuration for actor: " + actor_name)
} else {
log.console(key + ' = ' + format_value(value))
log.console("# Configuration for actor: " + actor_name)
log.console("")
print_config(config.actors[actor_name], 'actors.' + actor_name)
}
break
case 'set':
if (length(args) < 3) {
log.error("Usage: cell config set <key> <value>")
} else if (actor_cmd == 'get') {
if (length(args) < 4) {
log.error("Usage: cell config actor <name> get <key>")
$stop()
return
}
var key = args[1]
var value_str = args[2]
var path = parse_key(key)
var value = parse_value(value_str)
// Validate system keys
if (path[0] == 'system') {
var valid_system_keys = [
'ar_timer', 'actor_memory', 'net_service',
'reply_timeout', 'actor_max', 'stack_max'
]
if (find(valid_system_keys, path[1]) == null) {
log.error("Invalid system key. Valid keys: " + text(valid_system_keys, ', '))
$stop()
return
}
key = args[3]
path = parse_key(key)
value = get_nested(config.actors[actor_name], path)
if (value == null) {
log.error("Key not found for actor " + actor_name + ": " + key)
} else {
log.console('actors.' + actor_name + '.' + key + ' = ' + format_value(value))
}
set_nested(config, path, value)
} else if (actor_cmd == 'set') {
if (length(args) < 5) {
log.error("Usage: cell config actor <name> set <key> <value>")
$stop()
}
key = args[3]
value_str = args[4]
path = parse_key(key)
value = parse_value(value_str)
set_nested(config.actors[actor_name], path, value)
pkg.save_config(config)
log.console("Set " + key + " = " + format_value(value))
break
case 'actor':
// Handle actor-specific configuration
if (length(args) < 3) {
log.error("Usage: cell config actor <name> <command> [options]")
$stop()
return
}
var actor_name = args[1]
var actor_cmd = args[2]
// Initialize actors section if needed
config.actors = config.actors || {}
config.actors[actor_name] = config.actors[actor_name] || {}
switch (actor_cmd) {
case 'list':
if (length(array(config.actors[actor_name])) == 0) {
log.console("No configuration for actor: " + actor_name)
} else {
log.console("# Configuration for actor: " + actor_name)
log.console("")
print_config(config.actors[actor_name], 'actors.' + actor_name)
}
break
case 'get':
if (length(args) < 4) {
log.error("Usage: cell config actor <name> get <key>")
$stop()
return
}
key = args[3]
path = parse_key(key)
value = get_nested(config.actors[actor_name], path)
if (value == null) {
log.error("Key not found for actor " + actor_name + ": " + key)
} else {
log.console('actors.' + actor_name + '.' + key + ' = ' + format_value(value))
}
break
case 'set':
if (length(args) < 5) {
log.error("Usage: cell config actor <name> set <key> <value>")
$stop()
return
}
key = args[3]
var value_str = args[4]
path = parse_key(key)
value = parse_value(value_str)
set_nested(config.actors[actor_name], path, value)
pkg.save_config(config)
log.console("Set actors." + actor_name + "." + key + " = " + format_value(value))
break
default:
log.error("Unknown actor command: " + actor_cmd)
log.console("Valid commands: list, get, set")
}
break
default:
log.error("Unknown command: " + command)
print_help()
log.console("Set actors." + actor_name + "." + key + " = " + format_value(value))
} else {
log.error("Unknown actor command: " + actor_cmd)
log.console("Valid commands: list, get, set")
}
} else {
log.error("Unknown command: " + command)
print_help()
}
$stop()
$stop()

View File

@@ -238,9 +238,10 @@ static const JSCFunctionListEntry js_crypto_funcs[] = {
JS_CFUNC_DEF("unlock", 3, js_crypto_unlock),
};
JSValue js_crypto_use(JSContext *js)
JSValue js_core_crypto_use(JSContext *js)
{
JSValue obj = JS_NewObject(js);
JS_SetPropertyFunctionList(js, obj, js_crypto_funcs, sizeof(js_crypto_funcs)/sizeof(js_crypto_funcs[0]));
return obj;
JS_FRAME(js);
JS_ROOT(mod, JS_NewObject(js));
JS_SetPropertyFunctionList(js, mod.val, js_crypto_funcs, sizeof(js_crypto_funcs)/sizeof(js_crypto_funcs[0]));
JS_RETURN(mod.val);
}

View File

@@ -1,27 +1,15 @@
#include "cell.h"
// Return the current stack depth.
JSC_CCALL(debug_stack_depth, return number2js(js,js_debugger_stack_depth(js)))
// TODO: Reimplement stack depth for register VM
JSC_CCALL(debug_stack_depth, return number2js(js, 0))
// Return a backtrace of the current call stack.
JSC_CCALL(debug_build_backtrace, return js_debugger_build_backtrace(js))
// Return the closure variables for a given function.
JSC_CCALL(debug_closure_vars, return js_debugger_closure_variables(js,argv[0]))
JSC_CCALL(debug_set_closure_var,
js_debugger_set_closure_variable(js,argv[0],argv[1],argv[2]);
return JS_NULL;
)
// Return the local variables for a specific stack frame.
JSC_CCALL(debug_local_vars, return js_debugger_local_variables(js, js2number(js,argv[0])))
// Return metadata about a given function.
JSC_CCALL(debug_fn_info, return js_debugger_fn_info(js, argv[0]))
// Return an array of functions in the current backtrace.
JSC_CCALL(debug_backtrace_fns, return js_debugger_backtrace_fns(js))
// TODO: Reimplement debug introspection for register VM
JSC_CCALL(debug_build_backtrace, return JS_NewArray(js))
JSC_CCALL(debug_closure_vars, return JS_NewObject(js))
JSC_CCALL(debug_set_closure_var, return JS_NULL;)
JSC_CCALL(debug_local_vars, return JS_NewObject(js))
JSC_CCALL(debug_fn_info, return JS_NewObject(js))
JSC_CCALL(debug_backtrace_fns, return JS_NewArray(js))
static const JSCFunctionListEntry js_debug_funcs[] = {
MIST_FUNC_DEF(debug, stack_depth, 0),
@@ -33,8 +21,9 @@ static const JSCFunctionListEntry js_debug_funcs[] = {
MIST_FUNC_DEF(debug, backtrace_fns,0),
};
JSValue js_debug_use(JSContext *js) {
JSValue mod = JS_NewObject(js);
JS_SetPropertyFunctionList(js,mod,js_debug_funcs,countof(js_debug_funcs));
return mod;
}
JSValue js_core_debug_use(JSContext *js) {
JS_FRAME(js);
JS_ROOT(mod, JS_NewObject(js));
JS_SetPropertyFunctionList(js, mod.val, js_debug_funcs, countof(js_debug_funcs));
JS_RETURN(mod.val);
}

View File

@@ -1,48 +1,16 @@
#include "cell.h"
JSC_CCALL(os_mem_limit, JS_SetMemoryLimit(JS_GetRuntime(js), js2number(js,argv[0])))
JSC_CCALL(os_max_stacksize, JS_SetMaxStackSize(JS_GetRuntime(js), js2number(js,argv[0])))
JSC_CCALL(os_max_stacksize, JS_SetMaxStackSize(js, js2number(js,argv[0])))
// Compute the approximate size of a single JS value in memory.
// TODO: Reimplement memory usage reporting for new allocator
JSC_CCALL(os_calc_mem,
JSMemoryUsage mu;
JS_ComputeMemoryUsage(JS_GetRuntime(js),&mu);
ret = JS_NewObject(js);
JS_SetPropertyStr(js,ret,"malloc_size",number2js(js,mu.malloc_size));
JS_SetPropertyStr(js,ret,"malloc_limit",number2js(js,mu.malloc_limit));
JS_SetPropertyStr(js,ret,"memory_used_size",number2js(js,mu.memory_used_size));
JS_SetPropertyStr(js,ret,"malloc_count",number2js(js,mu.malloc_count));
JS_SetPropertyStr(js,ret,"memory_used_count",number2js(js,mu.memory_used_count));
JS_SetPropertyStr(js,ret,"str_count",number2js(js,mu.str_count));
JS_SetPropertyStr(js,ret,"str_size",number2js(js,mu.str_size));
JS_SetPropertyStr(js,ret,"obj_count",number2js(js,mu.obj_count));
JS_SetPropertyStr(js,ret,"obj_size",number2js(js,mu.obj_size));
JS_SetPropertyStr(js,ret,"prop_count",number2js(js,mu.prop_count));
JS_SetPropertyStr(js,ret,"prop_size",number2js(js,mu.prop_size));
JS_SetPropertyStr(js,ret,"shape_count",number2js(js,mu.shape_count));
JS_SetPropertyStr(js,ret,"shape_size",number2js(js,mu.shape_size));
JS_SetPropertyStr(js,ret,"js_func_count",number2js(js,mu.js_func_count));
JS_SetPropertyStr(js,ret,"js_func_size",number2js(js,mu.js_func_size));
JS_SetPropertyStr(js,ret,"js_func_code_size",number2js(js,mu.js_func_code_size));
JS_SetPropertyStr(js,ret,"js_func_pc2line_count",number2js(js,mu.js_func_pc2line_count));
JS_SetPropertyStr(js,ret,"js_func_pc2line_size",number2js(js,mu.js_func_pc2line_size));
JS_SetPropertyStr(js,ret,"c_func_count",number2js(js,mu.c_func_count));
JS_SetPropertyStr(js,ret,"array_count",number2js(js,mu.array_count));
JS_SetPropertyStr(js,ret,"fast_array_count",number2js(js,mu.fast_array_count));
JS_SetPropertyStr(js,ret,"fast_array_elements",number2js(js,mu.fast_array_elements));
JS_SetPropertyStr(js,ret,"binary_object_count",number2js(js,mu.binary_object_count));
JS_SetPropertyStr(js,ret,"binary_object_size",number2js(js,mu.binary_object_size));
)
// Disassemble a function object into a string.
JSC_CCALL(js_disassemble,
return js_debugger_fn_bytecode(js, argv[0]);
)
// Return metadata about a given function.
JSC_CCALL(js_fn_info,
return js_debugger_fn_info(js, argv[0]);
)
// TODO: Reimplement for register VM
JSC_CCALL(js_disassemble, return JS_NewArray(js);)
JSC_CCALL(js_fn_info, return JS_NewObject(js);)
static const JSCFunctionListEntry js_js_funcs[] = {
MIST_FUNC_DEF(os, calc_mem, 0),
@@ -52,8 +20,9 @@ static const JSCFunctionListEntry js_js_funcs[] = {
MIST_FUNC_DEF(js, fn_info, 1),
};
JSValue js_js_use(JSContext *js) {
JSValue mod = JS_NewObject(js);
JS_SetPropertyFunctionList(js,mod,js_js_funcs,countof(js_js_funcs));
return mod;
JSValue js_core_js_use(JSContext *js) {
JS_FRAME(js);
JS_ROOT(mod, JS_NewObject(js));
JS_SetPropertyFunctionList(js, mod.val, js_js_funcs, countof(js_js_funcs));
JS_RETURN(mod.val);
}

264
diff.ce Normal file
View File

@@ -0,0 +1,264 @@
// diff.ce — differential testing: run tests optimized vs unoptimized, compare results
//
// Usage:
// cell diff - diff all test files in current package
// cell diff suite - diff a specific test file (tests/suite.cm)
// cell diff tests/foo - diff a specific test file by path
var shop = use('internal/shop')
var pkg = use('package')
var fd = use('fd')
var time = use('time')
var _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
if (!run_ast_noopt_fn) {
log.console("error: run_ast_noopt_fn not available (rebuild bootstrap)")
$stop()
return
}
// Parse arguments: diff [test_path]
var target_test = null
if (length(_args) > 0) {
target_test = _args[0]
}
function is_valid_package(dir) {
var _dir = dir == null ? '.' : dir
return fd.is_file(_dir + '/cell.toml')
}
if (!is_valid_package('.')) {
log.console('No cell.toml found in current directory')
$stop()
return
}
// Collect test files
function collect_tests(specific_test) {
var files = pkg.list_files(null)
var test_files = []
var i = 0
var f = null
var test_name = null
var match_name = null
var match_base = null
for (i = 0; i < length(files); i++) {
f = files[i]
if (starts_with(f, "tests/") && ends_with(f, ".cm")) {
if (specific_test) {
test_name = text(f, 0, -3)
match_name = specific_test
if (!starts_with(match_name, 'tests/')) match_name = 'tests/' + match_name
match_base = ends_with(match_name, '.cm') ? text(match_name, 0, -3) : match_name
if (test_name != match_base) continue
}
push(test_files, f)
}
}
return test_files
}
// 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>"
}
// Run a single test file through both paths
function diff_test_file(file_path) {
var mod_path = text(file_path, 0, -3)
var src_path = fd.realpath('.') + '/' + file_path
var src = null
var ast = null
var mod_opt = null
var mod_noopt = null
var results = {file: file_path, tests: [], passed: 0, failed: 0, errors: []}
var use_pkg = fd.realpath('.')
var opt_error = null
var noopt_error = null
var keys = null
var i = 0
var k = null
var opt_result = null
var noopt_result = null
var opt_err = null
var noopt_err = null
var _run_one_opt = null
var _run_one_noopt = null
// Build env for module loading
var make_env = function() {
return stone({
use: function(path) {
return shop.use(path, use_pkg)
}
})
}
// Read and parse
var _read = function() {
src = text(fd.slurp(src_path))
ast = analyze(src, src_path)
} disruption {
push(results.errors, `failed to parse ${file_path}`)
return results
}
_read()
if (length(results.errors) > 0) return results
// Run optimized
var _run_opt = function() {
mod_opt = run_ast_fn(mod_path, ast, make_env())
} disruption {
opt_error = "disrupted"
}
_run_opt()
// Run unoptimized
var _run_noopt = function() {
mod_noopt = run_ast_noopt_fn(mod_path, ast, make_env())
} disruption {
noopt_error = "disrupted"
}
_run_noopt()
// Compare module-level behavior
if (opt_error != noopt_error) {
push(results.errors, `module load mismatch: opt=${opt_error != null ? opt_error : "ok"} noopt=${noopt_error != null ? noopt_error : "ok"}`)
results.failed = results.failed + 1
return results
}
if (opt_error != null) {
// Both disrupted during load — that's consistent
results.passed = results.passed + 1
push(results.tests, {name: "<module>", status: "passed"})
return results
}
// If module returns a record of functions, test each one
if (is_object(mod_opt) && is_object(mod_noopt)) {
keys = array(mod_opt)
while (i < length(keys)) {
k = keys[i]
if (is_function(mod_opt[k]) && is_function(mod_noopt[k])) {
opt_result = null
noopt_result = null
opt_err = null
noopt_err = null
_run_one_opt = function() {
opt_result = mod_opt[k]()
} disruption {
opt_err = "disrupted"
}
_run_one_opt()
_run_one_noopt = function() {
noopt_result = mod_noopt[k]()
} disruption {
noopt_err = "disrupted"
}
_run_one_noopt()
if (opt_err != noopt_err) {
push(results.tests, {name: k, status: "failed"})
push(results.errors, `${k}: disruption mismatch opt=${opt_err != null ? opt_err : "ok"} noopt=${noopt_err != null ? noopt_err : "ok"}`)
results.failed = results.failed + 1
} else if (!values_equal(opt_result, noopt_result)) {
push(results.tests, {name: k, status: "failed"})
push(results.errors, `${k}: result mismatch opt=${describe(opt_result)} noopt=${describe(noopt_result)}`)
results.failed = results.failed + 1
} else {
push(results.tests, {name: k, status: "passed"})
results.passed = results.passed + 1
}
}
i = i + 1
}
} else {
// Compare direct return values
if (!values_equal(mod_opt, mod_noopt)) {
push(results.tests, {name: "<return>", status: "failed"})
push(results.errors, `return value mismatch: opt=${describe(mod_opt)} noopt=${describe(mod_noopt)}`)
results.failed = results.failed + 1
} else {
push(results.tests, {name: "<return>", status: "passed"})
results.passed = results.passed + 1
}
}
return results
}
// Main
var test_files = collect_tests(target_test)
log.console(`Differential testing: ${text(length(test_files))} file(s)`)
var total_passed = 0
var total_failed = 0
var i = 0
var result = null
var j = 0
while (i < length(test_files)) {
result = diff_test_file(test_files[i])
log.console(` ${result.file}: ${text(result.passed)} passed, ${text(result.failed)} failed`)
j = 0
while (j < length(result.errors)) {
log.console(` MISMATCH: ${result.errors[j]}`)
j = j + 1
}
total_passed = total_passed + result.passed
total_failed = total_failed + result.failed
i = i + 1
}
log.console(`----------------------------------------`)
log.console(`Diff: ${text(total_passed)} passed, ${text(total_failed)} failed, ${text(total_passed + total_failed)} total`)
if (total_failed > 0) {
log.console(`DIFFERENTIAL FAILURES DETECTED`)
}
$stop()

View File

@@ -34,6 +34,7 @@ pit hello
- [**Actors and Modules**](/docs/actors/) — the execution model
- [**Requestors**](/docs/requestors/) — asynchronous composition
- [**Packages**](/docs/packages/) — code organization and sharing
- [**Shop Architecture**](/docs/shop/) — module resolution, compilation, and caching
## Reference
@@ -56,6 +57,7 @@ Modules loaded with `use()`:
## Tools
- [**Command Line**](/docs/cli/) — the `pit` tool
- [**Testing**](/docs/testing/) — writing and running tests
- [**Writing C Modules**](/docs/c-modules/) — native extensions
## Architecture

View File

@@ -224,7 +224,7 @@ use('json') // core json module
use('otherlib/foo') // dependency 'otherlib', file foo.cm
```
Files starting with underscore (`_helper.cm`) are private to the package.
Files in the `internal/` directory are private to the package.
## Example: Simple Actor System

View File

@@ -52,6 +52,11 @@ 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/game.ce` (AOT actor) -> `js_mypackage_game_program`
Actor files (`.ce`) use the `_program` suffix instead of `_use`.
**Note:** Having both a `.cm` and `.c` file with the same stem at the same scope is a build error.
## Required Headers
@@ -216,34 +221,6 @@ var n = vector.normalize(3, 4) // {x: 0.6, y: 0.8}
var d = vector.dot(1, 0, 0, 1) // 0
```
## Combining C and ƿit
A common pattern is to have a C file provide low-level functions and a `.cm` file provide a higher-level API:
```c
// _vector_native.c
// ... raw C functions ...
```
```javascript
// vector.cm
var native = this // C module passed as 'this'
var Vector = function(x, y) {
return {x: x, y: y}
}
Vector.length = function(v) {
return native.length(v.x, v.y)
}
Vector.normalize = function(v) {
return native.normalize(v.x, v.y)
}
return Vector
```
## Build Process
C files are automatically compiled when you run:
@@ -253,7 +230,7 @@ pit build
pit update
```
The resulting dynamic library is placed in `~/.pit/lib/`.
Each C file is compiled into a per-file dynamic library at `~/.pit/lib/<pkg>/<stem>.dylib`.
## Platform-Specific Code

View File

@@ -70,20 +70,22 @@ pit ls <package> # list files in specified package
### pit build
Build the current package.
Build the current package. Compiles C files into per-file dynamic libraries and installs them to `~/.pit/lib/<pkg>/<stem>.dylib`.
```bash
pit build
pit build # build current package
pit build <package> # build specific package
```
### pit test
Run tests.
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
@@ -121,6 +123,103 @@ Clean build artifacts.
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 resolve
Print the fully resolved dependency closure for a package.
```bash
pit resolve # resolve current package
pit resolve <package> # resolve specific package
pit resolve --locked # show lock state without links
```
### pit graph
Emit a dependency graph.
```bash
pit graph # tree of current package
pit graph --format dot # graphviz dot output
pit graph --format json # json output
pit graph --world # graph all installed packages
pit graph --locked # show lock view without links
```
### pit verify
Verify integrity and consistency of packages, links, and builds.
```bash
pit verify # verify current package
pit verify shop # verify entire shop
pit verify --deep # traverse full dependency closure
pit verify --target <triple>
```
### pit pack
Build a statically linked binary from a package and all its dependencies.
```bash
pit pack <package> # build static binary (output: app)
pit pack <package> -o myapp # specify output name
pit pack <package> -t <triple> # cross-compile for target
```
### pit config
Manage system and actor configuration values in `cell.toml`.
```bash
pit config list # list all config
pit config get system.ar_timer # get a value
pit config set system.ar_timer 5.0 # set a value
pit config actor <name> list # list actor config
pit config actor <name> get <key> # get actor config
pit config actor <name> set <key> <val> # set actor config
```
### pit help
Display help information.
@@ -130,16 +229,6 @@ pit help
pit help <command>
```
## Running Scripts
Any `.ce` file in the ƿit core can be run as a command:
```bash
pit version # runs version.ce
pit build # runs build.ce
pit test # runs test.ce
```
## Package Locators
Packages are identified by locators:
@@ -158,9 +247,11 @@ pit install /Users/john/work/mylib
```
~/.pit/
├── packages/ # installed packages
├── lib/ # compiled dynamic libraries
├── build/ # build cache
├── 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)
├── cache/ # downloaded archives
├── lock.toml # installed package versions
└── link.toml # local development links

264
docs/compiler-tools.md Normal file
View File

@@ -0,0 +1,264 @@
---
title: "Compiler Inspection Tools"
description: "Tools for inspecting and debugging the compiler pipeline"
weight: 50
type: "docs"
---
ƿit includes a set of tools for inspecting the compiler pipeline at every stage. These are useful for debugging, testing optimizations, and understanding what the compiler does with your code.
## Pipeline Overview
The compiler runs in stages:
```
source → tokenize → parse → fold → mcode → streamline → output
```
Each stage has a corresponding dump 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 |
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 what the optimizer changed
./cell --core . dump_stream.cm myfile.ce
# full optimizer report with events
./cell --core . ir_report.ce --full myfile.ce
```
## dump_ast.cm
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>
```
## dump_mcode.cm
Prints the raw mcode IR before any optimization. Shows the instruction array as formatted text with opcode, 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>
```
## streamline.ce
Runs the full pipeline (tokenize, parse, fold, mcode, streamline) and outputs the optimized IR as JSON. Useful for piping to `jq` or saving for comparison.
```bash
./cell --core . streamline.ce <file.ce|file.cm>
```
## 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>
```
### Options
| Flag | Description |
|------|-------------|
| `--summary` | Per-pass JSON summaries with instruction counts and timing (default) |
| `--events` | Include rewrite events showing each optimization applied |
| `--types` | Include type delta records showing inferred slot types |
| `--ir-before=PASS` | Print canonical IR before a specific pass |
| `--ir-after=PASS` | Print canonical IR after a specific pass |
| `--ir-all` | Print canonical IR before and after all passes |
| `--full` | Everything: summary + events + types + ir-all |
With no flags, `--summary` is the default.
### Output Format
Output is line-delimited JSON. Each line is a self-contained JSON object with a `type` field:
**`type: "pass"`** — Per-pass summary with categorized instruction counts before and after:
```json
{
"type": "pass",
"pass": "eliminate_type_checks",
"fn": "fib",
"ms": 0.12,
"changed": true,
"before": {"instr": 77, "nop": 0, "guard": 16, "branch": 28, ...},
"after": {"instr": 77, "nop": 1, "guard": 15, "branch": 28, ...},
"changes": {"guards_removed": 1, "nops_added": 1}
}
```
**`type: "event"`** — Individual rewrite event with before/after instructions and reasoning:
```json
{
"type": "event",
"pass": "eliminate_type_checks",
"rule": "incompatible_type_forces_jump",
"at": 3,
"before": [["is_int", 5, 2, 4, 9], ["jump_false", 5, "rel_ni_2", 4, 9]],
"after": ["_nop_tc_1", ["jump", "rel_ni_2", 4, 9]],
"why": {"slot": 2, "known_type": "float", "checked_type": "int"}
}
```
**`type: "types"`** — Inferred type information for a function:
```json
{
"type": "types",
"fn": "fib",
"param_types": {},
"slot_types": {"25": "null"}
}
```
**`type: "ir"`** — Canonical IR text for a function at a specific point:
```json
{
"type": "ir",
"when": "before",
"pass": "all",
"fn": "fib",
"text": "fn fib (args=1, slots=26)\n @0 access s2, 2\n ..."
}
```
### Rewrite Rules
Each pass records events with named rules:
**eliminate_type_checks:**
- `known_type_eliminates_guard` — type already known, guard removed
- `incompatible_type_forces_jump` — type conflicts, conditional jump becomes unconditional
- `num_subsumes_int_float` — num check satisfied by int or float
- `dynamic_to_field` — load_dynamic/store_dynamic narrowed to field access
- `dynamic_to_index` — load_dynamic/store_dynamic narrowed to index access
**simplify_algebra:**
- `add_zero`, `sub_zero`, `mul_one`, `div_one` — identity operations become moves
- `mul_zero` — multiplication by zero becomes constant
- `self_eq`, `self_ne` — same-slot comparisons become constants
**simplify_booleans:**
- `not_jump_false_fusion` — not + jump_false fused into jump_true
- `not_jump_true_fusion` — not + jump_true fused into jump_false
- `double_not` — not + not collapsed to move
**eliminate_moves:**
- `self_move` — move to same slot becomes nop
**eliminate_dead_jumps:**
- `jump_to_next` — jump to immediately following label becomes nop
### Canonical IR Format
The `--ir-all`, `--ir-before`, and `--ir-after` flags produce a deterministic text representation of the IR:
```
fn fib (args=1, slots=26)
@0 access s2, 2
@1 is_int s4, s1 ; [guard]
@2 jump_false s4, "rel_ni_2" ; [branch]
@3 --- nop (tc) ---
@4 jump "rel_ni_2" ; [branch]
@5 lt_int s3, s1, s2
@6 jump "rel_done_4" ; [branch]
rel_ni_2:
@8 is_num s4, s1 ; [guard]
```
Properties:
- `@N` is the raw array index, stable across passes (passes replace, never insert or delete)
- `sN` prefix distinguishes slot operands from literal values
- String operands are quoted
- Labels appear as indented headers with a colon
- Category tags in brackets: `[guard]`, `[branch]`, `[load]`, `[store]`, `[call]`, `[arith]`, `[move]`, `[const]`
- Nops shown as `--- nop (reason) ---` with reason codes: `tc` (type check), `bl` (boolean), `mv` (move), `dj` (dead jump), `ur` (unreachable)
### Examples
```bash
# what passes changed something?
./cell --core . ir_report.ce --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'
# diff IR before and after optimization
./cell --core . ir_report.ce --ir-all myfile.ce | jq -r 'select(.type == "ir") | .text'
# full report for analysis
./cell --core . ir_report.ce --full myfile.ce > report.json
```
## ir_stats.cm
A utility module used by `ir_report.ce` and available for custom tooling. Not a standalone tool.
```javascript
var ir_stats = use("ir_stats")
ir_stats.detailed_stats(func) // categorized instruction counts
ir_stats.ir_fingerprint(func) // djb2 hash of instruction array
ir_stats.canonical_ir(func, name, opts) // deterministic text representation
ir_stats.type_snapshot(slot_types) // frozen copy of type map
ir_stats.type_delta(before_types, after_types) // compute type changes
ir_stats.category_tag(op) // classify an opcode
```
### Instruction Categories
`detailed_stats` classifies each instruction into one of these categories:
| Category | Opcodes |
|----------|---------|
| load | `load_field`, `load_index`, `load_dynamic`, `get`, `access` (non-constant) |
| store | `store_field`, `store_index`, `store_dynamic`, `set_var`, `put`, `push` |
| branch | `jump`, `jump_true`, `jump_false`, `jump_not_null` |
| call | `invoke`, `goinvoke` |
| guard | `is_int`, `is_text`, `is_num`, `is_bool`, `is_null`, `is_array`, `is_func`, `is_record`, `is_stone` |
| arith | `add_int`, `sub_int`, ..., `add_float`, ..., `concat`, `neg_int`, `neg_float`, bitwise ops |
| move | `move` |
| const | `int`, `true`, `false`, `null`, `access` (with constant value) |
| label | string entries that are not nops |
| nop | strings starting with `_nop_` |
| other | everything else (`frame`, `setarg`, `array`, `record`, `function`, `return`, etc.) |

View File

@@ -19,7 +19,8 @@ mypackage/
├── helper/
│ └── math.cm # nested module
├── render.c # C extension
└── _internal.cm # private module (underscore prefix)
└── internal/
└── helpers.cm # private module (internal/ only)
```
## pit.toml
@@ -60,12 +61,12 @@ use('json') // core module
### Private Modules
Files starting with underscore are private:
Files in the `internal/` directory are private to their package:
```javascript
// _internal.cm is only accessible within the same package
use('internal') // OK from same package
use('myapp/internal') // Error from other packages
// internal/helpers.cm is only accessible within the same package
use('internal/helpers') // OK from same package
use('myapp/internal/helpers') // Error from other packages
```
## Package Locators
@@ -105,8 +106,11 @@ Local packages are symlinked into the shop, making development seamless.
│ └── work/
│ └── mylib -> /Users/john/work/mylib
├── lib/
│ ├── local.dylib
└── gitea_pockle_world_john_prosperon.dylib
│ ├── core/
│ ├── fd.dylib
│ │ └── time.mach
│ └── gitea_pockle_world_john_prosperon/
│ └── sprite.dylib
├── build/
│ └── <content-addressed cache>
├── cache/
@@ -171,16 +175,16 @@ pit link delete gitea.pockle.world/john/prosperon
## C Extensions
C files in a package are compiled into a dynamic library:
C files in a package are compiled into per-file dynamic libraries:
```
mypackage/
├── pit.toml
├── render.c # compiled to mypackage.dylib
└── render.cm # optional ƿit wrapper
├── render.c # compiled to lib/mypackage/render.dylib
└── physics.c # compiled to lib/mypackage/physics.dylib
```
The library is named after the package and placed in `~/.pit/lib/`.
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.
See [Writing C Modules](/docs/c-modules/) for details.

223
docs/shop.md Normal file
View File

@@ -0,0 +1,223 @@
---
title: "Shop Architecture"
description: "How the shop resolves, compiles, caches, and loads modules"
weight: 35
type: "docs"
---
The shop is the module resolution and loading engine behind `use()`. It handles finding modules, compiling them, caching the results, and loading C extensions. The shop lives in `internal/shop.cm`.
## Startup Pipeline
When `pit` runs a program, startup takes one of two paths:
### Fast path (warm cache)
```
C runtime → engine.cm (from cache) → shop.cm → user program
```
The C runtime hashes the source of `internal/engine.cm` with BLAKE2 and looks up the hash in the content-addressed cache (`~/.pit/build/<hash>`). On a cache hit, engine.cm loads directly — no bootstrap involved.
### Cold path (first run or cache cleared)
```
C runtime → bootstrap.cm → (seeds cache) → engine.cm (from cache) → shop.cm → user program
```
On a cache miss, the C runtime loads `boot/bootstrap.cm.mcode` (a pre-compiled seed). Bootstrap compiles engine.cm and the pipeline modules (tokenize, parse, fold, mcode, streamline) from source and caches the results. The C runtime then retries the engine cache lookup, which now succeeds.
### Engine
**engine.cm** is self-sufficient. It loads its own compilation pipeline from the content-addressed cache, with fallback to the pre-compiled seeds in `boot/`. It defines `analyze()` (source to AST), `compile_to_blob()` (AST to binary blob), and `use_core()` for loading core modules. It creates the actor runtime and loads shop.cm via `use_core('internal/shop')`.
### Shop
**shop.cm** receives its dependencies through the module environment — `analyze`, `run_ast_fn`, `use_cache`, `shop_path`, `runtime_env`, `content_hash`, `cache_path`, and others. It defines `Shop.use()`, which is the function behind every `use()` call in user code.
### Cache invalidation
All caching is content-addressed by BLAKE2 hash of the source. When any source file changes, its hash changes and the old cache entry is simply never looked up again. No manual invalidation is needed. To force a full rebuild, delete `~/.pit/build/`.
## Module Resolution
When `use('path')` is called from a package context, the shop resolves the module through a multi-layer search. Both the `.cm` script file and C symbol are resolved independently, and the one with the narrowest scope wins.
### Resolution Order
For a call like `use('sprite')` from package `myapp`:
1. **Own package**`~/.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
3. **Core** — built-in core modules and internal C symbols
For calls without a package context (from core modules), only core is searched.
### Private Modules
Paths starting with `internal/` are private to their package:
```javascript
use('internal/helpers') // OK from within the same package
// Cannot be accessed from other packages
```
### Explicit Package Imports
Paths containing a dot in the first component are treated as explicit package references:
```javascript
use('gitea.pockle.world/john/renderer/sprite')
// Resolves directly to the renderer package's sprite.cm
```
## Compilation and Caching
Every module goes through a content-addressed caching pipeline. The cache key is the BLAKE2 hash of the source content, so changing the source 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
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.
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:
```
~/.pit/build/
├── a1b2c3d4... # cached bytecode blob (no extension)
├── c9d0e1f2...mcode # cached JSON IR
└── f3a4b5c6... # compiled dylib (checked before copying to lib/)
```
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.
### Core Module Caching
Core modules loaded via `use_core()` in engine.cm follow the same content-addressed pattern. On first use, a module is compiled from source and cached by the BLAKE2 hash of its source content. Subsequent loads with unchanged source hit the cache directly.
User scripts (`.ce` files) are also cached. The first run compiles and caches; subsequent runs with unchanged source load from cache.
## C Extension Resolution
C extensions are resolved alongside script modules. A C module is identified by a symbol name derived from the package and file name:
```
package: gitea.pockle.world/john/prosperon
file: sprite.c
symbol: js_gitea_pockle_world_john_prosperon_sprite_use
```
### C Resolution Sources
1. **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)
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/`.
### Name Collisions
Having both a `.cm` script and a `.c` file with the same stem at the same scope is a **build error**. For example, `render.cm` and `render.c` in the same directory will fail. Use distinct names — e.g., `render.c` for the C implementation and `render_utils.cm` for the script wrapper.
## Environment Injection
When a module is loaded, the shop builds an `env` object that becomes the module's set of free variables. This includes:
- **Runtime functions** — `logical`, `some`, `every`, `starts_with`, `ends_with`, `is_actor`, `log`, `send`, `fallback`, `parallel`, `race`, `sequence`
- **Capability injections** — actor intrinsics like `$self`, `$delay`, `$start`, `$receiver`, `$fd`, etc.
- **`use` function** — scoped to the module's package context
The set of injected capabilities is controlled by `script_inject_for()`, which can be tuned per package or file.
## Shop Configuration
The shop reads an optional `shop.toml` file from the shop root (`~/.pit/shop.toml`). This file controls which loading methods are permitted through policy flags.
### Policy Flags
All flags default to `true`. Set a flag to `false` to disable that loading method.
```toml
[policy]
allow_dylib = true # per-file .dylib loading (requires dlopen)
allow_static = true # statically linked C symbols (fat builds)
allow_mach = true # pre-compiled .mach bytecode (lib/ and build cache)
allow_compile = true # on-the-fly source compilation
```
### Example Configurations
**Production lockdown** — only use pre-compiled artifacts, never compile from source:
```toml
[policy]
allow_compile = false
```
**Pure-script mode** — bytecode only, no native code:
```toml
[policy]
allow_dylib = false
allow_static = false
```
**No dlopen platforms** — static linking and bytecode only:
```toml
[policy]
allow_dylib = false
```
If `shop.toml` is missing or has no `[policy]` section, all methods are enabled (default behavior).
## Shop Directory Layout
```
~/.pit/
├── 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
├── cache/ # downloaded package zip archives
├── lock.toml # installed package versions and commit hashes
├── link.toml # local development link overrides
└── shop.toml # optional shop configuration and policy flags
```
## Key Files
| File | Role |
|------|------|
| `internal/bootstrap.cm` | Minimal cache seeder (cold start only) |
| `internal/engine.cm` | Self-sufficient entry point: compilation pipeline, actor runtime, `use_core()` |
| `internal/shop.cm` | Module resolution, compilation, caching, C extension loading |
| `internal/os.c` | OS intrinsics: dylib ops, internal symbol lookup, embedded modules |
| `package.cm` | Package directory detection, alias resolution, file listing |
| `link.cm` | Development link management (link.toml read/write) |
| `boot/*.cm.mcode` | Pre-compiled pipeline seeds (tokenize, parse, fold, mcode, bootstrap) |

3
docs/spec/.pages Normal file
View File

@@ -0,0 +1,3 @@
nav:
- pipeline.md
- mcode.md

View File

@@ -1,11 +1,13 @@
---
title: "Register VM"
description: "Register-based virtual machine (Mach)"
description: "Binary encoding of the Mach bytecode interpreter"
---
## Overview
The Mach VM is a register-based virtual machine using 32-bit instructions. It is modeled after Lua's register VM — operands are register indices rather than stack positions, reducing instruction count and improving performance.
The Mach VM is a register-based virtual machine that directly interprets the [Mcode IR](mcode.md) instruction set as compact 32-bit binary bytecode. It is modeled after Lua's register VM — operands are register indices rather than stack positions, reducing instruction count and improving performance.
The Mach serializer (`mach.c`) converts streamlined mcode JSON into binary instructions. Since the Mach bytecode is a direct encoding of the mcode, the [Mcode IR](mcode.md) reference is the authoritative instruction set documentation.
## Instruction Formats
@@ -45,95 +47,12 @@ Used for unconditional jumps with a 24-bit signed offset.
## Registers
Each function frame has a fixed number of register slots, determined at compile time. Registers hold:
Each function frame has a fixed number of register slots, determined at compile time:
- **R(0)** — `this` binding
- **R(1)..R(arity)** — function arguments
- **R(arity+1)..** — local variables and temporaries
## Instruction Set
### Loading
| Opcode | Format | Description |
|--------|--------|-------------|
| `LOADK` | iABx | `R(A) = K(Bx)` — load from constant pool |
| `LOADI` | iAsBx | `R(A) = sBx` — load small integer |
| `LOADNULL` | iA | `R(A) = null` |
| `LOADTRUE` | iA | `R(A) = true` |
| `LOADFALSE` | iA | `R(A) = false` |
| `MOVE` | iABC | `R(A) = R(B)` — register copy |
### Arithmetic
| Opcode | Format | Description |
|--------|--------|-------------|
| `ADD` | iABC | `R(A) = R(B) + R(C)` |
| `SUB` | iABC | `R(A) = R(B) - R(C)` |
| `MUL` | iABC | `R(A) = R(B) * R(C)` |
| `DIV` | iABC | `R(A) = R(B) / R(C)` |
| `MOD` | iABC | `R(A) = R(B) % R(C)` |
| `POW` | iABC | `R(A) = R(B) ^ R(C)` |
| `NEG` | iABC | `R(A) = -R(B)` |
| `INC` | iABC | `R(A) = R(B) + 1` |
| `DEC` | iABC | `R(A) = R(B) - 1` |
### Comparison
| Opcode | Format | Description |
|--------|--------|-------------|
| `EQ` | iABC | `R(A) = R(B) == R(C)` |
| `NEQ` | iABC | `R(A) = R(B) != R(C)` |
| `LT` | iABC | `R(A) = R(B) < R(C)` |
| `LE` | iABC | `R(A) = R(B) <= R(C)` |
| `GT` | iABC | `R(A) = R(B) > R(C)` |
| `GE` | iABC | `R(A) = R(B) >= R(C)` |
### Property Access
| Opcode | Format | Description |
|--------|--------|-------------|
| `GETFIELD` | iABC | `R(A) = R(B)[K(C)]` — named property |
| `SETFIELD` | iABC | `R(A)[K(B)] = R(C)` — set named property |
| `GETINDEX` | iABC | `R(A) = R(B)[R(C)]` — computed property |
| `SETINDEX` | iABC | `R(A)[R(B)] = R(C)` — set computed property |
### Variable Resolution
| Opcode | Format | Description |
|--------|--------|-------------|
| `GETNAME` | iABx | Unresolved variable (compiler placeholder) |
| `GETINTRINSIC` | iABx | Global intrinsic / built-in |
| `GETENV` | iABx | Module environment variable |
| `GETUP` | iABC | `R(A) = UpFrame(B).slots[C]` — closure upvalue |
| `SETUP` | iABC | `UpFrame(A).slots[B] = R(C)` — set closure upvalue |
### Control Flow
| Opcode | Format | Description |
|--------|--------|-------------|
| `JMP` | isJ | Unconditional jump |
| `JMPTRUE` | iAsBx | Jump if `R(A)` is true |
| `JMPFALSE` | iAsBx | Jump if `R(A)` is false |
| `JMPNULL` | iAsBx | Jump if `R(A)` is null |
### Function Calls
| Opcode | Format | Description |
|--------|--------|-------------|
| `CALL` | iABC | Call `R(A)` with `B` args starting at `R(A+1)`, `C`=keep result |
| `RETURN` | iA | Return `R(A)` |
| `RETNIL` | — | Return null |
| `CLOSURE` | iABx | Create closure from function pool entry `Bx` |
### Object / Array
| Opcode | Format | Description |
|--------|--------|-------------|
| `NEWOBJECT` | iA | `R(A) = {}` |
| `NEWARRAY` | iABC | `R(A) = array(B)` |
| `PUSH` | iABC | Push `R(B)` to array `R(A)` |
## JSCodeRegister
The compiled output for a function:
@@ -149,7 +68,7 @@ struct JSCodeRegister {
uint32_t func_count; // nested function count
JSCodeRegister **functions; // nested function table
JSValue name; // function name
uint16_t disruption_pc; // exception handler offset
uint16_t disruption_pc; // disruption handler offset
};
```
@@ -163,3 +82,14 @@ Named property instructions (`LOAD_FIELD`, `STORE_FIELD`, `DELETE`) use the iABC
2. `LOAD_DYNAMIC` / `STORE_DYNAMIC` / `DELETEINDEX` — use the register-based variant
This is transparent to the mcode compiler and streamline optimizer.
## Arithmetic Dispatch
Arithmetic ops (ADD, SUB, MUL, DIV, MOD, POW) are executed inline without calling the polymorphic `reg_vm_binop()` helper. Since mcode's type guard dispatch guarantees both operands are numbers:
1. **Int-int fast path**: `JS_VALUE_IS_BOTH_INT` → native integer arithmetic with int32 overflow check. Overflow promotes to float64.
2. **Float fallback**: `JS_ToFloat64` → native floating-point operation. Non-finite results produce null.
DIV and MOD check for zero divisor (→ null). POW uses `pow()` with non-finite handling for finite inputs.
Comparison ops (EQ through GE) and bitwise ops still use `reg_vm_binop()` for their slow paths, as they handle a wider range of type combinations (string comparisons, null equality, etc.).

View File

@@ -1,23 +1,287 @@
---
title: "Mcode IR"
description: "JSON-based intermediate representation"
description: "Instruction set reference for the JSON-based intermediate representation"
---
## Overview
Mcode is a JSON-based intermediate representation that can be interpreted directly. It represents the same operations as the Mach register VM but uses string-based instruction dispatch rather than binary opcodes. Mcode is intended as an intermediate step toward native code compilation.
## Pipeline
Mcode is the intermediate representation at the center of the ƿit compilation pipeline. All source code is lowered to mcode before execution or native compilation. The mcode instruction set is the **authoritative reference** for the operations supported by the ƿit runtime — the Mach VM bytecode is a direct binary encoding of these same instructions.
```
Source → Tokenize → Parse (AST) → Fold → Mcode (JSON) → Streamline → Mach VM (default)
→ Mcode Interpreter
→ QBE → Native
Source → Tokenize → Parse → Fold → Mcode → Streamline → Machine
```
Mcode is produced by `mcode.cm`, which lowers the folded AST to JSON instruction arrays. The streamline optimizer (`streamline.cm`) then eliminates redundant operations. The result is serialized to binary bytecode by the Mach compiler (`mach.c`), interpreted directly by `mcode.c`, or lowered to QBE IL by `qbe_emit.cm` for native compilation. See [Compilation Pipeline](pipeline.md) for the full overview.
Mcode is produced by `mcode.cm`, optimized by `streamline.cm`, then either serialized to 32-bit bytecode for the Mach VM (`mach.c`), or lowered to QBE/LLVM IL for native compilation (`qbe_emit.cm`). See [Compilation Pipeline](pipeline.md) for the full overview.
### Function Proxy Decomposition
## Module Structure
An `.mcode` file is a JSON object representing a compiled module:
| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Module name (typically the source filename) |
| `filename` | string | Source filename |
| `data` | object | Constant pool — string and number literals used by instructions |
| `main` | function | The top-level function (module body) |
| `functions` | array | Nested function definitions (referenced by `function dest, id`) |
### Function Record
Each function (both `main` and entries in `functions`) has:
| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Function name (`"<anonymous>"` for lambdas) |
| `filename` | string | Source filename |
| `nr_args` | integer | Number of parameters |
| `nr_slots` | integer | Total register slots needed (args + locals + temporaries) |
| `nr_close_slots` | integer | Number of closure slots captured from parent scope |
| `disruption_pc` | integer | Instruction index of the disruption handler (0 if none) |
| `instructions` | array | Instruction arrays and label strings |
Slot 0 is reserved. Slots 1 through `nr_args` hold parameters. Remaining slots up to `nr_slots - 1` are locals and temporaries.
## Instruction Format
Each instruction is a JSON array. The first element is the instruction name (string), followed by operands. The last two elements are line and column numbers for source mapping:
```json
["add_int", dest, a, b, line, col]
["load_field", dest, obj, "key", line, col]
["jump", "label_name"]
```
Operands are register slot numbers (integers), constant values (strings, numbers), or label names (strings).
## Instruction Reference
### Loading and Constants
| Instruction | Operands | Description |
|-------------|----------|-------------|
| `access` | `dest, name` | Load variable by name (intrinsic or environment) |
| `int` | `dest, value` | Load integer constant |
| `true` | `dest` | Load boolean `true` |
| `false` | `dest` | Load boolean `false` |
| `null` | `dest` | Load `null` |
| `move` | `dest, src` | Copy register value |
| `function` | `dest, id` | Load nested function by index |
| `regexp` | `dest, pattern` | Create regexp object |
### Arithmetic — Integer
| Instruction | Operands | Description |
|-------------|----------|-------------|
| `add_int` | `dest, a, b` | `dest = a + b` (integer) |
| `sub_int` | `dest, a, b` | `dest = a - b` (integer) |
| `mul_int` | `dest, a, b` | `dest = a * b` (integer) |
| `div_int` | `dest, a, b` | `dest = a / b` (integer) |
| `mod_int` | `dest, a, b` | `dest = a % b` (integer) |
| `neg_int` | `dest, src` | `dest = -src` (integer) |
### Arithmetic — Float
| Instruction | Operands | Description |
|-------------|----------|-------------|
| `add_float` | `dest, a, b` | `dest = a + b` (float) |
| `sub_float` | `dest, a, b` | `dest = a - b` (float) |
| `mul_float` | `dest, a, b` | `dest = a * b` (float) |
| `div_float` | `dest, a, b` | `dest = a / b` (float) |
| `mod_float` | `dest, a, b` | `dest = a % b` (float) |
| `neg_float` | `dest, src` | `dest = -src` (float) |
### Arithmetic — Generic
| Instruction | Operands | Description |
|-------------|----------|-------------|
| `pow` | `dest, a, b` | `dest = a ^ b` (exponentiation) |
### Text
| Instruction | Operands | Description |
|-------------|----------|-------------|
| `concat` | `dest, a, b` | `dest = a ~ b` (text concatenation) |
### Comparison — Integer
| Instruction | Operands | Description |
|-------------|----------|-------------|
| `eq_int` | `dest, a, b` | `dest = a == b` (integer) |
| `ne_int` | `dest, a, b` | `dest = a != b` (integer) |
| `lt_int` | `dest, a, b` | `dest = a < b` (integer) |
| `le_int` | `dest, a, b` | `dest = a <= b` (integer) |
| `gt_int` | `dest, a, b` | `dest = a > b` (integer) |
| `ge_int` | `dest, a, b` | `dest = a >= b` (integer) |
### Comparison — Float
| Instruction | Operands | Description |
|-------------|----------|-------------|
| `eq_float` | `dest, a, b` | `dest = a == b` (float) |
| `ne_float` | `dest, a, b` | `dest = a != b` (float) |
| `lt_float` | `dest, a, b` | `dest = a < b` (float) |
| `le_float` | `dest, a, b` | `dest = a <= b` (float) |
| `gt_float` | `dest, a, b` | `dest = a > b` (float) |
| `ge_float` | `dest, a, b` | `dest = a >= b` (float) |
### Comparison — Text
| Instruction | Operands | Description |
|-------------|----------|-------------|
| `eq_text` | `dest, a, b` | `dest = a == b` (text) |
| `ne_text` | `dest, a, b` | `dest = a != b` (text) |
| `lt_text` | `dest, a, b` | `dest = a < b` (lexicographic) |
| `le_text` | `dest, a, b` | `dest = a <= b` (lexicographic) |
| `gt_text` | `dest, a, b` | `dest = a > b` (lexicographic) |
| `ge_text` | `dest, a, b` | `dest = a >= b` (lexicographic) |
### Comparison — Boolean
| Instruction | Operands | Description |
|-------------|----------|-------------|
| `eq_bool` | `dest, a, b` | `dest = a == b` (boolean) |
| `ne_bool` | `dest, a, b` | `dest = a != b` (boolean) |
### Comparison — Special
| Instruction | Operands | Description |
|-------------|----------|-------------|
| `is_identical` | `dest, a, b` | Object identity check (same reference) |
| `eq_tol` | `dest, a, b` | Equality with tolerance |
| `ne_tol` | `dest, a, b` | Inequality with tolerance |
### Type Checks
Inlined from intrinsic function calls. Each sets `dest` to `true` or `false`.
| Instruction | Operands | Description |
|-------------|----------|-------------|
| `is_int` | `dest, src` | Check if integer |
| `is_num` | `dest, src` | Check if number (integer or float) |
| `is_text` | `dest, src` | Check if text |
| `is_bool` | `dest, src` | Check if logical |
| `is_null` | `dest, src` | Check if null |
| `is_array` | `dest, src` | Check if array |
| `is_func` | `dest, src` | Check if function |
| `is_record` | `dest, src` | Check if record (object) |
| `is_stone` | `dest, src` | Check if stone (immutable) |
| `is_proxy` | `dest, src` | Check if function proxy (arity 2) |
### Logical
| Instruction | Operands | Description |
|-------------|----------|-------------|
| `not` | `dest, src` | Logical NOT |
| `and` | `dest, a, b` | Logical AND |
| `or` | `dest, a, b` | Logical OR |
### Bitwise
| Instruction | Operands | Description |
|-------------|----------|-------------|
| `bitand` | `dest, a, b` | Bitwise AND |
| `bitor` | `dest, a, b` | Bitwise OR |
| `bitxor` | `dest, a, b` | Bitwise XOR |
| `bitnot` | `dest, src` | Bitwise NOT |
| `shl` | `dest, a, b` | Shift left |
| `shr` | `dest, a, b` | Arithmetic shift right |
| `ushr` | `dest, a, b` | Unsigned shift right |
### Property Access
Memory operations come in typed variants. The compiler selects the appropriate variant based on `type_tag` and `access_kind` annotations from parse and fold.
| Instruction | Operands | Description |
|-------------|----------|-------------|
| `load_field` | `dest, obj, key` | Load record property by string key |
| `store_field` | `obj, val, key` | Store record property by string key |
| `load_index` | `dest, obj, idx` | Load array element by integer index |
| `store_index` | `obj, val, idx` | Store array element by integer index |
| `load_dynamic` | `dest, obj, key` | Load property (dispatches at runtime) |
| `store_dynamic` | `obj, val, key` | Store property (dispatches at runtime) |
| `delete` | `obj, key` | Delete property |
| `in` | `dest, obj, key` | Check if property exists |
| `length` | `dest, src` | Get length of array or text |
### Object and Array Construction
| Instruction | Operands | Description |
|-------------|----------|-------------|
| `record` | `dest` | Create empty record `{}` |
| `array` | `dest, n` | Create empty array (elements added via `push`) |
| `push` | `arr, val` | Push value to array |
| `pop` | `dest, arr` | Pop value from array |
### Function Calls
Function calls are decomposed into three instructions:
| Instruction | Operands | Description |
|-------------|----------|-------------|
| `frame` | `dest, fn, argc` | Allocate call frame for `fn` with `argc` arguments |
| `setarg` | `frame, idx, val` | Set argument `idx` in call frame |
| `invoke` | `frame, result` | Execute the call, store result |
| `goframe` | `dest, fn, argc` | Allocate frame for async/concurrent call |
| `goinvoke` | `frame, result` | Invoke async/concurrent call |
### Variable Resolution
| Instruction | Operands | Description |
|-------------|----------|-------------|
| `access` | `dest, name` | Load variable (intrinsic or module environment) |
| `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 |
### Control Flow
| Instruction | Operands | Description |
|-------------|----------|-------------|
| `LABEL` | `name` | Define a named label (not executed) |
| `jump` | `label` | Unconditional jump |
| `jump_true` | `cond, label` | Jump if `cond` is true |
| `jump_false` | `cond, label` | Jump if `cond` is false |
| `jump_not_null` | `val, label` | Jump if `val` is not null |
| `return` | `src` | Return value from function |
| `disrupt` | — | Trigger disruption (error) |
## Typed Instruction Design
A key design principle of mcode is that **every type check is an explicit instruction**. Arithmetic and comparison operations come in type-specialized variants (`add_int`, `add_float`, `eq_text`, etc.) rather than a single polymorphic instruction.
When type information is available from the fold stage, the compiler emits the typed variant directly. When the type is unknown, the compiler emits a type-check/dispatch pattern:
```json
["is_int", check, a]
["jump_false", check, "float_path"]
["add_int", dest, a, b]
["jump", "done"]
["LABEL", "float_path"]
["add_float", dest, a, b]
["LABEL", "done"]
```
The [Streamline Optimizer](streamline.md) eliminates dead branches when types are statically known, collapsing the dispatch to a single typed instruction.
## Intrinsic Inlining
The mcode compiler recognizes calls to built-in intrinsic functions and emits direct opcodes instead of the generic frame/setarg/invoke call sequence:
| Source call | Emitted instruction |
|-------------|-------------------|
| `is_array(x)` | `is_array dest, src` |
| `is_function(x)` | `is_func dest, src` |
| `is_object(x)` | `is_record dest, src` |
| `is_stone(x)` | `is_stone dest, src` |
| `is_integer(x)` | `is_int dest, src` |
| `is_text(x)` | `is_text dest, src` |
| `is_number(x)` | `is_num dest, src` |
| `is_logical(x)` | `is_bool dest, src` |
| `is_null(x)` | `is_null dest, src` |
| `length(x)` | `length dest, src` |
| `push(arr, val)` | `push arr, val` |
## Function Proxy Decomposition
When the compiler encounters a method call `obj.method(args)`, it emits a branching pattern to handle ƿit's function proxy protocol. An arity-2 function used as a proxy target receives the method name and argument array instead of a normal method call:
@@ -25,9 +289,8 @@ When the compiler encounters a method call `obj.method(args)`, it emits a branch
["is_proxy", check, obj]
["jump_false", check, "record_path"]
// Proxy path: call obj(name, [args...]) with this=null
["access", name_slot, "method"]
["array", args_arr, N, arg0, arg1, ...]
["array", args_arr, N, arg0, arg1]
["null", null_slot]
["frame", f, obj, 2]
["setarg", f, 0, null_slot]
@@ -41,21 +304,38 @@ When the compiler encounters a method call `obj.method(args)`, it emits a branch
["frame", f2, method, N]
["setarg", f2, 0, obj]
["setarg", f2, 1, arg0]
...
["invoke", f2, dest]
["LABEL", "done"]
```
The streamline optimizer can eliminate the dead branch when the type of `obj` is statically known.
## Labels and Control Flow
## JSMCode Structure
Control flow uses named labels instead of numeric offsets:
```json
["LABEL", "loop_start"]
["add_int", 1, 1, 2]
["jump_false", 3, "loop_end"]
["jump", "loop_start"]
["LABEL", "loop_end"]
```
Labels are collected into a name-to-index map during loading, enabling O(1) jump resolution. The Mach serializer converts label names to numeric offsets in the binary bytecode.
## Nop Convention
The streamline optimizer replaces eliminated instructions with nop strings (e.g., `_nop_tc_1`, `_nop_bl_2`). Nop strings are skipped during interpretation and native code emission but preserved in the instruction array to maintain positional stability for jump targets.
## Internal Structures
### JSMCode (Mcode Interpreter)
```c
struct JSMCode {
uint16_t nr_args; // argument count
uint16_t nr_slots; // register count
cJSON **instrs; // pre-flattened instruction array
cJSON **instrs; // instruction array
uint32_t instr_count; // number of instructions
struct {
@@ -70,74 +350,25 @@ struct JSMCode {
cJSON *json_root; // keeps JSON alive
const char *name; // function name
const char *filename; // source file
uint16_t disruption_pc; // exception handler offset
uint16_t disruption_pc; // disruption handler offset
};
```
## Instruction Format
### JSCodeRegister (Mach VM Bytecode)
Each instruction is a JSON array. The first element is the instruction name (string), followed by operands (typically `[op, dest, ...args, line, col]`):
```json
["access", 3, 5, 1, 9]
["load_index", 10, 4, 9, 5, 11]
["store_dynamic", 4, 11, 12, 6, 3]
["frame", 15, 14, 1, 7, 7]
["setarg", 15, 0, 16, 7, 7]
["invoke", 15, 13, 7, 7]
```c
struct JSCodeRegister {
uint16_t arity; // argument count
uint16_t nr_slots; // total register count
uint32_t cpool_count; // constant pool size
JSValue *cpool; // constant pool
uint32_t instr_count; // instruction count
MachInstr32 *instructions; // 32-bit instruction array
uint32_t func_count; // nested function count
JSCodeRegister **functions; // nested function table
JSValue name; // function name
uint16_t disruption_pc; // disruption handler offset
};
```
### Typed Load/Store
Memory operations come in typed variants for optimization:
- `load_index dest, obj, idx` — array element by integer index
- `load_field dest, obj, key` — record property by string key
- `load_dynamic dest, obj, key` — unknown; dispatches at runtime
- `store_index obj, val, idx` — array element store
- `store_field obj, val, key` — record property store
- `store_dynamic obj, val, key` — unknown; dispatches at runtime
The compiler selects the appropriate variant based on `type_tag` and `access_kind` annotations from parse and fold.
### Decomposed Calls
Function calls are split into separate instructions:
- `frame dest, fn, argc` — allocate call frame
- `setarg frame, idx, val` — set argument
- `invoke frame, result` — execute the call
## Labels
Control flow uses named labels instead of numeric offsets:
```json
["LABEL", "loop_start"]
["ADD", 1, 1, 2]
["JMPFALSE", 3, "loop_end"]
["JMP", "loop_start"]
["LABEL", "loop_end"]
```
Labels are collected into a name-to-index map during loading, enabling O(1) jump resolution.
## Differences from Mach
| Property | Mcode | Mach |
|----------|-------|------|
| Instructions | cJSON arrays | 32-bit binary |
| Dispatch | String comparison | Switch on opcode byte |
| Constants | Inline in JSON | Separate constant pool |
| Jump targets | Named labels | Numeric offsets |
| Memory | Heap (cJSON nodes) | Off-heap (malloc) |
## Purpose
Mcode serves as an inspectable, debuggable intermediate format:
- **Human-readable** — the JSON representation can be printed and examined
- **Language-independent** — any tool that produces the correct JSON can target the ƿit runtime
- **Compilation target** — the Mach compiler can consume mcode as input, and future native code generators can work from the same representation
The cost of string-based dispatch makes mcode slower than the binary Mach VM, so it is primarily useful during development and as a compilation intermediate rather than for production execution.
The Mach serializer (`mach.c`) converts the JSON mcode into compact 32-bit instructions with a constant pool. See [Register VM](mach.md) for the binary encoding formats.

View File

@@ -26,7 +26,7 @@ Every heap-allocated object begins with a 64-bit header word (`objhdr_t`):
### Flags (bits 3-7)
- **Bit 3 (S)** — Stone flag. If set, the object is immutable and excluded from GC.
- **Bit 3 (S)** — Stone flag. If set, the object is immutable. Stone text in the constant table (ct) is not copied by GC since it lives outside the heap; stone objects on the GC heap are copied normally.
- **Bit 4 (P)** — Properties flag.
- **Bit 5 (A)** — Array flag.
- **Bit 7 (R)** — Reserved.
@@ -69,7 +69,9 @@ struct JSText {
};
```
Text is stored as UTF-32, with two 32-bit codepoints packed per 64-bit word. When a text object is stoned, the length field is repurposed to cache the hash value (computed via `fash64`), since stoned text is immutable and the hash never changes.
Text is stored as UTF-32, with two 32-bit codepoints packed per 64-bit word.
A mutable text (pretext) uses capacity for the allocated slot count and length for the current codepoint count. When a pretext is stoned, the capacity field is set to the actual length (codepoint count), and the length field is zeroed for use as a lazy hash cache (computed via `fash64` on first use as a key). Since stoned text is immutable, the hash never changes. Stoning is done in-place — no new allocation is needed.
## Record
@@ -111,7 +113,7 @@ struct JSFrame {
objhdr_t header; // type=6, capacity=slot count
JSValue function; // owning function
JSValue caller; // parent frame
uint32_t return_pc; // return address
JSValue address; // return address
JSValue slots[]; // [this][args][captured][locals][temps]
};
```
@@ -138,4 +140,4 @@ All objects are aligned to 8 bytes. The total size in bytes for each type:
| Record | `8 + 8 + 8 + (capacity + 1) * 16` |
| Function | `sizeof(JSFunction)` (fixed) |
| Code | `sizeof(JSFunctionBytecode)` (fixed) |
| Frame | `8 + 8 + 8 + 4 + capacity * 8` |
| Frame | `8 + 8 + 8 + 8 + capacity * 8` |

View File

@@ -5,14 +5,17 @@ description: "Overview of the compilation stages and optimizations"
## Overview
The compilation pipeline transforms source code through several stages, each adding information or lowering the representation toward execution. All backends share the same path through mcode and streamline. There are three execution backends: the Mach register VM (default), the Mcode interpreter (debug), and native code via QBE (experimental).
The compilation pipeline transforms source code through several stages, each adding information or lowering the representation toward execution. All backends share the same path through mcode and streamline.
```
Source → Tokenize → Parse → Fold → Mcode → Streamline → Mach VM (default)
→ Mcode Interpreter
→ QBE → Native
Source → Tokenize → Parse → Fold → Mcode → Streamline → Machine
```
The final **machine** stage has two targets:
- **Mach VM** — a register-based bytecode interpreter that directly executes the mcode instruction set as compact 32-bit binary
- **Native code** — lowers mcode to QBE or LLVM intermediate language, then compiles to machine code for the target CPU architecture
## Stages
### Tokenize (`tokenize.cm`)
@@ -24,7 +27,8 @@ Splits source text into tokens. Handles string interpolation by re-tokenizing te
Converts tokens into an AST. Also performs semantic analysis:
- **Scope records**: For each scope (global, function), builds a record mapping variable names to their metadata: `make` (var/def/function/input), `function_nr`, `nr_uses`, `closure` flag, and `level`.
- **Type tags**: When the right-hand side of a `def` is a syntactically obvious type, stamps `type_tag` on the scope record entry. Derivable types: `"integer"`, `"number"`, `"text"`, `"array"`, `"record"`, `"function"`, `"logical"`, `"null"`.
- **Type tags**: When the right-hand side of a `def` is a syntactically obvious type, stamps `type_tag` on the scope record entry. Derivable types: `"integer"`, `"number"`, `"text"`, `"array"`, `"record"`, `"function"`, `"logical"`. For `def` variables, type tags are also inferred from usage patterns: push (`x[] = v`) implies array, property access (`x.foo = v`) implies record, integer key implies array, text key implies record.
- **Type error detection**: For `def` variables with known type tags, provably wrong operations are reported as compile errors: property access on arrays, push on non-arrays, text keys on arrays, integer keys on records. Only `def` variables are checked because `var` can be reassigned.
- **Intrinsic resolution**: Names used but not locally bound are recorded in `ast.intrinsics`. Name nodes referencing intrinsics get `intrinsic: true`.
- **Access kind**: Subscript (`[`) nodes get `access_kind`: `"index"` for numeric subscripts, `"field"` for string subscripts, omitted otherwise.
- **Tail position**: Return statements where the expression is a call get `tail: true`.
@@ -37,8 +41,8 @@ Operates on the AST. Performs constant folding and type analysis:
- **Constant propagation**: Tracks `def` bindings whose values are known constants.
- **Type propagation**: Extends `type_tag` through operations. When both operands of an arithmetic op have known types, the result type is known. Propagates type tags to reference sites.
- **Intrinsic specialization**: When an intrinsic call's argument types are known, stamps a `hint` on the call node. For example, `length(x)` where x is a known array gets `hint: "array_length"`. Type checks like `is_array(known_array)` are folded to `true`.
- **Purity marking**: Stamps `pure: true` on expressions with no side effects (literals, name references, arithmetic on pure operands).
- **Dead code elimination**: Removes unreachable branches when conditions are known constants.
- **Purity analysis**: Expressions with no side effects are marked pure (literals, name references, arithmetic on pure operands, calls to pure intrinsics). The pure intrinsic set contains only `is_*` sensory functions — they are the only intrinsics guaranteed to never disrupt regardless of argument types. Other intrinsics like `text`, `number`, and `length` can disrupt on wrong argument types and are excluded.
- **Dead code elimination**: Removes unreachable branches when conditions are known constants. Removes unused `var`/`def` declarations with pure initializers. Removes standalone calls to pure intrinsics where the result is discarded.
### Mcode (`mcode.cm`)
@@ -47,57 +51,46 @@ Lowers the AST to a JSON-based intermediate representation with explicit operati
- **Typed load/store**: Emits `load_index` (array by integer), `load_field` (record by string), or `load_dynamic` (unknown) based on type information from fold.
- **Decomposed calls**: Function calls are split into `frame` (create call frame) + `setarg` (set arguments) + `invoke` (execute call).
- **Intrinsic access**: Intrinsic functions are loaded via `access` with an intrinsic marker rather than global lookup.
- **Intrinsic inlining**: Type-check intrinsics (`is_array`, `is_text`, `is_number`, `is_integer`, `is_logical`, `is_null`, `is_function`, `is_object`, `is_stone`), `length`, and `push` are emitted as direct opcodes instead of frame/setarg/invoke call sequences.
- **Disruption handler labels**: When a function has a disruption handler, a label is emitted before the handler code. This allows the streamline optimizer's unreachable code elimination to safely nop dead code after `return` without accidentally eliminating the handler.
- **Tail call marking**: When a return statement's expression is a call and the function has no disruption handler, the final `invoke` is renamed to `tail_invoke`. This marks the call site for future tail call optimization. Functions with disruption handlers cannot use TCO because the handler frame must remain on the stack.
See [Mcode IR](mcode.md) for instruction format details.
See [Mcode IR](mcode.md) for the instruction format and complete instruction reference.
### Streamline (`streamline.cm`)
Optimizes the Mcode IR. Operates per-function:
Optimizes the Mcode IR through a series of independent passes. Operates per-function:
- **Redundant instruction elimination**: Removes no-op patterns and redundant moves.
- **Dead code removal**: Eliminates instructions whose results are never used.
- **Type-based narrowing**: When type information is available, narrows `load_dynamic`/`store_dynamic` to typed variants.
1. **Backward type inference**: Infers parameter types from how they are used in typed operators (`add_int`, `store_index`, `load_field`, `push`, `pop`, etc.). Immutable `def` parameters keep their inferred type across label join points.
2. **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.
### QBE Emit (`qbe_emit.cm`)
See [Streamline Optimizer](streamline.md) for detailed pass descriptions.
Lowers optimized Mcode IR to QBE intermediate language for native code compilation. Each Mcode function becomes a QBE function that calls into the cell runtime (`cell_rt_*` functions) for operations that require the runtime (allocation, intrinsic dispatch, etc.).
### Machine
String constants are interned in a data section. Integer constants are NaN-boxed inline.
The streamlined mcode is lowered to a machine target for execution.
### QBE Macros (`qbe.cm`)
#### Mach VM (default)
Provides operation implementations as QBE IL templates. Each arithmetic, comparison, and type operation is defined as a function that emits the corresponding QBE instructions, handling type dispatch (integer, float, text paths) with proper guard checks.
## Execution Backends
### Mach VM (default)
Binary 32-bit register VM. The Mach serializer (`mach.c`) converts streamlined mcode JSON into compact 32-bit bytecode with a constant pool. Used for production execution and bootstrapping.
The Mach VM is a register-based virtual machine that directly interprets the mcode instruction set as 32-bit binary bytecode. The Mach serializer (`mach.c`) converts streamlined mcode JSON into compact 32-bit instructions with a constant pool. Since the mach bytecode is a direct encoding of the mcode, the [Mcode IR](mcode.md) reference serves as the authoritative instruction set documentation.
```
./cell script.ce
pit script.ce
```
Debug the mach bytecode output:
#### Native Code (QBE / LLVM)
Lowers the streamlined mcode to QBE or LLVM intermediate language for compilation to native machine code. Each mcode function becomes a native function that calls into the ƿit runtime (`cell_rt_*` functions) for operations that require the runtime (allocation, intrinsic dispatch, etc.).
String constants are interned in a data section. Integer constants are encoded inline.
```
./cell --core . --dump-mach script.ce
```
### Mcode Interpreter
JSON-based interpreter. Used for debugging the compilation pipeline.
```
./cell --mcode script.ce
```
### QBE Native (experimental)
Generates QBE IL that can be compiled to native code.
```
./cell --emit-qbe script.ce > output.ssa
pit --emit-qbe script.ce > output.ssa
```
## Files
@@ -111,7 +104,16 @@ Generates QBE IL that can be compiled to native code.
| `streamline.cm` | Mcode IR optimizer |
| `qbe_emit.cm` | Mcode IR → QBE IL emitter |
| `qbe.cm` | QBE IL operation templates |
| `internal/bootstrap.cm` | Pipeline orchestrator |
| `internal/bootstrap.cm` | Cache seeder (cold start only) |
| `internal/engine.cm` | Self-sufficient pipeline loader and orchestrator |
## Debug Tools
| File | Purpose |
|------|---------|
| `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 |
## Test Files
@@ -122,3 +124,5 @@ Generates QBE IL that can be compiled to native code.
| `mcode_test.ce` | Typed load/store, decomposed calls |
| `streamline_test.ce` | Optimization counts, IR before/after |
| `qbe_test.ce` | End-to-end QBE IL generation |
| `test_intrinsics.cm` | Inlined intrinsic opcodes (is_array, length, push, etc.) |
| `test_backward.cm` | Backward type propagation for parameters |

410
docs/spec/streamline.md Normal file
View File

@@ -0,0 +1,410 @@
---
title: "Streamline Optimizer"
description: "Mcode IR optimization passes"
---
## Overview
The streamline optimizer (`streamline.cm`) runs a series of independent passes over the Mcode IR to eliminate redundant operations. Each pass is a standalone function that can be enabled, disabled, or reordered. Passes communicate only through the instruction array they mutate in place, replacing eliminated instructions with nop strings (e.g., `_nop_tc_1`).
The optimizer runs after `mcode.cm` generates the IR and before the result is lowered to the Mach VM or emitted as QBE IL.
```
Fold (AST) → Mcode (JSON IR) → Streamline → Mach VM / QBE
```
## Type Lattice
The optimizer tracks a type for each slot in the register file:
| Type | Meaning |
|------|---------|
| `unknown` | No type information |
| `int` | Integer |
| `float` | Floating-point |
| `num` | Number (subsumes int and float) |
| `text` | String |
| `bool` | Logical (true/false) |
| `null` | Null value |
| `array` | Array |
| `record` | Record (object) |
| `function` | Function |
| `blob` | Binary blob |
Subsumption: `int` and `float` both satisfy a `num` check.
## Passes
### 1. infer_param_types (backward type inference)
Scans typed operators and generic arithmetic to determine what types their operands must be. For example, `subtract dest, a, b` implies both `a` and `b` are numbers.
When a parameter slot (1..nr_args) is consistently inferred as a single type, that type is recorded. Since parameters are immutable (`def`), the inferred type holds for the entire function and persists across label join points (loop headers, branch targets).
Backward inference rules:
| Operator class | Operand type inferred |
|---|---|
| `add`, `subtract`, `multiply`, `divide`, `modulo`, `pow`, `negate` | T_NUM |
| bitwise ops (`bitand`, `bitor`, `bitxor`, `shl`, `shr`, `ushr`, `bitnot`) | T_INT |
| `concat` | T_TEXT |
| `not`, `and`, `or` | T_BOOL |
| `store_index` (object operand) | T_ARRAY |
| `store_index` (index operand) | T_INT |
| `store_field` (object operand) | T_RECORD |
| `push` (array operand) | T_ARRAY |
| `load_index` (object operand) | T_ARRAY |
| `load_index` (index operand) | T_INT |
| `load_field` (object operand) | T_RECORD |
| `pop` (array operand) | T_ARRAY |
Typed comparison operators (`eq_int`, `lt_float`, `lt_text`, etc.) and typed boolean comparisons (`eq_bool`, `ne_bool`) are excluded from backward inference. These ops always appear inside guard dispatch patterns (`is_type` + `jump_false` + typed_op), where mutually exclusive branches use the same slot with different types. Including them would merge conflicting types (e.g., T_INT from `lt_int` + T_FLOAT from `lt_float` + T_TEXT from `lt_text`) into T_UNKNOWN, losing all type information. Only unconditionally executed ops contribute to backward inference.
Note: `add` infers T_NUM even though it is polymorphic (numeric addition or text concatenation). When `add` appears in the IR, both operands have already passed a `is_num` guard, so they are guaranteed to be numeric. The text concatenation path uses `concat` instead.
When a slot appears with conflicting type inferences, the merge widens: INT + FLOAT → NUM, INT + NUM → NUM, FLOAT + NUM → NUM. Incompatible types (e.g., NUM + TEXT) produce `unknown`.
**Nop prefix:** none (analysis only, does not modify instructions)
### 2. infer_slot_write_types (slot write-type invariance)
Scans all instructions to determine which non-parameter slots have a consistent write type. If every instruction that writes to a given slot produces the same type, that type is globally invariant and can safely persist across label join points.
This analysis is sound because:
- `alloc_slot()` in mcode.cm is monotonically increasing — temp slots are never reused
- All local variable declarations must be at function body level and initialized — slots are written before any backward jumps to loop headers
- `move` is conservatively treated as T_UNKNOWN, avoiding unsound transitive assumptions
Write type mapping:
| Instruction class | Write type |
|---|---|
| `int` | T_INT |
| `true`, `false` | T_BOOL |
| `null` | T_NULL |
| `access` | type of literal value |
| `array` | T_ARRAY |
| `record` | T_RECORD |
| `function` | T_FUNCTION |
| `length` | T_INT |
| bitwise ops | T_INT |
| `concat` | T_TEXT |
| `negate` | T_NUM |
| `add`, `subtract`, `multiply`, `divide`, `modulo`, `pow` | T_NUM |
| bool ops, comparisons, `in` | T_BOOL |
| `move`, `load_field`, `load_index`, `load_dynamic`, `pop`, `get` | T_UNKNOWN |
| `invoke`, `tail_invoke` | T_UNKNOWN |
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.
Common patterns this enables:
- **Length variables** (`var len = length(arr)`): written by `length` (T_INT) only → invariant T_INT
- **Boolean flags** (`var found = false; ... found = true`): written by `false` and `true` → invariant T_BOOL
- **Locally-created containers** (`var arr = []`): written by `array` only → invariant T_ARRAY
- **Numeric accumulators** (`var sum = 0; sum = sum - x`): written by `access 0` (T_INT) and `subtract` (T_NUM) → merges to T_NUM
Note: Loop counters using `+` (`var i = 0; i = i + 1`) may not achieve write-type invariance because the `+` operator emits a guard dispatch with both `concat` (T_TEXT) and `add` (T_NUM) paths writing to the same temp slot, producing T_UNKNOWN. However, when one operand is a known number literal, `mcode.cm` emits a numeric-only path (see "Known-Number Add Shortcut" below), avoiding the text dispatch. Other arithmetic ops (`-`, `*`, `/`, `%`, `**`) always emit a single numeric write path and work cleanly with write-type analysis.
**Nop prefix:** none (analysis only, does not modify instructions)
### 3. eliminate_type_checks (type-check + jump elimination)
Forward pass that tracks the known type of each slot. When a type check (`is_int`, `is_text`, `is_num`, etc.) is followed by a conditional jump, and the slot's type is already known, the check and jump can be eliminated or converted to an unconditional jump.
Five cases:
- **Known match** (e.g., `is_int` on a slot known to be `int`): both the check and the conditional jump are eliminated (nop'd).
- **Subsumption match** (e.g., `is_num` on a slot known to be `int` or `float`): since `int` and `float` are subtypes of `num`, both the check and jump are eliminated.
- **Subsumption partial** (e.g., `is_int` on a slot known to be `num`): the `num` type could be `int` or `float`, so the check must remain. On fallthrough, the slot narrows to the checked subtype (`int`). This is NOT a mismatch — `num` values can pass an `is_int` check.
- **Known mismatch** (e.g., `is_text` on a slot known to be `int`): the check is nop'd and the conditional jump is rewritten to an unconditional `jump`.
- **Unknown**: the check remains, but on fallthrough, the slot's type is narrowed to the checked type (enabling downstream eliminations).
This pass also reduces `load_dynamic`/`store_dynamic` to `load_field`/`store_field` or `load_index`/`store_index` when the key slot's type is known.
At label join points, all type information is reset except for parameter types from backward inference and write-invariant types from slot write-type analysis.
**Nop prefix:** `_nop_tc_`
### 4. simplify_algebra (same-slot comparison folding)
Tracks known constant values. Folds same-slot comparisons:
| Pattern | Rewrite |
|---------|---------|
| `eq_* dest, x, x` | `true dest` |
| `le_* dest, x, x` | `true dest` |
| `ge_* dest, x, x` | `true dest` |
| `is_identical dest, x, x` | `true dest` |
| `ne_* dest, x, x` | `false dest` |
| `lt_* dest, x, x` | `false dest` |
| `gt_* dest, x, x` | `false dest` |
**Nop prefix:** none (rewrites in place, does not create nops)
### 5. simplify_booleans (not + jump fusion)
Peephole pass that eliminates unnecessary `not` instructions:
| Pattern | Rewrite |
|---------|---------|
| `not d, x; jump_false d, L` | nop; `jump_true x, L` |
| `not d, x; jump_true d, L` | nop; `jump_false x, L` |
| `not d1, x; not d2, d1` | nop; `move d2, x` |
This is particularly effective on `if (!cond)` patterns, which the compiler generates as `not; jump_false`. After this pass, they become a single `jump_true`.
**Nop prefix:** `_nop_bl_`
### 6. eliminate_moves (self-move elimination)
Removes `move a, a` instructions where the source and destination are the same slot. These can arise from earlier passes rewriting binary operations into moves.
**Nop prefix:** `_nop_mv_`
### 7. eliminate_unreachable (dead code after return)
Nops instructions after `return` until the next real label. Only `return` is treated as a terminal instruction; `disrupt` is not, because the disruption handler code immediately follows `disrupt` and must remain reachable.
The mcode compiler emits a label at disruption handler entry points (see `emit_label(gen_label("disruption"))` in mcode.cm), which provides the label boundary that stops this pass from eliminating handler code.
**Nop prefix:** `_nop_ur_`
### 8. 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_`
## Pass Composition
All passes run in sequence in `optimize_function`:
```
infer_param_types → returns param_types map
infer_slot_write_types → returns write_types map
eliminate_type_checks → uses param_types + write_types
simplify_algebra
simplify_booleans
eliminate_moves
eliminate_unreachable
eliminate_dead_jumps
```
Each pass is independent and can be commented out for testing or benchmarking.
## Intrinsic Inlining
Before streamlining, `mcode.cm` recognizes calls to built-in intrinsic functions and emits direct opcodes instead of the generic frame/setarg/invoke call sequence. This reduces a 6-instruction call pattern to a single instruction:
| Call | Emitted opcode |
|------|---------------|
| `is_array(x)` | `is_array dest, src` |
| `is_function(x)` | `is_func dest, src` |
| `is_object(x)` | `is_record dest, src` |
| `is_stone(x)` | `is_stone dest, src` |
| `is_integer(x)` | `is_int dest, src` |
| `is_text(x)` | `is_text dest, src` |
| `is_number(x)` | `is_num dest, src` |
| `is_logical(x)` | `is_bool dest, src` |
| `is_null(x)` | `is_null dest, src` |
| `length(x)` | `length dest, src` |
| `push(arr, val)` | `push arr, val` |
These inlined opcodes have corresponding Mach VM implementations in `mach.c`.
## Unified Arithmetic
Arithmetic operations use generic opcodes: `add`, `subtract`, `multiply`, `divide`, `modulo`, `pow`, `negate`. There are no type-dispatched variants (e.g., no `add_int`/`add_float`).
The Mach VM handles arithmetic inline with a two-tier fast path. Since mcode's type guard dispatch guarantees both operands are numbers by the time arithmetic executes, the VM does not need polymorphic dispatch:
1. **Int-int fast path**: `JS_VALUE_IS_BOTH_INT` → native integer arithmetic with overflow check. If the result fits int32, returns int32; otherwise promotes to float64.
2. **Float fallback**: `JS_ToFloat64` both operands → native floating-point arithmetic. Non-finite results (infinity, NaN) produce null.
Division and modulo additionally check for zero divisor (→ null). Power uses `pow()` with non-finite handling.
The legacy `reg_vm_binop()` function remains available for comparison operators and any non-mcode bytecode paths, but arithmetic ops no longer call it.
Bitwise operations (`shl`, `shr`, `ushr`, `bitand`, `bitor`, `bitxor`, `bitnot`) remain integer-only and disrupt if operands are not integers.
The QBE/native backend maps generic arithmetic to helper calls (`qbe.add`, `qbe.sub`, etc.). The vision for the native path is that with sufficient type inference, the backend can unbox proven-numeric values to raw registers, operate directly, and only rebox at boundaries (returns, calls, stores).
## Known-Number Add Shortcut
The `+` operator is the only arithmetic op that is polymorphic at the mcode level — `emit_add_decomposed` in `mcode.cm` emits a guard dispatch that checks for text (→ `concat`) before numeric (→ `add`). This dual dispatch means the temp slot is written by both `concat` (T_TEXT) and `add` (T_NUM), producing T_UNKNOWN in write-type analysis.
When either operand is a known number literal (e.g., `i + 1`, `x + 0.5`), `emit_add_decomposed` skips the text dispatch entirely and emits `emit_numeric_binop("add")` — a single `is_num` guard + `add` with no `concat` path. This is safe because text concatenation requires both operands to be text; a known number can never participate in concat.
This optimization eliminates 6-8 instructions from the add block (two `is_text` checks, two conditional jumps, `concat`, `jump`) and produces a clean single-type write path that works with write-type analysis.
Other arithmetic ops (`subtract`, `multiply`, etc.) always use `emit_numeric_binop` and never have this problem.
## Target Slot Propagation
For simple local variable assignments (`i = expr`), the mcode compiler passes the variable's register slot as a `target` to the expression compiler. Binary operations that use `emit_numeric_binop` (subtract, multiply, divide, modulo, pow) can write directly to the target slot instead of allocating a temp and emitting a `move`:
```
// Before: i = i - 1
subtract 7, 2, 6 // temp = i - 1
move 2, 7 // i = temp
// After: i = i - 1
subtract 2, 2, 6 // i = i - 1 (direct)
```
The `+` operator 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.
## Debugging Tools
Three dump 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
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>
```
## Tail Call Marking
When a function's return expression is a call (`stmt.tail == true` from the parser) and the function has no disruption handler, mcode.cm renames the final `invoke` instruction to `tail_invoke`. This is semantically identical to `invoke` in the current Mach VM, but marks the call site for future tail call optimization.
The disruption handler restriction exists because TCO would discard the current frame, but the handler must remain on the stack to catch disruptions from the callee.
`tail_invoke` is handled by the same passes as `invoke` in streamline (type tracking, algebraic simplification) and executes identically in the VM.
## Type Propagation Architecture
Type information flows through three compilation stages, each building on the previous:
### Stage 1: Parse-time type tags (parse.cm)
The parser assigns `type_tag` strings to scope variable entries when the type is syntactically obvious:
- **From initializers**: `def a = []``type_tag: "array"`, `def n = 42``type_tag: "integer"`, `def r = {}``type_tag: "record"`
- **From usage patterns** (def only): `def x = null; x[] = v` infers `type_tag: "array"` from the push. `def x = null; x.foo = v` infers `type_tag: "record"` from property access.
- **Type error detection** (def only): When a `def` variable has a known type_tag, provably wrong operations are compile errors:
- Property access (`.`) on array
- Push (`[]`) on non-array
- Text key on array
- Integer key on record
Only `def` (constant) variables participate in type inference and error detection. `var` variables can be reassigned, making their initializer type unreliable.
### Stage 2: Fold-time type propagation (fold.cm)
The fold pass extends type information through the AST:
- **Intrinsic folding**: `is_array(known_array)` folds to `true`. `length(known_array)` gets `hint: "array_length"`.
- **Purity analysis**: Expressions involving only `is_*` intrinsic calls with pure arguments are considered pure. This enables dead code elimination for unused `var`/`def` bindings with pure initializers, and elimination of standalone pure call statements.
- **Dead code**: Unused pure `var`/`def` declarations are removed. Standalone calls to pure intrinsics (where the result is discarded) are removed. Unreachable branches with constant conditions are removed.
The `pure_intrinsics` set currently contains only `is_*` sensory functions (`is_array`, `is_text`, `is_number`, `is_integer`, `is_function`, `is_logical`, `is_null`, `is_object`, `is_stone`). Other intrinsics like `text`, `number`, and `length` can disrupt on wrong argument types, so they are excluded — removing a call that would disrupt changes observable behavior.
### Stage 3: Streamline-time type tracking (streamline.cm)
The streamline optimizer uses a numeric type lattice (`T_INT`, `T_FLOAT`, `T_TEXT`, etc.) for fine-grained per-instruction tracking:
- **Backward inference** (pass 1): Scans typed operators to infer parameter types. Since parameters are `def` (immutable), inferred types persist across label boundaries.
- **Write-type invariance** (pass 2): Scans all instructions to find local slots where every write produces the same type. These invariant types persist across label boundaries alongside parameter types.
- **Forward tracking** (pass 3): `track_types` follows instruction execution order, tracking the type of each slot. Known-type operations set their destination type (e.g., `concat` → T_TEXT, `length` → T_INT). Generic arithmetic produces T_UNKNOWN. Type checks on unknown slots narrow the type on fallthrough.
- **Type check elimination** (pass 3): When a slot's type is already known, `is_<type>` + conditional jump pairs are eliminated or converted to unconditional jumps.
- **Dynamic access narrowing** (pass 3): `load_dynamic`/`store_dynamic` are narrowed to `load_field`/`store_field` or `load_index`/`store_index` when the key type is known.
Type information resets at label join points (since control flow merges could bring different types), except for parameter types from backward inference and write-invariant types from slot write-type analysis.
## Future Work
### Copy Propagation
A basic-block-local copy propagation pass would replace uses of a copied variable with its source, enabling further move elimination. An implementation was attempted but encountered an unsolved bug where 2-position instruction operand replacement produces incorrect code during self-hosting (the replacement logic for 3-position instructions works correctly). The root cause is not yet understood. See the project memory files for detailed notes.
### Expanded Purity Analysis
The current purity set is conservative (only `is_*`). It could be expanded by:
- **Argument-type-aware purity**: If all arguments to an intrinsic are known to be the correct types (via type_tag or slot_types), the call cannot disrupt and is safe to eliminate. For example, `length(known_array)` is pure but `length(unknown)` is not.
- **User function purity**: Analyze user-defined function bodies during pre_scan. A function is pure if its body contains only pure expressions and calls to known-pure functions. This requires fixpoint iteration for mutual recursion.
- **Callback-aware purity**: Intrinsics like `filter`, `find`, `reduce`, `some`, `every` are pure if their callback argument is pure.
### Move Type Resolution in Write-Type Analysis
Currently, `move` instructions produce T_UNKNOWN in write-type analysis. This prevents type propagation through moves — e.g., a slot written by `access 0` (T_INT) and `move` from an `add` result (T_NUM) merges to T_UNKNOWN instead of T_NUM.
A two-pass approach would fix this: first compute write types for all non-move instructions, then resolve moves by looking up the source slot's computed type. If the source has a known type, merge it into the destination; if unknown, skip the move (don't poison the destination with T_UNKNOWN).
This was implemented and tested but causes a bootstrap failure during self-hosting convergence. The root cause is not yet understood — the optimizer modifies its own bytecode, and the move resolution changes the type landscape enough to produce different code on each pass, preventing convergence. Further investigation is needed; the fix is correct in isolation but interacts badly with the self-hosting fixed-point iteration.
### Target Slot Propagation for Add with Known Numbers
When the known-number add shortcut applies (one operand is a literal number), the generated code uses `emit_numeric_binop` which has a single write path. Target slot propagation 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`.
### Forward Type Narrowing from Typed Operations
With unified arithmetic (generic `add`/`subtract`/`multiply`/`divide`/`modulo`/`negate` instead of typed variants), this approach is no longer applicable. Typed comparisons (`eq_int`, `lt_float`, etc.) still exist and their operands have known types, but these are already handled by backward inference.
### Guard Hoisting for Parameters
When a type check on a parameter passes (falls through), the parameter's type could be promoted to `param_types` so it persists across label boundaries. This would allow the first type check on a parameter to prove its type for the entire function. However, this is unsound for polymorphic parameters — if a function is called with different argument types, the first check would wrongly eliminate checks for subsequent types.
A safe version would require proving that a parameter is monomorphic (called with only one type across all call sites), which requires interprocedural analysis.
**Note:** For local variables (non-parameters), the write-type invariance analysis (pass 2) achieves a similar effect safely — if every write to a slot produces the same type, that type persists across labels without needing to hoist any guard.
### Tail Call Optimization
`tail_invoke` instructions are currently marked but execute identically to `invoke`. Actual TCO would reuse the current call frame instead of creating a new one. This requires:
- Ensuring argument count matches (or the frame can be resized)
- No live locals needed after the call (guaranteed by tail position)
- No disruption handler on the current function (already enforced by the marking)
- VM support in mach.c to rewrite the frame in place
### Interprocedural Type Inference
Currently all type inference is intraprocedural (within a single function). Cross-function analysis could:
- Infer return types from function bodies
- Propagate argument types from call sites to callees
- Specialize functions for known argument types (cloning)
### Strength Reduction
Common patterns that could be lowered to cheaper operations when operand types are known:
- `multiply x, 2` with proven-int operands → shift left
- `divide x, 2` with proven-int → arithmetic shift right
- `modulo x, power_of_2` with proven-int → bitwise and
### Numeric Unboxing (QBE/native path)
With unified arithmetic and backward type inference, the native backend can identify regions where numeric values remain in registers without boxing/unboxing:
1. **Guard once**: When backward inference proves a parameter is T_NUM, emit a single type guard at function entry.
2. **Unbox**: Convert the tagged JSValue to a raw double register.
3. **Operate**: Use native FP/int instructions directly (no function calls, no tag checks).
4. **Rebox**: Convert back to tagged JSValue only at rebox points (function returns, calls, stores to arrays/records).
This requires inserting `unbox`/`rebox` IR annotations (no-ops in the Mach VM, meaningful only to QBE).
### Loop-Invariant Code Motion
Type checks that are invariant across loop iterations (checking a variable that doesn't change in the loop body) could be hoisted above the loop. This would require identifying loop boundaries and proving invariance.
### Algebraic Identity Optimization
With unified arithmetic, algebraic identities (x+0→x, x*1→x, x*0→0, x/1→x) require knowing operand values at compile time. Since generic `add`/`multiply` operate on any numeric type, the constant-tracking logic in `simplify_algebra` could be extended to handle these for known-constant slots.
## Nop Convention
Eliminated instructions are replaced with strings matching `_nop_<prefix>_<counter>`. The prefix identifies which pass created the nop. Nop strings are:
- Skipped during interpretation (the VM ignores them)
- Skipped during QBE emission
- Not counted in instruction statistics
- Preserved in the instruction array to maintain positional stability for jump targets

170
docs/testing.md Normal file
View File

@@ -0,0 +1,170 @@
---
title: "Testing"
description: "Writing and running tests in ƿit"
weight: 45
type: "docs"
---
ƿit has built-in support for writing and running tests. Tests live in the `tests/` directory of a package and are `.cm` modules that return a record of test functions.
## Writing Tests
A test file returns a record where each key starting with `test_` is a test function. A test passes if it returns `null` (or nothing). It fails if it returns a text string describing the failure.
```javascript
// tests/math.cm
return {
test_addition: function() {
if (1 + 2 != 3) return "expected 3"
},
test_division: function() {
if (10 / 3 != 3.333333333333333333) return "unexpected result"
}
}
```
Test functions take no arguments. Use early returns with a failure message to report errors:
```javascript
test_array_push: function() {
var a = [1, 2]
a[] = 3
if (length(a) != 3) return "expected length 3, got " + text(length(a))
if (a[2] != 3) return "expected a[2] to be 3"
}
```
## Running Tests
```bash
pit test # run all tests in current package
pit test suite # run a specific test file (tests/suite.cm)
pit test tests/math # same, with explicit path
pit test all # run all tests in current package
pit test package <name> # run all tests in a named package
pit test package <name> <test> # run a specific test in a named package
pit test package all # run tests from all installed packages
```
### Flags
```bash
pit test suite -g # run GC after each test (useful for detecting leaks)
pit test suite --verify # enable IR verification during compilation
pit test suite --diff # run each test optimized and unoptimized, compare results
```
`--verify` and `--diff` can be combined:
```bash
pit test suite --verify --diff
```
## IR Verification
The `--verify` flag enables structural validation of the compiler's intermediate representation after each optimizer pass. This catches bugs like invalid slot references, broken jump targets, and malformed instructions.
When verification fails, errors are printed with the pass name that introduced them:
```
[verify_ir] slot_bounds: slot 12 out of range 0..9 in instruction add_int
[verify_ir] 1 errors after dead_code_elimination
```
IR verification adds overhead and is intended for development, not production use.
## Differential Testing
Differential testing runs each test through two paths — with the optimizer enabled and with it disabled — and compares results. Any mismatch between the two indicates an optimizer bug.
### Inline Mode
The `--diff` flag on `pit test` runs each test module through both paths during a normal test run:
```bash
pit test suite --diff
```
Output includes a mismatch count at the end:
```
Tests: 493 passed, 0 failed, 493 total
Diff mismatches: 0
```
### Standalone Mode
`pit diff` is a dedicated differential testing tool with detailed mismatch reporting:
```bash
pit diff # diff all test files in current package
pit diff suite # diff a specific test file
pit diff tests/math # same, with explicit path
```
For each test function, it reports whether the optimized and unoptimized results match:
```
tests/suite.cm: 493 passed, 0 failed
----------------------------------------
Diff: 493 passed, 0 failed, 493 total
```
When a mismatch is found:
```
tests/suite.cm: 492 passed, 1 failed
MISMATCH: test_foo: result mismatch opt=42 noopt=43
```
## Fuzz Testing
The fuzzer generates random self-checking programs, compiles them, and runs them through both optimized and unoptimized paths. Each generated program contains test functions that validate their own expected results, so failures catch both correctness bugs and optimizer mismatches.
```bash
pit fuzz # 100 iterations, random seed
pit fuzz 500 # 500 iterations, random seed
pit fuzz --seed 42 # 100 iterations, deterministic seed
pit fuzz 1000 --seed 42 # 1000 iterations, deterministic seed
```
The fuzzer generates programs that exercise:
- Integer and float arithmetic with known expected results
- Control flow (if/else, while loops)
- Closures and captured variable mutation
- Records and property access
- Arrays and iteration
- Higher-order functions
- Disruption handling
- Text concatenation
On failure, the generated source is saved to `tests/fuzz_failures/` for reproduction:
```
Fuzzing: 1000 iterations, starting seed=42
FAIL seed=57: diff fuzz_3: opt=10 noopt=11
saved to tests/fuzz_failures/seed_57.cm
----------------------------------------
Fuzz: 999 passed, 1 failed, 1000 total
Failures saved to tests/fuzz_failures/
```
Saved failure files are valid `.cm` modules that can be run directly or added to the test suite.
## Test File Organization
Tests live in the `tests/` directory of a package:
```
mypackage/
├── pit.toml
├── math.cm
└── tests/
├── suite.cm # main test suite
├── math.cm # math-specific tests
└── disrupt.cm # disruption tests
```
All `.cm` files under `tests/` are discovered automatically by `pit test`.

16
dump_ast.cm Normal file
View File

@@ -0,0 +1,16 @@
// 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))

22
dump_ir.ce Normal file
View File

@@ -0,0 +1,22 @@
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')
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 instrs = optimized.main.instructions
var i = 0
while (i < length(instrs)) {
print(text(i) + ': ' + json.encode(instrs[i]))
i = i + 1
}

View File

@@ -1,20 +1,117 @@
// 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")
var streamline = use("streamline")
var name = args[0]
var src = text(fd.slurp(name))
var tok = tokenize(src, name)
var ast = parse(tok.tokens, src, name, tokenize)
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 optimized = streamline(compiled)
var out = json.encode(optimized)
var f = fd.open("/tmp/mcode_dump.json", "w")
fd.write(f, out)
fd.close(f)
print("wrote /tmp/mcode_dump.json")
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
}
}

166
dump_stream.cm Normal file
View File

@@ -0,0 +1,166 @@
// 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
}
}

237
dump_types.cm Normal file
View File

@@ -0,0 +1,237 @@
// 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
}
}

4
fd.cm
View File

@@ -1,4 +1,4 @@
var fd = native
var fd = use('internal/fd')
var wildstar = use('wildstar')
function last_pos(str, sep) {
@@ -97,4 +97,4 @@ fd.globfs = function(globs, dir) {
return results
}
return fd
return fd

View File

@@ -12,8 +12,9 @@ var shop = use('internal/shop')
// Parse arguments
var target_pkg = null
var i = 0
for (var i = 0; i < length(args); i++) {
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.")

9
fit.c
View File

@@ -248,9 +248,10 @@ static const JSCFunctionListEntry js_fit_funcs[] = {
MIST_FUNC_DEF(fit, zeros, 1),
};
JSValue js_fit_use(JSContext *js)
JSValue js_core_fit_use(JSContext *js)
{
JSValue mod = JS_NewObject(js);
JS_SetPropertyFunctionList(js, mod, js_fit_funcs, countof(js_fit_funcs));
return mod;
JS_FRAME(js);
JS_ROOT(mod, JS_NewObject(js));
JS_SetPropertyFunctionList(js, mod.val, js_fit_funcs, countof(js_fit_funcs));
JS_RETURN(mod.val);
}

82
fold.cm
View File

@@ -5,6 +5,34 @@ var fold = function(ast) {
var scopes = ast.scopes
var nr_scopes = length(scopes)
var type_tag_map = {
array: "array", record: "record", text: "text",
number: "number", blob: "blob"
}
var binary_ops = {
"+": true, "-": true, "*": true, "/": true, "%": true,
"**": true, "==": true, "!=": true, "<": true, ">": true,
"<=": true, ">=": true, "&": true, "|": true, "^": true,
"<<": true, ">>": true, ">>>": true, "&&": true, "||": true,
",": true, in: true
}
var unary_ops = {
"!": true, "~": true, "-unary": true, "+unary": true, delete: true
}
var assign_ops = {
assign: true, "+=": true, "-=": true, "*=": true,
"/=": true, "%=": true, "<<=": true, ">>=": true,
">>>=": true, "&=": true, "^=": true, "|=": true,
"**=": true, "&&=": true, "||=": true
}
var arith_ops = {
"+": true, "-": true, "*": true, "/": true, "%": true, "**": true
}
var comparison_ops = {
"==": true, "!=": true, "<": true, ">": true, "<=": true, ">=": true
}
// ============================================================
// Helpers
// ============================================================
@@ -15,10 +43,18 @@ var fold = function(ast) {
return k == "number" || k == "text" || k == "true" || k == "false" || k == "null"
}
// Only intrinsics that can NEVER disrupt regardless of argument types
var pure_intrinsics = {
is_array: true, is_text: true, is_number: true, is_integer: true,
is_function: true, is_logical: true, is_null: true, is_object: true,
is_stone: true
}
var is_pure = function(expr) {
if (expr == null) return true
var k = expr.kind
var i = 0
var target = null
if (k == "number" || k == "text" || k == "true" || k == "false" ||
k == "null" || k == "name" || k == "this") return true
if (k == "function") return true
@@ -47,6 +83,17 @@ var fold = function(ast) {
if (k == "==" || k == "!=" || k == "&&" || k == "||") {
return is_pure(expr.left) && is_pure(expr.right)
}
if (k == "(") {
target = expr.expression
if (target != null && target.intrinsic == true && pure_intrinsics[target.name] == true) {
i = 0
while (i < length(expr.list)) {
if (!is_pure(expr.list[i])) return false
i = i + 1
}
return true
}
}
return false
}
@@ -175,11 +222,7 @@ var fold = function(ast) {
if (rhs_target != null && rhs_target.intrinsic == true) {
sv = scope_var(fn_nr, name)
if (sv != null && sv.type_tag == null) {
if (rhs_target.name == "array") sv.type_tag = "array"
else if (rhs_target.name == "record") sv.type_tag = "record"
else if (rhs_target.name == "text") sv.type_tag = "text"
else if (rhs_target.name == "number") sv.type_tag = "number"
else if (rhs_target.name == "blob") sv.type_tag = "blob"
if (type_tag_map[rhs_target.name] != null) sv.type_tag = type_tag_map[rhs_target.name]
}
}
}
@@ -338,17 +381,13 @@ var fold = function(ast) {
var arg = null
// Recurse into children first (bottom-up)
if (k == "+" || k == "-" || k == "*" || k == "/" || k == "%" ||
k == "**" || k == "==" || k == "!=" || k == "<" || k == ">" ||
k == "<=" || k == ">=" || k == "&" || k == "|" || k == "^" ||
k == "<<" || k == ">>" || k == ">>>" || k == "&&" || k == "||" ||
k == "," || k == "in") {
if (binary_ops[k] == true) {
expr.left = fold_expr(expr.left, fn_nr)
expr.right = fold_expr(expr.right, fn_nr)
} else if (k == "." || k == "[") {
expr.left = fold_expr(expr.left, fn_nr)
if (k == "[" && expr.right != null) expr.right = fold_expr(expr.right, fn_nr)
} else if (k == "!" || k == "~" || k == "-unary" || k == "+unary" || k == "delete") {
} else if (unary_ops[k] == true) {
expr.expression = fold_expr(expr.expression, fn_nr)
} else if (k == "++" || k == "--") {
return expr
@@ -363,7 +402,7 @@ var fold = function(ast) {
expr.list[i] = fold_expr(expr.list[i], fn_nr)
i = i + 1
}
} else if (k == "array") {
} else if (k == "array" || k == "text literal") {
i = 0
while (i < length(expr.list)) {
expr.list[i] = fold_expr(expr.list[i], fn_nr)
@@ -375,19 +414,10 @@ var fold = function(ast) {
expr.list[i].right = fold_expr(expr.list[i].right, fn_nr)
i = i + 1
}
} else if (k == "text literal") {
i = 0
while (i < length(expr.list)) {
expr.list[i] = fold_expr(expr.list[i], fn_nr)
i = i + 1
}
} else if (k == "function") {
fold_fn(expr)
return expr
} else if (k == "assign" || k == "+=" || k == "-=" || k == "*=" ||
k == "/=" || k == "%=" || k == "<<=" || k == ">>=" ||
k == ">>>=" || k == "&=" || k == "^=" || k == "|=" ||
k == "**=" || k == "&&=" || k == "||=") {
} else if (assign_ops[k] == true) {
expr.right = fold_expr(expr.right, fn_nr)
return expr
}
@@ -409,7 +439,7 @@ var fold = function(ast) {
}
// Binary constant folding
if (k == "+" || k == "-" || k == "*" || k == "/" || k == "%" || k == "**") {
if (arith_ops[k] == true) {
left = expr.left
right = expr.right
if (left != null && right != null && left.kind == "number" && right.kind == "number") {
@@ -441,7 +471,7 @@ var fold = function(ast) {
}
// Comparison folding
if (k == "==" || k == "!=" || k == "<" || k == ">" || k == "<=" || k == ">=") {
if (comparison_ops[k] == true) {
left = expr.left
right = expr.right
if (left != null && right != null) {
@@ -676,6 +706,10 @@ var fold = function(ast) {
}
}
}
// Dead pure call elimination: standalone pure calls with no result
if (stmt.kind == "call" && is_pure(stmt.expression)) {
stmt.dead = true
}
// Dead function elimination
if (stmt.kind == "function" && stmt.name != null) {
sv = scope_var(fn_nr, stmt.name)

BIN
fold.mach

Binary file not shown.

278
fuzz.ce Normal file
View File

@@ -0,0 +1,278 @@
// fuzz.ce — fuzzer driver: generates random programs, runs differential, saves failures
//
// Usage:
// cell fuzz - run 100 iterations with a random seed
// cell fuzz 500 - run 500 iterations with a random seed
// cell fuzz --seed 42 - run 100 iterations starting at seed 42
// cell fuzz 500 --seed 42 - run 500 iterations starting at seed 42
//
// Each iteration generates a random self-checking program, compiles it,
// runs it through both optimized and unoptimized paths, and compares results.
// Failures are saved to tests/fuzz_failures/ for reproduction.
var fd = use('fd')
var time = use('time')
var json = use('json')
var os_ref = use('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
var fuzzgen = use('fuzzgen')
var _args = args == null ? [] : args
// Parse arguments: fuzz [iterations] [--seed N]
var iterations = 100
var start_seed = null
var i = 0
var n = null
var run_err = null
var _run_one = null
while (i < length(_args)) {
if (_args[i] == '--seed' && i + 1 < length(_args)) {
start_seed = number(_args[i + 1])
i = i + 2
} else {
n = number(_args[i])
if (n != null && n > 0) iterations = n
i = i + 1
}
}
if (start_seed == null) {
start_seed = floor(time.number() * 1000) % 1000000
}
if (!run_ast_noopt_fn) {
log.console("error: run_ast_noopt_fn not available (rebuild bootstrap)")
$stop()
return
}
// 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>"
}
// Run a single fuzz iteration
function run_fuzz(seed_val) {
var src = fuzzgen.generate(seed_val)
var name = "fuzz_" + text(seed_val)
var ast = null
var mod_opt = null
var mod_noopt = null
var opt_err = null
var noopt_err = null
var errors = []
var keys = null
var k = 0
var key = null
var ret = null
var _run = null
var run_err = null
var keys2 = null
var k2 = 0
var key2 = null
var opt_result = null
var noopt_result = null
var opt_fn_err = null
var noopt_fn_err = null
var _run_opt = null
var _run_noopt = null
// Parse
var _parse = function() {
ast = analyze(src, name + ".cm")
} disruption {
push(errors, "parse error")
}
_parse()
if (length(errors) > 0) return {seed: seed_val, errors: errors, src: src}
// Run optimized
var _opt = function() {
mod_opt = run_ast_fn(name, ast, stone({use: function(p) { return use(p) }}))
} disruption {
opt_err = "disrupted"
}
_opt()
// Run unoptimized
var _noopt = function() {
mod_noopt = run_ast_noopt_fn(name + "_noopt", ast, stone({use: function(p) { return use(p) }}))
} disruption {
noopt_err = "disrupted"
}
_noopt()
// Check module-level behavior
if (opt_err != noopt_err) {
push(errors, `module load: opt=${opt_err != null ? opt_err : "ok"} noopt=${noopt_err != null ? noopt_err : "ok"}`)
return {seed: seed_val, errors: errors, src: src}
}
if (opt_err != null) {
// Both failed to load — consistent
return {seed: seed_val, errors: errors, src: src}
}
// Run self-checks (optimized module)
if (is_object(mod_opt)) {
keys = array(mod_opt)
k = 0
while (k < length(keys)) {
key = keys[k]
if (is_function(mod_opt[key])) {
ret = null
run_err = null
_run = function() {
ret = mod_opt[key]()
} disruption {
run_err = "disrupted"
}
_run()
if (is_text(ret)) {
push(errors, `self-check ${key}: ${ret}`)
}
if (run_err != null) {
push(errors, `self-check ${key}: unexpected disruption`)
}
}
k = k + 1
}
}
// Differential check on each function
if (is_object(mod_opt) && is_object(mod_noopt)) {
keys2 = array(mod_opt)
k2 = 0
while (k2 < length(keys2)) {
key2 = keys2[k2]
if (is_function(mod_opt[key2]) && is_function(mod_noopt[key2])) {
opt_result = null
noopt_result = null
opt_fn_err = null
noopt_fn_err = null
_run_opt = function() {
opt_result = mod_opt[key2]()
} disruption {
opt_fn_err = "disrupted"
}
_run_opt()
_run_noopt = function() {
noopt_result = mod_noopt[key2]()
} disruption {
noopt_fn_err = "disrupted"
}
_run_noopt()
if (opt_fn_err != noopt_fn_err) {
push(errors, `diff ${key2}: opt=${opt_fn_err != null ? opt_fn_err : "ok"} noopt=${noopt_fn_err != null ? noopt_fn_err : "ok"}`)
} else if (!values_equal(opt_result, noopt_result)) {
push(errors, `diff ${key2}: opt=${describe(opt_result)} noopt=${describe(noopt_result)}`)
}
}
k2 = k2 + 1
}
}
return {seed: seed_val, errors: errors, src: src}
}
// Main loop
log.console(`Fuzzing: ${text(iterations)} iterations, starting seed=${text(start_seed)}`)
var total_pass = 0
var total_fail = 0
var result = null
var j = 0
var current_seed = 0
var fail_path = null
i = 0
while (i < iterations) {
current_seed = start_seed + i
run_err = null
_run_one = function() {
result = run_fuzz(current_seed)
} disruption {
run_err = "generator crashed"
}
_run_one()
if (run_err != null) {
result = {seed: current_seed, errors: [run_err], src: "// generator crashed"}
}
if (length(result.errors) > 0) {
total_fail = total_fail + 1
log.console(` FAIL seed=${text(current_seed)}: ${result.errors[0]}`)
// Save failure source for reproduction
ensure_dir(failures_dir)
fail_path = failures_dir + "/seed_" + text(current_seed) + ".cm"
fd.slurpwrite(fail_path, stone(blob(result.src)))
log.console(` saved to ${fail_path}`)
} else {
total_pass = total_pass + 1
}
// Progress report every 100 iterations
if ((i + 1) % 100 == 0) {
log.console(` progress: ${text(i + 1)}/${text(iterations)} (${text(total_pass)} passed, ${text(total_fail)} failed)`)
}
i = i + 1
}
log.console(`----------------------------------------`)
log.console(`Fuzz: ${text(total_pass)} passed, ${text(total_fail)} failed, ${text(iterations)} total`)
if (total_fail > 0) {
log.console(`Failures saved to ${failures_dir}/`)
}
$stop()

348
fuzzgen.cm Normal file
View File

@@ -0,0 +1,348 @@
// fuzzgen.cm — generates self-checking .cm programs for fuzz testing
// Each generated program returns a record of test functions that
// validate their own expected results.
// Newline constant — backtick strings don't interpret \n as escape
var NL = "\n"
// Simple seeded PRNG (xorshift32)
var _seed = 1
function seed(s) {
_seed = s != 0 ? s : 1
}
function rand() {
_seed = _seed ^ (_seed << 13)
_seed = _seed ^ (_seed >> 17)
_seed = _seed ^ (_seed << 5)
if (_seed < 0) _seed = -_seed
return _seed
}
function rand_int(lo, hi) {
return lo + (rand() % (hi - lo + 1))
}
function rand_float() {
return rand_int(-10000, 10000) / 100
}
function rand_bool() {
return rand() % 2 == 0
}
function pick(arr) {
return arr[rand() % length(arr)]
}
// Expression generators — each returns {src: "code", val: expected_value}
// depth is decremented to prevent infinite recursion
function gen_int_literal() {
var v = rand_int(-10000, 10000)
return {src: text(v), val: v}
}
function gen_float_literal() {
var v = rand_float()
return {src: text(v), val: v}
}
function gen_bool_literal() {
var v = rand_bool()
var s = "false"
if (v) s = "true"
return {src: s, val: v}
}
function gen_text_literal() {
var words = ["alpha", "beta", "gamma", "delta", "epsilon"]
var w = pick(words)
return {src: `"${w}"`, val: w}
}
function gen_null_literal() {
return {src: "null", val: null}
}
function gen_int_expr(depth) {
var a = null
var b = null
var op = null
var result = null
if (depth <= 0) return gen_int_literal()
a = gen_int_expr(depth - 1)
b = gen_int_expr(depth - 1)
// Avoid division by zero
if (b.val == 0) b = {src: "1", val: 1}
op = pick(["+", "-", "*"])
if (op == "+") {
result = a.val + b.val
} else if (op == "-") {
result = a.val - b.val
} else {
result = a.val * b.val
}
// Guard against overflow beyond safe integer range
if (result > 9007199254740991 || result < -9007199254740991) {
return gen_int_literal()
}
return {src: `(${a.src} ${op} ${b.src})`, val: result}
}
function gen_float_expr(depth) {
var a = null
var b = null
var op = null
var result = null
if (depth <= 0) return gen_float_literal()
a = gen_float_expr(depth - 1)
b = gen_float_expr(depth - 1)
if (b.val == 0) b = {src: "1.0", val: 1.0}
op = pick(["+", "-", "*"])
if (op == "+") {
result = a.val + b.val
} else if (op == "-") {
result = a.val - b.val
} else {
result = a.val * b.val
}
return {src: `(${a.src} ${op} ${b.src})`, val: result}
}
function gen_text_expr(depth) {
var a = null
var b = null
if (depth <= 0) return gen_text_literal()
a = gen_text_literal()
b = gen_text_literal()
return {src: `(${a.src} + ${b.src})`, val: a.val + b.val}
}
function gen_comparison_expr(depth) {
var a = null
var b = null
var op = null
var result = null
a = gen_int_expr(depth > 0 ? depth - 1 : 0)
b = gen_int_expr(depth > 0 ? depth - 1 : 0)
op = pick(["==", "!=", "<", ">", "<=", ">="])
if (op == "==") {
result = a.val == b.val
} else if (op == "!=") {
result = a.val != b.val
} else if (op == "<") {
result = a.val < b.val
} else if (op == ">") {
result = a.val > b.val
} else if (op == "<=") {
result = a.val <= b.val
} else {
result = a.val >= b.val
}
return {src: `(${a.src} ${op} ${b.src})`, val: result}
}
// Generate an if-else expression test
function gen_if_else_test() {
var cond = gen_comparison_expr(1)
var then_val = gen_int_literal()
var else_val = gen_int_literal()
var expected = cond.val ? then_val.val : else_val.val
var body = "var result = null" + NL
body = body + " if (" + cond.src + ") {" + NL
body = body + " result = " + then_val.src + NL
body = body + " } else {" + NL
body = body + " result = " + else_val.src + NL
body = body + " }" + NL
body = body + " if (result != " + text(expected) + ") return \"if_else: expected " + text(expected) + " got \" + text(result)"
return body
}
// Generate a loop accumulator test
function gen_loop_test() {
var count = rand_int(1, 50)
var step = rand_int(1, 10)
var expected = 0
var i = 0
while (i < count) {
expected = expected + step
i = i + 1
}
var body = "var acc = 0" + NL
body = body + " var i = 0" + NL
body = body + " while (i < " + text(count) + ") {" + NL
body = body + " acc = acc + " + text(step) + NL
body = body + " i = i + 1" + NL
body = body + " }" + NL
body = body + " if (acc != " + text(expected) + ") return \"loop: expected " + text(expected) + " got \" + text(acc)"
return body
}
// Generate a closure test
function gen_closure_test() {
var init_val = rand_int(1, 100)
var inc = rand_int(1, 10)
var calls = rand_int(1, 10)
var expected = init_val + (inc * calls)
var body = "var counter = " + text(init_val) + NL
body = body + " var inc = function() { counter = counter + " + text(inc) + " }" + NL
body = body + " var i = 0" + NL
body = body + " while (i < " + text(calls) + ") {" + NL
body = body + " inc()" + NL
body = body + " i = i + 1" + NL
body = body + " }" + NL
body = body + " if (counter != " + text(expected) + ") return \"closure: expected " + text(expected) + " got \" + text(counter)"
return body
}
// Generate a record property test
function gen_record_test() {
var a = gen_int_literal()
var b = gen_int_literal()
var sum = a.val + b.val
var body = "var r = {a: " + a.src + ", b: " + b.src + "}" + NL
body = body + " var result = r.a + r.b" + NL
body = body + " if (result != " + text(sum) + ") return \"record: expected " + text(sum) + " got \" + text(result)"
return body
}
// Generate an array test
function gen_array_test() {
var n = rand_int(2, 10)
var vals = []
var i = 0
var sum = 0
var v = 0
while (i < n) {
v = rand_int(-100, 100)
push(vals, v)
sum = sum + v
i = i + 1
}
var val_strs = []
i = 0
while (i < n) {
push(val_strs, text(vals[i]))
i = i + 1
}
var body = "var a = [" + text(val_strs, ", ") + "]" + NL
body = body + " var _sum = 0" + NL
body = body + " var i = 0" + NL
body = body + " while (i < length(a)) {" + NL
body = body + " _sum = _sum + a[i]" + NL
body = body + " i = i + 1" + NL
body = body + " }" + NL
body = body + " if (_sum != " + text(sum) + ") return \"array_sum: expected " + text(sum) + " got \" + text(_sum)"
return body
}
// Generate a nested function / higher-order test
function gen_higher_order_test() {
var mul = rand_int(2, 10)
var input = rand_int(1, 100)
var expected = input * mul
var body = "var make_mul = function(m) {" + NL
body = body + " return function(x) { return x * m }" + NL
body = body + " }" + NL
body = body + " var fn = make_mul(" + text(mul) + ")" + NL
body = body + " var result = fn(" + text(input) + ")" + NL
body = body + " if (result != " + text(expected) + ") return \"higher_order: expected " + text(expected) + " got \" + text(result)"
return body
}
// Generate a disruption handling test
function gen_disrupt_test() {
var body = "var caught = false" + NL
body = body + " var _fn = function() { disrupt } disruption { caught = true }" + NL
body = body + " _fn()" + NL
body = body + " if (!caught) return \"disrupt: expected to catch disruption\""
return body
}
// Generate a text operation test
function gen_text_op_test() {
var words = ["hello", "world", "foo", "bar", "baz"]
var w1 = pick(words)
var w2 = pick(words)
var expected = w1 + w2
var body = "var a = \"" + w1 + "\"" + NL
body = body + " var b = \"" + w2 + "\"" + NL
body = body + " var c = a + b" + NL
body = body + " if (c != \"" + expected + "\") return \"text_op: expected " + expected + " got \" + c"
return body
}
// All generators
var generators = [
gen_if_else_test,
gen_loop_test,
gen_closure_test,
gen_record_test,
gen_array_test,
gen_higher_order_test,
gen_disrupt_test,
gen_text_op_test
]
// Generate a complete self-checking .cm program
function generate(s) {
seed(s)
var num_tests = rand_int(5, 15)
var src = "// Auto-generated fuzz test (seed=" + text(s) + ")\nreturn {\n"
var i = 0
var gen = null
var body = null
while (i < num_tests) {
gen = pick(generators)
body = gen()
if (i > 0) src = src + ",\n"
src = src + " fuzz_" + text(i) + ": function() {\n"
src = src + " " + body + "\n"
src = src + " }"
i = i + 1
}
src = src + "\n}\n"
return src
}
return {
generate: generate,
seed: seed
}

View File

@@ -22,8 +22,10 @@ var target_locator = null
var format = 'tree'
var show_locked = false
var show_world = false
var i = 0
var resolved = null
for (var i = 0; i < length(args); i++) {
for (i = 0; i < length(args); i++) {
if (args[i] == '--format' || args[i] == '-f') {
if (i + 1 < length(args)) {
format = args[++i]
@@ -91,7 +93,7 @@ function gather_graph(locator, visited) {
add_node(locator)
try {
var _gather = function() {
var deps = pkg.dependencies(locator)
if (deps) {
arrfor(array(deps), function(alias) {
@@ -101,17 +103,19 @@ function gather_graph(locator, visited) {
gather_graph(dep_locator, visited)
})
}
} catch (e) {
} disruption {
// Package might not have dependencies
}
_gather()
}
// Gather graph from roots
var roots = []
var packages = null
if (show_world) {
// Use all packages in shop as roots
var packages = shop.list_packages()
packages = shop.list_packages()
arrfor(packages, function(p) {
if (p != 'core') {
push(roots, p)
@@ -125,7 +129,7 @@ if (show_world) {
// Resolve local paths
if (target_locator == '.' || starts_with(target_locator, './') || starts_with(target_locator, '../') || fd.is_dir(target_locator)) {
var resolved = fd.realpath(target_locator)
resolved = fd.realpath(target_locator)
if (resolved) {
target_locator = resolved
}
@@ -139,16 +143,24 @@ arrfor(roots, function(root) {
})
// Output based on format
var children = null
var j = 0
var output = null
if (format == 'tree') {
function print_tree(locator, prefix, is_last, visited) {
var node = null
var suffix = null
var children = null
var j = 0
var child_prefix = null
if (visited[locator]) {
log.console(prefix + (is_last ? "\\-- " : "|-- ") + locator + " (circular)")
return
}
visited[locator] = true
var node = nodes[locator]
var suffix = ""
node = nodes[locator]
suffix = ""
if (node.linked) suffix += " -> " + node.effective
if (node.commit) suffix += " @" + node.commit
if (node.local) suffix += " (local)"
@@ -156,30 +168,30 @@ if (format == 'tree') {
log.console(prefix + (is_last ? "\\-- " : "|-- ") + locator + suffix)
// Get children
var children = []
children = []
arrfor(edges, function(e) {
if (e.from == locator) {
push(children, e)
}
})
for (var i = 0; i < length(children); i++) {
var child_prefix = prefix + (is_last ? " " : "| ")
print_tree(children[i].to, child_prefix, i == length(children) - 1, visited)
for (j = 0; j < length(children); j++) {
child_prefix = prefix + (is_last ? " " : "| ")
print_tree(children[j].to, child_prefix, j == length(children) - 1, visited)
}
}
for (var i = 0; i < length(roots); i++) {
for (i = 0; i < length(roots); i++) {
log.console(roots[i])
var children = []
children = []
arrfor(edges, function(e) {
if (e.from == roots[i]) {
push(children, e)
}
})
for (var j = 0; j < length(children); j++) {
for (j = 0; j < length(children); j++) {
print_tree(children[j].to, "", j == length(children) - 1, {})
}
@@ -219,7 +231,7 @@ if (format == 'tree') {
log.console("}")
} else if (format == 'json') {
var output = {
output = {
nodes: [],
edges: []
}

16
help.ce
View File

@@ -3,27 +3,29 @@
var fd = use('fd')
var command = length(args) > 0 ? args[0] : null
var man_file = null
var stat = null
var content = null
// Display specific command help
if (command) {
var man_file = 'scripts/man/' + command + '.man'
var stat = fd.stat(man_file);
man_file = 'scripts/man/' + command + '.man'
stat = fd.stat(man_file)
if (stat && stat.isFile) {
var content = text(fd.slurp(man_file))
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.")
}
$stop()
return
}
// Display general help
var cell_man = 'scripts/man/cell.man'
var stat = fd.stat(cell_man);
man_file = 'scripts/man/cell.man'
stat = fd.stat(man_file)
if (stat && stat.isFile) {
var content = text(fd.slurp(cell_man))
content = text(fd.slurp(man_file))
log.console(content)
} else {
// Fallback if man file doesn't exist

View File

@@ -28,8 +28,10 @@ var locator = null
var target_triple = null
var refresh = false
var dry_run = false
var i = 0
var resolved = null
for (var i = 0; i < length(args); i++) {
for (i = 0; i < length(args); i++) {
if (args[i] == '--target' || args[i] == '-t') {
if (i + 1 < length(args)) {
target_triple = args[++i]
@@ -64,7 +66,7 @@ if (!locator) {
// 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)) {
var resolved = fd.realpath(locator)
resolved = fd.realpath(locator)
if (resolved) {
locator = resolved
}
@@ -83,6 +85,9 @@ 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
@@ -96,12 +101,12 @@ function gather_packages(pkg_locator) {
push(packages_to_install, pkg_locator)
// Try to read dependencies
try {
var _gather = function() {
// For packages not yet extracted, we need to update and extract first to read deps
var lock = shop.load_lock()
lock = shop.load_lock()
if (!lock[pkg_locator]) {
if (!dry_run) {
var update_result = shop.update(pkg_locator)
update_result = shop.update(pkg_locator)
if (update_result) {
shop.extract(pkg_locator)
} else {
@@ -117,19 +122,20 @@ function gather_packages(pkg_locator) {
}
}
var deps = pkg.dependencies(pkg_locator)
deps = pkg.dependencies(pkg_locator)
if (deps) {
arrfor(array(deps), function(alias) {
var dep_locator = deps[alias]
gather_packages(dep_locator)
})
}
} catch (e) {
} disruption {
// Package might not have dependencies or cell.toml issue
if (!dry_run) {
log.console(`Warning: Could not read dependencies for ${pkg_locator}: ${e.message}`)
log.console(`Warning: Could not read dependencies for ${pkg_locator}`)
}
}
_gather()
}
// Gather all packages
@@ -164,11 +170,12 @@ function install_package(pkg_locator) {
shop.build_package_scripts(pkg_locator)
// Build C code
try {
var _build_c = function() {
build.build_dynamic(pkg_locator, target_triple, 'release')
} catch (e) {
} disruption {
// Not all packages have C code
}
_build_c()
}
arrfor(packages_to_install, function(p) {

View File

@@ -1,283 +1,105 @@
// Hidden vars come from env:
// CLI mode (cell_init): os, args, core_path, shop_path, emit_qbe, dump_mach
// Actor spawn (script_startup): os, json, nota, wota, actorsym, init, core_path, shop_path
// args[0] = script name, args[1..] = user args
// 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
var load_internal = os.load_internal
function use_embed(name) {
return load_internal("js_" + name + "_use")
return load_internal("js_core_" + name + "_use")
}
var fd = use_embed('fd')
var json = use_embed('json')
var fd = use_embed('internal_fd')
var json_mod = use_embed('json')
var crypto = use_embed('crypto')
var use_cache = {}
use_cache['fd'] = fd
use_cache['os'] = os
use_cache['json'] = json
// Bootstrap: load tokenize.cm, parse.cm, fold.cm from pre-compiled mach bytecode
function use_basic(path) {
if (use_cache[path])
return use_cache[path]
var result = use_embed(replace(path, '/', '_'))
use_cache[path] = result
return result
function content_hash(content) {
return text(crypto.blake2(content), 'h')
}
// Load a module from .mach bytecode (bootstrap modules have no source fallback)
function boot_load(name, env) {
var mach_path = core_path + '/' + name + ".mach"
var data = null
if (fd.is_file(mach_path)) {
data = fd.slurp(mach_path)
return mach_load(data, env)
function cache_path(hash) {
if (!shop_path) return null
return shop_path + '/build/' + hash
}
function ensure_build_dir() {
if (!shop_path) return null
var dir = shop_path + '/build'
if (!fd.is_dir(dir)) fd.mkdir(dir)
return dir
}
// Load seed pipeline from boot/ (tokenize, parse, mcode only)
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")
disrupt
}
print("error: missing bootstrap bytecode: " + mach_path + "\n")
disrupt
mcode_blob = fd.slurp(mcode_path)
mach_blob = mach_compile_mcode_bin(name, text(mcode_blob))
return mach_load(mach_blob, stone({use: use_embed}))
}
var boot_env = {use: use_basic}
var tokenize_mod = boot_load("tokenize", boot_env)
var parse_mod = boot_load("parse", boot_env)
var fold_mod = boot_load("fold", boot_env)
use_cache['tokenize'] = tokenize_mod
use_cache['parse'] = parse_mod
use_cache['fold'] = fold_mod
var tokenize_mod = boot_load("tokenize")
var parse_mod = boot_load("parse")
var fold_mod = boot_load("fold")
var mcode_mod = boot_load("mcode")
// Always load mcode compiler module
var mcode_mod = boot_load("mcode", boot_env)
use_cache['mcode'] = mcode_mod
var streamline_mod = null
var qbe_emit_mod = null
// Warn if any .cm source is newer than its .mach bytecode
function check_mach_stale() {
var pairs = [
["tokenize.cm", "tokenize.mach"],
["parse.cm", "parse.mach"],
["fold.cm", "fold.mach"],
["mcode.cm", "mcode.mach"],
["streamline.cm", "streamline.mach"],
["qbe.cm", "qbe.mach"],
["qbe_emit.cm", "qbe_emit.mach"],
["internal/bootstrap.cm", "internal/bootstrap.mach"],
["internal/engine.cm", "internal/engine.mach"]
]
var stale = []
var _i = 0
var cm_path = null
var mach_path = null
var cm_stat = null
var mach_stat = null
while (_i < length(pairs)) {
cm_path = core_path + '/' + pairs[_i][0]
mach_path = core_path + '/' + pairs[_i][1]
if (fd.is_file(cm_path) && fd.is_file(mach_path)) {
cm_stat = fd.stat(cm_path)
mach_stat = fd.stat(mach_path)
if (cm_stat.mtime > mach_stat.mtime) {
push(stale, pairs[_i][0])
}
}
_i = _i + 1
}
if (length(stale) > 0) {
print("warning: bytecode is stale for: " + text(stale, ", ") + "\n")
print("run 'make regen' or './cell --core . regen.cm' to update\n")
}
}
check_mach_stale()
// analyze: tokenize + parse, check for errors
function analyze(src, filename) {
var tok_result = tokenize_mod(src, filename)
var ast = parse_mod(tok_result.tokens, src, filename, tokenize_mod)
var _i = 0
var prev_line = -1
var prev_msg = null
var e = null
var msg = null
var line = null
var col = null
var has_errors = ast.errors != null && length(ast.errors) > 0
if (has_errors) {
while (_i < length(ast.errors)) {
e = ast.errors[_i]
msg = e.message
line = e.line
col = e.column
if (msg != prev_msg || line != prev_line) {
if (line != null && col != null) {
print(`${filename}:${text(line)}:${text(col)}: error: ${msg}`)
} else {
print(`${filename}: error: ${msg}`)
}
}
prev_line = line
prev_msg = msg
if (e.line != null && e.column != null)
print(`${filename}:${text(e.line)}:${text(e.column)}: error: ${msg}`)
else
print(`${filename}: error: ${msg}`)
_i = _i + 1
}
disrupt
}
ast = fold_mod(ast)
return ast
return fold_mod(ast)
}
// Load a module from .mach bytecode, falling back to source compilation
function load_module(name, env) {
var mach_path = core_path + '/' + name + ".mach"
var data = null
var src_path = null
var src = null
function compile_and_cache(name, source_path) {
var source_blob = fd.slurp(source_path)
var hash = content_hash(source_blob)
var cached = cache_path(hash)
var ast = null
var compiled = null
var optimized = null
if (fd.is_file(mach_path)) {
data = fd.slurp(mach_path)
return mach_load(data, env)
}
src_path = core_path + '/' + name + ".cm"
src = text(fd.slurp(src_path))
ast = analyze(src, src_path)
var mcode_json = null
var mach_blob = null
if (cached && fd.is_file(cached)) return
ast = analyze(text(source_blob), source_path)
compiled = mcode_mod(ast)
optimized = streamline_mod(compiled)
return mach_eval_mcode(name, json.encode(optimized), env)
mcode_json = json_mod.encode(compiled)
mach_blob = mach_compile_mcode_bin(name, mcode_json)
if (cached) {
ensure_build_dir()
fd.slurpwrite(cached, mach_blob)
}
}
// Load optimization pipeline modules (needs analyze to be defined)
var qbe_macros = null
streamline_mod = load_module("streamline", boot_env)
use_cache['streamline'] = streamline_mod
if (emit_qbe) {
qbe_macros = load_module("qbe", boot_env)
qbe_emit_mod = load_module("qbe_emit", boot_env)
use_cache['qbe'] = qbe_macros
use_cache['qbe_emit'] = qbe_emit_mod
}
// Run AST through mcode pipeline → register VM
function run_ast(name, ast, env) {
var compiled = mcode_mod(ast)
var optimized = streamline_mod(compiled)
var qbe_il = null
if (emit_qbe) {
qbe_il = qbe_emit_mod(optimized, qbe_macros)
print(qbe_il)
return null
}
if (dump_mach) {
mach_dump_mcode(name, json.encode(optimized), env)
return null
}
return mach_eval_mcode(name, json.encode(optimized), env)
}
// use() with ƿit pipeline for .cm modules
function use_fn(path) {
var file_path = null
var mach_path = null
var data = null
var script = null
var ast = null
var result = null
if (use_cache[path])
return use_cache[path]
// Try .mach bytecode first (CWD then core_path)
mach_path = path + '.mach'
if (!fd.is_file(mach_path))
mach_path = core_path + '/' + path + '.mach'
if (fd.is_file(mach_path)) {
data = fd.slurp(mach_path)
result = mach_load(data, {use: use_fn})
use_cache[path] = result
return result
}
// Try .cm source (CWD then core_path)
file_path = path + '.cm'
if (!fd.is_file(file_path))
file_path = core_path + '/' + path + '.cm'
if (fd.is_file(file_path)) {
script = text(fd.slurp(file_path))
ast = analyze(script, file_path)
result = run_ast(path, ast, {use: use_fn})
use_cache[path] = result
return result
}
// Fallback to embedded C module
result = use_embed(replace(path, '/', '_'))
use_cache[path] = result
return result
}
// Helper to load engine.cm and run it with given env
function load_engine(env) {
var engine_path = core_path + '/internal/engine.mach'
var data = null
var engine_src = null
var engine_ast = null
if (fd.is_file(engine_path)) {
data = fd.slurp(engine_path)
return mach_load(data, env)
}
engine_path = core_path + '/internal/engine.cm'
engine_src = text(fd.slurp(engine_path))
engine_ast = analyze(engine_src, engine_path)
return run_ast('engine', engine_ast, env)
}
// Detect mode and route
// CLI mode has 'args'; actor spawn mode has 'init'
var program = null
var user_args = []
var _j = 0
var script_file = null
var script = null
var ast = null
if (args != null) {
// CLI mode — parse args
program = args[0]
_j = 1
while (_j < length(args)) {
push(user_args, args[_j])
_j = _j + 1
}
// Resolve script file: try .cm then .ce in CWD then core_path
script_file = program
if (!ends_with(script_file, '.ce') && !ends_with(script_file, '.cm'))
script_file = program + '.cm'
if (!fd.is_file(script_file))
script_file = core_path + '/' + program + '.cm'
if (!fd.is_file(script_file))
script_file = program + '.ce'
if (!fd.is_file(script_file))
script_file = core_path + '/' + program + '.ce'
if (ends_with(script_file, '.ce')) {
// Actor script — delegate to engine
load_engine({
os: os, actorsym: actorsym,
init: {program: program, arg: user_args},
core_path: core_path, shop_path: shop_path, json: json,
analyze: analyze, run_ast_fn: run_ast
})
} else {
// Module script — run directly
script = text(fd.slurp(script_file))
ast = analyze(script, script_file)
run_ast(program, ast, {use: use_fn, args: user_args, json: json})
}
} else {
// Actor spawn mode — load engine.cm with full actor env
load_engine({
os: os, actorsym: actorsym, init: init,
core_path: core_path, shop_path: shop_path, json: json, nota: nota, wota: wota,
analyze: analyze, run_ast_fn: run_ast
})
// 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
}
print("bootstrap: cache seeded\n")

Binary file not shown.

View File

@@ -1,5 +1,5 @@
// Hidden vars (os, actorsym, init, core_path, shop_path, analyze, run_ast_fn, json) come from env
// In actor spawn mode, also: nota, wota
// Hidden vars (os, actorsym, init, core_path, shop_path, json, args) come from env
// Engine is self-sufficient: defines its own compilation pipeline
var ACTORDATA = actorsym
var SYSYM = '__SYSTEM__'
@@ -15,13 +15,16 @@ var cases = {
var dylib_ext = cases[os.platform()]
var MOD_EXT = '.cm'
var ACTOR_EXT = '.ce'
var ACTOR_EXT = '.ce'
var load_internal = os.load_internal
function use_embed(name) {
return load_internal("js_" + name + "_use")
return load_internal("js_core_" + name + "_use")
}
// These duplicate C builtins from runtime.c, but are needed because the GC can
// lose properties on the global object after compaction. The runtime_env provides
// a stable way for modules to access them via env_record linking.
function logical(val1) {
if (val1 == 0 || val1 == false || val1 == "false" || val1 == null)
return false;
@@ -46,14 +49,161 @@ function ends_with(str, suffix) {
return search(str, suffix, -length(suffix)) != null
}
var fd = use_embed('fd')
var fd = use_embed('internal_fd')
var js = use_embed('js')
var crypto = use_embed('crypto')
// core_path and shop_path come from env (bootstrap.cm passes them through)
// core_path and shop_path come from env (C runtime passes them through)
// shop_path may be null if --core was used without --shop
var packages_path = shop_path ? shop_path + '/packages' : null
// Self-sufficient initialization: content-addressed cache
var use_cache = {}
function content_hash(content) {
return text(crypto.blake2(content), 'h')
}
function cache_path(hash) {
if (!shop_path) return null
return shop_path + '/build/' + hash
}
function ensure_build_dir() {
if (!shop_path) return null
var dir = shop_path + '/build'
if (!fd.is_dir(dir)) fd.mkdir(dir)
return dir
}
// Load a pipeline module from cache, with boot/ seed fallback
function load_pipeline_module(name, env) {
var source_path = core_path + '/' + name + '.cm'
var source_blob = null
var hash = null
var cached = null
var mcode_path = null
var mcode_blob = null
var mach_blob = null
if (fd.is_file(source_path)) {
source_blob = fd.slurp(source_path)
hash = content_hash(source_blob)
cached = cache_path(hash)
if (cached && fd.is_file(cached))
return mach_load(fd.slurp(cached), env)
}
// Boot seed fallback
mcode_path = core_path + '/boot/' + name + '.cm.mcode'
if (fd.is_file(mcode_path)) {
mcode_blob = fd.slurp(mcode_path)
mach_blob = mach_compile_mcode_bin(name, text(mcode_blob))
return mach_load(mach_blob, env)
}
print("error: cannot load pipeline module: " + name + "\n")
disrupt
}
// Load compilation pipeline
var pipeline_env = stone({use: use_embed})
var tokenize_mod = load_pipeline_module('tokenize', pipeline_env)
var parse_mod = load_pipeline_module('parse', pipeline_env)
var fold_mod = load_pipeline_module('fold', pipeline_env)
var mcode_mod = load_pipeline_module('mcode', pipeline_env)
var streamline_mod = load_pipeline_module('streamline', pipeline_env)
use_cache['tokenize'] = tokenize_mod
use_cache['parse'] = parse_mod
use_cache['fold'] = fold_mod
use_cache['mcode'] = mcode_mod
use_cache['core/mcode'] = mcode_mod
use_cache['streamline'] = streamline_mod
use_cache['core/streamline'] = streamline_mod
// analyze: tokenize + parse + fold, check for errors
function analyze(src, filename) {
var tok_result = tokenize_mod(src, filename)
var _ast = parse_mod(tok_result.tokens, src, filename, tokenize_mod)
var _i = 0
var prev_line = -1
var prev_msg = null
var e = null
var msg = null
var line = null
var col = null
var has_errors = _ast.errors != null && length(_ast.errors) > 0
if (has_errors) {
while (_i < length(_ast.errors)) {
e = _ast.errors[_i]
msg = e.message
line = e.line
col = e.column
if (msg != prev_msg || line != prev_line) {
if (line != null && col != null)
print(`${filename}:${text(line)}:${text(col)}: error: ${msg}`)
else
print(`${filename}: error: ${msg}`)
}
prev_line = line
prev_msg = msg
_i = _i + 1
}
disrupt
}
return fold_mod(_ast)
}
// Lazy-loaded verify_ir module (loaded on first use)
var _verify_ir_mod = null
// Run AST through mcode pipeline -> register VM
function run_ast_fn(name, ast, env) {
var compiled = mcode_mod(ast)
if (os._verify_ir) {
if (_verify_ir_mod == null) {
_verify_ir_mod = load_pipeline_module('verify_ir', pipeline_env)
}
compiled._verify = true
compiled._verify_mod = _verify_ir_mod
}
var optimized = streamline_mod(compiled)
if (optimized._verify) {
delete optimized._verify
delete optimized._verify_mod
}
var mcode_json = json.encode(optimized)
var mach_blob = mach_compile_mcode_bin(name, mcode_json)
return mach_load(mach_blob, env)
}
// Run AST through mcode pipeline WITHOUT optimization -> register VM
function run_ast_noopt_fn(name, ast, env) {
var compiled = mcode_mod(ast)
var mcode_json = json.encode(compiled)
var mach_blob = mach_compile_mcode_bin(name, mcode_json)
return mach_load(mach_blob, env)
}
// Compile AST to blob without loading (for caching)
function compile_to_blob(name, ast) {
var compiled = mcode_mod(ast)
var optimized = streamline_mod(compiled)
return mach_compile_mcode_bin(name, json.encode(optimized))
}
// If loaded directly by C runtime (not via bootstrap), convert args -> init
var _program = null
var _user_args = []
var _j = 1
var _init = init
if (args != null && _init == null) {
_program = args[0]
while (_j < length(args)) {
push(_user_args, args[_j])
_j = _j + 1
}
_init = {program: _program, arg: _user_args}
}
use_cache['core/os'] = os
// Extra env properties added as engine initializes (log, runtime fns, etc.)
@@ -70,26 +220,43 @@ function use_core(path) {
var result = null
var script = null
var ast = null
var _load_mod = null
// Build env: merge core_extras, include C embed as 'native' if available
// Build env: merge core_extras
env = {use: use_core}
arrfor(array(core_extras), function(k) { env[k] = core_extras[k] })
if (sym) env.native = sym
env = stone(env)
// Check for pre-compiled .mach file first
var mach_path = core_path + '/' + path + '.mach'
if (fd.is_file(mach_path)) {
result = mach_load(fd.slurp(mach_path), env)
use_cache[cache_key] = result
return result
}
var hash = null
var cached_path = null
var mach_blob = null
var source_blob = null
var file_path = null
// Fall back to source .cm file — compile at runtime
var file_path = core_path + '/' + path + MOD_EXT
// Compile from source .cm file
file_path = core_path + '/' + path + MOD_EXT
if (fd.is_file(file_path)) {
script = text(fd.slurp(file_path))
ast = analyze(script, file_path)
result = run_ast_fn('core:' + path, ast, env)
_load_mod = function() {
source_blob = fd.slurp(file_path)
hash = content_hash(source_blob)
cached_path = cache_path(hash)
if (cached_path && fd.is_file(cached_path)) {
result = mach_load(fd.slurp(cached_path), env)
} else {
script = text(source_blob)
ast = analyze(script, file_path)
mach_blob = compile_to_blob('core:' + path, ast)
if (cached_path) {
ensure_build_dir()
fd.slurpwrite(cached_path, mach_blob)
}
result = mach_load(mach_blob, env)
}
} disruption {
print("use('" + path + "'): failed to compile or load " + file_path + "\n")
disrupt
}
_load_mod()
use_cache[cache_key] = result
return result
}
@@ -188,27 +355,44 @@ function actor_die(err)
//actor_mod.on_exception(actor_die)
_cell.args = init != null ? init : {}
_cell.id = "newguy"
_cell.args = _init != null ? _init : {}
function create_actor(desc) {
var _desc = desc == null ? {id:guid()} : desc
var actor = {}
actor[ACTORDATA] = _desc
stone(actor)
return actor
}
var $_ = {}
$_.self = create_actor()
os.use_cache = use_cache
os.global_shop_path = shop_path
os.$_ = $_
os.analyze = analyze
os.run_ast_fn = run_ast_fn
os.json = json
use_cache['core/json'] = json
// Create runtime_env early (empty) -- filled after pronto loads.
// Shop accesses it lazily (in inject_env, called at module-use time, not load time)
// so it sees the filled version.
var runtime_env = {}
// Populate core_extras with everything shop (and other core modules) need
core_extras.use_cache = use_cache
core_extras.core_path = core_path
core_extras.shop_path = shop_path
core_extras.analyze = analyze
core_extras.run_ast_fn = run_ast_fn
core_extras.run_ast_noopt_fn = run_ast_noopt_fn
os.analyze = analyze
os.run_ast_fn = run_ast_fn
os.run_ast_noopt_fn = run_ast_noopt_fn
core_extras.core_json = json
core_extras.actor_api = $_
core_extras.runtime_env = runtime_env
core_extras.content_hash = content_hash
core_extras.cache_path = cache_path
core_extras.ensure_build_dir = ensure_build_dir
core_extras.compile_to_blob = compile_to_blob
// NOW load shop -- it receives all of the above via env
var shop = use_core('internal/shop')
var time = use_core('time')
@@ -218,29 +402,26 @@ var parallel = pronto.parallel
var race = pronto.race
var sequence = pronto.sequence
// Create runtime environment for modules
var runtime_env = {
logical: logical,
some: some,
every: every,
starts_with: starts_with,
ends_with: ends_with,
actor: actor,
is_actor: is_actor,
log: log,
send: send,
fallback: fallback,
parallel: parallel,
race: race,
sequence: sequence
}
// Fill runtime_env — includes duplicates of C builtins because the GC can
// lose global object properties after compaction. Modules resolve these via
// env_record, not the global.
runtime_env.logical = logical
runtime_env.some = some
runtime_env.every = every
runtime_env.starts_with = starts_with
runtime_env.ends_with = ends_with
runtime_env.actor = actor
runtime_env.is_actor = is_actor
runtime_env.log = log
runtime_env.send = send
runtime_env.fallback = fallback
runtime_env.parallel = parallel
runtime_env.race = race
runtime_env.sequence = sequence
// Make runtime functions available to modules loaded via use_core
arrfor(array(runtime_env), function(k) { core_extras[k] = runtime_env[k] })
// Pass to os for shop to access
os.runtime_env = runtime_env
$_.time_limit = function(requestor, seconds)
{
if (!pronto.is_requestor(requestor)) {
@@ -334,7 +515,7 @@ REPLYTIMEOUT = config.reply_timeout
replycc: the actor that is waiting for the reply
target: ID of the actor that's supposed to receive the message. Only added to non direct sends (out of portals)
return: reply ID so the replycc actor can know what callback to send the message to
data: the actual content of the message
}
@@ -405,7 +586,7 @@ $_.connection = function(callback, actor, config) {
callback({type:"local"})
return
}
callback()
}
@@ -486,10 +667,10 @@ $_.start = function start(cb, program) {
if (!program) return
var id = guid()
var startup = {
id,
overling: $_.self,
root,
var startup = {
id,
overling: $_.self,
root,
program,
}
greeters[id] = cb
@@ -680,7 +861,7 @@ stone(send)
if (!_cell.args.id) _cell.id = guid()
else _cell.id = _cell.args.id
$_.self[ACTORDATA].id = _cell.id
$_.self = create_actor({id: _cell.id})
// Actor's timeslice for processing a single message
function turn(msg)
@@ -695,7 +876,7 @@ actor_mod.register_actor(_cell.id, turn, true, config.ar_timer)
if (config.actor_memory)
js.mem_limit(config.actor_memory)
if (config.stack_max)
js.max_stacksize(config.system.stack_max);
@@ -728,7 +909,7 @@ function report_to_overling(msg)
var program = _cell.args.program
if (!program) {
log.error('No program specified. Usage: cell <program.ce> [args...]')
log.error('No program specified. Usage: cell <program> [args...]')
os.exit(1)
}
@@ -785,12 +966,8 @@ function handle_message(msg) {
if (msg.type == "user") {
letter = msg.data // what the sender really sent
_ObjectDefineProperty(letter, HEADER, {
value: msg, enumerable: false
})
_ObjectDefineProperty(letter, ACTORDATA, { // this is so is_actor == true
value: { reply: msg.reply }, enumerable: false
})
letter[HEADER] = msg
letter[ACTORDATA] = { reply: msg.reply }
if (msg.return) {
fn = replies[msg.return]
@@ -808,7 +985,7 @@ function handle_message(msg) {
function enet_check()
{
if (portal) portal.service(handle_host)
$_.delay(enet_check, ENETSERVICE);
}
@@ -818,6 +995,10 @@ function enet_check()
actor_mod.setname(_cell.args.program)
var prog = _cell.args.program
if (ends_with(prog, '.cm')) {
os.print(`error: ${prog} is a module (.cm), not a program (.ce)\n`)
os.exit(1)
}
if (ends_with(prog, '.ce')) prog = text(prog, 0, -3)
var package = use_core('package')
@@ -864,14 +1045,33 @@ $_.clock(_ => {
env.use = function(path) {
var ck = 'core/' + path
if (use_cache[ck]) return use_cache[ck]
var core_mod = use_core(path)
if (core_mod) return core_mod
return shop.use(path, pkg)
}
env.args = _cell.args.arg
env.log = log
env = stone(env)
var script = text(fd.slurp(prog_path))
var ast = analyze(script, prog_path)
var val = run_ast_fn(prog, ast, env)
var source_blob = fd.slurp(prog_path)
var hash = content_hash(source_blob)
var cached_path = cache_path(hash)
var val = null
var script = null
var ast = null
var mach_blob = null
if (cached_path && fd.is_file(cached_path)) {
val = mach_load(fd.slurp(cached_path), env)
} else {
script = text(source_blob)
ast = analyze(script, prog_path)
mach_blob = compile_to_blob(prog, ast)
if (cached_path) {
ensure_build_dir()
fd.slurpwrite(cached_path, mach_blob)
}
val = mach_load(mach_blob, env)
}
if (val) {
log.error('Program must not return anything')
disrupt

Binary file not shown.

View File

@@ -412,117 +412,117 @@ JSC_CCALL(fd_close,
JSC_CCALL(fd_fstat,
int fd = js2fd(js, argv[0]);
if (fd < 0) return JS_EXCEPTION;
struct stat st;
if (fstat(fd, &st) != 0)
return JS_ThrowInternalError(js, "fstat failed: %s", strerror(errno));
JSValue obj = JS_NewObject(js);
JS_SetPropertyStr(js, obj, "size", JS_NewInt64(js, st.st_size));
JS_SetPropertyStr(js, obj, "mode", JS_NewInt32(js, st.st_mode));
JS_SetPropertyStr(js, obj, "uid", JS_NewInt32(js, st.st_uid));
JS_SetPropertyStr(js, obj, "gid", JS_NewInt32(js, st.st_gid));
JS_SetPropertyStr(js, obj, "atime", JS_NewInt64(js, st.st_atime));
JS_SetPropertyStr(js, obj, "mtime", JS_NewInt64(js, st.st_mtime));
JS_SetPropertyStr(js, obj, "ctime", JS_NewInt64(js, st.st_ctime));
JS_SetPropertyStr(js, obj, "nlink", JS_NewInt32(js, st.st_nlink));
JS_SetPropertyStr(js, obj, "ino", JS_NewInt64(js, st.st_ino));
JS_SetPropertyStr(js, obj, "dev", JS_NewInt32(js, st.st_dev));
JS_SetPropertyStr(js, obj, "rdev", JS_NewInt32(js, st.st_rdev));
JS_FRAME(js);
JS_ROOT(obj, JS_NewObject(js));
JS_SetPropertyStr(js, obj.val, "size", JS_NewInt64(js, st.st_size));
JS_SetPropertyStr(js, obj.val, "mode", JS_NewInt32(js, st.st_mode));
JS_SetPropertyStr(js, obj.val, "uid", JS_NewInt32(js, st.st_uid));
JS_SetPropertyStr(js, obj.val, "gid", JS_NewInt32(js, st.st_gid));
JS_SetPropertyStr(js, obj.val, "atime", JS_NewInt64(js, st.st_atime));
JS_SetPropertyStr(js, obj.val, "mtime", JS_NewInt64(js, st.st_mtime));
JS_SetPropertyStr(js, obj.val, "ctime", JS_NewInt64(js, st.st_ctime));
JS_SetPropertyStr(js, obj.val, "nlink", JS_NewInt32(js, st.st_nlink));
JS_SetPropertyStr(js, obj.val, "ino", JS_NewInt64(js, st.st_ino));
JS_SetPropertyStr(js, obj.val, "dev", JS_NewInt32(js, st.st_dev));
JS_SetPropertyStr(js, obj.val, "rdev", JS_NewInt32(js, st.st_rdev));
#ifndef _WIN32
JS_SetPropertyStr(js, obj, "blksize", JS_NewInt32(js, st.st_blksize));
JS_SetPropertyStr(js, obj, "blocks", JS_NewInt64(js, st.st_blocks));
JS_SetPropertyStr(js, obj.val, "blksize", JS_NewInt32(js, st.st_blksize));
JS_SetPropertyStr(js, obj.val, "blocks", JS_NewInt64(js, st.st_blocks));
#else
JS_SetPropertyStr(js, obj, "blksize", JS_NewInt32(js, 4096));
JS_SetPropertyStr(js, obj, "blocks", JS_NewInt64(js, st.st_size / 512));
JS_SetPropertyStr(js, obj.val, "blksize", JS_NewInt32(js, 4096));
JS_SetPropertyStr(js, obj.val, "blocks", JS_NewInt64(js, st.st_size / 512));
#endif
// Add boolean properties for file type
JS_SetPropertyStr(js, obj, "isFile", JS_NewBool(js, S_ISREG(st.st_mode)));
JS_SetPropertyStr(js, obj, "isDirectory", JS_NewBool(js, S_ISDIR(st.st_mode)));
JS_SetPropertyStr(js, obj, "isSymlink", JS_NewBool(js, S_ISLNK(st.st_mode)));
JS_SetPropertyStr(js, obj, "isFIFO", JS_NewBool(js, S_ISFIFO(st.st_mode)));
JS_SetPropertyStr(js, obj, "isSocket", JS_NewBool(js, S_ISSOCK(st.st_mode)));
JS_SetPropertyStr(js, obj, "isCharDevice", JS_NewBool(js, S_ISCHR(st.st_mode)));
JS_SetPropertyStr(js, obj, "isBlockDevice", JS_NewBool(js, S_ISBLK(st.st_mode)));
return obj;
JS_SetPropertyStr(js, obj.val, "isFile", JS_NewBool(js, S_ISREG(st.st_mode)));
JS_SetPropertyStr(js, obj.val, "isDirectory", JS_NewBool(js, S_ISDIR(st.st_mode)));
JS_SetPropertyStr(js, obj.val, "isSymlink", JS_NewBool(js, S_ISLNK(st.st_mode)));
JS_SetPropertyStr(js, obj.val, "isFIFO", JS_NewBool(js, S_ISFIFO(st.st_mode)));
JS_SetPropertyStr(js, obj.val, "isSocket", JS_NewBool(js, S_ISSOCK(st.st_mode)));
JS_SetPropertyStr(js, obj.val, "isCharDevice", JS_NewBool(js, S_ISCHR(st.st_mode)));
JS_SetPropertyStr(js, obj.val, "isBlockDevice", JS_NewBool(js, S_ISBLK(st.st_mode)));
JS_RETURN(obj.val);
)
JSC_CCALL(fd_stat,
const char *path = JS_ToCString(js, argv[0]);
if (!path) return JS_EXCEPTION;
struct stat st;
if (stat(path, &st) != 0) {
JS_FreeCString(js, path);
return JS_NewObject(js);
}
JSValue obj = JS_NewObject(js);
JS_SetPropertyStr(js, obj, "size", JS_NewInt64(js, st.st_size));
JS_SetPropertyStr(js, obj, "mode", JS_NewInt32(js, st.st_mode));
JS_SetPropertyStr(js, obj, "uid", JS_NewInt32(js, st.st_uid));
JS_SetPropertyStr(js, obj, "gid", JS_NewInt32(js, st.st_gid));
JS_SetPropertyStr(js, obj, "atime", JS_NewInt64(js, st.st_atime));
JS_SetPropertyStr(js, obj, "mtime", JS_NewInt64(js, st.st_mtime));
JS_SetPropertyStr(js, obj, "ctime", JS_NewInt64(js, st.st_ctime));
JS_SetPropertyStr(js, obj, "nlink", JS_NewInt32(js, st.st_nlink));
JS_SetPropertyStr(js, obj, "ino", JS_NewInt64(js, st.st_ino));
JS_SetPropertyStr(js, obj, "dev", JS_NewInt32(js, st.st_dev));
JS_SetPropertyStr(js, obj, "rdev", JS_NewInt32(js, st.st_rdev));
JS_FRAME(js);
JS_ROOT(obj, JS_NewObject(js));
JS_SetPropertyStr(js, obj.val, "size", JS_NewInt64(js, st.st_size));
JS_SetPropertyStr(js, obj.val, "mode", JS_NewInt32(js, st.st_mode));
JS_SetPropertyStr(js, obj.val, "uid", JS_NewInt32(js, st.st_uid));
JS_SetPropertyStr(js, obj.val, "gid", JS_NewInt32(js, st.st_gid));
JS_SetPropertyStr(js, obj.val, "atime", JS_NewInt64(js, st.st_atime));
JS_SetPropertyStr(js, obj.val, "mtime", JS_NewInt64(js, st.st_mtime));
JS_SetPropertyStr(js, obj.val, "ctime", JS_NewInt64(js, st.st_ctime));
JS_SetPropertyStr(js, obj.val, "nlink", JS_NewInt32(js, st.st_nlink));
JS_SetPropertyStr(js, obj.val, "ino", JS_NewInt64(js, st.st_ino));
JS_SetPropertyStr(js, obj.val, "dev", JS_NewInt32(js, st.st_dev));
JS_SetPropertyStr(js, obj.val, "rdev", JS_NewInt32(js, st.st_rdev));
#ifndef _WIN32
JS_SetPropertyStr(js, obj, "blksize", JS_NewInt32(js, st.st_blksize));
JS_SetPropertyStr(js, obj, "blocks", JS_NewInt64(js, st.st_blocks));
JS_SetPropertyStr(js, obj.val, "blksize", JS_NewInt32(js, st.st_blksize));
JS_SetPropertyStr(js, obj.val, "blocks", JS_NewInt64(js, st.st_blocks));
#else
JS_SetPropertyStr(js, obj, "blksize", JS_NewInt32(js, 4096));
JS_SetPropertyStr(js, obj, "blocks", JS_NewInt64(js, st.st_size / 512));
JS_SetPropertyStr(js, obj.val, "blksize", JS_NewInt32(js, 4096));
JS_SetPropertyStr(js, obj.val, "blocks", JS_NewInt64(js, st.st_size / 512));
#endif
// Add boolean properties for file type
JS_SetPropertyStr(js, obj, "isFile", JS_NewBool(js, S_ISREG(st.st_mode)));
JS_SetPropertyStr(js, obj, "isDirectory", JS_NewBool(js, S_ISDIR(st.st_mode)));
JS_SetPropertyStr(js, obj, "isSymlink", JS_NewBool(js, S_ISLNK(st.st_mode)));
JS_SetPropertyStr(js, obj, "isFIFO", JS_NewBool(js, S_ISFIFO(st.st_mode)));
JS_SetPropertyStr(js, obj, "isSocket", JS_NewBool(js, S_ISSOCK(st.st_mode)));
JS_SetPropertyStr(js, obj, "isCharDevice", JS_NewBool(js, S_ISCHR(st.st_mode)));
JS_SetPropertyStr(js, obj, "isBlockDevice", JS_NewBool(js, S_ISBLK(st.st_mode)));
JS_SetPropertyStr(js, obj.val, "isFile", JS_NewBool(js, S_ISREG(st.st_mode)));
JS_SetPropertyStr(js, obj.val, "isDirectory", JS_NewBool(js, S_ISDIR(st.st_mode)));
JS_SetPropertyStr(js, obj.val, "isSymlink", JS_NewBool(js, S_ISLNK(st.st_mode)));
JS_SetPropertyStr(js, obj.val, "isFIFO", JS_NewBool(js, S_ISFIFO(st.st_mode)));
JS_SetPropertyStr(js, obj.val, "isSocket", JS_NewBool(js, S_ISSOCK(st.st_mode)));
JS_SetPropertyStr(js, obj.val, "isCharDevice", JS_NewBool(js, S_ISCHR(st.st_mode)));
JS_SetPropertyStr(js, obj.val, "isBlockDevice", JS_NewBool(js, S_ISBLK(st.st_mode)));
JS_FreeCString(js, path);
return obj;
JS_RETURN(obj.val);
)
JSC_SCALL(fd_readdir,
JS_FRAME(js);
#ifdef _WIN32
WIN32_FIND_DATA ffd;
char path[PATH_MAX];
snprintf(path, sizeof(path), "%s\\*", str);
HANDLE hFind = FindFirstFile(path, &ffd);
if (hFind == INVALID_HANDLE_VALUE) {
ret = JS_ThrowInternalError(js, "FindFirstFile failed for %s", path);
ret = JS_ThrowInternalError(js, "FindFirstFile failed for %s", path);
} else {
ret = JS_NewArray(js);
do {
if (strcmp(ffd.cFileName, ".") == 0 || strcmp(ffd.cFileName, "..") == 0) continue;
JS_ArrayPush(js, &ret, JS_NewString(js, ffd.cFileName));
} while (FindNextFile(hFind, &ffd) != 0);
FindClose(hFind);
JS_ROOT(arr, JS_NewArray(js));
do {
if (strcmp(ffd.cFileName, ".") == 0 || strcmp(ffd.cFileName, "..") == 0) continue;
JS_ArrayPush(js, &arr.val, JS_NewString(js, ffd.cFileName));
} while (FindNextFile(hFind, &ffd) != 0);
FindClose(hFind);
ret = arr.val;
}
#else
DIR *d;
struct dirent *dir;
d = opendir(str);
if (d) {
ret = JS_NewArray(js);
JS_ROOT(arr, JS_NewArray(js));
while ((dir = readdir(d)) != NULL) {
if (strcmp(dir->d_name, ".") == 0 || strcmp(dir->d_name, "..") == 0) continue;
JS_ArrayPush(js, &ret, JS_NewString(js, dir->d_name));
JS_ArrayPush(js, &arr.val, JS_NewString(js, dir->d_name));
}
closedir(d);
ret = arr.val;
} else {
ret = JS_ThrowInternalError(js, "opendir failed for %s: %s", str, strerror(errno));
}
#endif
JS_RestoreFrame(_js_ctx, _js_gc_frame, _js_local_frame);
)
JSC_CCALL(fd_is_file,
@@ -585,9 +585,9 @@ JSC_CCALL(fd_slurpwrite,
)
// Helper function for recursive enumeration
static void visit_directory(JSContext *js, JSValue results, int *result_count, const char *curr_path, const char *rel_prefix, int recurse) {
static void visit_directory(JSContext *js, JSValue *results, int *result_count, const char *curr_path, const char *rel_prefix, int recurse) {
if (!curr_path) return;
#ifdef _WIN32
WIN32_FIND_DATA ffd;
char search_path[PATH_MAX];
@@ -602,7 +602,7 @@ static void visit_directory(JSContext *js, JSValue results, int *result_count, c
} else {
strcpy(item_rel, ffd.cFileName);
}
JS_SetPropertyNumber(js, results, (*result_count)++, JS_NewString(js, item_rel));
JS_SetPropertyNumber(js, *results, (*result_count)++, JS_NewString(js, item_rel));
if (recurse) {
struct stat st;
@@ -627,7 +627,7 @@ static void visit_directory(JSContext *js, JSValue results, int *result_count, c
} else {
strcpy(item_rel, dir->d_name);
}
JS_SetPropertyNumber(js, results, (*result_count)++, JS_NewString(js, item_rel));
JS_SetPropertyNumber(js, *results, (*result_count)++, JS_NewString(js, item_rel));
if (recurse) {
struct stat st;
@@ -651,14 +651,16 @@ JSC_SCALL(fd_enumerate,
if (argc > 1)
recurse = JS_ToBool(js, argv[1]);
JSValue results = JS_NewArray(js);
JS_FRAME(js);
JS_ROOT(arr, JS_NewArray(js));
int result_count = 0;
struct stat st;
if (stat(path, &st) == 0 && S_ISDIR(st.st_mode))
visit_directory(js, results, &result_count, path, "", recurse);
visit_directory(js, &arr.val, &result_count, path, "", recurse);
ret = results;
ret = arr.val;
JS_RestoreFrame(_js_ctx, _js_gc_frame, _js_local_frame);
)
JSC_CCALL(fd_realpath,
@@ -752,8 +754,9 @@ static const JSCFunctionListEntry js_fd_funcs[] = {
MIST_FUNC_DEF(fd, readlink, 1),
};
JSValue js_fd_use(JSContext *js) {
JSValue mod = JS_NewObject(js);
JS_SetPropertyFunctionList(js, mod, js_fd_funcs, countof(js_fd_funcs));
return mod;
}
JSValue js_core_internal_fd_use(JSContext *js) {
JS_FRAME(js);
JS_ROOT(mod, JS_NewObject(js));
JS_SetPropertyFunctionList(js, mod.val, js_fd_funcs, countof(js_fd_funcs));
JS_RETURN(mod.val);
}

View File

@@ -73,9 +73,10 @@ static const JSCFunctionListEntry js_kim_funcs[] = {
MIST_FUNC_DEF(kim, decode, 1),
};
JSValue js_kim_use(JSContext *js)
JSValue js_core_kim_use(JSContext *js)
{
JSValue mod = JS_NewObject(js);
JS_SetPropertyFunctionList(js, mod, js_kim_funcs, countof(js_kim_funcs));
return mod;
JS_FRAME(js);
JS_ROOT(mod, JS_NewObject(js));
JS_SetPropertyFunctionList(js, mod.val, js_kim_funcs, countof(js_kim_funcs));
JS_RETURN(mod.val);
}

View File

@@ -277,29 +277,31 @@ JSC_CCALL(os_mallinfo,
)
static JSValue js_os_rusage(JSContext *js, JSValue self, int argc, JSValue *argv) {
JSValue ret = JS_NULL;
ret = JS_NewObject(js);
JS_FRAME(js);
JS_ROOT(ret, JS_NewObject(js));
#if defined(__linux__) || defined(__APPLE__)
struct rusage jsmem;
getrusage(RUSAGE_SELF, &jsmem);
JSJMEMRET(ru_maxrss);
JSJMEMRET(ru_ixrss);
JSJMEMRET(ru_idrss);
JSJMEMRET(ru_isrss);
JSJMEMRET(ru_minflt);
JSJMEMRET(ru_majflt);
JSJMEMRET(ru_nswap);
JSJMEMRET(ru_inblock);
JSJMEMRET(ru_oublock);
JSJMEMRET(ru_msgsnd);
JSJMEMRET(ru_msgrcv);
JSJMEMRET(ru_nsignals);
JSJMEMRET(ru_nvcsw);
JSJMEMRET(ru_nivcsw);
#define JSJMEMRET_GC(FIELD) JS_SetPropertyStr(js, ret.val, #FIELD, number2js(js, jsmem.FIELD));
JSJMEMRET_GC(ru_maxrss);
JSJMEMRET_GC(ru_ixrss);
JSJMEMRET_GC(ru_idrss);
JSJMEMRET_GC(ru_isrss);
JSJMEMRET_GC(ru_minflt);
JSJMEMRET_GC(ru_majflt);
JSJMEMRET_GC(ru_nswap);
JSJMEMRET_GC(ru_inblock);
JSJMEMRET_GC(ru_oublock);
JSJMEMRET_GC(ru_msgsnd);
JSJMEMRET_GC(ru_msgrcv);
JSJMEMRET_GC(ru_nsignals);
JSJMEMRET_GC(ru_nvcsw);
JSJMEMRET_GC(ru_nivcsw);
#undef JSJMEMRET_GC
#endif
return ret;
JS_RETURN(ret.val);
}
JSC_SCALL(os_system,
@@ -425,6 +427,59 @@ static JSValue js_os_dylib_has_symbol(JSContext *js, JSValue self, int argc, JSV
return JS_NewBool(js, symbol != NULL);
}
static JSValue js_os_dylib_close(JSContext *js, JSValue self, int argc, JSValue *argv)
{
if (argc < 1)
return JS_ThrowTypeError(js, "dylib_close requires a dylib object");
void *handle = JS_GetOpaque(argv[0], js_dylib_class_id);
if (handle) {
#ifdef _WIN32
FreeLibrary((HMODULE)handle);
#else
dlclose(handle);
#endif
JS_SetOpaque(argv[0], NULL);
}
return JS_NULL;
}
/* Load a native .cm module from a dylib handle.
Uses cell_rt_native_module_load from qbe_helpers.c */
extern JSValue cell_rt_native_module_load(JSContext *ctx, void *dl_handle, JSValue env);
extern JSValue cell_rt_native_module_load_named(JSContext *ctx, void *dl_handle, const char *sym_name, JSValue env);
static JSValue js_os_native_module_load(JSContext *js, JSValue self, int argc, JSValue *argv)
{
if (argc < 1)
return JS_ThrowTypeError(js, "native_module_load requires a dylib object");
void *handle = JS_GetOpaque(argv[0], js_dylib_class_id);
if (!handle)
return JS_ThrowTypeError(js, "First argument must be a dylib object");
JSValue env = (argc >= 2) ? argv[1] : JS_NULL;
return cell_rt_native_module_load(js, handle, env);
}
static JSValue js_os_native_module_load_named(JSContext *js, JSValue self, int argc, JSValue *argv)
{
if (argc < 2)
return JS_ThrowTypeError(js, "native_module_load_named requires (dylib, sym_name)");
void *handle = JS_GetOpaque(argv[0], js_dylib_class_id);
if (!handle)
return JS_ThrowTypeError(js, "First argument must be a dylib object");
const char *sym_name = JS_ToCString(js, argv[1]);
if (!sym_name)
return JS_EXCEPTION;
JSValue env = (argc >= 3) ? argv[2] : JS_NULL;
JSValue result = cell_rt_native_module_load_named(js, handle, sym_name, env);
JS_FreeCString(js, sym_name);
return result;
}
JSC_CCALL(os_print,
size_t len;
const char *str = JS_ToCStringLen(js, &len, argv[0]);
@@ -552,6 +607,37 @@ JSC_CCALL(os_getenv,
ret = JS_NULL;
)
/* --- Embedded module table (generated for static builds) ---
Uses dlsym to check if cell_embedded_module_lookup exists at runtime.
When linking a static build with a generated module_table.c, the symbol
will be found; in dynamic builds it returns NULL gracefully. */
struct cell_embedded_entry {
const char *name;
const unsigned char *data;
size_t size;
};
typedef const struct cell_embedded_entry *(*cell_embed_lookup_fn)(const char *);
static JSValue js_os_embedded_module(JSContext *js, JSValue self, int argc, JSValue *argv)
{
cell_embed_lookup_fn lookup = (cell_embed_lookup_fn)dlsym(RTLD_DEFAULT, "cell_embedded_module_lookup");
if (!lookup)
return JS_NULL;
const char *name = JS_ToCString(js, argv[0]);
if (!name) return JS_NULL;
const struct cell_embedded_entry *entry = lookup(name);
JS_FreeCString(js, name);
if (!entry) return JS_NULL;
/* Return the mach blob as a stoned blob */
return js_new_blob_stoned_copy(js, (void *)entry->data, entry->size);
}
static const JSCFunctionListEntry js_os_funcs[] = {
MIST_FUNC_DEF(os, platform, 0),
MIST_FUNC_DEF(os, arch, 0),
@@ -566,8 +652,12 @@ static const JSCFunctionListEntry js_os_funcs[] = {
MIST_FUNC_DEF(os, exit, 0),
MIST_FUNC_DEF(os, sleep, 1),
MIST_FUNC_DEF(os, dylib_open, 1),
MIST_FUNC_DEF(os, dylib_close, 1),
MIST_FUNC_DEF(os, dylib_symbol, 2),
MIST_FUNC_DEF(os, dylib_has_symbol, 2),
MIST_FUNC_DEF(os, native_module_load, 2),
MIST_FUNC_DEF(os, native_module_load_named, 3),
MIST_FUNC_DEF(os, embedded_module, 1),
MIST_FUNC_DEF(os, load_internal, 1),
MIST_FUNC_DEF(os, internal_exists, 1),
MIST_FUNC_DEF(os, print, 1),
@@ -575,11 +665,12 @@ static const JSCFunctionListEntry js_os_funcs[] = {
MIST_FUNC_DEF(os, getenv, 1),
};
JSValue js_os_use(JSContext *js) {
JSValue js_core_os_use(JSContext *js) {
JS_NewClassID(&js_dylib_class_id);
JS_NewClass(js, js_dylib_class_id, &js_dylib_class);
JSValue mod = JS_NewObject(js);
JS_SetPropertyFunctionList(js,mod,js_os_funcs,countof(js_os_funcs));
return mod;
JS_FRAME(js);
JS_ROOT(mod, JS_NewObject(js));
JS_SetPropertyFunctionList(js, mod.val, js_os_funcs, countof(js_os_funcs));
JS_RETURN(mod.val);
}

View File

@@ -12,12 +12,63 @@ var pkg_tools = use('package')
var os = use('os')
var link = use('link')
var analyze = os.analyze
var run_ast_fn = os.run_ast_fn
var shop_json = os.json
// These come from env (via core_extras in engine.cm):
// analyze, run_ast_fn, core_json, use_cache, shop_path, actor_api, runtime_env,
// content_hash, cache_path, ensure_build_dir
var shop_json = core_json
var global_shop_path = shop_path
var my$_ = actor_api
var core = "core"
// Generate a random 5-letter uppercase identifier for package symbol naming.
// Avoids collisions with existing IDs and "CORE".
function generate_package_id() {
var lock = Shop.load_lock()
var existing = {}
var keys = array(lock)
var _i = 0
while (_i < length(keys)) {
if (lock[keys[_i]] && lock[keys[_i]].id)
existing[lock[keys[_i]].id] = true
_i = _i + 1
}
existing["CORE"] = true
var id = null
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
var _j = 0
while (true) {
id = ""
_j = 0
while (_j < 5) {
id = id + chars[abs(os.random()) % 26]
_j = _j + 1
}
if (!existing[id]) return id
}
}
// Get the assigned ID for a package, generating one if needed.
// Core always returns "core". Other packages get a random 5-letter ID
// assigned lazily on first use and persisted in lock.toml.
function get_package_id(pkg) {
if (pkg == core) return core
var lock = Shop.load_lock()
var entry = lock[pkg]
if (entry && entry.id) return entry.id
var id = generate_package_id()
if (!entry) {
entry = {}
lock[pkg] = entry
}
entry.id = id
Shop.save_lock(lock)
return id
}
function pull_from_cache(content)
{
var path = hash_path(content)
@@ -45,11 +96,6 @@ function ensure_dir(path) {
}
}
function content_hash(content)
{
return text(crypto.blake2(content), 'h')
}
function hash_path(content)
{
return global_shop_path + '/build' + '/' + content_hash(content)
@@ -66,9 +112,6 @@ var ACTOR_EXT = '.ce'
var dylib_ext = '.dylib' // Default extension
var use_cache = os.use_cache
var global_shop_path = os.global_shop_path
var my$_ = os.$_
Shop.get_package_dir = function(name) {
return global_shop_path + '/packages/' + name
@@ -270,11 +313,6 @@ function package_cache_path(pkg)
return global_shop_path + '/cache/' + replace(replace(pkg, '/', '_'), '@', '_')
}
function get_shared_lib_path()
{
return get_global_build_dir() + '/' + 'lib'
}
// Load lock.toml configuration (from global shop)
var _lock = null
Shop.load_lock = function() {
@@ -301,6 +339,48 @@ Shop.save_lock = function(lock) {
}
// Shop configuration (shop.toml) with policy flags
var _shop_config = null
var _default_policy = {
allow_dylib: true,
allow_static: true,
allow_mach: true,
allow_compile: true
}
Shop.load_config = function() {
if (_shop_config) return _shop_config
if (!global_shop_path) {
_shop_config = {policy: object(_default_policy)}
return _shop_config
}
var path = global_shop_path + '/shop.toml'
if (!fd.is_file(path)) {
_shop_config = {policy: object(_default_policy)}
fd.slurpwrite(path, stone(blob(toml.encode(_shop_config))))
return _shop_config
}
var content = text(fd.slurp(path))
if (!length(content)) {
_shop_config = {policy: object(_default_policy)}
return _shop_config
}
_shop_config = toml.decode(content)
if (!_shop_config.policy) _shop_config.policy = {}
var keys = array(_default_policy)
var i = 0
for (i = 0; i < length(keys); i++) {
if (_shop_config.policy[keys[i]] == null)
_shop_config.policy[keys[i]] = _default_policy[keys[i]]
}
return _shop_config
}
function get_policy() {
var config = Shop.load_config()
return config.policy
}
// Get information about how to resolve a package
// Local packages always start with /
Shop.resolve_package_info = function(pkg) {
@@ -377,9 +457,34 @@ Shop.extract_commit_hash = function(pkg, response) {
return null
}
var dylib_visited = {}
var open_dls = {}
// Host target detection for native dylib resolution
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
}
var host_target = detect_host_target()
// Check for a native .cm dylib at the deterministic lib path
// Returns the loaded module value, or null if no native dylib exists
function try_native_mod_dylib(pkg, stem) {
var dylib_path = get_dylib_path(pkg, stem)
if (!fd.is_file(dylib_path)) return null
var handle = os.dylib_open(dylib_path)
if (!handle) return null
var sym = Shop.c_symbol_for_file(pkg, stem)
return os.native_module_load_named(handle, sym)
}
// Default capabilities injected into scripts
// These map to $_ properties in engine.cm
var SHOP_DEFAULT_INJECT = ['$self', '$overling', '$clock', '$delay', '$start', '$receiver', '$contact', '$portal', '$time_limit', '$couple', '$stop', '$unneeded', '$connection', '$fd']
@@ -404,9 +509,8 @@ Shop.get_script_capabilities = function(path) {
// Matches engine.cm's approach: env properties become free variables in the module.
function inject_env(inject) {
var env = {}
var rt = my$_.os ? my$_.os.runtime_env : null
if (rt) {
arrfor(array(rt), function(k) { env[k] = rt[k] })
if (runtime_env) {
arrfor(array(runtime_env), function(k) { env[k] = runtime_env[k] })
}
// Add capability injections with $ prefix
@@ -433,39 +537,87 @@ function resolve_mod_fn(path, pkg) {
if (!fd.is_file(path)) { print(`path ${path} is not a file`); disrupt }
var content = text(fd.slurp(path))
var cached = pull_from_cache(stone(blob(content)))
var content_key = stone(blob(content))
var native_result = null
var cached = null
var ast = null
var compiled = null
var mach_path = null
var mach_blob = null
var mcode_path = null
var ir = null
var optimized = null
var mcode_json = null
var cached_mcode_path = null
var _pkg_dir = null
var _stem = null
var policy = null
// Check cache for pre-compiled .mach blob
if (cached) {
return cached
policy = get_policy()
// Compute _pkg_dir and _stem early so all paths can use them
if (pkg) {
_pkg_dir = get_packages_dir() + '/' + safe_package_path(pkg)
if (starts_with(path, _pkg_dir + '/')) {
_stem = text(path, length(_pkg_dir) + 1)
}
}
// Check for pre-compiled .mach file alongside .cm source
if (ends_with(path, '.cm')) {
mach_path = text(path, 0, length(path) - 3) + '.mach'
if (fd.is_file(mach_path)) {
mach_blob = fd.slurp(mach_path)
put_into_cache(stone(blob(content)), mach_blob)
return mach_blob
// Check for native .cm dylib at deterministic path first
if (policy.allow_dylib && pkg && _stem) {
native_result = try_native_mod_dylib(pkg, _stem)
if (native_result != null) {
return {_native: true, value: native_result}
}
}
// Check cache for pre-compiled .mach blob
if (policy.allow_mach) {
cached = pull_from_cache(content_key)
if (cached) {
return cached
}
}
// Check for cached mcode in content-addressed store (salted hash to distinguish from mach)
if (policy.allow_compile) {
cached_mcode_path = global_shop_path + '/build/' + content_hash(stone(blob(text(content_key) + "\nmcode")))
if (fd.is_file(cached_mcode_path)) {
mcode_json = text(fd.slurp(cached_mcode_path))
compiled = mach_compile_mcode_bin(path, mcode_json)
put_into_cache(content_key, compiled)
return compiled
}
}
// Compile via full pipeline: analyze → mcode → streamline → serialize
if (!_mcode_mod) _mcode_mod = Shop.use("mcode", null)
if (!_streamline_mod) _streamline_mod = Shop.use("streamline", null)
ast = analyze(content, path)
ir = _mcode_mod(ast)
optimized = _streamline_mod(ir)
compiled = mach_compile_mcode_bin(path, shop_json.encode(optimized))
put_into_cache(stone(blob(content)), compiled)
// Load compiler modules from use_cache directly (NOT via Shop.use, which
// would re-enter resolve_locator → resolve_mod_fn → infinite recursion)
if (policy.allow_compile) {
if (!_mcode_mod) _mcode_mod = use_cache['core/mcode'] || use_cache['mcode']
if (!_streamline_mod) _streamline_mod = use_cache['core/streamline'] || use_cache['streamline']
if (!_mcode_mod || !_streamline_mod) {
print(`error: compiler modules not loaded (mcode=${_mcode_mod != null}, streamline=${_streamline_mod != null})`)
disrupt
}
ast = analyze(content, path)
ir = _mcode_mod(ast)
optimized = _streamline_mod(ir)
mcode_json = shop_json.encode(optimized)
return compiled
// Cache mcode (architecture-independent) in content-addressed store
ensure_dir(global_shop_path + '/build')
fd.slurpwrite(cached_mcode_path, stone(blob(mcode_json)))
// Cache mach blob
compiled = mach_compile_mcode_bin(path, mcode_json)
put_into_cache(content_key, compiled)
return compiled
}
print(`Module ${path} could not be loaded: no artifact found or all methods blocked by policy`)
disrupt
}
// given a path and a package context
@@ -554,71 +706,55 @@ function resolve_locator(path, ctx)
}
// Generate symbol name for a C module file
// Uses the same format as Shop.c_symbol_for_file
// Symbol names are based on canonical package names, not link targets
// Uses the package's assigned ID (5-letter random or "core") instead of the full name
function make_c_symbol(pkg, file) {
var pkg_safe = replace(replace(replace(pkg, '/', '_'), '.', '_'), '-', '_')
var pkg_id = get_package_id(pkg)
var file_safe = replace(replace(replace(file, '/', '_'), '.', '_'), '-', '_')
return 'js_' + pkg_safe + '_' + file_safe + '_use'
return 'js_' + pkg_id + '_' + file_safe + '_use'
}
// Get the library path for a package in .cell/lib
// Library names are based on canonical package names, not link targets
function get_lib_path(pkg) {
var lib_name = replace(replace(replace(pkg, '/', '_'), '.', '_'), '-', '_')
return global_shop_path + '/lib/' + lib_name + dylib_ext
// Get the deterministic dylib path for a module in lib/<pkg>/<stem>.dylib
function get_dylib_path(pkg, stem) {
return global_shop_path + '/lib/' + safe_package_path(pkg) + '/' + stem + dylib_ext
}
// Open a package's dynamic library and all its dependencies
Shop.open_package_dylib = function(pkg) {
if (pkg == 'core' || !pkg) return
if (dylib_visited[pkg]) return
dylib_visited[pkg] = true
// Get the deterministic mach path for a module in lib/<pkg>/<stem>.mach
function get_mach_path(pkg, stem) {
return global_shop_path + '/lib/' + safe_package_path(pkg) + '/' + stem + '.mach'
}
var link_target = link.get_target(pkg)
var resolved_pkg = link_target ? link_target : pkg
// Open a per-module dylib and return the dlopen handle
function open_module_dylib(dylib_path) {
if (open_dls[dylib_path]) return open_dls[dylib_path]
if (!fd.is_file(dylib_path)) return null
open_dls[dylib_path] = os.dylib_open(dylib_path)
return open_dls[dylib_path]
}
var pkg_dir = null
if (starts_with(resolved_pkg, '/')) {
pkg_dir = resolved_pkg
} else {
pkg_dir = get_packages_dir() + '/' + safe_package_path(resolved_pkg)
}
var toml_path = pkg_dir + '/cell.toml'
var content = null
var cfg = null
if (fd.is_file(toml_path)) {
content = text(fd.slurp(toml_path))
cfg = toml.decode(content)
if (cfg.dependencies) {
arrfor(array(cfg.dependencies), function(alias, i) {
var dep_pkg = cfg.dependencies[alias]
Shop.open_package_dylib(dep_pkg)
})
}
}
var dl_path = get_lib_path(pkg)
if (fd.is_file(dl_path)) {
if (!open_dls[dl_path]) {
open_dls[dl_path] = os.dylib_open(dl_path)
}
}
// Try to resolve a C symbol from the deterministic dylib path
// Returns a loader function or null
function try_dylib_symbol(sym, pkg, file_stem) {
var dylib_path = get_dylib_path(pkg, file_stem)
var handle = open_module_dylib(dylib_path)
if (!handle) return null
if (!os.dylib_has_symbol(handle, sym)) return null
return function() { return os.dylib_symbol(handle, sym) }
}
// Resolve a C symbol by searching:
// 1. If package_context is null, only check core internal symbols
// 2. Otherwise: own package (internal then dylib) -> other packages (internal then dylib) -> core (internal only)
// Core is never loaded as a dynamic library via dlopen
// At each scope: check lib/ dylib first, then internal (static)
function resolve_c_symbol(path, package_context) {
var explicit = split_explicit_package_import(path)
var sym = null
var dl_path = null
var loader = null
var _path = null
var core_sym = null
var canon_pkg = null
var mod_name = null
var file_stem = null
var policy = null
policy = get_policy()
if (explicit) {
if (is_internal_path(explicit.path) && package_context && explicit.package != package_context)
@@ -626,7 +762,23 @@ function resolve_c_symbol(path, package_context) {
}
if (explicit) {
sym = make_c_symbol(explicit.package, explicit.path)
if (os.internal_exists(sym)) {
file_stem = replace(explicit.path, '.c', '')
// Check lib/ dylib first
if (policy.allow_dylib) {
loader = try_dylib_symbol(sym, explicit.package, file_stem)
if (loader) {
return {
symbol: loader,
scope: SCOPE_PACKAGE,
package: explicit.package,
path: sym
}
}
}
// Then check internal/static
if (policy.allow_static && os.internal_exists(sym)) {
return {
symbol: function() { return os.load_internal(sym) },
scope: SCOPE_PACKAGE,
@@ -634,24 +786,25 @@ function resolve_c_symbol(path, package_context) {
path: sym
}
}
Shop.open_package_dylib(explicit.package)
dl_path = get_lib_path(explicit.package)
if (open_dls[dl_path] && os.dylib_has_symbol(open_dls[dl_path], sym)) {
return {
symbol: function() { return os.dylib_symbol(open_dls[dl_path], sym) },
scope: SCOPE_PACKAGE,
package: explicit.package,
path: sym
}
}
}
// If no package context, only check core internal symbols
// If no package context, only check core
if (!package_context || package_context == 'core') {
_path = replace(path, '/', '_')
core_sym = `js_${_path}_use`
if (os.internal_exists(core_sym)) {
core_sym = make_c_symbol('core', path)
// Check lib/ dylib first for core
if (policy.allow_dylib) {
loader = try_dylib_symbol(core_sym, 'core', path)
if (loader) {
return {
symbol: loader,
scope: SCOPE_CORE,
path: core_sym
}
}
}
if (policy.allow_static && os.internal_exists(core_sym)) {
return {
symbol: function() { return os.load_internal(core_sym) },
scope: SCOPE_CORE,
@@ -661,22 +814,23 @@ function resolve_c_symbol(path, package_context) {
return null
}
// 1. Check own package first (internal, then dylib)
// 1. Check own package (dylib first, then internal)
sym = make_c_symbol(package_context, path)
if (os.internal_exists(sym)) {
return {
symbol: function() { return os.load_internal(sym) },
scope: SCOPE_LOCAL,
path: sym
if (policy.allow_dylib) {
loader = try_dylib_symbol(sym, package_context, path)
if (loader) {
return {
symbol: loader,
scope: SCOPE_LOCAL,
path: sym
}
}
}
Shop.open_package_dylib(package_context)
dl_path = get_lib_path(package_context)
if (open_dls[dl_path] && os.dylib_has_symbol(open_dls[dl_path], sym)) {
if (policy.allow_static && os.internal_exists(sym)) {
return {
symbol: function() { return os.dylib_symbol(open_dls[dl_path], sym) },
symbol: function() { return os.load_internal(sym) },
scope: SCOPE_LOCAL,
path: sym
}
@@ -693,22 +847,21 @@ function resolve_c_symbol(path, package_context) {
mod_name = get_import_name(path)
sym = make_c_symbol(canon_pkg, mod_name)
// Check internal first
if (os.internal_exists(sym)) {
return {
symbol: function() { return os.load_internal(sym) },
scope: SCOPE_PACKAGE,
package: canon_pkg,
path: sym
if (policy.allow_dylib) {
loader = try_dylib_symbol(sym, canon_pkg, mod_name)
if (loader) {
return {
symbol: loader,
scope: SCOPE_PACKAGE,
package: canon_pkg,
path: sym
}
}
}
// Then check dylib
Shop.open_package_dylib(canon_pkg)
dl_path = get_lib_path(canon_pkg)
if (open_dls[dl_path] && os.dylib_has_symbol(open_dls[dl_path], sym)) {
if (policy.allow_static && os.internal_exists(sym)) {
return {
symbol: function() { return os.dylib_symbol(open_dls[dl_path], sym) },
symbol: function() { return os.load_internal(sym) },
scope: SCOPE_PACKAGE,
package: canon_pkg,
path: sym
@@ -717,9 +870,21 @@ function resolve_c_symbol(path, package_context) {
}
}
// 3. Check core internal symbols (core is never a dynamic library)
core_sym = `js_${replace(path, '/', '_')}_use`
if (os.internal_exists(core_sym)) {
// 3. Check core (dylib first, then internal)
core_sym = make_c_symbol('core', path)
if (policy.allow_dylib) {
loader = try_dylib_symbol(core_sym, 'core', path)
if (loader) {
return {
symbol: loader,
scope: SCOPE_CORE,
path: core_sym
}
}
}
if (policy.allow_static && os.internal_exists(core_sym)) {
return {
symbol: function() { return os.load_internal(core_sym) },
scope: SCOPE_CORE,
@@ -834,20 +999,21 @@ function execute_module(info)
var pkg = null
if (mod_resolve.scope < 900) {
// Build env with runtime fns, capabilities, and use function
file_info = Shop.file_info(mod_resolve.path)
inject = Shop.script_inject_for(file_info)
env = inject_env(inject)
pkg = file_info.package
env.use = make_use_fn(pkg)
// Check if native dylib was resolved
if (is_object(mod_resolve.symbol) && mod_resolve.symbol._native) {
used = mod_resolve.symbol.value
} else {
// Build env with runtime fns, capabilities, and use function
file_info = Shop.file_info(mod_resolve.path)
inject = Shop.script_inject_for(file_info)
env = inject_env(inject)
pkg = file_info.package
env.use = make_use_fn(pkg)
env = stone(env)
// Add C module as native context if available
if (c_resolve.scope < 900) {
env.native = call_c_module(c_resolve)
// Load compiled bytecode with env
used = mach_load(mod_resolve.symbol, env)
}
// Load compiled bytecode with env
used = mach_load(mod_resolve.symbol, env)
} else if (c_resolve.scope < 900) {
// C only
used = call_c_module(c_resolve)
@@ -869,6 +1035,22 @@ function get_module(path, package_context) {
}
Shop.use = function use(path, package_context) {
// Check for embedded module (static builds)
var embed_key = 'embedded:' + path
var embedded = null
var embed_env = null
if (use_cache[embed_key]) return use_cache[embed_key]
if (os.embedded_module) {
embedded = os.embedded_module(path)
if (embedded) {
embed_env = inject_env(SHOP_DEFAULT_INJECT)
embed_env.use = make_use_fn(package_context)
embed_env = stone(embed_env)
use_cache[embed_key] = mach_load(embedded, embed_env)
return use_cache[embed_key]
}
}
var info = resolve_module_info(path, package_context)
if (!info) { print(`Module ${path} could not be found in ${package_context}`); disrupt }
@@ -1222,26 +1404,38 @@ Shop.file_reload = function(file)
Shop.module_reload = function(path, package) {
if (!Shop.is_loaded(path,package)) return
// Clear the module info cache for this path
var lookup_key = package ? package + ':' + path : ':' + path
module_info_cache[lookup_key] = null
// Close old dylib handle if any
var old_dylib_path = null
if (package) {
old_dylib_path = get_dylib_path(package, path)
if (open_dls[old_dylib_path]) {
os.dylib_close(open_dls[old_dylib_path])
open_dls[old_dylib_path] = null
}
}
var info = resolve_module_info(path, package)
if (!info) return
var cache_key = info.cache_key
var old = use_cache[cache_key]
use_cache[cache_key] = null
var newmod = get_module(path, package)
use_cache[cache_key] = newmod
arrfor(array(newmod), function(i, idx) {
old[i] = newmod[i]
})
arrfor(array(old), function(i, idx) {
if (!(i in newmod))
old[i] = null
})
if (old && is_object(old) && is_object(newmod)) {
arrfor(array(newmod), function(k) { old[k] = newmod[k] })
arrfor(array(old), function(k) {
if (!(k in newmod)) old[k] = null
})
use_cache[cache_key] = old
}
}
function get_package_scripts(package)
@@ -1283,6 +1477,8 @@ Shop.get_lib_dir = function() {
return global_shop_path + '/lib'
}
Shop.ensure_dir = ensure_dir
Shop.get_local_dir = function() {
return global_shop_path + "/local"
}
@@ -1298,28 +1494,153 @@ Shop.get_package_dir = function(pkg) {
}
// Generate C symbol name for a file within a package
// e.g., c_symbol_for_file('gitea.pockle.world/john/prosperon', 'sprite.c')
// -> 'js_gitea_pockle_world_john_prosperon_sprite_use'
// Uses the package's assigned ID (e.g., "WAWOF") instead of the full name
// e.g., c_symbol_for_file('gitea.pockle.world/john/prosperon', 'sprite.c')
// -> 'js_WAWOF_sprite_use'
Shop.c_symbol_for_file = function(pkg, file) {
var pkg_safe = replace(replace(replace(pkg, '/', '_'), '.', '_'), '-', '_')
var pkg_id = get_package_id(pkg)
var file_safe = replace(replace(fd.stem(file), '/', '_'), '.', '_')
return 'js_' + pkg_safe + '_' + file_safe + '_use'
var suffix = ends_with(file, '.ce') ? '_program' : '_use'
return 'js_' + pkg_id + '_' + file_safe + suffix
}
// Generate C symbol prefix for a package
// e.g., c_symbol_prefix('gitea.pockle.world/john/prosperon') -> 'js_gitea_pockle_world_john_prosperon_'
// e.g., c_symbol_prefix('gitea.pockle.world/john/prosperon') -> 'js_WAWOF_'
Shop.c_symbol_prefix = function(pkg) {
var pkg_safe = replace(replace(replace(pkg, '/', '_'), '.', '_'), '-', '_')
return 'js_' + pkg_safe + '_'
var pkg_id = get_package_id(pkg)
return 'js_' + pkg_id + '_'
}
// Get the library name for a package (without extension)
// e.g., 'gitea.pockle.world/john/prosperon' -> 'gitea_pockle_world_john_prosperon'
// Get the library name for a package
// e.g., 'gitea.pockle.world/john/prosperon' -> 'WAWOF'
Shop.lib_name_for_package = function(pkg) {
return replace(replace(replace(pkg, '/', '_'), '.', '_'), '-', '_')
return get_package_id(pkg)
}
// Get the assigned package ID (public API)
Shop.get_package_id = get_package_id
// Returns { ok: bool, results: [{pkg, ok, error}] }
// Get the deterministic dylib path for a module (public API)
Shop.get_dylib_path = function(pkg, stem) {
return get_dylib_path(pkg, stem)
}
// Get the deterministic mach path for a module (public API)
Shop.get_mach_path = function(pkg, stem) {
return get_mach_path(pkg, stem)
}
// Load a module explicitly as mach bytecode, bypassing dylib resolution.
// Returns the loaded module value. Disrupts if the module cannot be found.
Shop.load_as_mach = function(path, pkg) {
var locator = resolve_locator(path + '.cm', pkg)
var file_path = null
var content = null
var content_key = null
var cached = null
var cached_mcode_path = null
var mcode_json = null
var compiled = null
var ast = null
var ir = null
var optimized = null
var pkg_dir = null
var stem = null
var mach_path = null
var file_info = null
var inject = null
var env = null
if (!locator) { print('Module ' + path + ' not found'); disrupt }
file_path = locator.path
content = text(fd.slurp(file_path))
content_key = stone(blob(content))
// Try installed .mach in lib/
if (pkg) {
pkg_dir = get_packages_dir() + '/' + safe_package_path(pkg)
if (starts_with(file_path, pkg_dir + '/')) {
stem = text(file_path, length(pkg_dir) + 1)
mach_path = get_mach_path(pkg, stem)
if (fd.is_file(mach_path)) {
compiled = fd.slurp(mach_path)
}
}
}
// Try cached mach blob
if (!compiled) {
cached = pull_from_cache(content_key)
if (cached) compiled = cached
}
// Try cached mcode -> compile to mach
if (!compiled) {
cached_mcode_path = global_shop_path + '/build/' + content_hash(stone(blob(text(content_key) + "\nmcode")))
if (fd.is_file(cached_mcode_path)) {
mcode_json = text(fd.slurp(cached_mcode_path))
compiled = mach_compile_mcode_bin(file_path, mcode_json)
put_into_cache(content_key, compiled)
}
}
// Full compile from source
if (!compiled) {
if (!_mcode_mod) _mcode_mod = use_cache['core/mcode'] || use_cache['mcode']
if (!_streamline_mod) _streamline_mod = use_cache['core/streamline'] || use_cache['streamline']
if (!_mcode_mod || !_streamline_mod) {
print('error: compiler modules not loaded')
disrupt
}
ast = analyze(content, file_path)
ir = _mcode_mod(ast)
optimized = _streamline_mod(ir)
mcode_json = shop_json.encode(optimized)
cached_mcode_path = global_shop_path + '/build/' + content_hash(stone(blob(text(content_key) + "\nmcode")))
ensure_dir(global_shop_path + '/build')
fd.slurpwrite(cached_mcode_path, stone(blob(mcode_json)))
compiled = mach_compile_mcode_bin(file_path, mcode_json)
put_into_cache(content_key, compiled)
}
// Load the mach blob with proper env
file_info = Shop.file_info(file_path)
inject = Shop.script_inject_for(file_info)
env = inject_env(inject)
env.use = make_use_fn(file_info.package)
env = stone(env)
return mach_load(compiled, env)
}
// Load a module explicitly as a native dylib, bypassing mach resolution.
// Returns the loaded module value, or null if no dylib exists.
Shop.load_as_dylib = function(path, pkg) {
var locator = resolve_locator(path + '.cm', pkg)
var file_path = null
var file_info = null
var pkg_dir = null
var stem = null
var result = null
var real_pkg = pkg
if (!locator) { print('Module ' + path + ' not found'); disrupt }
file_path = locator.path
if (!real_pkg) {
file_info = Shop.file_info(file_path)
real_pkg = file_info.package
}
if (!real_pkg) return null
pkg_dir = get_packages_dir() + '/' + safe_package_path(real_pkg)
if (!starts_with(file_path, pkg_dir + '/')) return null
stem = text(file_path, length(pkg_dir) + 1)
result = try_native_mod_dylib(real_pkg, stem)
return result
}
Shop.audit_packages = function() {
var packages = Shop.list_packages()
@@ -1360,4 +1681,34 @@ Shop.parse_package = function(locator) {
}
}
Shop.use_native = function(path, package_context) {
var src_path = path
if (!starts_with(path, '/'))
src_path = fd.realpath(path)
if (!fd.is_file(src_path)) { print('File not found: ' + path); disrupt }
var file_info = Shop.file_info(src_path)
var pkg = file_info.package || package_context
var sym_name = null
if (pkg)
sym_name = Shop.c_symbol_for_file(pkg, fd.basename(src_path))
var build = Shop.use('build', 'core')
var dylib_path = build.compile_native(src_path, null, null, pkg)
var handle = os.dylib_open(dylib_path)
if (!handle) { print('Failed to open native dylib: ' + dylib_path); disrupt }
// Build env with runtime functions and capabilities
var inject = Shop.script_inject_for(file_info)
var env = inject_env(inject)
env.use = make_use_fn(pkg)
env = stone(env)
if (sym_name)
return os.native_module_load_named(handle, sym_name, env)
return os.native_module_load(handle, env)
}
return Shop

View File

@@ -69,18 +69,13 @@ static const JSCFunctionListEntry js_time_funcs[] = {
};
JSValue
js_internal_time_use(JSContext *ctx)
js_core_internal_time_use(JSContext *ctx)
{
JSValue obj = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, obj,
JS_FRAME(ctx);
JS_ROOT(mod, JS_NewObject(ctx));
JS_SetPropertyFunctionList(ctx, mod.val,
js_time_funcs,
sizeof(js_time_funcs) /
sizeof(js_time_funcs[0]));
return obj;
}
JSValue
js_time_use(JSContext *ctx)
{
return js_internal_time_use(ctx);
JS_RETURN(mod.val);
}

200
ir_report.ce Normal file
View File

@@ -0,0 +1,200 @@
// ir_report.ce — optimizer flight recorder CLI
//
// Usage: ./cell --core . ir_report.ce [options] <file.cm|file.ce>
//
// Options:
// --summary Per-pass JSON summaries (default)
// --events Include rewrite events
// --types Include type deltas
// --ir-before=PASS Print canonical IR before PASS
// --ir-after=PASS Print canonical IR after PASS
// --ir-all Print canonical IR before/after every pass
// --full Everything (summary + events + types + ir-all)
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")
var ir_stats = use("ir_stats")
// --- Parse arguments ---
var filename = null
var opt_events = false
var opt_types = false
var opt_ir_before = null
var opt_ir_after = null
var opt_ir_all = false
var i = 0
var arg = null
var p = null
var e = null
var td = null
while (i < length(args)) {
arg = args[i]
if (arg == "--events") {
opt_events = true
} else if (arg == "--types") {
opt_types = true
} else if (arg == "--ir-all") {
opt_ir_all = true
} else if (arg == "--full") {
opt_events = true
opt_types = true
opt_ir_all = true
} else if (arg == "--summary") {
// default, no-op
} else if (starts_with(arg, "--ir-before=")) {
opt_ir_before = text(arg, 12)
} else if (starts_with(arg, "--ir-after=")) {
opt_ir_after = text(arg, 11)
} else if (!starts_with(arg, "--")) {
filename = arg
} else {
print(`unknown option: ${arg}\n`)
print("usage: cell --core . ir_report.ce [options] <file>\n")
$stop()
}
i = i + 1
}
if (filename == null) {
print("usage: cell --core . ir_report.ce [options] <file.cm|file.ce>\n")
print(" --summary per-pass JSON summaries (default)\n")
print(" --events include rewrite events\n")
print(" --types include type deltas\n")
print(" --ir-before=PASS print canonical IR before PASS\n")
print(" --ir-after=PASS print canonical IR after PASS\n")
print(" --ir-all print canonical IR before/after every pass\n")
print(" --full everything\n")
$stop()
}
// --- Compile ---
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)
// --- Determine which passes need IR snapshots ---
var need_snapshots = opt_ir_all || opt_ir_before != null || opt_ir_after != null
// Deep copy for before snapshot if we need IR printing
var before_ir = null
if (need_snapshots) {
before_ir = json.decode(json.encode(compiled))
}
// --- Set up log ---
var log = {
passes: [],
events: null,
type_deltas: null
}
if (opt_events) {
log.events = []
}
if (opt_types) {
log.type_deltas = []
}
// --- Run optimizer ---
var optimized = streamline(compiled, log)
// --- Output ---
var emit = function(obj) {
print(json.encode(obj))
print("\n")
}
// Pass summaries (always)
i = 0
while (i < length(log.passes)) {
p = log.passes[i]
p.type = "pass"
emit(p)
i = i + 1
}
// Rewrite events
if (opt_events && log.events != null) {
i = 0
while (i < length(log.events)) {
e = log.events[i]
e.type = "event"
emit(e)
i = i + 1
}
}
// Type deltas
if (opt_types && log.type_deltas != null) {
i = 0
while (i < length(log.type_deltas)) {
td = log.type_deltas[i]
td.type = "types"
emit(td)
i = i + 1
}
}
// --- Canonical IR printing ---
var print_ir = function(ir_obj, when_label, pass_name) {
var fname = null
var fi = 0
var func = null
if (ir_obj.main != null) {
fname = ir_obj.name != null ? ir_obj.name : "<main>"
emit({
type: "ir",
when: when_label,
pass: pass_name,
fn: fname,
text: ir_stats.canonical_ir(ir_obj.main, fname, {show_nops: true})
})
}
if (ir_obj.functions != null) {
fi = 0
while (fi < length(ir_obj.functions)) {
func = ir_obj.functions[fi]
fname = func.name != null ? func.name : `<func_${text(fi)}>`
emit({
type: "ir",
when: when_label,
pass: pass_name,
fn: fname,
text: ir_stats.canonical_ir(func, fname, {show_nops: true})
})
fi = fi + 1
}
}
return null
}
if (need_snapshots) {
if (opt_ir_all) {
print_ir(before_ir, "before", "all")
print_ir(optimized, "after", "all")
} else {
if (opt_ir_before != null) {
print_ir(before_ir, "before", opt_ir_before)
}
if (opt_ir_after != null) {
print_ir(optimized, "after", opt_ir_after)
}
}
}
$stop()

357
ir_stats.cm Normal file
View File

@@ -0,0 +1,357 @@
// ir_stats.cm — IR statistics, fingerprinting, and canonical printing
//
// Usage: var ir_stats = use("ir_stats")
// ir_stats.detailed_stats(func)
// ir_stats.ir_fingerprint(func)
// ir_stats.canonical_ir(func, name, opts)
// ir_stats.type_snapshot(slot_types)
// ir_stats.type_delta(before_types, after_types)
var json = use("json")
// --- Category maps ---
var load_ops = {
load_field: true, load_index: true, load_dynamic: true,
get: true
}
var store_ops = {
store_field: true, store_index: true, store_dynamic: true,
set_var: true, put: true, push: true
}
var branch_ops = {
jump: true, jump_true: true, jump_false: true, jump_not_null: true
}
var call_ops = {
invoke: true, goinvoke: true
}
var guard_ops = {
is_int: true, is_text: true, is_num: true, is_bool: true,
is_null: true, is_array: true, is_func: true, is_record: true,
is_stone: true
}
var arith_ops = {
add_int: true, sub_int: true, mul_int: true, div_int: true, mod_int: true,
add_float: true, sub_float: true, mul_float: true, div_float: true, mod_float: true,
concat: true, neg_int: true, neg_float: true,
bitnot: true, bitand: true, bitor: true, bitxor: true,
shl: true, shr: true, ushr: true
}
var move_ops = {
move: true
}
var const_ops = {
int: true, true: true, false: true, null: true
}
var nop_reasons = {
tc: "tc",
bl: "bl",
mv: "mv",
dj: "dj",
ur: "ur"
}
var category_tag = function(op) {
if (guard_ops[op] == true) { return "guard" }
if (branch_ops[op] == true) { return "branch" }
if (load_ops[op] == true) { return "load" }
if (store_ops[op] == true) { return "store" }
if (call_ops[op] == true) { return "call" }
if (arith_ops[op] == true) { return "arith" }
if (move_ops[op] == true) { return "move" }
if (const_ops[op] == true) { return "const" }
return null
}
// --- detailed_stats ---
var detailed_stats = function(func) {
var instructions = func.instructions
var stats = {
instr: 0, nop: 0,
load: 0, store: 0, branch: 0, call: 0,
guard: 0, arith: 0, move: 0, const: 0,
label: 0, other: 0
}
var i = 0
var instr = null
var op = null
var num = 0
if (instructions == null) {
return stats
}
num = length(instructions)
while (i < num) {
instr = instructions[i]
if (is_text(instr)) {
if (starts_with(instr, "_nop_")) {
stats.nop = stats.nop + 1
stats.instr = stats.instr + 1
} else {
stats.label = stats.label + 1
}
} else if (is_array(instr)) {
stats.instr = stats.instr + 1
op = instr[0]
if (op == "access" && !is_number(instr[2]) && !is_logical(instr[2])) {
stats.load = stats.load + 1
} else if (op == "access") {
stats.const = stats.const + 1
} else if (load_ops[op] == true) {
stats.load = stats.load + 1
} else if (store_ops[op] == true) {
stats.store = stats.store + 1
} else if (branch_ops[op] == true) {
stats.branch = stats.branch + 1
} else if (call_ops[op] == true) {
stats.call = stats.call + 1
} else if (guard_ops[op] == true) {
stats.guard = stats.guard + 1
} else if (arith_ops[op] == true) {
stats.arith = stats.arith + 1
} else if (move_ops[op] == true) {
stats.move = stats.move + 1
} else if (const_ops[op] == true) {
stats.const = stats.const + 1
} else {
stats.other = stats.other + 1
}
}
i = i + 1
}
return stats
}
// --- ir_fingerprint ---
// djb2 hash computed over the JSON-encoded instructions
var djb2 = function(s) {
var chars = array(s)
var hash = 5381
var i = 0
var num = length(chars)
while (i < num) {
hash = ((hash * 33) + number(chars[i])) % 4294967296
i = i + 1
}
return text(hash, 16)
}
var ir_fingerprint = function(func) {
return djb2(json.encode(func.instructions))
}
// --- canonical_ir ---
var pad_right = function(s, w) {
var r = s
while (length(r) < w) {
r = r + " "
}
return r
}
var nop_reason = function(s) {
// extract reason from _nop_XX_NNN
var parts = array(s, "_")
// parts: ["", "nop", "XX", "NNN"]
if (length(parts) >= 3) {
return parts[2]
}
return "?"
}
var fmt_operand = function(v) {
if (is_null(v)) {
return "null"
}
if (is_number(v)) {
return text(v)
}
if (is_text(v)) {
return `"${v}"`
}
if (is_logical(v)) {
if (v) { return "true" }
return "false"
}
return text(v)
}
var canonical_ir = function(func, name, opts) {
var instructions = func.instructions
var nr_args = func.nr_args != null ? func.nr_args : 0
var nr_slots = func.nr_slots != null ? func.nr_slots : 0
var show_nops = false
var show_types = false
var slot_types = null
var lines = []
var i = 0
var instr = null
var op = null
var n = 0
var parts = null
var j = 0
var idx_str = null
var op_str = null
var operands = null
var suffix = null
var tag = null
var typ = null
var reason = null
var num = 0
if (opts != null) {
if (opts.show_nops == true) { show_nops = true }
if (opts.show_types == true) { show_types = true }
if (opts.slot_types != null) { slot_types = opts.slot_types }
}
lines[] = `fn ${name != null ? name : "<anon>"} (args=${text(nr_args)}, slots=${text(nr_slots)})`
if (instructions == null) {
return text(lines, "\n")
}
num = length(instructions)
while (i < num) {
instr = instructions[i]
if (is_text(instr)) {
if (starts_with(instr, "_nop_")) {
if (show_nops) {
reason = nop_reason(instr)
idx_str = pad_right(`@${text(i)}`, 6)
lines[] = ` ${idx_str}--- nop (${reason}) ---`
}
} else {
lines[] = ` ${instr}:`
}
i = i + 1
continue
}
if (!is_array(instr)) {
i = i + 1
continue
}
op = instr[0]
n = length(instr)
parts = []
j = 1
while (j < n - 2) {
if (is_number(instr[j]) && op != "int" && !(op == "access" && j == 2)) {
parts[] = `s${text(instr[j])}`
} else {
parts[] = fmt_operand(instr[j])
}
j = j + 1
}
operands = text(parts, ", ")
idx_str = pad_right(`@${text(i)}`, 6)
op_str = pad_right(op, 16)
suffix = ""
tag = category_tag(op)
if (show_types && slot_types != null) {
// show type for dest slot if known
if (is_number(instr[1])) {
typ = slot_types[text(instr[1])]
if (typ != null) {
suffix = `; -> ${typ}`
}
}
if (tag != null) {
suffix = suffix + ` [${tag}]`
}
} else if (tag != null) {
suffix = suffix + `; [${tag}]`
}
if (length(suffix) > 0) {
lines[] = ` ${idx_str}${op_str}${operands} ${suffix}`
} else {
lines[] = ` ${idx_str}${op_str}${operands}`
}
i = i + 1
}
return text(lines, "\n")
}
// --- type_snapshot ---
var type_snapshot = function(slot_types) {
if (slot_types == null) {
return {}
}
return stone(record(slot_types))
}
// --- type_delta ---
var type_delta = function(before_types, after_types) {
var result = {
added: {},
removed: {},
strengthened: {},
weakened: {}
}
var bt = before_types != null ? before_types : {}
var at = after_types != null ? after_types : {}
var keys = null
var i = 0
var k = null
var bv = null
var av = null
// check after for added/changed
keys = array(at)
i = 0
while (i < length(keys)) {
k = keys[i]
av = at[k]
bv = bt[k]
if (bv == null) {
result.added[k] = av
} else if (bv != av) {
if (bv == "unknown" || (bv == "num" && (av == "int" || av == "float"))) {
result.strengthened[k] = {from: bv, to: av}
} else if (av == "unknown" || (av == "num" && (bv == "int" || bv == "float"))) {
result.weakened[k] = {from: bv, to: av}
} else {
result.strengthened[k] = {from: bv, to: av}
}
}
i = i + 1
}
// check before for removed
keys = array(bt)
i = 0
while (i < length(keys)) {
k = keys[i]
if (at[k] == null) {
result.removed[k] = bt[k]
}
i = i + 1
}
return result
}
return {
detailed_stats: detailed_stats,
ir_fingerprint: ir_fingerprint,
canonical_ir: canonical_ir,
type_snapshot: type_snapshot,
type_delta: type_delta,
category_tag: category_tag
}

113
link.ce
View File

@@ -17,6 +17,24 @@ var shop = use('internal/shop')
var fd = use('fd')
var toml = use('toml')
var links = null
var count = 0
var result = null
var i = 0
var pkg = null
var cmd = null
var pkg_name = null
var target = null
var start_idx = 0
var arg1 = null
var arg2 = null
var cwd = null
var toml_path = null
var content = null
var _restore = null
var _read_toml = null
var _add_link = null
if (length(args) < 1) {
log.console("Usage: link <command> [args] or link [package] <target>")
log.console("Commands:")
@@ -27,154 +45,149 @@ if (length(args) < 1) {
log.console(" <path> Link the package in <path> to that path")
log.console(" <package> <target> Link <package> to <target> (path or package)")
$stop()
return
}
var cmd = args[0]
cmd = args[0]
if (cmd == 'list') {
var links = link.load()
var count = 0
links = link.load()
count = 0
arrfor(array(links), function(k) {
log.console(k + " -> " + links[k])
count++
})
if (count == 0) log.console("No links.")
} else if (cmd == 'sync') {
log.console("Syncing links...")
var result = link.sync_all(shop)
result = link.sync_all(shop)
log.console("Synced " + result.synced + " link(s)")
if (length(result.errors) > 0) {
log.console("Errors:")
for (var i = 0; i < length(result.errors); i++) {
for (i = 0; i < length(result.errors); i++) {
log.console(" " + result.errors[i])
}
}
} else if (cmd == 'delete' || cmd == 'rm') {
if (length(args) < 2) {
log.console("Usage: link delete <package>")
$stop()
return
}
var pkg = args[1]
pkg = args[1]
_restore = null
if (link.remove(pkg)) {
// Try to restore the original package
log.console("Restoring " + pkg + "...")
try {
_restore = function() {
shop.fetch(pkg)
shop.extract(pkg)
log.console("Restored " + pkg)
} catch (e) {
log.console("Could not restore: " + e.message)
} disruption {
log.console("Could not restore")
log.console("Run 'cell update " + pkg + "' to restore")
}
_restore()
} else {
log.console("No link found for " + pkg)
}
} else if (cmd == 'clear') {
link.clear()
log.console("Links cleared. Run 'cell update' to restore packages.")
} else {
// Linking logic
var pkg_name = null
var target = null
pkg_name = null
target = null
// Check for 'add' compatibility
var start_idx = 0
start_idx = 0
if (cmd == 'add') {
start_idx = 1
}
var arg1 = args[start_idx]
var arg2 = (length(args) > start_idx + 1) ? args[start_idx + 1] : null
arg1 = args[start_idx]
arg2 = (length(args) > start_idx + 1) ? args[start_idx + 1] : null
if (!arg1) {
log.console("Error: target or package required")
$stop()
return
}
if (arg2) {
// Two arguments: explicit package name and target
pkg_name = arg1
target = arg2
// Resolve target if it's a local path
if (target == '.' || fd.is_dir(target)) {
target = fd.realpath(target)
} else if (starts_with(target, './') || starts_with(target, '../')) {
// Relative path that doesn't exist yet - try to resolve anyway
var cwd = fd.realpath('.')
cwd = fd.realpath('.')
if (starts_with(target, './')) {
target = cwd + text(target, 1)
} else {
// For ../ paths, var fd.realpath handle it if possible
// For ../ paths, let fd.realpath handle it if possible
target = fd.realpath(target) || target
}
}
// Otherwise target is a package name (e.g., github.com/prosperon)
} else {
// One argument: assume it's a local path, infer package name from cell.toml
target = arg1
// Resolve path
if (target == '.' || fd.is_dir(target)) {
target = fd.realpath(target)
} else if (starts_with(target, './') || starts_with(target, '../')) {
target = fd.realpath(target) || target
}
// Must be a local path with cell.toml
var toml_path = target + '/cell.toml'
toml_path = target + '/cell.toml'
if (!fd.is_file(toml_path)) {
log.console("Error: No cell.toml found at " + target)
log.console("For linking to another package, use: link <package> <target>")
$stop()
return
}
// Read package name from cell.toml
try {
var content = toml.decode(text(fd.slurp(toml_path)))
_read_toml = function() {
content = toml.decode(text(fd.slurp(toml_path)))
if (content.package) {
pkg_name = content.package
} else {
log.console("Error: cell.toml at " + target + " does not define 'package'")
$stop()
return
}
} catch (e) {
log.console("Error reading cell.toml: " + e)
} disruption {
log.console("Error reading cell.toml")
$stop()
return
}
_read_toml()
}
// Validate: if target is a local path, it must have cell.toml
if (starts_with(target, '/')) {
if (!fd.is_file(target + '/cell.toml')) {
log.console("Error: " + target + " is not a valid package (no cell.toml)")
$stop()
return
}
}
// Add the link (this also creates the symlink)
try {
_add_link = function() {
link.add(pkg_name, target, shop)
} catch (e) {
log.console("Error: " + e.message)
log.error(e)
} disruption {
log.console("Error adding link")
$stop()
return
}
_add_link()
}
$stop()
$stop()

View File

@@ -4,17 +4,19 @@
var toml = use('toml')
var fd = use('fd')
var blob = use('blob')
var os = use('os')
var runtime = use('runtime')
var global_shop_path = os.global_shop_path
var global_shop_path = runtime.shop_path
// Get the links file path (in the global shop)
function get_links_path() {
if (!global_shop_path) return null
return global_shop_path + '/link.toml'
}
// Get the packages directory (in the global shop)
function get_packages_dir() {
if (!global_shop_path) return null
return global_shop_path + '/packages'
}
@@ -62,7 +64,7 @@ var link_cache = null
Link.load = function() {
if (link_cache) return link_cache
var path = get_links_path()
if (!fd.is_file(path)) {
if (!path || !fd.is_file(path)) {
link_cache = {}
return link_cache
}

52
list.ce
View File

@@ -12,6 +12,13 @@ var fd = use('fd')
var mode = 'local'
var target_pkg = null
var resolved = null
var i = 0
var deps = null
var packages = null
var local_pkgs = null
var linked_pkgs = null
var remote_pkgs = null
if (args && length(args) > 0) {
if (args[0] == 'shop') {
@@ -32,7 +39,7 @@ if (args && length(args) > 0) {
// Resolve local paths
if (target_pkg == '.' || starts_with(target_pkg, './') || starts_with(target_pkg, '../') || fd.is_dir(target_pkg)) {
var resolved = fd.realpath(target_pkg)
resolved = fd.realpath(target_pkg)
if (resolved) {
target_pkg = resolved
}
@@ -43,22 +50,24 @@ if (args && length(args) > 0) {
var links = link.load()
var lock = shop.load_lock()
function print_deps(ctx, indent) {
indent = indent || ""
var deps
try {
function print_deps(ctx, raw_indent) {
var aliases = null
var indent = raw_indent || ""
deps = null
var _read = function() {
deps = pkg.dependencies(ctx)
} catch (e) {
} disruption {
log.console(indent + " (could not read dependencies)")
return
}
_read()
if (!deps) {
log.console(indent + " (none)")
return
}
var aliases = array(deps)
aliases = array(deps)
aliases = sort(aliases)
if (length(aliases) == 0) {
@@ -66,19 +75,26 @@ function print_deps(ctx, indent) {
return
}
for (var i = 0; i < length(aliases); i++) {
var alias = aliases[i]
var locator = deps[alias]
var link_target = links[locator]
var lock_entry = lock[locator]
var j = 0
var alias = null
var locator = null
var link_target = null
var lock_entry = null
var line = null
var status = null
for (j = 0; j < length(aliases); j++) {
alias = aliases[j]
locator = deps[alias]
link_target = links[locator]
lock_entry = lock[locator]
var line = indent + " " + alias
line = indent + " " + alias
if (alias != locator) {
line += " -> " + locator
}
// Add status indicators
var status = []
status = []
if (link_target) {
push(status, "linked -> " + link_target)
}
@@ -110,16 +126,16 @@ if (mode == 'local') {
log.console("Shop packages:")
log.console("")
var packages = shop.list_packages()
packages = shop.list_packages()
if (length(packages) == 0) {
log.console(" (none)")
} else {
packages = sort(packages)
// Group by type
var local_pkgs = []
var linked_pkgs = []
var remote_pkgs = []
local_pkgs = []
linked_pkgs = []
remote_pkgs = []
arrfor(packages, function(p) {
if (p == 'core') return

5
ls.ce
View File

@@ -9,13 +9,14 @@ var ctx = null
var pkg = args[0] || package.find_package_dir('.')
var modules = package.list_modules(pkg)
var programs = package.list_programs(pkg)
var i = 0
log.console("Modules in " + pkg + ":")
modules = sort(modules)
if (length(modules) == 0) {
log.console(" (none)")
} else {
for (var i = 0; i < length(modules); i++) {
for (i = 0; i < length(modules); i++) {
log.console(" " + modules[i])
}
}
@@ -26,7 +27,7 @@ programs = sort(programs)
if (length(programs) == 0) {
log.console(" (none)")
} else {
for (var i = 0; i < length(programs); i++) {
for (i = 0; i < length(programs); i++) {
log.console(" " + programs[i])
}
}

722
mcode.cm
View File

@@ -28,6 +28,13 @@ var mcode = function(ast) {
"<<=": "shl", ">>=": "shr", ">>>=": "ushr"
}
var sensory_ops = {
is_array: "is_array", is_function: "is_func", is_object: "is_record",
is_stone: "is_stone", is_integer: "is_int", is_text: "is_text",
is_number: "is_num", is_logical: "is_bool", is_null: "is_null",
length: "length"
}
// Compiler state
var s_instructions = null
var s_data = null
@@ -52,6 +59,7 @@ var mcode = function(ast) {
var s_cur_line = 0
var s_cur_col = 0
var s_filename = null
var s_has_disruption = false
// Shared closure vars for binop helpers (avoids >4 param functions)
var _bp_dest = 0
@@ -78,7 +86,8 @@ var mcode = function(ast) {
function_nr: s_function_nr,
intrinsic_cache: s_intrinsic_cache,
cur_line: s_cur_line,
cur_col: s_cur_col
cur_col: s_cur_col,
has_disruption: s_has_disruption
}
}
@@ -99,6 +108,7 @@ var mcode = function(ast) {
s_intrinsic_cache = saved.intrinsic_cache
s_cur_line = saved.cur_line
s_cur_col = saved.cur_col
s_has_disruption = saved.has_disruption
}
// Slot allocation
@@ -270,88 +280,44 @@ var mcode = function(ast) {
return node.kind == "null"
}
// emit_add_decomposed: int path -> text path -> float path -> disrupt
// emit_add_decomposed: emit type-dispatched add (text → concat, num → add)
// reads _bp_dest, _bp_left, _bp_right, _bp_ln, _bp_rn from closure
var emit_add_decomposed = function() {
var dest = _bp_dest
var left = _bp_left
var right = _bp_right
var t0 = 0
var t1 = 0
var left_is_int = is_known_int(_bp_ln)
var left_is_text = is_known_text(_bp_ln)
var left_is_num = is_known_number(_bp_ln)
var right_is_int = is_known_int(_bp_rn)
var right_is_text = is_known_text(_bp_rn)
var right_is_num = is_known_number(_bp_rn)
var not_int = null
var not_text = null
var done = null
var err = null
// Both sides known int
if (left_is_int && right_is_int) {
emit_3("add_int", dest, left, right)
if (is_known_text(_bp_ln) && is_known_text(_bp_rn)) {
emit_3("concat", _bp_dest, _bp_left, _bp_right)
return null
}
// Both sides known text
if (left_is_text && right_is_text) {
emit_3("concat", dest, left, right)
if (is_known_number(_bp_ln) && is_known_number(_bp_rn)) {
emit_3("add", _bp_dest, _bp_left, _bp_right)
return null
}
// Both sides known number (but not both int)
if (left_is_num && right_is_num) {
if (left_is_int && right_is_int) {
emit_3("add_int", dest, left, right)
} else {
emit_3("add_float", dest, left, right)
}
// If either operand is a known number, concat is impossible
if (is_known_number(_bp_ln) || is_known_number(_bp_rn)) {
emit_numeric_binop("add")
return null
}
// Unknown types: emit full dispatch
var t0 = alloc_slot()
var t1 = alloc_slot()
var done = gen_label("add_done")
var check_num = gen_label("add_cn")
not_int = gen_label("add_ni")
not_text = gen_label("add_nt")
done = gen_label("add_done")
err = gen_label("add_err")
// Int path
t0 = alloc_slot()
if (!left_is_int) {
emit_2("is_int", t0, left)
emit_jump_cond("jump_false", t0, not_int)
}
t1 = alloc_slot()
if (!right_is_int) {
emit_2("is_int", t1, right)
emit_jump_cond("jump_false", t1, not_int)
}
emit_3("add_int", dest, left, right)
// Check text path first (since add doubles as concat)
emit_2("is_text", t0, _bp_left)
emit_jump_cond("jump_false", t0, check_num)
emit_2("is_text", t1, _bp_right)
emit_jump_cond("jump_false", t1, check_num)
emit_3("concat", _bp_dest, _bp_left, _bp_right)
emit_jump(done)
// Text path
emit_label(not_int)
if (!left_is_text) {
emit_2("is_text", t0, left)
emit_jump_cond("jump_false", t0, not_text)
}
if (!right_is_text) {
emit_2("is_text", t1, right)
emit_jump_cond("jump_false", t1, not_text)
}
emit_3("concat", dest, left, right)
emit_jump(done)
// Float path
emit_label(not_text)
if (!left_is_num) {
emit_2("is_num", t0, left)
emit_jump_cond("jump_false", t0, err)
}
if (!right_is_num) {
emit_2("is_num", t1, right)
emit_jump_cond("jump_false", t1, err)
}
emit_3("add_float", dest, left, right)
// Numeric path
var err = gen_label("add_err")
emit_label(check_num)
emit_2("is_num", t0, _bp_left)
emit_jump_cond("jump_false", t0, err)
emit_2("is_num", t1, _bp_right)
emit_jump_cond("jump_false", t1, err)
emit_3("add", _bp_dest, _bp_left, _bp_right)
emit_jump(done)
emit_label(err)
@@ -360,60 +326,22 @@ var mcode = function(ast) {
return null
}
// emit_numeric_binop: int path -> float path -> disrupt
// emit_numeric_binop: emit type-guarded numeric binary op
// reads _bp_dest, _bp_left, _bp_right, _bp_ln, _bp_rn from closure
var emit_numeric_binop = function(int_op, float_op) {
var dest = _bp_dest
var left = _bp_left
var right = _bp_right
var t0 = 0
var t1 = 0
var left_is_int = is_known_int(_bp_ln)
var left_is_num = is_known_number(_bp_ln)
var right_is_int = is_known_int(_bp_rn)
var right_is_num = is_known_number(_bp_rn)
var not_int = null
var done = null
var err = null
// Both sides known int
if (left_is_int && right_is_int) {
emit_3(int_op, dest, left, right)
var emit_numeric_binop = function(op_str) {
if (is_known_number(_bp_ln) && is_known_number(_bp_rn)) {
emit_3(op_str, _bp_dest, _bp_left, _bp_right)
return null
}
// Both sides known number (but not both int)
if (left_is_num && right_is_num) {
emit_3(float_op, dest, left, right)
return null
}
not_int = gen_label("num_ni")
done = gen_label("num_done")
err = gen_label("num_err")
t0 = alloc_slot()
if (!left_is_int) {
emit_2("is_int", t0, left)
emit_jump_cond("jump_false", t0, not_int)
}
t1 = alloc_slot()
if (!right_is_int) {
emit_2("is_int", t1, right)
emit_jump_cond("jump_false", t1, not_int)
}
emit_3(int_op, dest, left, right)
emit_jump(done)
emit_label(not_int)
if (!left_is_num) {
emit_2("is_num", t0, left)
emit_jump_cond("jump_false", t0, err)
}
if (!right_is_num) {
emit_2("is_num", t1, right)
emit_jump_cond("jump_false", t1, err)
}
emit_3(float_op, dest, left, right)
var t0 = alloc_slot()
var t1 = alloc_slot()
var err = gen_label("num_err")
var done = gen_label("num_done")
emit_2("is_num", t0, _bp_left)
emit_jump_cond("jump_false", t0, err)
emit_2("is_num", t1, _bp_right)
emit_jump_cond("jump_false", t1, err)
emit_3(op_str, _bp_dest, _bp_left, _bp_right)
emit_jump(done)
emit_label(err)
@@ -646,36 +574,18 @@ var mcode = function(ast) {
return null
}
// emit_neg_decomposed: int path -> float path -> disrupt
// emit_neg_decomposed: emit type-guarded negate
var emit_neg_decomposed = function(dest, src, src_node) {
var t0 = 0
var not_int = null
var done = null
var err = null
if (is_known_int(src_node)) {
emit_2("neg_int", dest, src)
return null
}
if (is_known_number(src_node)) {
emit_2("neg_float", dest, src)
emit_2("negate", dest, src)
return null
}
not_int = gen_label("neg_ni")
done = gen_label("neg_done")
err = gen_label("neg_err")
t0 = alloc_slot()
emit_2("is_int", t0, src)
emit_jump_cond("jump_false", t0, not_int)
emit_2("neg_int", dest, src)
emit_jump(done)
emit_label(not_int)
var t0 = alloc_slot()
var err = gen_label("neg_err")
var done = gen_label("neg_done")
emit_2("is_num", t0, src)
emit_jump_cond("jump_false", t0, err)
emit_2("neg_float", dest, src)
emit_2("negate", dest, src)
emit_jump(done)
emit_label(err)
@@ -686,35 +596,34 @@ var mcode = function(ast) {
// Central router: maps op string to decomposition helper
// Sets _bp_* closure vars then calls helper with reduced args
var relational_ops = {
lt: ["lt_int", "lt_float", "lt_text"],
le: ["le_int", "le_float", "le_text"],
gt: ["gt_int", "gt_float", "gt_text"],
ge: ["ge_int", "ge_float", "ge_text"]
}
var emit_binop = function(op_str, dest, left, right) {
var rel = null
_bp_dest = dest
_bp_left = left
_bp_right = right
if (op_str == "add") {
emit_add_decomposed()
} else if (op_str == "subtract") {
emit_numeric_binop("sub_int", "sub_float")
} else if (op_str == "multiply") {
emit_numeric_binop("mul_int", "mul_float")
} else if (op_str == "divide") {
emit_numeric_binop("div_int", "div_float")
} else if (op_str == "modulo") {
emit_numeric_binop("mod_int", "mod_float")
} else if (op_str == "eq") {
emit_eq_decomposed()
} else if (op_str == "ne") {
emit_ne_decomposed()
} else if (op_str == "lt") {
emit_relational("lt_int", "lt_float", "lt_text")
} else if (op_str == "le") {
emit_relational("le_int", "le_float", "le_text")
} else if (op_str == "gt") {
emit_relational("gt_int", "gt_float", "gt_text")
} else if (op_str == "ge") {
emit_relational("ge_int", "ge_float", "ge_text")
} else {
// Passthrough for bitwise, pow, in, etc.
emit_3(op_str, dest, left, right)
rel = relational_ops[op_str]
if (rel != null) {
emit_relational(rel[0], rel[1], rel[2])
} else if (op_str == "subtract" || op_str == "multiply" ||
op_str == "divide" || op_str == "modulo" || op_str == "pow") {
emit_numeric_binop(op_str)
} else {
// Passthrough for bitwise, in, etc.
emit_3(op_str, dest, left, right)
}
}
return null
}
@@ -782,13 +691,12 @@ var mcode = function(ast) {
var name_str = alloc_slot()
emit_const_str(name_str, prop)
var args_arr = alloc_slot()
var arr_instr = ["array", args_arr, argc]
add_instr(["array", args_arr, 0])
_i = 0
while (_i < argc) {
push(arr_instr, args[_i])
emit_2("push", args_arr, args[_i])
_i = _i + 1
}
add_instr(arr_instr)
var pf = alloc_slot()
emit_3("frame", pf, obj, 2)
emit_3("setarg", pf, 0, null_slot)
@@ -836,13 +744,12 @@ var mcode = function(ast) {
var null_slot = alloc_slot()
emit_const_null(null_slot)
var args_arr = alloc_slot()
var arr_instr = ["array", args_arr, argc]
add_instr(["array", args_arr, 0])
_i = 0
while (_i < argc) {
push(arr_instr, args[_i])
emit_2("push", args_arr, args[_i])
_i = _i + 1
}
add_instr(arr_instr)
var pf = alloc_slot()
emit_3("frame", pf, obj, 2)
emit_3("setarg", pf, 0, null_slot)
@@ -942,7 +849,7 @@ var mcode = function(ast) {
if (scope == null) {
return null
}
var keys = array(scope)
var keys = sort(array(scope))
var _i = 0
var name = null
var v = null
@@ -986,6 +893,323 @@ var mcode = function(ast) {
return -1
}
// --- Inline expansion toggle flags ---
var inline_arrfor = true
var inline_filter = true
var inline_every = true
var inline_some = true
var inline_reduce = true
// --- Helper: emit a reduce loop body ---
// r = {acc, i, arr, fn, len}; emits loop updating acc in-place.
// Caller must emit the done_label after calling this.
var emit_reduce_loop = function(r, forward, done_label) {
var acc = r.acc
var i = r.i
var arr_slot = r.arr
var fn_slot = r.fn
var len = r.len
var check = alloc_slot()
var item = alloc_slot()
var null_s = alloc_slot()
var one = alloc_slot()
var zero = alloc_slot()
var f = alloc_slot()
var loop_label = gen_label("reduce_loop")
emit_2("int", one, 1)
emit_1("null", null_s)
emit_label(loop_label)
if (forward) {
emit_3("lt_int", check, i, len)
} else {
emit_2("int", zero, 0)
emit_3("ge_int", check, i, zero)
}
emit_jump_cond("jump_false", check, done_label)
emit_3("load_index", item, arr_slot, i)
emit_3("frame", f, fn_slot, 2)
emit_3("setarg", f, 0, null_s)
emit_3("setarg", f, 1, acc)
emit_3("setarg", f, 2, item)
emit_2("invoke", f, acc)
if (forward) {
emit_3("add", i, i, one)
} else {
emit_3("subtract", i, i, one)
}
emit_jump(loop_label)
}
// --- Inline expansion: arrfor(arr, fn) ---
var expand_inline_arrfor = function(dest, arr_slot, fn_slot) {
var len = alloc_slot()
var i = alloc_slot()
var check = alloc_slot()
var item = alloc_slot()
var null_s = alloc_slot()
var one = alloc_slot()
var f = alloc_slot()
var discard = alloc_slot()
var loop_label = gen_label("arrfor_loop")
var done_label = gen_label("arrfor_done")
emit_2("length", len, arr_slot)
emit_2("int", i, 0)
emit_2("int", one, 1)
emit_1("null", null_s)
emit_label(loop_label)
emit_3("lt_int", check, i, len)
emit_jump_cond("jump_false", check, done_label)
emit_3("load_index", item, arr_slot, i)
emit_3("frame", f, fn_slot, 2)
emit_3("setarg", f, 0, null_s)
emit_3("setarg", f, 1, item)
emit_3("setarg", f, 2, i)
emit_2("invoke", f, discard)
emit_3("add", i, i, one)
emit_jump(loop_label)
emit_label(done_label)
emit_1("null", dest)
return dest
}
// --- Inline expansion: every(arr, fn) ---
var expand_inline_every = function(dest, arr_slot, fn_slot) {
var len = alloc_slot()
var i = alloc_slot()
var check = alloc_slot()
var item = alloc_slot()
var null_s = alloc_slot()
var one = alloc_slot()
var f = alloc_slot()
var val = alloc_slot()
var loop_label = gen_label("every_loop")
var ret_true = gen_label("every_true")
var ret_false = gen_label("every_false")
var done_label = gen_label("every_done")
emit_2("length", len, arr_slot)
emit_2("int", i, 0)
emit_2("int", one, 1)
emit_1("null", null_s)
emit_label(loop_label)
emit_3("lt_int", check, i, len)
emit_jump_cond("jump_false", check, ret_true)
emit_3("load_index", item, arr_slot, i)
emit_3("frame", f, fn_slot, 1)
emit_3("setarg", f, 0, null_s)
emit_3("setarg", f, 1, item)
emit_2("invoke", f, val)
emit_jump_cond("jump_false", val, ret_false)
emit_3("add", i, i, one)
emit_jump(loop_label)
emit_label(ret_true)
emit_1("true", dest)
emit_jump(done_label)
emit_label(ret_false)
emit_1("false", dest)
emit_label(done_label)
return dest
}
// --- Inline expansion: some(arr, fn) ---
var expand_inline_some = function(dest, arr_slot, fn_slot) {
var len = alloc_slot()
var i = alloc_slot()
var check = alloc_slot()
var item = alloc_slot()
var null_s = alloc_slot()
var one = alloc_slot()
var f = alloc_slot()
var val = alloc_slot()
var loop_label = gen_label("some_loop")
var ret_true = gen_label("some_true")
var ret_false = gen_label("some_false")
var done_label = gen_label("some_done")
emit_2("length", len, arr_slot)
emit_2("int", i, 0)
emit_2("int", one, 1)
emit_1("null", null_s)
emit_label(loop_label)
emit_3("lt_int", check, i, len)
emit_jump_cond("jump_false", check, ret_false)
emit_3("load_index", item, arr_slot, i)
emit_3("frame", f, fn_slot, 1)
emit_3("setarg", f, 0, null_s)
emit_3("setarg", f, 1, item)
emit_2("invoke", f, val)
emit_jump_cond("jump_true", val, ret_true)
emit_3("add", i, i, one)
emit_jump(loop_label)
emit_label(ret_true)
emit_1("true", dest)
emit_jump(done_label)
emit_label(ret_false)
emit_1("false", dest)
emit_label(done_label)
return dest
}
// --- Inline expansion: filter(arr, fn) ---
var expand_inline_filter = function(dest, arr_slot, fn_slot) {
var result = alloc_slot()
var len = alloc_slot()
var i = alloc_slot()
var check = alloc_slot()
var item = alloc_slot()
var null_s = alloc_slot()
var one = alloc_slot()
var f = alloc_slot()
var val = alloc_slot()
var loop_label = gen_label("filter_loop")
var skip_label = gen_label("filter_skip")
var done_label = gen_label("filter_done")
add_instr(["array", result, 0])
emit_2("length", len, arr_slot)
emit_2("int", i, 0)
emit_2("int", one, 1)
emit_1("null", null_s)
emit_label(loop_label)
emit_3("lt_int", check, i, len)
emit_jump_cond("jump_false", check, done_label)
emit_3("load_index", item, arr_slot, i)
emit_3("frame", f, fn_slot, 2)
emit_3("setarg", f, 0, null_s)
emit_3("setarg", f, 1, item)
emit_3("setarg", f, 2, i)
emit_2("invoke", f, val)
emit_jump_cond("jump_false", val, skip_label)
emit_2("push", result, item)
emit_label(skip_label)
emit_3("add", i, i, one)
emit_jump(loop_label)
emit_label(done_label)
emit_2("move", dest, result)
return dest
}
// --- Inline expansion: reduce(arr, fn[, initial[, reverse]]) ---
var expand_inline_reduce = function(dest, args, nargs) {
var arr_slot = args.arr
var fn_slot = args.fn
var init_slot = args.init
var rev_slot = args.rev
var len = alloc_slot()
var acc = alloc_slot()
var i = alloc_slot()
var check = alloc_slot()
var zero = alloc_slot()
var one = alloc_slot()
var final_label = gen_label("reduce_final")
var has_init = null
var no_init_rev = null
var init_rev = null
var null_label = null
var d1 = null
var d2 = null
var d3 = null
var d4 = null
var r = null
emit_2("length", len, arr_slot)
emit_2("int", zero, 0)
emit_2("int", one, 1)
r = {acc: acc, i: i, arr: arr_slot, fn: fn_slot, len: len}
if (nargs == 2) {
null_label = gen_label("reduce_null")
d1 = gen_label("reduce_d1")
emit_3("lt_int", check, zero, len)
emit_jump_cond("jump_false", check, null_label)
emit_3("load_index", acc, arr_slot, zero)
emit_2("move", i, one)
emit_reduce_loop(r, true, d1)
emit_label(d1)
emit_2("move", dest, acc)
emit_jump(final_label)
emit_label(null_label)
emit_1("null", dest)
emit_label(final_label)
} else if (nargs == 3) {
has_init = gen_label("reduce_has_init")
null_label = gen_label("reduce_null")
d1 = gen_label("reduce_d1")
d2 = gen_label("reduce_d2")
emit_2("is_null", check, init_slot)
emit_jump_cond("jump_false", check, has_init)
// No initial, forward
emit_3("lt_int", check, zero, len)
emit_jump_cond("jump_false", check, null_label)
emit_3("load_index", acc, arr_slot, zero)
emit_2("move", i, one)
emit_reduce_loop(r, true, d1)
emit_label(d1)
emit_2("move", dest, acc)
emit_jump(final_label)
emit_label(null_label)
emit_1("null", dest)
emit_jump(final_label)
// Has initial, forward
emit_label(has_init)
emit_2("move", acc, init_slot)
emit_2("int", i, 0)
emit_reduce_loop(r, true, d2)
emit_label(d2)
emit_2("move", dest, acc)
emit_label(final_label)
} else {
// nargs == 4: full branching
has_init = gen_label("reduce_has_init")
no_init_rev = gen_label("reduce_no_init_rev")
init_rev = gen_label("reduce_init_rev")
null_label = gen_label("reduce_null")
d1 = gen_label("reduce_d1")
d2 = gen_label("reduce_d2")
d3 = gen_label("reduce_d3")
d4 = gen_label("reduce_d4")
emit_2("is_null", check, init_slot)
emit_jump_cond("jump_false", check, has_init)
// No initial
emit_3("lt_int", check, zero, len)
emit_jump_cond("jump_false", check, null_label)
emit_jump_cond("jump_true", rev_slot, no_init_rev)
// No initial, forward
emit_3("load_index", acc, arr_slot, zero)
emit_2("move", i, one)
emit_reduce_loop(r, true, d1)
emit_label(d1)
emit_2("move", dest, acc)
emit_jump(final_label)
// No initial, reverse
emit_label(no_init_rev)
emit_3("subtract", i, len, one)
emit_3("load_index", acc, arr_slot, i)
emit_3("subtract", i, i, one)
emit_reduce_loop(r, false, d2)
emit_label(d2)
emit_2("move", dest, acc)
emit_jump(final_label)
emit_label(null_label)
emit_1("null", dest)
emit_jump(final_label)
// Has initial
emit_label(has_init)
emit_jump_cond("jump_true", rev_slot, init_rev)
// Has initial, forward
emit_2("move", acc, init_slot)
emit_2("int", i, 0)
emit_reduce_loop(r, true, d3)
emit_label(d3)
emit_2("move", dest, acc)
emit_jump(final_label)
// Has initial, reverse
emit_label(init_rev)
emit_2("move", acc, init_slot)
emit_3("subtract", i, len, one)
emit_reduce_loop(r, false, d4)
emit_label(d4)
emit_2("move", dest, acc)
emit_label(final_label)
}
return dest
}
// Forward declarations via var
var gen_expr = null
var gen_statement = null
@@ -998,7 +1222,7 @@ var mcode = function(ast) {
}
// Binary expression compilation
var gen_binary = function(node) {
var gen_binary = function(node, target) {
var kind = node.kind
var left = node.left
var right = node.right
@@ -1053,7 +1277,8 @@ var mcode = function(ast) {
// Standard binary ops
left_slot = gen_expr(left, -1)
right_slot = gen_expr(right, -1)
dest = alloc_slot()
// Use target slot for ops without multi-type dispatch (add has text+num paths)
dest = (target >= 0 && kind != "+") ? target : alloc_slot()
op = binop_map[kind]
if (op == null) {
op = "add"
@@ -1181,23 +1406,35 @@ var mcode = function(ast) {
var obj_slot = 0
var idx_expr = null
var idx_slot = 0
var guard_t = 0
var guard_err = null
var guard_done = null
if (cop != null) {
return gen_compound_assign(node, cop)
}
// Push syntax: arr[] = val
// Push syntax: arr[] = val (guarded)
if (node.push == true) {
arr_expr = left.left
arr_slot = gen_expr(arr_expr, -1)
val_slot = gen_expr(right, -1)
guard_t = alloc_slot()
guard_err = gen_label("push_err")
guard_done = gen_label("push_done")
emit_2("is_array", guard_t, arr_slot)
emit_jump_cond("jump_false", guard_t, guard_err)
emit_2("push", arr_slot, val_slot)
emit_jump(guard_done)
emit_label(guard_err)
emit_0("disrupt")
emit_label(guard_done)
return val_slot
}
val_slot = gen_expr(right, -1)
left_kind = left.kind
// For local name assignments, try to write directly to the var's slot
if (left_kind == "name") {
name = left.name
level = left.level
@@ -1207,17 +1444,30 @@ var mcode = function(ast) {
if (level == 0 || level == -1) {
slot = find_var(name)
if (slot >= 0) {
emit_2("move", slot, val_slot)
} else if (level == -1) {
val_slot = gen_expr(right, slot)
if (val_slot != slot) {
emit_2("move", slot, val_slot)
}
return val_slot
}
val_slot = gen_expr(right, -1)
if (level == -1) {
add_instr(["set_var", name, val_slot])
}
} else if (level > 0) {
_lv = level - 1
pstate = parent_states[length(parent_states) - 1 - _lv]
pslot = find_var_in_saved(pstate, name)
emit_3("put", val_slot, pslot, level)
} else {
val_slot = gen_expr(right, -1)
if (level > 0) {
_lv = level - 1
pstate = parent_states[length(parent_states) - 1 - _lv]
pslot = find_var_in_saved(pstate, name)
emit_3("put", val_slot, pslot, level)
}
}
} else if (left_kind == ".") {
return val_slot
}
val_slot = gen_expr(right, -1)
if (left_kind == ".") {
obj = left.left
prop = left.right
obj_slot = gen_expr(obj, -1)
@@ -1270,6 +1520,7 @@ var mcode = function(ast) {
var a0 = 0
var a1 = 0
var a2 = 0
var a3 = 0
var d = 0
var top = null
var arg_slots = null
@@ -1306,6 +1557,9 @@ var mcode = function(ast) {
var kname = null
var func = null
var func_id = 0
var guard_t = 0
var guard_err = null
var guard_done = null
if (expr == null) {
return -1
@@ -1343,13 +1597,12 @@ var mcode = function(ast) {
}
// Create array from expression results
arr_slot = alloc_slot()
arr_instr = ["array", arr_slot, nexpr]
add_instr(["array", arr_slot, 0])
_i = 0
while (_i < nexpr) {
push(arr_instr, expr_slots[_i])
emit_2("push", arr_slot, expr_slots[_i])
_i = _i + 1
}
add_instr(arr_instr)
// Load format intrinsic
fmt_func_slot = find_intrinsic("format")
if (fmt_func_slot < 0) {
@@ -1509,6 +1762,68 @@ var mcode = function(ast) {
return d
}
// Tier 1 intrinsic inlining: emit direct opcodes instead of frame/invoke
if (callee_kind == "name" && callee.intrinsic == true) {
fname = callee.name
nargs = args_list != null ? length(args_list) : 0
// 1-arg type check intrinsics → direct opcode
if (nargs == 1 && sensory_ops[fname] != null) {
a0 = gen_expr(args_list[0], -1)
d = alloc_slot()
emit_2(sensory_ops[fname], d, a0)
return d
}
// 2-arg push: push(arr, val) → guarded direct opcode
if (nargs == 2 && fname == "push") {
a0 = gen_expr(args_list[0], -1)
a1 = gen_expr(args_list[1], -1)
guard_t = alloc_slot()
guard_err = gen_label("push_err")
guard_done = gen_label("push_done")
emit_2("is_array", guard_t, a0)
emit_jump_cond("jump_false", guard_t, guard_err)
emit_2("push", a0, a1)
emit_jump(guard_done)
emit_label(guard_err)
emit_0("disrupt")
emit_label(guard_done)
return a1
}
// Callback intrinsics → inline mcode loops
if (nargs == 2 && fname == "arrfor" && inline_arrfor) {
a0 = gen_expr(args_list[0], -1)
a1 = gen_expr(args_list[1], -1)
d = alloc_slot()
return expand_inline_arrfor(d, a0, a1)
}
if (nargs == 2 && fname == "every" && inline_every) {
a0 = gen_expr(args_list[0], -1)
a1 = gen_expr(args_list[1], -1)
d = alloc_slot()
return expand_inline_every(d, a0, a1)
}
if (nargs == 2 && fname == "some" && inline_some) {
a0 = gen_expr(args_list[0], -1)
a1 = gen_expr(args_list[1], -1)
d = alloc_slot()
return expand_inline_some(d, a0, a1)
}
if (nargs == 2 && fname == "filter" && inline_filter) {
a0 = gen_expr(args_list[0], -1)
a1 = gen_expr(args_list[1], -1)
d = alloc_slot()
return expand_inline_filter(d, a0, a1)
}
if (fname == "reduce" && nargs >= 2 && nargs <= 4 && inline_reduce) {
a0 = gen_expr(args_list[0], -1)
a1 = gen_expr(args_list[1], -1)
a2 = nargs >= 3 ? gen_expr(args_list[2], -1) : -1
a3 = nargs >= 4 ? gen_expr(args_list[3], -1) : -1
d = alloc_slot()
return expand_inline_reduce(d, {arr: a0, fn: a1, init: a2, rev: a3}, nargs)
}
}
// Collect arg slots
arg_slots = []
_i = 0
@@ -1686,13 +2001,12 @@ var mcode = function(ast) {
_i = _i + 1
}
dest = alloc_slot()
instr = ["array", dest, count]
add_instr(["array", dest, count])
_i = 0
while (_i < count) {
push(instr, elem_slots[_i])
emit_2("push", dest, elem_slots[_i])
_i = _i + 1
}
push(s_instructions, instr)
return dest
}
@@ -1700,7 +2014,7 @@ var mcode = function(ast) {
if (kind == "record") {
list = expr.list
dest = alloc_slot()
push(s_instructions, ["record", dest, 0])
push(s_instructions, ["record", dest, length(list)])
_i = 0
while (_i < length(list)) {
pair = list[_i]
@@ -1750,7 +2064,7 @@ var mcode = function(ast) {
}
// Binary operators (fallback)
return gen_binary(expr)
return gen_binary(expr, target)
}
// Statement compilation
@@ -1810,6 +2124,10 @@ var mcode = function(ast) {
var func = null
var func_id = 0
var dest = 0
var guard_t = 0
var guard_err = null
var guard_done = null
var last_instr = null
if (stmt == null) {
return null
@@ -1825,12 +2143,21 @@ var mcode = function(ast) {
right = stmt.right
name = left.name
local_slot = find_var(name)
// Pop: var val = arr[]
// Pop: var val = arr[] (guarded)
if (stmt.pop == true && right != null) {
arr_expr = right.left
arr_slot = gen_expr(arr_expr, -1)
if (local_slot >= 0) {
guard_t = alloc_slot()
guard_err = gen_label("pop_err")
guard_done = gen_label("pop_done")
emit_2("is_array", guard_t, arr_slot)
emit_jump_cond("jump_false", guard_t, guard_err)
emit_2("pop", local_slot, arr_slot)
emit_jump(guard_done)
emit_label(guard_err)
emit_0("disrupt")
emit_label(guard_done)
}
return null
}
@@ -2007,6 +2334,13 @@ var mcode = function(ast) {
expr = stmt.expression
if (expr != null) {
slot = gen_expr(expr, -1)
// Mark tail calls: rename last invoke to tail_invoke
if (stmt.tail == true && !s_has_disruption) {
last_instr = s_instructions[length(s_instructions) - 1]
if (is_array(last_instr) && last_instr[0] == "invoke") {
last_instr[0] = "tail_invoke"
}
}
emit_1("return", slot)
} else {
null_slot = alloc_slot()
@@ -2189,6 +2523,7 @@ var mcode = function(ast) {
s_label_map = {}
s_is_arrow = is_arrow
s_has_disruption = disrupt_clause != null && is_array(disrupt_clause)
s_function_nr = fn_nr_node != null ? fn_nr_node : 0
@@ -2297,6 +2632,7 @@ var mcode = function(ast) {
// Compile disruption clause
if (disrupt_clause != null && is_array(disrupt_clause)) {
emit_label(gen_label("disruption"))
disruption_start = length(s_instructions)
_i = 0
while (_i < length(disrupt_clause)) {

Binary file not shown.

View File

@@ -18,6 +18,15 @@ add_project_arguments(
)
add_project_arguments('-Wno-narrowing', language: 'cpp')
if get_option('validate_gc')
add_project_arguments('-DVALIDATE_GC', language: 'c')
endif
if get_option('force_gc')
add_project_arguments('-DFORCE_GC_AT_MALLOC', language: 'c')
endif
deps = []
if host_machine.system() == 'darwin'
@@ -59,10 +68,10 @@ scripts = [
'fit.c',
'crypto.c',
'internal/kim.c',
'time.c',
'internal/time.c',
'debug/debug.c',
'internal/os.c',
'fd.c',
'internal/fd.c',
'net/http.c',
'net/enet.c',
'wildstar.c',

4
meson.options Normal file
View File

@@ -0,0 +1,4 @@
option('validate_gc', type: 'boolean', value: false,
description: 'Enable GC validation checks (stale pointer detection, pre-GC frame validation)')
option('force_gc', type: 'boolean', value: false,
description: 'Force GC on every allocation (makes stale pointer bugs deterministic)')

View File

@@ -568,21 +568,23 @@ static const JSCFunctionListEntry js_enet_peer_funcs[] = {
// JS_CGETSET_DEF("address", js_enet_peer_get_address, NULL),
};
JSValue js_enet_use(JSContext *ctx)
JSValue js_core_enet_use(JSContext *ctx)
{
JS_FRAME(ctx);
JS_NewClassID(&enet_host_id);
JS_NewClass(ctx, enet_host_id, &enet_host);
JSValue host_proto = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, host_proto, js_enet_host_funcs, countof(js_enet_host_funcs));
JS_SetClassProto(ctx, enet_host_id, host_proto);
JS_ROOT(host_proto, JS_NewObject(ctx));
JS_SetPropertyFunctionList(ctx, host_proto.val, js_enet_host_funcs, countof(js_enet_host_funcs));
JS_SetClassProto(ctx, enet_host_id, host_proto.val);
JS_NewClassID(&enet_peer_class_id);
JS_NewClass(ctx, enet_peer_class_id, &enet_peer_class);
JSValue peer_proto = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, peer_proto, js_enet_peer_funcs, countof(js_enet_peer_funcs));
JS_SetClassProto(ctx, enet_peer_class_id, peer_proto);
JS_ROOT(peer_proto, JS_NewObject(ctx));
JS_SetPropertyFunctionList(ctx, peer_proto.val, js_enet_peer_funcs, countof(js_enet_peer_funcs));
JS_SetClassProto(ctx, enet_peer_class_id, peer_proto.val);
JSValue export_obj = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, export_obj, js_enet_funcs, countof(js_enet_funcs));
return export_obj;
JS_ROOT(export_obj, JS_NewObject(ctx));
JS_SetPropertyFunctionList(ctx, export_obj.val, js_enet_funcs, countof(js_enet_funcs));
JS_RETURN(export_obj.val);
}

View File

@@ -318,10 +318,11 @@ static const JSCFunctionListEntry js_http_funcs[] = {
JS_CFUNC_DEF("fetch", 2, js_fetch_picoparser),
};
JSValue js_http_use(JSContext *js) {
JSValue js_core_http_use(JSContext *js) {
JS_FRAME(js);
par_easycurl_init(0); // Initialize platform HTTP backend
JSValue obj = JS_NewObject(js);
JS_SetPropertyFunctionList(js, obj, js_http_funcs,
JS_ROOT(mod, JS_NewObject(js));
JS_SetPropertyFunctionList(js, mod.val, js_http_funcs,
sizeof(js_http_funcs)/sizeof(js_http_funcs[0]));
return obj;
JS_RETURN(mod.val);
}

View File

@@ -594,27 +594,28 @@ static const JSCFunctionListEntry js_socket_funcs[] = {
MIST_FUNC_DEF(socket, close, 1),
};
JSValue js_socket_use(JSContext *js) {
JSValue mod = JS_NewObject(js);
JS_SetPropertyFunctionList(js, mod, js_socket_funcs, countof(js_socket_funcs));
JSValue js_core_socket_use(JSContext *js) {
JS_FRAME(js);
JS_ROOT(mod, JS_NewObject(js));
JS_SetPropertyFunctionList(js, mod.val, js_socket_funcs, countof(js_socket_funcs));
// Add constants
JS_SetPropertyStr(js, mod, "AF_UNSPEC", JS_NewInt32(js, AF_UNSPEC));
JS_SetPropertyStr(js, mod, "AF_INET", JS_NewInt32(js, AF_INET));
JS_SetPropertyStr(js, mod, "AF_INET6", JS_NewInt32(js, AF_INET6));
JS_SetPropertyStr(js, mod, "AF_UNIX", JS_NewInt32(js, AF_UNIX));
JS_SetPropertyStr(js, mod, "SOCK_STREAM", JS_NewInt32(js, SOCK_STREAM));
JS_SetPropertyStr(js, mod, "SOCK_DGRAM", JS_NewInt32(js, SOCK_DGRAM));
JS_SetPropertyStr(js, mod, "AI_PASSIVE", JS_NewInt32(js, AI_PASSIVE));
JS_SetPropertyStr(js, mod, "SHUT_RD", JS_NewInt32(js, SHUT_RD));
JS_SetPropertyStr(js, mod, "SHUT_WR", JS_NewInt32(js, SHUT_WR));
JS_SetPropertyStr(js, mod, "SHUT_RDWR", JS_NewInt32(js, SHUT_RDWR));
JS_SetPropertyStr(js, mod, "SOL_SOCKET", JS_NewInt32(js, SOL_SOCKET));
JS_SetPropertyStr(js, mod, "SO_REUSEADDR", JS_NewInt32(js, SO_REUSEADDR));
return mod;
JS_SetPropertyStr(js, mod.val, "AF_UNSPEC", JS_NewInt32(js, AF_UNSPEC));
JS_SetPropertyStr(js, mod.val, "AF_INET", JS_NewInt32(js, AF_INET));
JS_SetPropertyStr(js, mod.val, "AF_INET6", JS_NewInt32(js, AF_INET6));
JS_SetPropertyStr(js, mod.val, "AF_UNIX", JS_NewInt32(js, AF_UNIX));
JS_SetPropertyStr(js, mod.val, "SOCK_STREAM", JS_NewInt32(js, SOCK_STREAM));
JS_SetPropertyStr(js, mod.val, "SOCK_DGRAM", JS_NewInt32(js, SOCK_DGRAM));
JS_SetPropertyStr(js, mod.val, "AI_PASSIVE", JS_NewInt32(js, AI_PASSIVE));
JS_SetPropertyStr(js, mod.val, "SHUT_RD", JS_NewInt32(js, SHUT_RD));
JS_SetPropertyStr(js, mod.val, "SHUT_WR", JS_NewInt32(js, SHUT_WR));
JS_SetPropertyStr(js, mod.val, "SHUT_RDWR", JS_NewInt32(js, SHUT_RDWR));
JS_SetPropertyStr(js, mod.val, "SOL_SOCKET", JS_NewInt32(js, SOL_SOCKET));
JS_SetPropertyStr(js, mod.val, "SO_REUSEADDR", JS_NewInt32(js, SO_REUSEADDR));
JS_RETURN(mod.val);
}

16
pack.ce
View File

@@ -13,6 +13,7 @@ var target = null
var output_name = 'app'
var target_package = null
var buildtype = 'debug'
var i = 0
if (length(args) < 1) {
log.error('Usage: cell pack <package> [options]')
@@ -24,12 +25,11 @@ if (length(args) < 1) {
log.error('')
log.error('Available targets: ' + text(build.list_targets(), ', '))
$stop()
return
}
target_package = args[0]
for (var i = 1; i < length(args); i++) {
for (i = 1; i < length(args); i++) {
if (args[i] == '-t' || args[i] == '--target') {
if (i + 1 < length(args)) {
target = args[++i]
@@ -87,7 +87,7 @@ if (target && !build.has_target(target)) {
var packages = ['core']
var deps = pkg_tools.gather_dependencies(target_package)
for (var i = 0; i < length(deps); i++) {
for (i = 0; i < length(deps); i++) {
push(packages, deps[i])
}
push(packages, target_package)
@@ -95,7 +95,7 @@ push(packages, target_package)
// Remove duplicates
var unique_packages = []
var seen = {}
for (var i = 0; i < length(packages); i++) {
for (i = 0; i < length(packages); i++) {
if (!seen[packages[i]]) {
seen[packages[i]] = true
push(unique_packages, packages[i])
@@ -111,13 +111,13 @@ arrfor(packages, function(package) {
log.console('Building static binary from ' + text(length(packages)) + ' packages: ' + text(packages, ', '))
try {
var _build = function() {
var result = build.build_static(packages, target, output_name, buildtype)
log.console('Build complete: ' + result)
} catch (e) {
log.error('Build failed: ')
log.error(e)
} disruption {
log.error('Build failed')
$stop()
}
_build()
$stop()

View File

@@ -2,9 +2,11 @@ var package = {}
var fd = use('fd')
var toml = use('toml')
var json = use('json')
var os = use('os')
var runtime = use('runtime')
var link = use('link')
var global_shop_path = runtime.shop_path
// Cache for loaded configs to avoid toml re-parsing corruption
var config_cache = {}
@@ -35,11 +37,11 @@ function get_path(name)
if (starts_with(link_target, '/'))
return link_target
// Otherwise it's another package name, resolve that
return os.global_shop_path + '/packages/' + replace(replace(link_target, '/', '_'), '@', '_')
return global_shop_path + '/packages/' + replace(replace(link_target, '/', '_'), '@', '_')
}
// Remote packages use nested directories, so don't transform slashes
return os.global_shop_path + '/packages/' + replace(name, '@', '_')
return global_shop_path + '/packages/' + replace(name, '@', '_')
}
package.load_config = function(name)

View File

@@ -6,4 +6,4 @@ var filename = args[0]
var src = text(fd.slurp(filename))
var result = tokenize(src, filename)
var ast = parse(result.tokens, src, filename, tokenize)
print(json.encode(ast))
print(json.encode(ast, true))

106
parse.cm
View File

@@ -1,6 +1,11 @@
var parse = function(tokens, src, filename, tokenizer) {
var _src_len = length(src)
var template_escape_map = {
n: "\n", t: "\t", r: "\r", "\\": "\\",
"`": "`", "$": "$", "0": character(0)
}
// ============================================================
// Parser Cursor
// ============================================================
@@ -175,6 +180,7 @@ var parse = function(tokens, src, filename, tokenizer) {
var tc = null
var tq = null
var esc_ch = null
var esc_val = null
var expr_tokens = null
var sub_ast = null
var sub_stmt = null
@@ -223,13 +229,8 @@ var parse = function(tokens, src, filename, tokenizer) {
while (tvi < tvlen) {
if (tv[tvi] == "\\" && tvi + 1 < tvlen) {
esc_ch = tv[tvi + 1]
if (esc_ch == "n") { push(fmt_parts, "\n") }
else if (esc_ch == "t") { push(fmt_parts, "\t") }
else if (esc_ch == "r") { push(fmt_parts, "\r") }
else if (esc_ch == "\\") { push(fmt_parts, "\\") }
else if (esc_ch == "`") { push(fmt_parts, "`") }
else if (esc_ch == "$") { push(fmt_parts, "$") }
else if (esc_ch == "0") { push(fmt_parts, character(0)) }
esc_val = template_escape_map[esc_ch]
if (esc_val != null) { push(fmt_parts, esc_val) }
else { push(fmt_parts, esc_ch) }
tvi = tvi + 2
} else if (tv[tvi] == "$" && tvi + 1 < tvlen && tv[tvi + 1] == "{") {
@@ -692,10 +693,10 @@ var parse = function(tokens, src, filename, tokenizer) {
if (tok.kind == "?") {
start = tok
advance()
then_expr = parse_expr()
then_expr = parse_assign_expr()
if (tok.kind == ":") advance()
else parse_error(tok, "expected ':' in ternary expression")
else_expr = parse_expr()
else_expr = parse_assign_expr()
node = ast_node("then", start)
node.expression = cond
node.then = then_expr
@@ -1425,7 +1426,9 @@ var parse = function(tokens, src, filename, tokenizer) {
vars: [],
in_loop: opts.in_loop == true,
function_nr: fn_nr,
is_function_scope: opts.is_func == true
is_function_scope: opts.is_func == true,
func_node: null,
has_inner_func: false
}
}
@@ -1478,6 +1481,15 @@ var parse = function(tokens, src, filename, tokenizer) {
return false
}
var sem_find_func_scope = function(scope) {
var s = scope
while (s != null) {
if (s.is_function_scope) return s
s = s.parent
}
return null
}
var sem_add_intrinsic = function(name) {
if (find(intrinsics, name) == null) push(intrinsics, name)
}
@@ -1616,6 +1628,46 @@ var parse = function(tokens, src, filename, tokenizer) {
if (kind == "[" && left_node.right != null) {
sem_check_expr(scope, left_node.right)
}
// Type error detection for known-type constant objects
if (obj_expr != null && obj_expr.kind == "name" && obj_expr.name != null) {
v = sem_find_var(scope, obj_expr.name)
if (v != null && v.is_const && v.type_tag != null) {
if (kind == ".") {
if (v.type_tag == "array") {
sem_error(left_node, "cannot set property on array '" + obj_expr.name + "'")
}
} else if (kind == "[") {
if (left_node.right == null) {
// Push: a[] = val
if (v.type_tag != "array") {
sem_error(left_node, "push only works on arrays, not " + v.type_tag + " '" + obj_expr.name + "'")
}
} else if (v.type_tag == "array") {
if (left_node.right.kind == "text") {
sem_error(left_node, "cannot use text key on array '" + obj_expr.name + "'")
}
} else if (v.type_tag == "record") {
if (left_node.right.kind == "number" && is_integer(left_node.right.number)) {
sem_error(left_node, "cannot use integer key on record '" + obj_expr.name + "'; use text key")
}
}
}
} else if (v != null && v.is_const && v.type_tag == null) {
// Infer type_tag from usage pattern (def only)
if (kind == ".") {
v.type_tag = "record"
} else if (kind == "[") {
if (left_node.right == null) {
// Push: a[] = val → array
v.type_tag = "array"
} else if (left_node.right.kind == "number" && is_integer(left_node.right.number)) {
v.type_tag = "array"
} else if (left_node.right.kind == "text") {
v.type_tag = "record"
}
}
}
}
}
}
@@ -1635,6 +1687,7 @@ var parse = function(tokens, src, filename, tokenizer) {
var pname = null
var def_val = null
var sr = null
var enclosing = null
if (_assign_kinds[kind] == true) {
sem_check_assign_target(scope, expr.left)
@@ -1736,9 +1789,12 @@ var parse = function(tokens, src, filename, tokenizer) {
}
if (kind == "function") {
enclosing = sem_find_func_scope(scope)
if (enclosing != null) enclosing.has_inner_func = true
fn_nr_val = expr.function_nr
if (fn_nr_val == null) fn_nr_val = scope.function_nr
fn_scope = make_scope(scope, fn_nr_val, {is_func: true})
fn_scope.func_node = expr
expr.outer = scope.function_nr
i = 0
while (i < length(expr.list)) {
@@ -1819,6 +1875,8 @@ var parse = function(tokens, src, filename, tokenizer) {
var pname = null
var def_val = null
var sr = null
var enclosing = null
var func_scope = null
var tt = null
if (kind == "var_list") {
@@ -1861,7 +1919,7 @@ var parse = function(tokens, src, filename, tokenizer) {
sem_check_expr(scope, stmt.right)
if (name != null) {
tt = derive_type_tag(stmt.right)
if (tt != null) {
if (tt != null && tt != "null") {
existing = sem_find_var(scope, name)
if (existing != null) existing.type_tag = tt
}
@@ -1941,7 +1999,26 @@ var parse = function(tokens, src, filename, tokenizer) {
return null
}
if (kind == "return" || kind == "go") {
if (kind == "go") {
sem_check_expr(scope, stmt.expression)
if (stmt.expression == null || stmt.expression.kind != "(") {
sem_error(stmt, "'go' must be followed by a function call")
} else {
func_scope = sem_find_func_scope(scope)
if (func_scope != null && func_scope.func_node != null) {
if (func_scope.func_node.disruption != null) {
sem_error(stmt, "cannot use 'go' in a function with a disruption clause")
}
if (func_scope.has_inner_func) {
sem_error(stmt, "cannot use 'go' in a function that defines inner functions")
}
}
stmt.tail = true
}
return null
}
if (kind == "return") {
sem_check_expr(scope, stmt.expression)
if (stmt.expression != null && stmt.expression.kind == "(") {
stmt.tail = true
@@ -1982,11 +2059,14 @@ var parse = function(tokens, src, filename, tokenizer) {
}
if (kind == "function") {
enclosing = sem_find_func_scope(scope)
if (enclosing != null) enclosing.has_inner_func = true
name = stmt.name
if (name != null) sem_add_var(scope, name, {make: "function", fn_nr: scope.function_nr})
if (name != null && sem_find_var(scope, name) == null) sem_add_var(scope, name, {make: "function", fn_nr: scope.function_nr})
fn_nr_val = stmt.function_nr
if (fn_nr_val == null) fn_nr_val = scope.function_nr
fn_scope = make_scope(scope, fn_nr_val, {is_func: true})
fn_scope.func_node = stmt
stmt.outer = scope.function_nr
i = 0
while (i < length(stmt.list)) {

Binary file not shown.

531
qbe.cm
View File

@@ -98,6 +98,7 @@ var is_text = function(p, v) {
jmp @${p}.done
@${p}.no
%${p} =w copy 0
jmp @${p}.done
@${p}.done
`
}
@@ -174,6 +175,7 @@ var to_float64 = function(p, v) {
%${p}.fbits =l or %${p}.fs63, %${p}.fe52
%${p}.fbits =l or %${p}.fbits, %${p}.fmant
%${p} =d cast %${p}.fbits
jmp @${p}.done
@${p}.done
`
}
@@ -199,201 +201,37 @@ var new_bool = function(p, b) {
// new_float64 — C call to __JS_NewFloat64(ctx, val). Result: %{p}
var new_float64 = function(p, ctx, d) {
return ` %${p} =l call $__JS_NewFloat64(l ${ctx}, d ${d})
return ` %${p} =l call $qbe_new_float64(l ${ctx}, d ${d})
`
}
// ============================================================
// Arithmetic — add(p, ctx, a, b)
// Int fast path inline, text concat and float as C calls.
// Jumps to @disrupt on type mismatch.
// Arithmetic — add/sub/mul/div/mod(p, ctx, a, b)
// Simple C call wrappers. Type dispatch is handled in mcode.cm.
// ============================================================
var add = function(p, ctx, a, b) {
return `@${p}.start
%${p}.at =l and ${a}, 1
%${p}.bt =l and ${b}, 1
%${p}.not_int =l or %${p}.at, %${p}.bt
jnz %${p}.not_int, @${p}.not_both_int, @${p}.int_path
@${p}.int_path
%${p}.ia =l sar ${a}, 1
%${p}.ib =l sar ${b}, 1
%${p}.sum =l add %${p}.ia, %${p}.ib
%${p}.lo =w csltl %${p}.sum, ${int32_min}
%${p}.hi =w csgtl %${p}.sum, ${int32_max}
%${p}.ov =w or %${p}.lo, %${p}.hi
jnz %${p}.ov, @${p}.int_overflow, @${p}.int_ok
@${p}.int_ok
%${p}.rw =w copy %${p}.sum
%${p}.rext =l extuw %${p}.rw
%${p} =l shl %${p}.rext, 1
jmp @${p}.done
@${p}.int_overflow
%${p}.fd =d sltof %${p}.sum
%${p} =l call $__JS_NewFloat64(l ${ctx}, d %${p}.fd)
jmp @${p}.done
@${p}.not_both_int
%${p}.a_is_text =w call $JS_IsText(l ${a})
%${p}.b_is_text =w call $JS_IsText(l ${b})
%${p}.both_text =w and %${p}.a_is_text, %${p}.b_is_text
jnz %${p}.both_text, @${p}.text_path, @${p}.chk_num
@${p}.text_path
%${p} =l call $JS_ConcatString(l ${ctx}, l ${a}, l ${b})
jmp @${p}.done
@${p}.chk_num
%${p}.a_is_num =w call $JS_IsNumber(l ${a})
%${p}.b_is_num =w call $JS_IsNumber(l ${b})
%${p}.both_num =w and %${p}.a_is_num, %${p}.b_is_num
jnz %${p}.both_num, @${p}.float_path, @disrupt
@${p}.float_path
%${p} =l call $qbe_float_add(l ${ctx}, l ${a}, l ${b})
@${p}.done
return ` %${p} =l call $qbe_float_add(l ${ctx}, l ${a}, l ${b})
`
}
var sub = function(p, ctx, a, b) {
return `@${p}.start
%${p}.at =l and ${a}, 1
%${p}.bt =l and ${b}, 1
%${p}.not_int =l or %${p}.at, %${p}.bt
jnz %${p}.not_int, @${p}.not_both_int, @${p}.int_path
@${p}.int_path
%${p}.ia =l sar ${a}, 1
%${p}.ib =l sar ${b}, 1
%${p}.diff =l sub %${p}.ia, %${p}.ib
%${p}.lo =w csltl %${p}.diff, ${int32_min}
%${p}.hi =w csgtl %${p}.diff, ${int32_max}
%${p}.ov =w or %${p}.lo, %${p}.hi
jnz %${p}.ov, @${p}.int_overflow, @${p}.int_ok
@${p}.int_ok
%${p}.rw =w copy %${p}.diff
%${p}.rext =l extuw %${p}.rw
%${p} =l shl %${p}.rext, 1
jmp @${p}.done
@${p}.int_overflow
%${p}.fd =d sltof %${p}.diff
%${p} =l call $__JS_NewFloat64(l ${ctx}, d %${p}.fd)
jmp @${p}.done
@${p}.not_both_int
%${p}.a_is_num =w call $JS_IsNumber(l ${a})
%${p}.b_is_num =w call $JS_IsNumber(l ${b})
%${p}.both_num =w and %${p}.a_is_num, %${p}.b_is_num
jnz %${p}.both_num, @${p}.float_path, @disrupt
@${p}.float_path
%${p} =l call $qbe_float_sub(l ${ctx}, l ${a}, l ${b})
@${p}.done
return ` %${p} =l call $qbe_float_sub(l ${ctx}, l ${a}, l ${b})
`
}
var mul = function(p, ctx, a, b) {
return `@${p}.start
%${p}.at =l and ${a}, 1
%${p}.bt =l and ${b}, 1
%${p}.not_int =l or %${p}.at, %${p}.bt
jnz %${p}.not_int, @${p}.not_both_int, @${p}.int_path
@${p}.int_path
%${p}.ia =l sar ${a}, 1
%${p}.ib =l sar ${b}, 1
%${p}.prod =l mul %${p}.ia, %${p}.ib
%${p}.lo =w csltl %${p}.prod, ${int32_min}
%${p}.hi =w csgtl %${p}.prod, ${int32_max}
%${p}.ov =w or %${p}.lo, %${p}.hi
jnz %${p}.ov, @${p}.int_overflow, @${p}.int_ok
@${p}.int_ok
%${p}.rw =w copy %${p}.prod
%${p}.rext =l extuw %${p}.rw
%${p} =l shl %${p}.rext, 1
jmp @${p}.done
@${p}.int_overflow
%${p}.fd =d sltof %${p}.prod
%${p} =l call $__JS_NewFloat64(l ${ctx}, d %${p}.fd)
jmp @${p}.done
@${p}.not_both_int
%${p}.a_is_num =w call $JS_IsNumber(l ${a})
%${p}.b_is_num =w call $JS_IsNumber(l ${b})
%${p}.both_num =w and %${p}.a_is_num, %${p}.b_is_num
jnz %${p}.both_num, @${p}.float_path, @disrupt
@${p}.float_path
%${p} =l call $qbe_float_mul(l ${ctx}, l ${a}, l ${b})
@${p}.done
return ` %${p} =l call $qbe_float_mul(l ${ctx}, l ${a}, l ${b})
`
}
var div = function(p, ctx, a, b) {
return `@${p}.start
%${p}.at =l and ${a}, 1
%${p}.bt =l and ${b}, 1
%${p}.not_int =l or %${p}.at, %${p}.bt
jnz %${p}.not_int, @${p}.not_both_int, @${p}.int_path
@${p}.int_path
%${p}.ia =w copy 0
%${p}.tmp =l sar ${a}, 1
%${p}.ia =w copy %${p}.tmp
%${p}.ib =w copy 0
%${p}.tmp2 =l sar ${b}, 1
%${p}.ib =w copy %${p}.tmp2
%${p}.div0 =w ceqw %${p}.ib, 0
jnz %${p}.div0, @${p}.ret_null, @${p}.chk_exact
@${p}.ret_null
%${p} =l copy ${js_null}
jmp @${p}.done
@${p}.chk_exact
%${p}.rem =w rem %${p}.ia, %${p}.ib
%${p}.exact =w ceqw %${p}.rem, 0
jnz %${p}.exact, @${p}.int_div, @${p}.int_to_float
@${p}.int_div
%${p}.q =w div %${p}.ia, %${p}.ib
%${p}.qext =l extuw %${p}.q
%${p} =l shl %${p}.qext, 1
jmp @${p}.done
@${p}.int_to_float
%${p}.da =d swtof %${p}.ia
%${p}.db =d swtof %${p}.ib
%${p}.dr =d div %${p}.da, %${p}.db
%${p} =l call $__JS_NewFloat64(l ${ctx}, d %${p}.dr)
jmp @${p}.done
@${p}.not_both_int
%${p}.a_is_num =w call $JS_IsNumber(l ${a})
%${p}.b_is_num =w call $JS_IsNumber(l ${b})
%${p}.both_num =w and %${p}.a_is_num, %${p}.b_is_num
jnz %${p}.both_num, @${p}.float_path, @disrupt
@${p}.float_path
%${p} =l call $qbe_float_div(l ${ctx}, l ${a}, l ${b})
@${p}.done
return ` %${p} =l call $qbe_float_div(l ${ctx}, l ${a}, l ${b})
`
}
var mod = function(p, ctx, a, b) {
return `@${p}.start
%${p}.at =l and ${a}, 1
%${p}.bt =l and ${b}, 1
%${p}.not_int =l or %${p}.at, %${p}.bt
jnz %${p}.not_int, @${p}.not_both_int, @${p}.int_path
@${p}.int_path
%${p}.ia =w copy 0
%${p}.tmp =l sar ${a}, 1
%${p}.ia =w copy %${p}.tmp
%${p}.ib =w copy 0
%${p}.tmp2 =l sar ${b}, 1
%${p}.ib =w copy %${p}.tmp2
%${p}.div0 =w ceqw %${p}.ib, 0
jnz %${p}.div0, @${p}.ret_null, @${p}.do_mod
@${p}.ret_null
%${p} =l copy ${js_null}
jmp @${p}.done
@${p}.do_mod
%${p}.r =w rem %${p}.ia, %${p}.ib
%${p}.rext =l extuw %${p}.r
%${p} =l shl %${p}.rext, 1
jmp @${p}.done
@${p}.not_both_int
%${p}.a_is_num =w call $JS_IsNumber(l ${a})
%${p}.b_is_num =w call $JS_IsNumber(l ${b})
%${p}.both_num =w and %${p}.a_is_num, %${p}.b_is_num
jnz %${p}.both_num, @${p}.float_path, @disrupt
@${p}.float_path
%${p} =l call $qbe_float_mod(l ${ctx}, l ${a}, l ${b})
@${p}.done
return ` %${p} =l call $qbe_float_mod(l ${ctx}, l ${a}, l ${b})
`
}
@@ -484,6 +322,7 @@ var cmp = function(p, ctx, a, b) {
jmp @${p}.done
@${p}.mismatch
%${p} =l copy ${mismatch_val}
jmp @${p}.done
@${p}.done
`
}
@@ -518,90 +357,28 @@ var gt = function(p, ctx, a, b) {
var ge = function(p, ctx, a, b) {
_qflags = {int_cmp_op: "csgew", float_id: 5, is_eq: false, is_ne: false, null_true: true}
return cmp(p, ctx, a, b)
}
// ============================================================
// Unary Ops
// ============================================================
// neg(p, ctx, v) — negate. Int fast path (INT32_MIN edge case), else C call.
// neg(p, ctx, v) — negate via C call (type guards in mcode)
var neg = function(p, ctx, v) {
return `@${p}.start
%${p}.tag =l and ${v}, 1
%${p}.is_int =w ceql %${p}.tag, 0
jnz %${p}.is_int, @${p}.int_path, @${p}.float_path
@${p}.int_path
%${p}.sl =l sar ${v}, 1
%${p}.iw =w copy %${p}.sl
%${p}.is_min =w ceqw %${p}.iw, ${int32_min}
jnz %${p}.is_min, @${p}.min_overflow, @${p}.int_ok
@${p}.min_overflow
%${p}.fd =d swtof %${p}.iw
%${p}.fdn =d neg %${p}.fd
%${p} =l call $__JS_NewFloat64(l ${ctx}, d %${p}.fdn)
jmp @${p}.done
@${p}.int_ok
%${p}.ni =w sub 0, %${p}.iw
%${p}.niext =l extuw %${p}.ni
%${p} =l shl %${p}.niext, 1
jmp @${p}.done
@${p}.float_path
%${p} =l call $qbe_float_neg(l ${ctx}, l ${v})
@${p}.done
return ` %${p} =l call $qbe_float_neg(l ${ctx}, l ${v})
`
}
// inc(p, ctx, v) — increment. Int fast path (INT32_MAX edge case), else C call.
// inc(p, ctx, v) — increment via C call (type guards in mcode)
var inc = function(p, ctx, v) {
return `@${p}.start
%${p}.tag =l and ${v}, 1
%${p}.is_int =w ceql %${p}.tag, 0
jnz %${p}.is_int, @${p}.int_path, @${p}.float_path
@${p}.int_path
%${p}.sl =l sar ${v}, 1
%${p}.iw =w copy %${p}.sl
%${p}.is_max =w ceqw %${p}.iw, ${int32_max}
jnz %${p}.is_max, @${p}.max_overflow, @${p}.int_ok
@${p}.max_overflow
%${p}.fd =d swtof %${p}.iw
%${p}.fd1 =d add %${p}.fd, d_1.0
%${p} =l call $__JS_NewFloat64(l ${ctx}, d %${p}.fd1)
jmp @${p}.done
@${p}.int_ok
%${p}.ni =w add %${p}.iw, 1
%${p}.niext =l extuw %${p}.ni
%${p} =l shl %${p}.niext, 1
jmp @${p}.done
@${p}.float_path
%${p} =l call $qbe_float_inc(l ${ctx}, l ${v})
@${p}.done
return ` %${p} =l call $qbe_float_inc(l ${ctx}, l ${v})
`
}
// dec(p, ctx, v) — decrement. Int fast path (INT32_MIN edge case), else C call.
// dec(p, ctx, v) — decrement via C call (type guards in mcode)
var dec = function(p, ctx, v) {
return `@${p}.start
%${p}.tag =l and ${v}, 1
%${p}.is_int =w ceql %${p}.tag, 0
jnz %${p}.is_int, @${p}.int_path, @${p}.float_path
@${p}.int_path
%${p}.sl =l sar ${v}, 1
%${p}.iw =w copy %${p}.sl
%${p}.is_min =w ceqw %${p}.iw, ${int32_min}
jnz %${p}.is_min, @${p}.min_overflow, @${p}.int_ok
@${p}.min_overflow
%${p}.fd =d swtof %${p}.iw
%${p}.fd1 =d sub %${p}.fd, d_1.0
%${p} =l call $__JS_NewFloat64(l ${ctx}, d %${p}.fd1)
jmp @${p}.done
@${p}.int_ok
%${p}.ni =w sub %${p}.iw, 1
%${p}.niext =l extuw %${p}.ni
%${p} =l shl %${p}.niext, 1
jmp @${p}.done
@${p}.float_path
%${p} =l call $qbe_float_dec(l ${ctx}, l ${v})
@${p}.done
return ` %${p} =l call $qbe_float_dec(l ${ctx}, l ${v})
`
}
@@ -615,22 +392,9 @@ var lnot = function(p, ctx, v) {
`
}
// bnot(p, ctx, v) — bitwise not. Convert to int32, ~, re-tag.
// bnot(p, ctx, v) — bitwise not via C call
var bnot = function(p, ctx, v) {
return `@${p}.start
%${p}.tag =l and ${v}, 1
%${p}.is_int =w ceql %${p}.tag, 0
jnz %${p}.is_int, @${p}.int_path, @${p}.slow_path
@${p}.int_path
%${p}.sl =l sar ${v}, 1
%${p}.iw =w copy %${p}.sl
%${p}.nw =w xor %${p}.iw, -1
%${p}.nex =l extuw %${p}.nw
%${p} =l shl %${p}.nex, 1
jmp @${p}.done
@${p}.slow_path
%${p} =l call $qbe_bnot(l ${ctx}, l ${v})
@${p}.done
return ` %${p} =l call $qbe_bnot(l ${ctx}, l ${v})
`
}
@@ -639,92 +403,34 @@ var bnot = function(p, ctx, v) {
// Both operands must be numeric. Int fast path, float -> convert to int32.
// ============================================================
// reads _qop from closure
var bitwise_op = function(p, ctx, a, b) {
var qbe_op = _qop
return `@${p}.start
%${p}.at =l and ${a}, 1
%${p}.bt =l and ${b}, 1
%${p}.not_int =l or %${p}.at, %${p}.bt
jnz %${p}.not_int, @${p}.slow_path, @${p}.int_path
@${p}.int_path
%${p}.ia =l sar ${a}, 1
%${p}.iaw =w copy %${p}.ia
%${p}.ib =l sar ${b}, 1
%${p}.ibw =w copy %${p}.ib
%${p}.rw =w ${qbe_op} %${p}.iaw, %${p}.ibw
%${p}.rext =l extuw %${p}.rw
%${p} =l shl %${p}.rext, 1
jmp @${p}.done
@${p}.slow_path
%${p}.a_is_num =w call $JS_IsNumber(l ${a})
%${p}.b_is_num =w call $JS_IsNumber(l ${b})
%${p}.both_num =w and %${p}.a_is_num, %${p}.b_is_num
jnz %${p}.both_num, @${p}.float_to_int, @disrupt
@${p}.float_to_int
%${p} =l call $qbe_bitwise_${qbe_op}(l ${ctx}, l ${a}, l ${b})
@${p}.done
var band = function(p, ctx, a, b) {
return ` %${p} =l call $qbe_bitwise_and(l ${ctx}, l ${a}, l ${b})
`
}
var band = function(p, ctx, a, b) {
_qop = "and"
return bitwise_op(p, ctx, a, b)
}
var bor = function(p, ctx, a, b) {
_qop = "or"
return bitwise_op(p, ctx, a, b)
return ` %${p} =l call $qbe_bitwise_or(l ${ctx}, l ${a}, l ${b})
`
}
var bxor = function(p, ctx, a, b) {
_qop = "xor"
return bitwise_op(p, ctx, a, b)
}
// Shift ops: mask shift amount to 5 bits (& 31)
// reads _qop from closure
var shift_op = function(p, ctx, a, b) {
var qbe_op = _qop
return `@${p}.start
%${p}.at =l and ${a}, 1
%${p}.bt =l and ${b}, 1
%${p}.not_int =l or %${p}.at, %${p}.bt
jnz %${p}.not_int, @${p}.slow_path, @${p}.int_path
@${p}.int_path
%${p}.ia =l sar ${a}, 1
%${p}.iaw =w copy %${p}.ia
%${p}.ib =l sar ${b}, 1
%${p}.ibw =w copy %${p}.ib
%${p}.sh =w and %${p}.ibw, 31
%${p}.rw =w ${qbe_op} %${p}.iaw, %${p}.sh
%${p}.rext =l extuw %${p}.rw
%${p} =l shl %${p}.rext, 1
jmp @${p}.done
@${p}.slow_path
%${p}.a_is_num =w call $JS_IsNumber(l ${a})
%${p}.b_is_num =w call $JS_IsNumber(l ${b})
%${p}.both_num =w and %${p}.a_is_num, %${p}.b_is_num
jnz %${p}.both_num, @${p}.float_to_int, @disrupt
@${p}.float_to_int
%${p} =l call $qbe_shift_${qbe_op}(l ${ctx}, l ${a}, l ${b})
@${p}.done
return ` %${p} =l call $qbe_bitwise_xor(l ${ctx}, l ${a}, l ${b})
`
}
var shl = function(p, ctx, a, b) {
_qop = "shl"
return shift_op(p, ctx, a, b)
return ` %${p} =l call $qbe_shift_shl(l ${ctx}, l ${a}, l ${b})
`
}
var shr = function(p, ctx, a, b) {
_qop = "sar"
return shift_op(p, ctx, a, b)
return ` %${p} =l call $qbe_shift_sar(l ${ctx}, l ${a}, l ${b})
`
}
var ushr = function(p, ctx, a, b) {
_qop = "shr"
return shift_op(p, ctx, a, b)
return ` %${p} =l call $qbe_shift_shr(l ${ctx}, l ${a}, l ${b})
`
}
// ============================================================
@@ -732,167 +438,6 @@ var ushr = function(p, ctx, a, b) {
// These map directly to the new IR ops emitted by mcode.cm.
// ============================================================
// --- Arithmetic (int path) ---
// add_int: assume both operands are tagged ints. Overflow -> float.
var add_int = function(p, ctx, a, b) {
return ` %${p}.ia =l sar ${a}, 1
%${p}.ib =l sar ${b}, 1
%${p}.sum =l add %${p}.ia, %${p}.ib
%${p}.lo =w csltl %${p}.sum, ${int32_min}
%${p}.hi =w csgtl %${p}.sum, ${int32_max}
%${p}.ov =w or %${p}.lo, %${p}.hi
jnz %${p}.ov, @${p}.ov, @${p}.ok
@${p}.ok
%${p}.rw =w copy %${p}.sum
%${p}.rext =l extuw %${p}.rw
%${p} =l shl %${p}.rext, 1
jmp @${p}.done
@${p}.ov
%${p}.fd =d sltof %${p}.sum
%${p} =l call $__JS_NewFloat64(l ${ctx}, d %${p}.fd)
@${p}.done
`
}
var sub_int = function(p, ctx, a, b) {
return ` %${p}.ia =l sar ${a}, 1
%${p}.ib =l sar ${b}, 1
%${p}.diff =l sub %${p}.ia, %${p}.ib
%${p}.lo =w csltl %${p}.diff, ${int32_min}
%${p}.hi =w csgtl %${p}.diff, ${int32_max}
%${p}.ov =w or %${p}.lo, %${p}.hi
jnz %${p}.ov, @${p}.ov, @${p}.ok
@${p}.ok
%${p}.rw =w copy %${p}.diff
%${p}.rext =l extuw %${p}.rw
%${p} =l shl %${p}.rext, 1
jmp @${p}.done
@${p}.ov
%${p}.fd =d sltof %${p}.diff
%${p} =l call $__JS_NewFloat64(l ${ctx}, d %${p}.fd)
@${p}.done
`
}
var mul_int = function(p, ctx, a, b) {
return ` %${p}.ia =l sar ${a}, 1
%${p}.ib =l sar ${b}, 1
%${p}.prod =l mul %${p}.ia, %${p}.ib
%${p}.lo =w csltl %${p}.prod, ${int32_min}
%${p}.hi =w csgtl %${p}.prod, ${int32_max}
%${p}.ov =w or %${p}.lo, %${p}.hi
jnz %${p}.ov, @${p}.ov, @${p}.ok
@${p}.ok
%${p}.rw =w copy %${p}.prod
%${p}.rext =l extuw %${p}.rw
%${p} =l shl %${p}.rext, 1
jmp @${p}.done
@${p}.ov
%${p}.fd =d sltof %${p}.prod
%${p} =l call $__JS_NewFloat64(l ${ctx}, d %${p}.fd)
@${p}.done
`
}
var div_int = function(p, ctx, a, b) {
return ` %${p}.ia =w copy 0
%${p}.tmp =l sar ${a}, 1
%${p}.ia =w copy %${p}.tmp
%${p}.ib =w copy 0
%${p}.tmp2 =l sar ${b}, 1
%${p}.ib =w copy %${p}.tmp2
%${p}.div0 =w ceqw %${p}.ib, 0
jnz %${p}.div0, @${p}.null, @${p}.chk
@${p}.null
%${p} =l copy ${js_null}
jmp @${p}.done
@${p}.chk
%${p}.rem =w rem %${p}.ia, %${p}.ib
%${p}.exact =w ceqw %${p}.rem, 0
jnz %${p}.exact, @${p}.idiv, @${p}.fdiv
@${p}.idiv
%${p}.q =w div %${p}.ia, %${p}.ib
%${p}.qext =l extuw %${p}.q
%${p} =l shl %${p}.qext, 1
jmp @${p}.done
@${p}.fdiv
%${p}.da =d swtof %${p}.ia
%${p}.db =d swtof %${p}.ib
%${p}.dr =d div %${p}.da, %${p}.db
%${p} =l call $__JS_NewFloat64(l ${ctx}, d %${p}.dr)
@${p}.done
`
}
var mod_int = function(p, ctx, a, b) {
return ` %${p}.ia =w copy 0
%${p}.tmp =l sar ${a}, 1
%${p}.ia =w copy %${p}.tmp
%${p}.ib =w copy 0
%${p}.tmp2 =l sar ${b}, 1
%${p}.ib =w copy %${p}.tmp2
%${p}.div0 =w ceqw %${p}.ib, 0
jnz %${p}.div0, @${p}.null, @${p}.do_mod
@${p}.null
%${p} =l copy ${js_null}
jmp @${p}.done
@${p}.do_mod
%${p}.r =w rem %${p}.ia, %${p}.ib
%${p}.rext =l extuw %${p}.r
%${p} =l shl %${p}.rext, 1
@${p}.done
`
}
var neg_int = function(p, ctx, v) {
return ` %${p}.sl =l sar ${v}, 1
%${p}.iw =w copy %${p}.sl
%${p}.is_min =w ceqw %${p}.iw, ${int32_min}
jnz %${p}.is_min, @${p}.ov, @${p}.ok
@${p}.ov
%${p}.fd =d swtof %${p}.iw
%${p}.fdn =d neg %${p}.fd
%${p} =l call $__JS_NewFloat64(l ${ctx}, d %${p}.fdn)
jmp @${p}.done
@${p}.ok
%${p}.ni =w sub 0, %${p}.iw
%${p}.niext =l extuw %${p}.ni
%${p} =l shl %${p}.niext, 1
@${p}.done
`
}
// --- Arithmetic (float path) ---
var add_float = function(p, ctx, a, b) {
return ` %${p} =l call $qbe_float_add(l ${ctx}, l ${a}, l ${b})
`
}
var sub_float = function(p, ctx, a, b) {
return ` %${p} =l call $qbe_float_sub(l ${ctx}, l ${a}, l ${b})
`
}
var mul_float = function(p, ctx, a, b) {
return ` %${p} =l call $qbe_float_mul(l ${ctx}, l ${a}, l ${b})
`
}
var div_float = function(p, ctx, a, b) {
return ` %${p} =l call $qbe_float_div(l ${ctx}, l ${a}, l ${b})
`
}
var mod_float = function(p, ctx, a, b) {
return ` %${p} =l call $qbe_float_mod(l ${ctx}, l ${a}, l ${b})
`
}
var neg_float = function(p, ctx, v) {
return ` %${p} =l call $qbe_float_neg(l ${ctx}, l ${v})
`
}
// --- Text concat ---
var concat = function(p, ctx, a, b) {
return ` %${p} =l call $JS_ConcatString(l ${ctx}, l ${a}, l ${b})
@@ -1039,20 +584,6 @@ return {
shl: shl,
shr: shr,
ushr: ushr,
// decomposed arithmetic (int path)
add_int: add_int,
sub_int: sub_int,
mul_int: mul_int,
div_int: div_int,
mod_int: mod_int,
neg_int: neg_int,
// decomposed arithmetic (float path)
add_float: add_float,
sub_float: sub_float,
mul_float: mul_float,
div_float: div_float,
mod_float: mod_float,
neg_float: neg_float,
// text concat
concat: concat,
// decomposed comparisons (int)

BIN
qbe.mach

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

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