Compare commits
72 Commits
fix_aot
...
async_http
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e45695399d | ||
|
|
52b55cb1ad | ||
|
|
fa9d2609b1 | ||
|
|
e38c2f07bf | ||
|
|
ecc1777b24 | ||
|
|
1cfd5b8133 | ||
|
|
9c1141f408 | ||
|
|
696cca530b | ||
|
|
c92a4087a6 | ||
|
|
01637c49b0 | ||
|
|
f9e660ebaa | ||
|
|
4f8fada57d | ||
|
|
adcaa92bea | ||
|
|
fc36707b39 | ||
|
|
7cb8ce7945 | ||
|
|
bb7997a751 | ||
|
|
327b990442 | ||
|
|
51c0a0b306 | ||
|
|
8ac82016dd | ||
|
|
d0bf757d91 | ||
|
|
c77f1f8639 | ||
|
|
2b877e6b0c | ||
|
|
3d4c0ec3d3 | ||
|
|
33d9013409 | ||
|
|
c2f57d1dae | ||
|
|
7bd17c6476 | ||
|
|
5ac1620b48 | ||
|
|
124c9536b4 | ||
|
|
940807c37a | ||
|
|
70f560550f | ||
|
|
060a494f47 | ||
|
|
6812d3edbc | ||
|
|
4da15d2a3e | ||
|
|
a34566a0c1 | ||
|
|
9f7d861932 | ||
|
|
c5536697ff | ||
|
|
d066ab03cd | ||
|
|
9c1cb43c7d | ||
|
|
99fb575c9c | ||
|
|
193991c532 | ||
|
|
76552c6854 | ||
|
|
f26b6e853d | ||
|
|
94c28f0e17 | ||
|
|
a18584afd3 | ||
|
|
3f6cfad7ef | ||
|
|
b03edb0d90 | ||
|
|
62440d3ed6 | ||
|
|
4edc4b7cc5 | ||
|
|
012b507415 | ||
|
|
ee6398ada9 | ||
|
|
173438e8bc | ||
|
|
7ac5ac63d2 | ||
|
|
a05f180356 | ||
|
|
d88692cd30 | ||
|
|
b0ac5de7e2 | ||
|
|
1d4fc11772 | ||
|
|
7372b80e07 | ||
|
|
d27047dd82 | ||
|
|
8e96379377 | ||
|
|
8f415fea80 | ||
|
|
cec0b99207 | ||
|
|
2d4645da9c | ||
|
|
017b63ba80 | ||
|
|
99fa86a09c | ||
|
|
6d6b53009f | ||
|
|
cc7fc6b667 | ||
|
|
bbeb757e40 | ||
|
|
2ac446f7cf | ||
|
|
517bd64275 | ||
|
|
7d0c96f328 | ||
|
|
e7ed6bd8b2 | ||
|
|
700b640cf1 |
38
.gitea/workflows/ci.yml
Normal file
38
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,38 @@
|
||||
name: CI
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: gitea.pockle.world/john/cell-ci:latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build
|
||||
run: |
|
||||
meson setup build -Dbuildtype=release
|
||||
meson compile -C build
|
||||
- name: Prepare tests
|
||||
run: |
|
||||
cp build/libcell_runtime.so .
|
||||
cp build/cell .
|
||||
mkdir -p ~/.cell/packages
|
||||
ln -s "$PWD" ~/.cell/packages/core
|
||||
- name: VM tests
|
||||
run: ./cell --dev vm_suite
|
||||
- name: Language tests
|
||||
run: ./cell --dev test suite
|
||||
- name: Fuzz
|
||||
run: ./cell --dev fuzz
|
||||
|
||||
build-windows:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: gitea.pockle.world/john/cell-ci:latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build
|
||||
run: |
|
||||
meson setup build_win --cross-file cross/mingw64.ini -Dbuildtype=release
|
||||
meson compile -C build_win
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,7 +1,12 @@
|
||||
.git/
|
||||
.obj/
|
||||
website/public/
|
||||
website/site/
|
||||
website/.hugo_build.lock
|
||||
.cache
|
||||
.cell
|
||||
cell
|
||||
libcell_runtime*
|
||||
bin/
|
||||
build/
|
||||
*.zip
|
||||
|
||||
@@ -57,7 +57,7 @@ The creator functions are **polymorphic** — behavior depends on argument types
|
||||
- `record(record, another)` — merge
|
||||
- `record(array_of_keys)` — create record from keys
|
||||
|
||||
Other key intrinsics: `length()`, `stone()`, `is_stone()`, `print()`, `filter()`, `find()`, `reduce()`, `sort()`, `reverse()`, `some()`, `every()`, `starts_with()`, `ends_with()`, `meme()`, `proto()`, `isa()`, `splat()`, `apply()`, `extract()`, `replace()`, `search()`, `format()`, `lower()`, `upper()`, `trim()`
|
||||
Other key intrinsics: `object()`, `length()`, `stone()`, `is_stone()`, `filter()`, `find()`, `reduce()`, `sort()`, `reverse()`, `some()`, `every()`, `starts_with()`, `ends_with()`, `meme()`, `proto()`, `isa()`, `splat()`, `apply()`, `extract()`, `replace()`, `search()`, `format()`, `lower()`, `upper()`, `trim()`
|
||||
|
||||
Sensory functions: `is_array()`, `is_text()`, `is_number()`, `is_object()`, `is_function()`, `is_null()`, `is_logical()`, `is_integer()`, `is_stone()`, etc.
|
||||
|
||||
|
||||
4
Makefile
4
Makefile
@@ -24,7 +24,7 @@ $(BUILD_DBG)/build.ninja:
|
||||
install: all $(CELL_SHOP)
|
||||
cp cell $(INSTALL_BIN)/cell
|
||||
cp libcell_runtime.dylib $(INSTALL_LIB)/
|
||||
cp source/cell.h source/quickjs.h source/wota.h $(INSTALL_INC)/
|
||||
cp source/cell.h $(INSTALL_INC)/
|
||||
rm -rf $(CELL_SHOP)/packages/core
|
||||
ln -s $(CURDIR) $(CELL_SHOP)/packages/core
|
||||
@echo "Installed cell to $(INSTALL_BIN) and $(INSTALL_LIB)"
|
||||
@@ -32,7 +32,7 @@ install: all $(CELL_SHOP)
|
||||
install_debug: debug $(CELL_SHOP)
|
||||
cp cell $(INSTALL_BIN)/cell
|
||||
cp libcell_runtime.dylib $(INSTALL_LIB)/
|
||||
cp source/cell.h source/quickjs.h source/wota.h $(INSTALL_INC)/
|
||||
cp source/cell.h $(INSTALL_INC)/
|
||||
rm -rf $(CELL_SHOP)/packages/core
|
||||
ln -s $(CURDIR) $(CELL_SHOP)/packages/core
|
||||
@echo "Installed cell (debug+asan) to $(INSTALL_BIN) and $(INSTALL_LIB)"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "quickjs.h"
|
||||
#include "miniz.h"
|
||||
#include "cell.h"
|
||||
#include "miniz.h"
|
||||
|
||||
static JSClassID js_reader_class_id;
|
||||
static JSClassID js_writer_class_id;
|
||||
@@ -319,7 +318,6 @@ JSValue js_reader_list(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
|
||||
JSValue filename = JS_NewString(js, file_stat.m_filename);
|
||||
if (JS_IsException(filename)) {
|
||||
JS_FreeValue(js, arr);
|
||||
return filename;
|
||||
}
|
||||
JS_SetPropertyNumber(js, arr, arr_index++, filename);
|
||||
|
||||
162
audit.ce
162
audit.ce
@@ -4,90 +4,146 @@
|
||||
// cell audit Audit all packages
|
||||
// cell audit <locator> Audit specific package
|
||||
// cell audit . Audit current directory package
|
||||
// cell audit --function-hoist [<locator>] Report function hoisting usage
|
||||
//
|
||||
// Compiles every script in the package(s) to check for errors.
|
||||
// Continues past failures and reports all issues at the end.
|
||||
|
||||
var shop = use('internal/shop')
|
||||
var pkg = use('package')
|
||||
var fd = use('fd')
|
||||
|
||||
var target_package = null
|
||||
var function_hoist = false
|
||||
var i = 0
|
||||
|
||||
var run = function() {
|
||||
var packages = null
|
||||
var tokenize_mod = null
|
||||
var parse_mod = null
|
||||
var hoist_files = 0
|
||||
var hoist_refs = 0
|
||||
var total_ok = 0
|
||||
var total_errors = 0
|
||||
var total_scripts = 0
|
||||
var all_failures = []
|
||||
var all_unresolved = []
|
||||
var summary = null
|
||||
|
||||
for (i = 0; i < length(args); i++) {
|
||||
if (args[i] == '--help' || args[i] == '-h') {
|
||||
log.console("Usage: cell audit [<locator>]")
|
||||
log.console("Usage: cell audit [--function-hoist] [<locator>]")
|
||||
log.console("")
|
||||
log.console("Test-compile all .ce and .cm scripts in package(s).")
|
||||
log.console("Reports all errors without stopping at the first failure.")
|
||||
log.console("")
|
||||
log.console("Flags:")
|
||||
log.console(" --function-hoist Report files that rely on function hoisting")
|
||||
return
|
||||
} else if (args[i] == '--function-hoist') {
|
||||
function_hoist = true
|
||||
} else if (!starts_with(args[i], '-')) {
|
||||
target_package = args[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve local paths
|
||||
if (target_package) {
|
||||
target_package = shop.resolve_locator(target_package)
|
||||
}
|
||||
// Resolve local paths
|
||||
if (target_package) {
|
||||
target_package = shop.resolve_locator(target_package)
|
||||
}
|
||||
|
||||
var packages = null
|
||||
var total_ok = 0
|
||||
var total_errors = 0
|
||||
var total_scripts = 0
|
||||
var all_failures = []
|
||||
var all_unresolved = []
|
||||
if (target_package) {
|
||||
packages = [target_package]
|
||||
} else {
|
||||
packages = shop.list_packages()
|
||||
}
|
||||
|
||||
if (target_package) {
|
||||
packages = [target_package]
|
||||
} else {
|
||||
packages = shop.list_packages()
|
||||
}
|
||||
if (function_hoist) {
|
||||
tokenize_mod = use('tokenize')
|
||||
parse_mod = use('parse')
|
||||
|
||||
arrfor(packages, function(p) {
|
||||
var scripts = shop.get_package_scripts(p)
|
||||
if (length(scripts) == 0) return
|
||||
arrfor(packages, function(p) {
|
||||
var scripts = shop.get_package_scripts(p)
|
||||
var pkg_dir = shop.get_package_dir(p)
|
||||
if (length(scripts) == 0) return
|
||||
|
||||
log.console("Auditing " + p + " (" + text(length(scripts)) + " scripts)...")
|
||||
var result = shop.build_package_scripts(p)
|
||||
total_ok = total_ok + result.ok
|
||||
total_errors = total_errors + length(result.errors)
|
||||
total_scripts = total_scripts + result.total
|
||||
arrfor(scripts, function(script) {
|
||||
var src_path = pkg_dir + '/' + script
|
||||
var src = null
|
||||
var tok_result = null
|
||||
var ast = null
|
||||
var scan = function() {
|
||||
if (!fd.is_file(src_path)) return
|
||||
src = text(fd.slurp(src_path))
|
||||
tok_result = tokenize_mod(src, script)
|
||||
ast = parse_mod(tok_result.tokens, src, script, tokenize_mod)
|
||||
if (ast._hoisted_fns != null && length(ast._hoisted_fns) > 0) {
|
||||
log.console(p + '/' + script + ":")
|
||||
hoist_files = hoist_files + 1
|
||||
arrfor(ast._hoisted_fns, function(ref) {
|
||||
var msg = " " + ref.name
|
||||
if (ref.line != null) msg = msg + " (ref line " + text(ref.line)
|
||||
if (ref.decl_line != null) msg = msg + ", declared line " + text(ref.decl_line)
|
||||
if (ref.line != null) msg = msg + ")"
|
||||
log.console(msg)
|
||||
hoist_refs = hoist_refs + 1
|
||||
})
|
||||
}
|
||||
} disruption {
|
||||
// skip files that fail to parse
|
||||
}
|
||||
scan()
|
||||
})
|
||||
})
|
||||
|
||||
arrfor(result.errors, function(e) {
|
||||
push(all_failures, p + ": " + e)
|
||||
log.console("")
|
||||
log.console("Summary: " + text(hoist_files) + " files with function hoisting, " + text(hoist_refs) + " total forward references")
|
||||
return
|
||||
}
|
||||
|
||||
arrfor(packages, function(p) {
|
||||
var scripts = shop.get_package_scripts(p)
|
||||
var result = null
|
||||
var resolution = null
|
||||
if (length(scripts) == 0) return
|
||||
|
||||
log.console("Auditing " + p + " (" + text(length(scripts)) + " scripts)...")
|
||||
result = shop.build_package_scripts(p)
|
||||
total_ok = total_ok + result.ok
|
||||
total_errors = total_errors + length(result.errors)
|
||||
total_scripts = total_scripts + result.total
|
||||
|
||||
arrfor(result.errors, function(e) {
|
||||
push(all_failures, p + ": " + e)
|
||||
})
|
||||
|
||||
// Check use() resolution
|
||||
resolution = shop.audit_use_resolution(p)
|
||||
arrfor(resolution.unresolved, function(u) {
|
||||
push(all_unresolved, p + '/' + u.script + ": use('" + u.module + "') cannot be resolved")
|
||||
})
|
||||
})
|
||||
|
||||
// Check use() resolution
|
||||
var resolution = shop.audit_use_resolution(p)
|
||||
arrfor(resolution.unresolved, function(u) {
|
||||
push(all_unresolved, p + '/' + u.script + ": use('" + u.module + "') cannot be resolved")
|
||||
})
|
||||
})
|
||||
|
||||
log.console("")
|
||||
if (length(all_failures) > 0) {
|
||||
log.console("Failed scripts:")
|
||||
arrfor(all_failures, function(f) {
|
||||
log.console(" " + f)
|
||||
})
|
||||
log.console("")
|
||||
}
|
||||
if (length(all_failures) > 0) {
|
||||
log.console("Failed scripts:")
|
||||
arrfor(all_failures, function(f) {
|
||||
log.console(" " + f)
|
||||
})
|
||||
log.console("")
|
||||
}
|
||||
|
||||
if (length(all_unresolved) > 0) {
|
||||
log.console("Unresolved modules:")
|
||||
arrfor(all_unresolved, function(u) {
|
||||
log.console(" " + u)
|
||||
})
|
||||
log.console("")
|
||||
}
|
||||
if (length(all_unresolved) > 0) {
|
||||
log.console("Unresolved modules:")
|
||||
arrfor(all_unresolved, function(u) {
|
||||
log.console(" " + u)
|
||||
})
|
||||
log.console("")
|
||||
}
|
||||
|
||||
var summary = "Audit complete: " + text(total_ok) + "/" + text(total_scripts) + " scripts compiled"
|
||||
if (total_errors > 0) summary = summary + ", " + text(total_errors) + " failed"
|
||||
if (length(all_unresolved) > 0) summary = summary + ", " + text(length(all_unresolved)) + " unresolved use() calls"
|
||||
log.console(summary)
|
||||
summary = "Audit complete: " + text(total_ok) + "/" + text(total_scripts) + " scripts compiled"
|
||||
if (total_errors > 0) summary = summary + ", " + text(total_errors) + " failed"
|
||||
if (length(all_unresolved) > 0) summary = summary + ", " + text(length(all_unresolved)) + " unresolved use() calls"
|
||||
log.console(summary)
|
||||
}
|
||||
run()
|
||||
|
||||
$stop()
|
||||
|
||||
194
bench_native.ce
194
bench_native.ce
@@ -1,194 +0,0 @@
|
||||
// bench_native.ce — compare VM vs native execution speed
|
||||
//
|
||||
// Usage:
|
||||
// cell --dev bench_native.ce <module.cm> [iterations]
|
||||
//
|
||||
// Compiles (if needed) and benchmarks a module via both VM and native dylib.
|
||||
// Reports median/mean timing per benchmark + speedup ratio.
|
||||
|
||||
var os = use('internal/os')
|
||||
var fd = use('fd')
|
||||
|
||||
if (length(args) < 1) {
|
||||
log.bench('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 ---
|
||||
|
||||
log.bench('loading VM module: ' + file)
|
||||
var vm_mod = use(name)
|
||||
var vm_benches = collect_benches(vm_mod)
|
||||
|
||||
if (length(vm_benches) == 0) {
|
||||
log.bench('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) {
|
||||
log.bench('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 {
|
||||
log.bench('no ' + dylib_path + ' found -- VM-only benchmarking')
|
||||
log.bench(' hint: cell --dev compile.ce ' + file)
|
||||
}
|
||||
|
||||
// --- Run benchmarks ---
|
||||
|
||||
log.bench('')
|
||||
log.bench('samples: ' + text(iterations) + ' (warmup: ' + text(WARMUP) + ')')
|
||||
log.bench('')
|
||||
|
||||
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')
|
||||
|
||||
log.bench(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')
|
||||
log.bench(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
|
||||
log.bench(pad('', 20) + ' speedup: ' + text(round(speedup * 100) / 100) + 'x')
|
||||
}
|
||||
found = true
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
|
||||
if (has_native && !found) {
|
||||
log.bench(pad('', 20) + ' NT: (no matching function)')
|
||||
}
|
||||
|
||||
log.bench('')
|
||||
i = i + 1
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run hyperfine with parameter lists
|
||||
# This will create a cross-product of all libraries × all scenarios
|
||||
hyperfine \
|
||||
--warmup 3 \
|
||||
--runs 20 \
|
||||
-i \
|
||||
--export-csv wota_vs_nota_vs_json.csv \
|
||||
--export-json wota_vs_nota_vs_json.json \
|
||||
--export-markdown wota_vs_nota_vs_json.md \
|
||||
--parameter-list lib wota,nota,json \
|
||||
--parameter-list scen empty,integers,floats,strings,objects,nested,large_array \
|
||||
'cell benchmarks/wota_nota_json {lib} {scen}'
|
||||
|
||||
|
||||
echo "Benchmark complete! Results saved to:"
|
||||
echo " - wota_vs_nota_vs_json.csv"
|
||||
echo " - wota_vs_nota_vs_json.json"
|
||||
echo " - wota_vs_nota_vs_json.md"
|
||||
2132
benchmarks/nota.json
2132
benchmarks/nota.json
File diff suppressed because it is too large
Load Diff
192
boot.ce
Normal file
192
boot.ce
Normal file
@@ -0,0 +1,192 @@
|
||||
// cell boot [--native] <program> - Pre-compile all module dependencies in parallel
|
||||
//
|
||||
// Discovers all transitive module dependencies for a program,
|
||||
// checks which are not yet cached, and compiles uncached ones
|
||||
// in parallel using worker actors composed via parallel() requestors.
|
||||
//
|
||||
// Also used as a child actor by engine.cm for auto-boot.
|
||||
|
||||
var shop = use('internal/shop')
|
||||
var fd = use('fd')
|
||||
var pkg_tools = use('package')
|
||||
var build = use('build')
|
||||
|
||||
var is_native = false
|
||||
var target_prog = null
|
||||
var target_pkg = null
|
||||
var i = 0
|
||||
|
||||
// Child actor mode: receive message from engine.cm
|
||||
var _child_mode = false
|
||||
var run_boot = null
|
||||
|
||||
$receiver(function(msg) {
|
||||
_child_mode = true
|
||||
is_native = msg.native || false
|
||||
target_prog = msg.program
|
||||
target_pkg = msg.package
|
||||
run_boot()
|
||||
})
|
||||
|
||||
// CLI mode: parse arguments
|
||||
if (args && length(args) > 0) {
|
||||
for (i = 0; i < length(args); i = i + 1) {
|
||||
if (args[i] == '--native') {
|
||||
is_native = true
|
||||
} else if (args[i] == '--help' || args[i] == '-h') {
|
||||
log.console("Usage: cell boot [--native] <program>")
|
||||
log.console("")
|
||||
log.console("Pre-compile all module dependencies for a program.")
|
||||
log.console("Uncached modules are compiled in parallel.")
|
||||
$stop()
|
||||
} else if (!starts_with(args[i], '-')) {
|
||||
target_prog = args[i]
|
||||
}
|
||||
}
|
||||
if (!target_prog) {
|
||||
log.error("boot: no program specified")
|
||||
$stop()
|
||||
}
|
||||
}
|
||||
|
||||
// Discover all transitive module dependencies for a file
|
||||
function discover_deps(file_path) {
|
||||
return shop.trace_deps(file_path)
|
||||
}
|
||||
|
||||
// Filter out already-cached modules
|
||||
function filter_uncached(deps) {
|
||||
var uncached = []
|
||||
var j = 0
|
||||
var s = null
|
||||
|
||||
j = 0
|
||||
while (j < length(deps.scripts)) {
|
||||
s = deps.scripts[j]
|
||||
if (is_native) {
|
||||
if (!shop.is_native_cached(s.path, s.package)) {
|
||||
uncached[] = {type: 'native_script', path: s.path, package: s.package}
|
||||
}
|
||||
} else {
|
||||
if (!shop.is_cached(s.path)) {
|
||||
uncached[] = {type: 'script', path: s.path, package: s.package}
|
||||
}
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
|
||||
// Expand C packages into individual files for parallel compilation
|
||||
var target = build.detect_host_target()
|
||||
var pkg = null
|
||||
var c_files = null
|
||||
var k = 0
|
||||
j = 0
|
||||
while (j < length(deps.c_packages)) {
|
||||
pkg = deps.c_packages[j]
|
||||
if (pkg != 'core') {
|
||||
c_files = pkg_tools.get_c_files(pkg, target, true)
|
||||
k = 0
|
||||
while (k < length(c_files)) {
|
||||
uncached[] = {type: 'c_file', package: pkg, file: c_files[k]}
|
||||
k = k + 1
|
||||
}
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
|
||||
return uncached
|
||||
}
|
||||
|
||||
function item_name(item) {
|
||||
if (item.path) return item.path
|
||||
if (item.file) return item.package + '/' + item.file
|
||||
return item.package
|
||||
}
|
||||
|
||||
// Create a requestor that spawns a compile_worker actor for one item
|
||||
function make_compile_requestor(item) {
|
||||
var worker = null
|
||||
var name = item_name(item)
|
||||
return function(callback, value) {
|
||||
log.console('boot: spawning worker for ' + name)
|
||||
$start(function(event) {
|
||||
if (event.type == 'greet') {
|
||||
worker = event.actor
|
||||
send(event.actor, {
|
||||
type: item.type,
|
||||
path: item.path,
|
||||
package: item.package,
|
||||
file: item.file
|
||||
})
|
||||
}
|
||||
if (event.type == 'stop') {
|
||||
callback(name)
|
||||
}
|
||||
if (event.type == 'disrupt') {
|
||||
log.error('boot: worker failed for ' + name)
|
||||
callback(null, {message: 'compile failed: ' + name})
|
||||
}
|
||||
}, 'compile_worker')
|
||||
return function cancel(reason) {
|
||||
if (worker) $stop(worker)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run_boot = function() {
|
||||
var prog_path = null
|
||||
var prog_info = null
|
||||
var deps = null
|
||||
var uncached = null
|
||||
var requestors = null
|
||||
var p = null
|
||||
|
||||
// Resolve the program path
|
||||
if (target_prog) {
|
||||
p = target_prog
|
||||
if (ends_with(p, '.ce')) p = text(p, 0, -3)
|
||||
prog_info = shop.resolve_program ? shop.resolve_program(p, target_pkg) : null
|
||||
if (prog_info) {
|
||||
prog_path = prog_info.path
|
||||
if (!target_pkg && prog_info.pkg) target_pkg = prog_info.pkg
|
||||
} else {
|
||||
prog_path = p + '.ce'
|
||||
if (!fd.is_file(prog_path)) {
|
||||
prog_path = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!prog_path || !fd.is_file(prog_path)) {
|
||||
log.error('boot: could not find program: ' + text(target_prog || ''))
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
|
||||
// Discover all transitive deps
|
||||
deps = discover_deps(prog_path)
|
||||
uncached = filter_uncached(deps)
|
||||
|
||||
if (length(uncached) == 0) {
|
||||
log.console('boot: all modules cached')
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
|
||||
// Compile uncached modules in parallel using worker actors
|
||||
log.console('boot: ' + text(length(uncached)) + ' modules to compile')
|
||||
requestors = array(uncached, make_compile_requestor)
|
||||
parallel(requestors)(function(results, reason) {
|
||||
if (reason) {
|
||||
log.error('boot: ' + (reason.message || text(reason)))
|
||||
} else {
|
||||
log.console('boot: compiled ' + text(length(results)) + ' modules')
|
||||
}
|
||||
$stop()
|
||||
}, null)
|
||||
}
|
||||
|
||||
// CLI mode: start immediately
|
||||
if (!_child_mode && target_prog) {
|
||||
run_boot()
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
8828
boot/fold.cm.mcode
8828
boot/fold.cm.mcode
File diff suppressed because one or more lines are too long
25127
boot/mcode.cm.mcode
25127
boot/mcode.cm.mcode
File diff suppressed because one or more lines are too long
13804
boot/parse.cm.mcode
13804
boot/parse.cm.mcode
File diff suppressed because one or more lines are too long
2764
boot/qbe.cm.mcode
Normal file
2764
boot/qbe.cm.mcode
Normal file
File diff suppressed because it is too large
Load Diff
34967
boot/qbe_emit.cm.mcode
Normal file
34967
boot/qbe_emit.cm.mcode
Normal file
File diff suppressed because one or more lines are too long
27810
boot/streamline.cm.mcode
27810
boot/streamline.cm.mcode
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
79
build.cm
79
build.cm
@@ -464,7 +464,7 @@ Build.compile_file = function(pkg, file, target, opts) {
|
||||
|
||||
// Compile
|
||||
log.shop('compiling ' + file)
|
||||
log.console('Compiling ' + file)
|
||||
log.build('Compiling ' + file)
|
||||
err_path = '/tmp/cell_build_err_' + content_hash(setup.src_path) + '.log'
|
||||
full_cmd = setup.cmd_str + ' -o "' + obj_path + '" 2>"' + err_path + '"'
|
||||
ret = os.system(full_cmd)
|
||||
@@ -603,6 +603,8 @@ Build.build_module_dylib = function(pkg, file, target, opts) {
|
||||
var post_probe = null
|
||||
var fallback_probe = null
|
||||
var _fail_msg2 = null
|
||||
var link_err_path = null
|
||||
var link_err_text = null
|
||||
|
||||
if (probe && probe.fail) {
|
||||
_fail_msg2 = probe.fail_path ? text(fd.slurp(probe.fail_path)) : null
|
||||
@@ -697,10 +699,16 @@ Build.build_module_dylib = function(pkg, file, target, opts) {
|
||||
cmd_str = text(cmd_parts, ' ')
|
||||
if (_opts.verbose) log.build('[verbose] link: ' + cmd_str)
|
||||
log.shop('linking ' + file)
|
||||
log.console('Linking module ' + file + ' -> ' + fd.basename(dylib_path))
|
||||
ret = os.system(cmd_str)
|
||||
log.build('Linking module ' + file + ' -> ' + fd.basename(dylib_path))
|
||||
link_err_path = '/tmp/cell_link_err_' + content_hash(file) + '.log'
|
||||
ret = os.system(cmd_str + ' 2>"' + link_err_path + '"')
|
||||
if (ret != 0) {
|
||||
log.error('Linking failed: ' + file)
|
||||
if (fd.is_file(link_err_path))
|
||||
link_err_text = text(fd.slurp(link_err_path))
|
||||
if (link_err_text)
|
||||
log.error('Linking failed: ' + file + '\n' + link_err_text)
|
||||
else
|
||||
log.error('Linking failed: ' + file)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -714,6 +722,28 @@ Build.build_module_dylib = function(pkg, file, target, opts) {
|
||||
return dylib_path
|
||||
}
|
||||
|
||||
// Compile a single C module file for a package (support objects + one dylib).
|
||||
// Used by parallel boot workers. No manifest writing — the runtime handles that.
|
||||
Build.compile_c_module = function(pkg, file, target, opts) {
|
||||
var _target = target || Build.detect_host_target()
|
||||
var _opts = opts || {}
|
||||
var _buildtype = _opts.buildtype || 'release'
|
||||
var pkg_dir = shop.get_package_dir(pkg)
|
||||
var cached_cflags = replace_sigils_array(pkg_tools.get_flags(pkg, 'CFLAGS', _target), pkg_dir)
|
||||
|
||||
// Compile support sources to cached objects (content-addressed, safe for concurrent workers)
|
||||
var sources = pkg_tools.get_sources(pkg)
|
||||
var support_objects = []
|
||||
if (pkg != 'core') {
|
||||
arrfor(sources, function(src_file) {
|
||||
var obj = Build.compile_file(pkg, src_file, _target, {buildtype: _buildtype, cflags: cached_cflags})
|
||||
if (obj != null) push(support_objects, obj)
|
||||
})
|
||||
}
|
||||
|
||||
return Build.build_module_dylib(pkg, file, _target, {buildtype: _buildtype, extra_objects: support_objects, cflags: cached_cflags})
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -723,6 +753,9 @@ Build.build_dynamic = function(pkg, target, buildtype, opts) {
|
||||
var _opts = opts || {}
|
||||
var c_files = pkg_tools.get_c_files(pkg, _target, true)
|
||||
var results = []
|
||||
var total = length(c_files)
|
||||
var done = 0
|
||||
var failed = 0
|
||||
|
||||
// Pre-fetch cflags once to avoid repeated TOML reads
|
||||
var pkg_dir = shop.get_package_dir(pkg)
|
||||
@@ -738,14 +771,24 @@ Build.build_dynamic = function(pkg, target, buildtype, opts) {
|
||||
})
|
||||
}
|
||||
|
||||
if (total > 0)
|
||||
os.print(' Building C modules ')
|
||||
|
||||
arrfor(c_files, function(file) {
|
||||
var sym_name = shop.c_symbol_for_file(pkg, file)
|
||||
var dylib = Build.build_module_dylib(pkg, file, _target, {buildtype: _buildtype, extra_objects: support_objects, cflags: cached_cflags, verbose: _opts.verbose, force: _opts.force})
|
||||
if (dylib) {
|
||||
push(results, {file: file, symbol: sym_name, dylib: dylib})
|
||||
} else {
|
||||
failed = failed + 1
|
||||
}
|
||||
done = done + 1
|
||||
os.print('.')
|
||||
})
|
||||
|
||||
if (total > 0)
|
||||
os.print(` ${text(done)}/${text(total)}${failed > 0 ? `, ${text(failed)} failed` : ''}\n`)
|
||||
|
||||
// Write manifest so runtime can find dylibs without the build module
|
||||
var mpath = manifest_path(pkg)
|
||||
fd.slurpwrite(mpath, stone(blob(json.encode(results))))
|
||||
@@ -920,9 +963,17 @@ Build.compile_native = function(src_path, target, buildtype, pkg) {
|
||||
var qbe_emit = use('qbe_emit')
|
||||
|
||||
// Step 2: Generate QBE IL
|
||||
// Derive package from the file itself (not the caller's context) to ensure correct symbol names
|
||||
var sym_name = null
|
||||
if (pkg) {
|
||||
sym_name = shop.c_symbol_for_file(pkg, fd.basename(src_path))
|
||||
var _file_info = shop.file_info(src_path)
|
||||
var _actual_pkg = _file_info.package || pkg
|
||||
var _sym_stem = null
|
||||
if (_actual_pkg) {
|
||||
if (_file_info.name)
|
||||
_sym_stem = _file_info.name + (_file_info.is_actor ? '.ce' : '.cm')
|
||||
else
|
||||
_sym_stem = fd.basename(src_path)
|
||||
sym_name = shop.c_symbol_for_file(_actual_pkg, _sym_stem)
|
||||
}
|
||||
var il_parts = qbe_emit(optimized, qbe_macros, sym_name)
|
||||
|
||||
@@ -971,7 +1022,7 @@ Build.compile_native = function(src_path, target, buildtype, pkg) {
|
||||
log.error('Linking native dylib failed for: ' + src_path); disrupt
|
||||
}
|
||||
|
||||
log.console('Built native: ' + fd.basename(dylib_path))
|
||||
log.shop('compiled native: ' + src_path)
|
||||
|
||||
return dylib_path
|
||||
}
|
||||
@@ -993,9 +1044,17 @@ Build.compile_native_ir = function(optimized, src_path, opts) {
|
||||
var qbe_macros = use('qbe')
|
||||
var qbe_emit = use('qbe_emit')
|
||||
|
||||
// Derive package from the file itself (not the caller's context)
|
||||
var sym_name = null
|
||||
if (pkg) {
|
||||
sym_name = shop.c_symbol_for_file(pkg, fd.basename(src_path))
|
||||
var _file_info2 = shop.file_info(src_path)
|
||||
var _actual_pkg2 = _file_info2.package || pkg
|
||||
var _sym_stem2 = null
|
||||
if (_actual_pkg2) {
|
||||
if (_file_info2.name)
|
||||
_sym_stem2 = _file_info2.name + (_file_info2.is_actor ? '.ce' : '.cm')
|
||||
else
|
||||
_sym_stem2 = fd.basename(src_path)
|
||||
sym_name = shop.c_symbol_for_file(_actual_pkg2, _sym_stem2)
|
||||
}
|
||||
var il_parts = qbe_emit(optimized, qbe_macros, sym_name)
|
||||
|
||||
@@ -1043,7 +1102,7 @@ Build.compile_native_ir = function(optimized, src_path, opts) {
|
||||
log.error('Linking native dylib failed for: ' + src_path); disrupt
|
||||
}
|
||||
|
||||
log.console('Built native: ' + fd.basename(dylib_path))
|
||||
log.shop('compiled native: ' + src_path)
|
||||
|
||||
return dylib_path
|
||||
}
|
||||
|
||||
164
cellfs.cm
164
cellfs.cm
@@ -4,10 +4,11 @@ var fd = use('fd')
|
||||
var miniz = use('miniz')
|
||||
var qop = use('internal/qop')
|
||||
var wildstar = use('internal/wildstar')
|
||||
var blib = use('blob')
|
||||
|
||||
var mounts = []
|
||||
|
||||
var writepath = "."
|
||||
var write_mount = null
|
||||
|
||||
function normalize_path(path) {
|
||||
if (!path) return ""
|
||||
@@ -30,7 +31,7 @@ function mount_exists(mount, path) {
|
||||
result = mount.handle.stat(path) != null
|
||||
} disruption {}
|
||||
_check()
|
||||
} else {
|
||||
} else if (mount.type == 'fs') {
|
||||
full_path = fd.join_paths(mount.source, path)
|
||||
_check = function() {
|
||||
st = fd.stat(full_path)
|
||||
@@ -119,12 +120,12 @@ function resolve(path, must_exist) {
|
||||
}
|
||||
|
||||
function mount(source, name) {
|
||||
var st = fd.stat(source)
|
||||
var st = null
|
||||
var blob = null
|
||||
var qop_archive = null
|
||||
var zip = null
|
||||
|
||||
var _try_qop = null
|
||||
var http = null
|
||||
|
||||
var mount_info = {
|
||||
source: source,
|
||||
@@ -134,6 +135,29 @@ function mount(source, name) {
|
||||
zip_blob: null
|
||||
}
|
||||
|
||||
if (starts_with(source, 'http://') || starts_with(source, 'https://')) {
|
||||
http = use('http')
|
||||
mount_info.type = 'http'
|
||||
mount_info.handle = {
|
||||
base_url: source,
|
||||
get: function(path, callback) {
|
||||
var url = source + '/' + path
|
||||
$clock(function(_t) {
|
||||
var resp = http.request('GET', url, null, null)
|
||||
if (resp && resp.status == 200) {
|
||||
callback(resp.body)
|
||||
} else {
|
||||
callback(null, "HTTP " + text(resp ? resp.status : 0) + ": " + url)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
push(mounts, mount_info)
|
||||
return
|
||||
}
|
||||
|
||||
st = fd.stat(source)
|
||||
|
||||
if (st.isDirectory) {
|
||||
mount_info.type = 'fs'
|
||||
} else if (st.isFile) {
|
||||
@@ -146,18 +170,18 @@ function mount(source, name) {
|
||||
_try_qop()
|
||||
|
||||
if (qop_archive) {
|
||||
mount_info.type = 'qop'
|
||||
mount_info.handle = qop_archive
|
||||
mount_info.zip_blob = blob
|
||||
mount_info.type = 'qop'
|
||||
mount_info.handle = qop_archive
|
||||
mount_info.zip_blob = blob
|
||||
} else {
|
||||
zip = miniz.read(blob)
|
||||
if (!is_object(zip) || !is_function(zip.count)) {
|
||||
log.error("Invalid archive file (not zip or qop): " + source); disrupt
|
||||
}
|
||||
zip = miniz.read(blob)
|
||||
if (!is_object(zip) || !is_function(zip.count)) {
|
||||
log.error("Invalid archive file (not zip or qop): " + source); disrupt
|
||||
}
|
||||
|
||||
mount_info.type = 'zip'
|
||||
mount_info.handle = zip
|
||||
mount_info.zip_blob = blob
|
||||
mount_info.type = 'zip'
|
||||
mount_info.handle = zip
|
||||
mount_info.zip_blob = blob
|
||||
}
|
||||
} else {
|
||||
log.error("Unsupported mount source type: " + source); disrupt
|
||||
@@ -191,11 +215,13 @@ function slurp(path) {
|
||||
}
|
||||
|
||||
function slurpwrite(path, data) {
|
||||
var full_path = writepath + "/" + path
|
||||
|
||||
var f = fd.open(full_path, 'w')
|
||||
fd.write(f, data)
|
||||
fd.close(f)
|
||||
var full_path = null
|
||||
if (write_mount) {
|
||||
full_path = fd.join_paths(write_mount.source, path)
|
||||
} else {
|
||||
full_path = fd.join_paths(".", path)
|
||||
}
|
||||
fd.slurpwrite(full_path, data)
|
||||
}
|
||||
|
||||
function exists(path) {
|
||||
@@ -276,12 +302,25 @@ function rm(path) {
|
||||
}
|
||||
|
||||
function mkdir(path) {
|
||||
var full = fd.join_paths(writepath, path)
|
||||
var full = null
|
||||
if (write_mount) {
|
||||
full = fd.join_paths(write_mount.source, path)
|
||||
} else {
|
||||
full = fd.join_paths(".", path)
|
||||
}
|
||||
fd.mkdir(full)
|
||||
}
|
||||
|
||||
function set_writepath(path) {
|
||||
writepath = path
|
||||
function set_writepath(mount_name) {
|
||||
var found = null
|
||||
if (mount_name == null) { write_mount = null; return }
|
||||
arrfor(mounts, function(m) {
|
||||
if (m.name == mount_name) { found = m; return true }
|
||||
}, false, true)
|
||||
if (!found || found.type != 'fs') {
|
||||
log.error("writepath: must be an fs mount"); disrupt
|
||||
}
|
||||
write_mount = found
|
||||
}
|
||||
|
||||
function basedir() {
|
||||
@@ -449,6 +488,82 @@ function globfs(globs, _dir) {
|
||||
return results
|
||||
}
|
||||
|
||||
// Requestor factory: returns a requestor for reading a file at path
|
||||
function get(path) {
|
||||
return function get_requestor(callback, value) {
|
||||
var res = resolve(path, false)
|
||||
var full = null
|
||||
var f = null
|
||||
var acc = null
|
||||
var cancelled = false
|
||||
var data = null
|
||||
var _close = null
|
||||
|
||||
if (!res) { callback(null, "not found: " + path); return }
|
||||
|
||||
if (res.mount.type == 'zip') {
|
||||
callback(res.mount.handle.slurp(res.path))
|
||||
return
|
||||
}
|
||||
if (res.mount.type == 'qop') {
|
||||
data = res.mount.handle.read(res.path)
|
||||
if (data) {
|
||||
callback(data)
|
||||
} else {
|
||||
callback(null, "not found in qop: " + path)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (res.mount.type == 'http') {
|
||||
res.mount.handle.get(res.path, callback)
|
||||
return
|
||||
}
|
||||
|
||||
full = fd.join_paths(res.mount.source, res.path)
|
||||
f = fd.open(full, 'r')
|
||||
acc = blob()
|
||||
|
||||
function next(_t) {
|
||||
var chunk = null
|
||||
if (cancelled) return
|
||||
chunk = fd.read(f, 65536)
|
||||
if (length(chunk) == 0) {
|
||||
fd.close(f)
|
||||
stone(acc)
|
||||
callback(acc)
|
||||
return
|
||||
}
|
||||
acc.write_blob(chunk)
|
||||
$clock(next)
|
||||
}
|
||||
|
||||
next()
|
||||
return function cancel() {
|
||||
cancelled = true
|
||||
_close = function() { fd.close(f) } disruption {}
|
||||
_close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Requestor factory: returns a requestor for writing data to path
|
||||
function put(path, data) {
|
||||
return function put_requestor(callback, value) {
|
||||
var _data = data != null ? data : value
|
||||
var full = null
|
||||
var _do = null
|
||||
if (!write_mount) { callback(null, "no write mount set"); return }
|
||||
full = fd.join_paths(write_mount.source, path)
|
||||
_do = function() {
|
||||
fd.slurpwrite(full, _data)
|
||||
callback(true)
|
||||
} disruption {
|
||||
callback(null, "write failed: " + path)
|
||||
}
|
||||
_do()
|
||||
}
|
||||
}
|
||||
|
||||
cellfs.mount = mount
|
||||
cellfs.mount_package = mount_package
|
||||
cellfs.unmount = unmount
|
||||
@@ -467,7 +582,8 @@ cellfs.writepath = set_writepath
|
||||
cellfs.basedir = basedir
|
||||
cellfs.prefdir = prefdir
|
||||
cellfs.realdir = realdir
|
||||
|
||||
cellfs.mount('.')
|
||||
cellfs.get = get
|
||||
cellfs.put = put
|
||||
cellfs.resolve = resolve
|
||||
|
||||
return cellfs
|
||||
|
||||
40
compile_worker.ce
Normal file
40
compile_worker.ce
Normal file
@@ -0,0 +1,40 @@
|
||||
// compile_worker - Worker actor that compiles a single module and replies
|
||||
//
|
||||
// Receives a message with:
|
||||
// {type: 'script', path, package} — bytecode compile
|
||||
// {type: 'native_script', path, package} — native compile
|
||||
// {type: 'c_package', package} — C package build
|
||||
// {type: 'c_file', package, file} — single C module build
|
||||
//
|
||||
// Replies with {ok: true/false, path} and stops.
|
||||
|
||||
var shop = use('internal/shop')
|
||||
var build = use('build')
|
||||
|
||||
$receiver(function(msg) {
|
||||
var name = msg.path || (msg.file ? msg.package + '/' + msg.file : msg.package)
|
||||
var _work = function() {
|
||||
if (msg.type == 'script') {
|
||||
log.console('compile_worker: compiling ' + name)
|
||||
shop.precompile(msg.path, msg.package)
|
||||
} else if (msg.type == 'native_script') {
|
||||
log.console('compile_worker: native compiling ' + name)
|
||||
build.compile_native(msg.path, null, null, msg.package)
|
||||
} else if (msg.type == 'c_package') {
|
||||
log.console('compile_worker: building package ' + name)
|
||||
build.build_dynamic(msg.package, null, null, null)
|
||||
} else if (msg.type == 'c_file') {
|
||||
log.console('compile_worker: building ' + name)
|
||||
build.compile_c_module(msg.package, msg.file)
|
||||
}
|
||||
log.console('compile_worker: done ' + name)
|
||||
send(msg, {ok: true, path: name})
|
||||
} disruption {
|
||||
log.error('compile_worker: failed ' + name)
|
||||
send(msg, {ok: false, error: 'compile failed'})
|
||||
}
|
||||
_work()
|
||||
$stop()
|
||||
})
|
||||
|
||||
var _t = $delay($stop, 120)
|
||||
12
cross/mingw64.ini
Normal file
12
cross/mingw64.ini
Normal file
@@ -0,0 +1,12 @@
|
||||
[binaries]
|
||||
c = 'x86_64-w64-mingw32-gcc'
|
||||
cpp = 'x86_64-w64-mingw32-g++'
|
||||
ar = 'x86_64-w64-mingw32-ar'
|
||||
strip = 'x86_64-w64-mingw32-strip'
|
||||
windres = 'x86_64-w64-mingw32-windres'
|
||||
|
||||
[host_machine]
|
||||
system = 'windows'
|
||||
cpu_family = 'x86_64'
|
||||
cpu = 'x86_64'
|
||||
endian = 'little'
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "cell.h"
|
||||
#include "pit_internal.h"
|
||||
|
||||
JSC_CCALL(os_mem_limit, JS_SetMemoryLimit(JS_GetRuntime(js), js2number(js,argv[0])))
|
||||
JSC_CCALL(os_max_stacksize, JS_SetMaxStackSize(js, js2number(js,argv[0])))
|
||||
|
||||
233
docs/library/probe.md
Normal file
233
docs/library/probe.md
Normal file
@@ -0,0 +1,233 @@
|
||||
---
|
||||
title: "probe"
|
||||
description: "Runtime observability for actors"
|
||||
weight: 90
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
Runtime observability for actors. Register named probe functions on any actor and query them over HTTP while the program runs.
|
||||
|
||||
```javascript
|
||||
var probe = use('probe')
|
||||
```
|
||||
|
||||
The probe server starts automatically on the first `register()` call, listening on `127.0.0.1:9000`.
|
||||
|
||||
## Registering Probes
|
||||
|
||||
### probe.register(target, probes)
|
||||
|
||||
Register a group of probe functions under a target name. Each probe is a function that receives an `args` record and returns a value.
|
||||
|
||||
```javascript
|
||||
var probe = use('probe')
|
||||
|
||||
var world = {
|
||||
entities: [
|
||||
{id: 1, name: "player", x: 10, y: 20, hp: 100},
|
||||
{id: 2, name: "goblin", x: 55, y: 30, hp: 40}
|
||||
],
|
||||
tick: 0
|
||||
}
|
||||
|
||||
probe.register("game", {
|
||||
state: function(args) {
|
||||
return world
|
||||
},
|
||||
entities: function(args) {
|
||||
return world.entities
|
||||
},
|
||||
entity: function(args) {
|
||||
return find(world.entities, function(e) {
|
||||
return e.id == args.id
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
probe.register("render", {
|
||||
info: function(args) {
|
||||
return {fps: 60, draw_calls: 128, batches: 12}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
A target is just a namespace — group related probes under the same target name. Register as many targets as you like; the server starts once and serves them all.
|
||||
|
||||
## HTTP Endpoints
|
||||
|
||||
All responses are JSON with an `ok` field.
|
||||
|
||||
### GET /discover
|
||||
|
||||
Lists all registered targets and their probe names. Designed for tooling — an LLM or dashboard can call this first to learn what's available, then query specific probes.
|
||||
|
||||
```
|
||||
$ curl http://127.0.0.1:9000/discover
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"targets": {
|
||||
"game": ["state", "entities", "entity"],
|
||||
"render": ["info"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /probe
|
||||
|
||||
Call a single probe function by target and name. Optionally pass arguments.
|
||||
|
||||
```
|
||||
$ curl -X POST -H "Content-Type: application/json" \
|
||||
-d '{"target":"game","name":"state"}' \
|
||||
http://127.0.0.1:9000/probe
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"result": {
|
||||
"entities": [
|
||||
{"id": 1, "name": "player", "x": 10, "y": 20, "hp": 100},
|
||||
{"id": 2, "name": "goblin", "x": 55, "y": 30, "hp": 40}
|
||||
],
|
||||
"tick": 4821
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
With arguments:
|
||||
|
||||
```
|
||||
$ curl -X POST -H "Content-Type: application/json" \
|
||||
-d '{"target":"game","name":"entity","args":{"id":1}}' \
|
||||
http://127.0.0.1:9000/probe
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"result": {"id": 1, "name": "player", "x": 10, "y": 20, "hp": 100}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /snapshot
|
||||
|
||||
Call multiple probes in one request. Returns all results keyed by `target/name`.
|
||||
|
||||
```
|
||||
$ curl -X POST -H "Content-Type: application/json" \
|
||||
-d '{"probes":[{"target":"game","name":"state"},{"target":"render","name":"info"}]}' \
|
||||
http://127.0.0.1:9000/snapshot
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"results": {
|
||||
"game/state": {
|
||||
"entities": [
|
||||
{"id": 1, "name": "player", "x": 10, "y": 20, "hp": 100},
|
||||
{"id": 2, "name": "goblin", "x": 55, "y": 30, "hp": 40}
|
||||
],
|
||||
"tick": 4821
|
||||
},
|
||||
"render/info": {
|
||||
"fps": 60,
|
||||
"draw_calls": 128,
|
||||
"batches": 12
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Errors
|
||||
|
||||
Unknown paths return 404:
|
||||
|
||||
```json
|
||||
{"ok": false, "error": "not found"}
|
||||
```
|
||||
|
||||
Unknown targets or probe names:
|
||||
|
||||
```json
|
||||
{"ok": false, "error": "unknown probe: game/nonexistent"}
|
||||
```
|
||||
|
||||
If a probe function disrupts:
|
||||
|
||||
```json
|
||||
{"ok": false, "error": "probe failed"}
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
A game actor with a simulation loop and probe observability:
|
||||
|
||||
```javascript
|
||||
// game.ce
|
||||
var probe = use('probe')
|
||||
|
||||
var state = {
|
||||
entities: [
|
||||
{id: 1, name: "player", x: 0, y: 0, hp: 100},
|
||||
{id: 2, name: "enemy", x: 50, y: 50, hp: 60}
|
||||
],
|
||||
frame: 0,
|
||||
paused: false
|
||||
}
|
||||
|
||||
probe.register("game", {
|
||||
state: function(args) {
|
||||
return state
|
||||
},
|
||||
entities: function(args) {
|
||||
return state.entities
|
||||
},
|
||||
entity: function(args) {
|
||||
return find(state.entities, function(e) {
|
||||
return e.id == args.id
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// game loop
|
||||
def tick = function(_) {
|
||||
if (!state.paused) {
|
||||
state.frame = state.frame + 1
|
||||
// ... update entities, physics, AI ...
|
||||
}
|
||||
$delay(tick, 0.016)
|
||||
}
|
||||
$delay(tick, 0.016)
|
||||
```
|
||||
|
||||
While the game runs, query it from a terminal:
|
||||
|
||||
```
|
||||
$ curl -s http://127.0.0.1:9000/discover | jq .targets
|
||||
{
|
||||
"game": ["state", "entities", "entity"]
|
||||
}
|
||||
|
||||
$ curl -s -X POST -d '{"target":"game","name":"state"}' \
|
||||
-H "Content-Type: application/json" \
|
||||
http://127.0.0.1:9000/probe | jq .result.frame
|
||||
7834
|
||||
|
||||
$ curl -s -X POST -d '{"target":"game","name":"entity","args":{"id":1}}' \
|
||||
-H "Content-Type: application/json" \
|
||||
http://127.0.0.1:9000/probe | jq .result
|
||||
{
|
||||
"id": 1,
|
||||
"name": "player",
|
||||
"x": 142,
|
||||
"y": 87,
|
||||
"hp": 100
|
||||
}
|
||||
```
|
||||
|
||||
Probes run inside the actor's normal turn, so the values are always consistent — never a half-updated frame.
|
||||
@@ -9,13 +9,16 @@ Logging in ƿit is channel-based. Any `log.X(value)` call writes to channel `"X"
|
||||
|
||||
## Channels
|
||||
|
||||
Three channels are conventional:
|
||||
These channels are conventional:
|
||||
|
||||
| Channel | Usage |
|
||||
|---------|-------|
|
||||
| `log.console(msg)` | General output |
|
||||
| `log.error(msg)` | Errors and warnings |
|
||||
| `log.error(msg)` | Errors |
|
||||
| `log.warn(msg)` | Compiler diagnostics and warnings |
|
||||
| `log.system(msg)` | Internal system messages |
|
||||
| `log.build(msg)` | Per-file compile/link output |
|
||||
| `log.shop(msg)` | Package management internals |
|
||||
|
||||
Any name works. `log.debug(msg)` creates channel `"debug"`, `log.perf(msg)` creates `"perf"`, and so on.
|
||||
|
||||
@@ -29,16 +32,18 @@ Non-text values are JSON-encoded automatically.
|
||||
|
||||
## Default Behavior
|
||||
|
||||
With no configuration, a default sink routes `console`, `error`, and `system` to the terminal in pretty format. The `error` channel includes a stack trace by default:
|
||||
With no configuration, a default sink routes `console` and `error` to the terminal in clean format. The `error` channel includes a stack trace by default:
|
||||
|
||||
```
|
||||
[a3f12] [console] server started on port 8080
|
||||
[a3f12] [error] connection refused
|
||||
server started on port 8080
|
||||
error: connection refused
|
||||
at handle_request (server.ce:42:3)
|
||||
at main (main.ce:5:1)
|
||||
```
|
||||
|
||||
The format is `[actor_id] [channel] message`. Error stack traces are always on unless you explicitly configure a sink without them.
|
||||
Clean format prints messages without actor ID or channel prefix. Error messages are prefixed with `error:`. Other formats (`pretty`, `bare`) include actor IDs and are available for debugging. Stack traces are always on for errors unless you explicitly configure a sink without them.
|
||||
|
||||
Channels like `warn`, `build`, and `shop` are not routed to the terminal by default. Enable them with `pit log enable <channel>`.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -67,7 +72,7 @@ exclude = ["console"]
|
||||
| Field | Values | Description |
|
||||
|-------|--------|-------------|
|
||||
| `type` | `"console"`, `"file"` | Where output goes |
|
||||
| `format` | `"pretty"`, `"bare"`, `"json"` | How output is formatted |
|
||||
| `format` | `"clean"`, `"pretty"`, `"bare"`, `"json"` | How output is formatted |
|
||||
| `channels` | array of names, or `["*"]` | Which channels this sink receives. Quote `'*'` on the CLI to prevent shell glob expansion. |
|
||||
| `exclude` | array of names | Channels to skip (useful with `"*"`) |
|
||||
| `stack` | array of channel names | Channels that capture a stack trace |
|
||||
@@ -75,6 +80,13 @@ exclude = ["console"]
|
||||
|
||||
### Formats
|
||||
|
||||
**clean** — CLI-friendly. No actor ID or channel prefix. Error channel messages are prefixed with `error:`. This is the default format.
|
||||
|
||||
```
|
||||
server started on port 8080
|
||||
error: connection refused
|
||||
```
|
||||
|
||||
**pretty** — human-readable, one line per message. Includes actor ID, channel, source location, and message.
|
||||
|
||||
```
|
||||
@@ -158,7 +170,10 @@ The `pit log` command manages sinks and reads log files. See [CLI — pit log](/
|
||||
|
||||
```bash
|
||||
pit log list # show sinks
|
||||
pit log add terminal console --format=bare --channels=console
|
||||
pit log channels # list channels with enabled/disabled status
|
||||
pit log enable warn # enable a channel on the terminal sink
|
||||
pit log disable warn # disable a channel
|
||||
pit log add terminal console --format=clean --channels=console
|
||||
pit log add dump file .cell/logs/dump.jsonl '--channels=*' --exclude=console
|
||||
pit log add debug console --channels=error,debug --stack=error,debug
|
||||
pit log remove terminal
|
||||
@@ -166,6 +181,16 @@ pit log read dump --lines=20 --channel=error
|
||||
pit log tail dump
|
||||
```
|
||||
|
||||
### Channel toggling
|
||||
|
||||
The `enable` and `disable` commands modify the terminal sink's channel list without touching other sink configuration. This is the easiest way to opt in to extra output:
|
||||
|
||||
```bash
|
||||
pit log enable warn # see compiler warnings
|
||||
pit log enable build # see per-file compile/link output
|
||||
pit log disable warn # hide warnings again
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Development setup
|
||||
|
||||
@@ -241,7 +241,7 @@ source/
|
||||
|
||||
**`cell_runtime.c`** is the single file that defines the native code contract. It should:
|
||||
|
||||
1. Include `quickjs-internal.h` for access to value representation and heap types
|
||||
1. Include `pit_internal.h` for access to value representation and heap types
|
||||
2. Export all `cell_rt_*` functions with C linkage (no `static`)
|
||||
3. Keep each function thin — delegate to existing `JS_*` functions where possible
|
||||
4. Handle GC safety: after any allocation (frame, string, array), callers' frames may have moved
|
||||
|
||||
3
fold.cm
3
fold.cm
@@ -362,6 +362,7 @@ var fold = function(ast) {
|
||||
var fold_expr = null
|
||||
var fold_stmt = null
|
||||
var fold_stmts = null
|
||||
var fold_fn = null
|
||||
|
||||
fold_expr = function(expr, fn_nr) {
|
||||
if (expr == null) return null
|
||||
@@ -592,8 +593,6 @@ var fold = function(ast) {
|
||||
return expr
|
||||
}
|
||||
|
||||
var fold_fn = null
|
||||
|
||||
fold_stmt = function(stmt, fn_nr) {
|
||||
if (stmt == null) return null
|
||||
var k = stmt.kind
|
||||
|
||||
403
gc_plan.md
403
gc_plan.md
@@ -1,403 +0,0 @@
|
||||
# Plan: Complete Copying GC Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
Remove reference counting (DupValue/FreeValue) entirely and complete the Cheney copying garbage collector. Each JSContext will use bump allocation from a heap block, and when out of memory, request a new heap from JSRuntime's buddy allocator and copy live objects to the new heap.
|
||||
|
||||
## Target Architecture (from docs/memory.md)
|
||||
|
||||
### Object Types (simplified from current):
|
||||
|
||||
**Type 0 - Array**: `{ header, length, elements[] }`
|
||||
**Type 1 - Blob**: `{ header, length, bits[] }`
|
||||
**Type 2 - Text**: `{ header, length_or_hash, packed_chars[] }`
|
||||
**Type 3 - Record**: `{ header, prototype, length, key_value_pairs[] }`
|
||||
**Type 4 - Function**: `{ header, code_ptr, outer_frame_ptr }` - 3 words only, always stone
|
||||
**Type 5 - Frame**: `{ header, function_ptr, caller_ptr, ret_addr, args[], closure_vars[], local_vars[], temps[] }`
|
||||
**Type 6 - Code**: Lives in immutable memory only, never copied
|
||||
**Type 7 - Forward**: Object has moved; cap56 contains new address
|
||||
|
||||
### Key Design Points:
|
||||
- **JSFunction** is just a pointer to code and a pointer to the frame that created it (3 words)
|
||||
- **Closure variables live in frames** - when a function returns, its frame is "reduced" to just the closure variables
|
||||
- **Code objects are immutable** - stored in stone memory, never copied during GC
|
||||
- **Frame reduction**: When a function returns, `caller` is set to zero, signaling the frame can be shrunk
|
||||
|
||||
## Current State (needs refactoring)
|
||||
|
||||
1. **Partial Cheney GC exists** at `source/quickjs.c:1844-2030`: `ctx_gc`, `gc_copy_value`, `gc_scan_object`
|
||||
2. **744 calls to JS_DupValue/JS_FreeValue** scattered throughout (currently undefined, causing compilation errors)
|
||||
3. **Current JSFunction** is bloated (has kind, name, union of cfunc/bytecode/bound) - needs simplification
|
||||
4. **Current JSVarRef** is a separate object - should be eliminated, closures live in frames
|
||||
5. **Bump allocator** in `js_malloc` (line 1495) with `heap_base`/`heap_free`/`heap_end`
|
||||
6. **Buddy allocator** for memory blocks (lines 1727-1837)
|
||||
7. **Header offset inconsistency** - some structs have header at offset 0, some at offset 8
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Define No-Op DupValue/FreeValue (To Enable Compilation)
|
||||
|
||||
Add these near line 100 in `source/quickjs.c`:
|
||||
|
||||
```c
|
||||
/* Copying GC - no reference counting needed */
|
||||
#define JS_DupValue(ctx, v) (v)
|
||||
#define JS_FreeValue(ctx, v) ((void)0)
|
||||
#define JS_DupValueRT(rt, v) (v)
|
||||
#define JS_FreeValueRT(rt, v) ((void)0)
|
||||
```
|
||||
|
||||
This makes the code compile while keeping existing call sites (they become no-ops).
|
||||
|
||||
### Phase 2: Standardize Object Headers (offset 0)
|
||||
|
||||
Remove `JSGCObjectHeader` (ref counting remnant) and put `objhdr_t` at offset 0:
|
||||
|
||||
```c
|
||||
typedef struct JSArray {
|
||||
objhdr_t hdr; // offset 0
|
||||
word_t length;
|
||||
JSValue values[];
|
||||
} JSArray;
|
||||
|
||||
typedef struct JSRecord {
|
||||
objhdr_t hdr; // offset 0
|
||||
JSRecord *proto;
|
||||
word_t length;
|
||||
slot slots[];
|
||||
} JSRecord;
|
||||
|
||||
typedef struct JSText {
|
||||
objhdr_t hdr; // offset 0
|
||||
word_t length; // pretext: length, text: hash
|
||||
word_t packed[];
|
||||
} JSText;
|
||||
|
||||
typedef struct JSBlob {
|
||||
objhdr_t hdr; // offset 0
|
||||
word_t length;
|
||||
uint8_t bits[];
|
||||
} JSBlob;
|
||||
|
||||
/* Simplified JSFunction per memory.md - 3 words */
|
||||
typedef struct JSFunction {
|
||||
objhdr_t hdr; // offset 0, always stone
|
||||
JSCode *code; // pointer to immutable code object
|
||||
struct JSFrame *outer; // frame that created this function
|
||||
} JSFunction;
|
||||
|
||||
/* JSFrame per memory.md */
|
||||
typedef struct JSFrame {
|
||||
objhdr_t hdr; // offset 0
|
||||
JSFunction *function; // function being executed
|
||||
struct JSFrame *caller; // calling frame (NULL = reduced/returned)
|
||||
word_t ret_addr; // return instruction address
|
||||
JSValue slots[]; // args, closure vars, locals, temps
|
||||
} JSFrame;
|
||||
|
||||
/* JSCode - always in immutable (stone) memory */
|
||||
typedef struct JSCode {
|
||||
objhdr_t hdr; // offset 0, always stone
|
||||
word_t arity; // max number of inputs
|
||||
word_t frame_size; // capacity of activation frame
|
||||
word_t closure_size; // reduced capacity for returned frames
|
||||
word_t entry_point; // address to begin execution
|
||||
word_t disruption_point;// address of disruption clause
|
||||
uint8_t bytecode[]; // actual bytecode
|
||||
} JSCode;
|
||||
```
|
||||
|
||||
### Phase 3: Complete gc_object_size for All Types
|
||||
|
||||
Update `gc_object_size` (line 1850) to read header at offset 0:
|
||||
|
||||
```c
|
||||
static size_t gc_object_size(void *ptr) {
|
||||
objhdr_t hdr = *(objhdr_t*)ptr; // Header at offset 0
|
||||
uint8_t type = objhdr_type(hdr);
|
||||
uint64_t cap = objhdr_cap56(hdr);
|
||||
|
||||
switch (type) {
|
||||
case OBJ_ARRAY:
|
||||
return sizeof(JSArray) + cap * sizeof(JSValue);
|
||||
case OBJ_BLOB:
|
||||
return sizeof(JSBlob) + (cap + 7) / 8; // cap is bits
|
||||
case OBJ_TEXT:
|
||||
return sizeof(JSText) + ((cap + 1) / 2) * sizeof(uint64_t);
|
||||
case OBJ_RECORD:
|
||||
return sizeof(JSRecord) + (cap + 1) * sizeof(slot); // cap is mask
|
||||
case OBJ_FUNCTION:
|
||||
return sizeof(JSFunction); // 3 words
|
||||
case OBJ_FRAME:
|
||||
return sizeof(JSFrame) + cap * sizeof(JSValue); // cap is slot count
|
||||
case OBJ_CODE:
|
||||
return 0; // Code is never copied (immutable)
|
||||
default:
|
||||
return 64; // Conservative fallback
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 4: Complete gc_scan_object for All Types
|
||||
|
||||
Update `gc_scan_object` (line 1924):
|
||||
|
||||
```c
|
||||
static void gc_scan_object(JSContext *ctx, void *ptr, uint8_t **to_free, uint8_t *to_end) {
|
||||
objhdr_t hdr = *(objhdr_t*)ptr;
|
||||
uint8_t type = objhdr_type(hdr);
|
||||
|
||||
switch (type) {
|
||||
case OBJ_ARRAY: {
|
||||
JSArray *arr = (JSArray*)ptr;
|
||||
for (uint32_t i = 0; i < arr->length; i++) {
|
||||
arr->values[i] = gc_copy_value(ctx, arr->values[i], to_free, to_end);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OBJ_RECORD: {
|
||||
JSRecord *rec = (JSRecord*)ptr;
|
||||
// Copy prototype
|
||||
if (rec->proto) {
|
||||
JSValue proto_val = JS_MKPTR(rec->proto);
|
||||
proto_val = gc_copy_value(ctx, proto_val, to_free, to_end);
|
||||
rec->proto = (JSRecord*)JS_VALUE_GET_PTR(proto_val);
|
||||
}
|
||||
// Copy table entries
|
||||
uint32_t mask = objhdr_cap56(rec->hdr);
|
||||
for (uint32_t i = 1; i <= mask; i++) { // Skip slot 0
|
||||
JSValue k = rec->slots[i].key;
|
||||
if (!rec_key_is_empty(k) && !rec_key_is_tomb(k)) {
|
||||
rec->slots[i].key = gc_copy_value(ctx, k, to_free, to_end);
|
||||
rec->slots[i].value = gc_copy_value(ctx, rec->slots[i].value, to_free, to_end);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OBJ_FUNCTION: {
|
||||
JSFunction *func = (JSFunction*)ptr;
|
||||
// Code is immutable, don't copy - but outer frame needs copying
|
||||
if (func->outer) {
|
||||
JSValue outer_val = JS_MKPTR(func->outer);
|
||||
outer_val = gc_copy_value(ctx, outer_val, to_free, to_end);
|
||||
func->outer = (JSFrame*)JS_VALUE_GET_PTR(outer_val);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OBJ_FRAME: {
|
||||
JSFrame *frame = (JSFrame*)ptr;
|
||||
// Copy function pointer
|
||||
if (frame->function) {
|
||||
JSValue func_val = JS_MKPTR(frame->function);
|
||||
func_val = gc_copy_value(ctx, func_val, to_free, to_end);
|
||||
frame->function = (JSFunction*)JS_VALUE_GET_PTR(func_val);
|
||||
}
|
||||
// Copy caller (unless NULL = reduced frame)
|
||||
if (frame->caller) {
|
||||
JSValue caller_val = JS_MKPTR(frame->caller);
|
||||
caller_val = gc_copy_value(ctx, caller_val, to_free, to_end);
|
||||
frame->caller = (JSFrame*)JS_VALUE_GET_PTR(caller_val);
|
||||
}
|
||||
// Copy all slots (args, closure vars, locals, temps)
|
||||
uint32_t slot_count = objhdr_cap56(frame->hdr);
|
||||
for (uint32_t i = 0; i < slot_count; i++) {
|
||||
frame->slots[i] = gc_copy_value(ctx, frame->slots[i], to_free, to_end);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OBJ_TEXT:
|
||||
case OBJ_BLOB:
|
||||
case OBJ_CODE:
|
||||
// No internal references to scan
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 5: Fix gc_copy_value Forwarding
|
||||
|
||||
Update `gc_copy_value` (line 1883) for offset 0 headers:
|
||||
|
||||
```c
|
||||
static JSValue gc_copy_value(JSContext *ctx, JSValue v, uint8_t **to_free, uint8_t *to_end) {
|
||||
if (!JS_IsPtr(v)) return v; // Immediate value
|
||||
|
||||
void *ptr = JS_VALUE_GET_PTR(v);
|
||||
|
||||
// Stone memory - don't copy (includes Code objects)
|
||||
objhdr_t hdr = *(objhdr_t*)ptr;
|
||||
if (objhdr_s(hdr)) return v;
|
||||
|
||||
// Check if in current heap
|
||||
if ((uint8_t*)ptr < ctx->heap_base || (uint8_t*)ptr >= ctx->heap_end)
|
||||
return v; // External allocation
|
||||
|
||||
// Already forwarded?
|
||||
if (objhdr_type(hdr) == OBJ_FORWARD) {
|
||||
void *new_ptr = (void*)(uintptr_t)objhdr_cap56(hdr);
|
||||
return JS_MKPTR(new_ptr);
|
||||
}
|
||||
|
||||
// Copy object to new space
|
||||
size_t size = gc_object_size(ptr);
|
||||
void *new_ptr = *to_free;
|
||||
*to_free += size;
|
||||
memcpy(new_ptr, ptr, size);
|
||||
|
||||
// Leave forwarding pointer in old location
|
||||
*(objhdr_t*)ptr = objhdr_make((uint64_t)(uintptr_t)new_ptr, OBJ_FORWARD, 0, 0, 0, 0);
|
||||
|
||||
return JS_MKPTR(new_ptr);
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 6: Complete GC Root Tracing
|
||||
|
||||
Update `ctx_gc` (line 1966) to trace all roots including JSGCRef:
|
||||
|
||||
```c
|
||||
static int ctx_gc(JSContext *ctx) {
|
||||
// ... existing setup code ...
|
||||
|
||||
// Copy roots: global object, class prototypes, etc. (existing)
|
||||
ctx->global_obj = gc_copy_value(ctx, ctx->global_obj, &to_free, to_end);
|
||||
ctx->global_var_obj = gc_copy_value(ctx, ctx->global_var_obj, &to_free, to_end);
|
||||
// ... other existing root copying ...
|
||||
|
||||
// Copy GC root stack (JS_PUSH_VALUE/JS_POP_VALUE)
|
||||
for (JSGCRef *ref = ctx->top_gc_ref; ref; ref = ref->prev) {
|
||||
ref->val = gc_copy_value(ctx, ref->val, &to_free, to_end);
|
||||
}
|
||||
|
||||
// Copy GC root list (JS_AddGCRef/JS_DeleteGCRef)
|
||||
for (JSGCRef *ref = ctx->last_gc_ref; ref; ref = ref->prev) {
|
||||
ref->val = gc_copy_value(ctx, ref->val, &to_free, to_end);
|
||||
}
|
||||
|
||||
// Copy current exception
|
||||
ctx->current_exception = gc_copy_value(ctx, ctx->current_exception, &to_free, to_end);
|
||||
|
||||
// Cheney scan (existing)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 7: Trigger GC on Allocation Failure
|
||||
|
||||
Update `js_malloc` (line 1495):
|
||||
|
||||
```c
|
||||
void *js_malloc(JSContext *ctx, size_t size) {
|
||||
size = (size + 7) & ~7; // Align to 8 bytes
|
||||
|
||||
if ((uint8_t*)ctx->heap_free + size > (uint8_t*)ctx->heap_end) {
|
||||
if (ctx_gc(ctx) < 0) {
|
||||
JS_ThrowOutOfMemory(ctx);
|
||||
return NULL;
|
||||
}
|
||||
// Retry after GC
|
||||
if ((uint8_t*)ctx->heap_free + size > (uint8_t*)ctx->heap_end) {
|
||||
JS_ThrowOutOfMemory(ctx);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void *ptr = ctx->heap_free;
|
||||
ctx->heap_free = (uint8_t*)ctx->heap_free + size;
|
||||
return ptr;
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 8: Frame Reduction (for closures)
|
||||
|
||||
When a function returns, "reduce" its frame to just closure variables:
|
||||
|
||||
```c
|
||||
static void reduce_frame(JSContext *ctx, JSFrame *frame) {
|
||||
if (frame->caller == NULL) return; // Already reduced
|
||||
|
||||
JSCode *code = frame->function->code;
|
||||
uint32_t closure_size = code->closure_size;
|
||||
|
||||
// Shrink capacity to just closure variables
|
||||
frame->hdr = objhdr_make(closure_size, OBJ_FRAME, 0, 0, 0, 0);
|
||||
frame->caller = NULL; // Signal: frame is reduced
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 9: Remove Unused Reference Counting Code
|
||||
|
||||
Delete:
|
||||
- `gc_decref`, `gc_decref_child` functions
|
||||
- `gc_scan_incref_child`, `gc_scan_incref_child2` functions
|
||||
- `JS_GCPhaseEnum`, `gc_phase` fields
|
||||
- `JSGCObjectHeader` struct (merge into objhdr_t)
|
||||
- `ref_count` fields from any remaining structs
|
||||
- `mark_function_children_decref` function
|
||||
- All `free_*` functions that rely on ref counting
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. **source/quickjs.c** - Main implementation:
|
||||
- Add DupValue/FreeValue no-op macros (~line 100)
|
||||
- Restructure JSArray, JSBlob, JSText, JSRecord (lines 468-499)
|
||||
- Simplify JSFunction to 3-word struct (line 1205)
|
||||
- Add JSFrame as heap object (new)
|
||||
- Restructure JSCode/JSFunctionBytecode (line 1293)
|
||||
- Fix gc_object_size (line 1850)
|
||||
- Fix gc_copy_value (line 1883)
|
||||
- Complete gc_scan_object (line 1924)
|
||||
- Update ctx_gc for all roots (line 1966)
|
||||
- Update js_malloc to trigger GC (line 1495)
|
||||
- Delete ref counting code throughout
|
||||
|
||||
2. **source/quickjs.h** - Public API:
|
||||
- Remove JSGCObjectHeader
|
||||
- Update JSValue type checks if needed
|
||||
- Ensure JS_IsStone works with offset 0 headers
|
||||
|
||||
## Execution Order
|
||||
|
||||
1. **First**: Add DupValue/FreeValue macros (enables compilation)
|
||||
2. **Second**: Standardize struct layouts (header at offset 0)
|
||||
3. **Third**: Fix gc_object_size and gc_copy_value
|
||||
4. **Fourth**: Complete gc_scan_object for all types
|
||||
5. **Fifth**: Update ctx_gc with complete root tracing
|
||||
6. **Sixth**: Wire js_malloc to trigger GC
|
||||
7. **Seventh**: Add frame reduction for closures
|
||||
8. **Finally**: Remove ref counting dead code
|
||||
|
||||
## Verification
|
||||
|
||||
1. **Compile test**: `make` should succeed without errors
|
||||
2. **Basic test**: Run simple scripts:
|
||||
```js
|
||||
var a = [1, 2, 3]
|
||||
log.console(a[1])
|
||||
```
|
||||
3. **Stress test**: Allocate many objects to trigger GC:
|
||||
```js
|
||||
for (var i = 0; i < 100000; i++) {
|
||||
var x = { value: i }
|
||||
}
|
||||
log.console("done")
|
||||
```
|
||||
4. **Closure test**: Test functions with closures survive GC:
|
||||
```js
|
||||
fn make_counter() {
|
||||
var count = 0
|
||||
fn inc() { count = count + 1; return count }
|
||||
return inc
|
||||
}
|
||||
var c = make_counter()
|
||||
log.console(c()) // 1
|
||||
log.console(c()) // 2
|
||||
```
|
||||
5. **GC stress with closures**: Create many closures, trigger GC, verify they still work
|
||||
|
||||
## Key Design Decisions (Resolved)
|
||||
|
||||
1. **JSCode storage**: Lives in stone (immutable) memory, never copied during GC ✓
|
||||
2. **Header offset**: Standardized to offset 0 for all heap objects ✓
|
||||
3. **Closure variables**: Live in JSFrame objects; frames are "reduced" when functions return ✓
|
||||
4. **JSVarRef**: Eliminated - closures reference their outer frame directly ✓
|
||||
742
http.cm
Normal file
742
http.cm
Normal file
@@ -0,0 +1,742 @@
|
||||
var socket = use('socket')
|
||||
var tls = use('net/tls')
|
||||
var Blob = use('blob')
|
||||
|
||||
def CRLF = "\r\n"
|
||||
|
||||
def status_texts = {
|
||||
"200": "OK", "201": "Created", "204": "No Content",
|
||||
"301": "Moved Permanently", "302": "Found", "307": "Temporary Redirect",
|
||||
"400": "Bad Request", "401": "Unauthorized", "403": "Forbidden",
|
||||
"404": "Not Found", "405": "Method Not Allowed", "500": "Internal Server Error"
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Server (unchanged)
|
||||
// ============================================================
|
||||
|
||||
function serve(port) {
|
||||
var fd = socket.socket("AF_INET", "SOCK_STREAM")
|
||||
socket.setsockopt(fd, "SOL_SOCKET", "SO_REUSEADDR", true)
|
||||
socket.bind(fd, {address: "127.0.0.1", port: port})
|
||||
socket.listen(fd, 16)
|
||||
return fd
|
||||
}
|
||||
|
||||
function parse_request(conn_fd) {
|
||||
var data = socket.recv(conn_fd, 65536)
|
||||
var raw = text(data)
|
||||
|
||||
var hdr_end = search(raw, CRLF + CRLF)
|
||||
if (hdr_end == null) disrupt
|
||||
var header_text = text(raw, 0, hdr_end)
|
||||
var body_text = text(raw, hdr_end + 4)
|
||||
|
||||
var lines = array(header_text, CRLF)
|
||||
var parts = array(lines[0], " ")
|
||||
var method = parts[0]
|
||||
var url = parts[1]
|
||||
var qpos = search(url, "?")
|
||||
var path = qpos != null ? text(url, 0, qpos) : url
|
||||
|
||||
var headers = {}
|
||||
var i = 1
|
||||
var colon = null
|
||||
var key = null
|
||||
var val = null
|
||||
while (i < length(lines)) {
|
||||
colon = search(lines[i], ": ")
|
||||
if (colon != null) {
|
||||
key = lower(text(lines[i], 0, colon))
|
||||
val = text(lines[i], colon + 2)
|
||||
headers[key] = val
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
var cl = headers["content-length"]
|
||||
var content_length = null
|
||||
var remaining = null
|
||||
var more = null
|
||||
if (cl != null) content_length = number(cl)
|
||||
if (content_length != null && length(body_text) < content_length) {
|
||||
remaining = content_length - length(body_text)
|
||||
more = socket.recv(conn_fd, remaining)
|
||||
body_text = body_text + text(more)
|
||||
}
|
||||
if (content_length == null || content_length == 0) body_text = null
|
||||
|
||||
return {
|
||||
method: method, path: path, url: url,
|
||||
headers: headers, body: body_text, _conn: conn_fd
|
||||
}
|
||||
}
|
||||
|
||||
function accept(server_fd) {
|
||||
var conn = socket.accept(server_fd)
|
||||
return parse_request(conn.socket)
|
||||
}
|
||||
|
||||
function on_request(server_fd, handler) {
|
||||
var _accept = function() {
|
||||
var conn = socket.accept(server_fd)
|
||||
var req = null
|
||||
var _parse = function() {
|
||||
req = parse_request(conn.socket)
|
||||
} disruption {
|
||||
req = null
|
||||
}
|
||||
_parse()
|
||||
if (req != null) handler(req)
|
||||
socket.on_readable(server_fd, _accept)
|
||||
}
|
||||
socket.on_readable(server_fd, _accept)
|
||||
}
|
||||
|
||||
function respond(conn, status, headers, body) {
|
||||
var st = status_texts[text(status)]
|
||||
if (st == null) st = "Unknown"
|
||||
var out = "HTTP/1.1 " + text(status) + " " + st + CRLF
|
||||
out = out + "Connection: close" + CRLF
|
||||
|
||||
var body_str = ""
|
||||
var keys = null
|
||||
var i = 0
|
||||
if (body != null) {
|
||||
if (is_text(body)) body_str = body
|
||||
else body_str = text(body)
|
||||
}
|
||||
|
||||
if (headers != null) {
|
||||
keys = array(headers)
|
||||
i = 0
|
||||
while (i < length(keys)) {
|
||||
out = out + keys[i] + ": " + headers[keys[i]] + CRLF
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
out = out + "Content-Length: " + text(length(body_str)) + CRLF
|
||||
out = out + CRLF + body_str
|
||||
|
||||
socket.send(conn, out)
|
||||
socket.close(conn)
|
||||
}
|
||||
|
||||
function sse_open(conn, headers) {
|
||||
var out = "HTTP/1.1 200 OK" + CRLF
|
||||
out = out + "Content-Type: text/event-stream" + CRLF
|
||||
out = out + "Cache-Control: no-cache" + CRLF
|
||||
out = out + "Connection: keep-alive" + CRLF
|
||||
var keys = null
|
||||
var i = 0
|
||||
if (headers != null) {
|
||||
keys = array(headers)
|
||||
i = 0
|
||||
while (i < length(keys)) {
|
||||
out = out + keys[i] + ": " + headers[keys[i]] + CRLF
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
out = out + CRLF
|
||||
socket.send(conn, out)
|
||||
}
|
||||
|
||||
function sse_event(conn, event, data) {
|
||||
var frame = "event: " + event + "\ndata: " + data + "\n\n"
|
||||
var ok = true
|
||||
var _send = function() {
|
||||
socket.send(conn, frame)
|
||||
} disruption {
|
||||
ok = false
|
||||
}
|
||||
_send()
|
||||
return ok
|
||||
}
|
||||
|
||||
function sse_close(conn) {
|
||||
socket.close(conn)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Blocking client request (kept for compatibility)
|
||||
// ============================================================
|
||||
|
||||
function request(method, url, headers, body) {
|
||||
var parts = array(url, "/")
|
||||
var host_port = parts[2]
|
||||
var path = "/" + text(array(parts, 3, length(parts)), "/")
|
||||
var hp = array(host_port, ":")
|
||||
var host = hp[0]
|
||||
var port = length(hp) > 1 ? number(hp[1]) : 80
|
||||
|
||||
var fd = socket.socket("AF_INET", "SOCK_STREAM")
|
||||
var raw = null
|
||||
var hdr_end = null
|
||||
var _do = function() {
|
||||
socket.connect(fd, {address: host, port: port})
|
||||
var body_str = ""
|
||||
if (body != null) {
|
||||
if (is_text(body)) body_str = body
|
||||
else body_str = text(body)
|
||||
}
|
||||
var keys = null
|
||||
var i = 0
|
||||
var req = method + " " + path + " HTTP/1.1" + CRLF
|
||||
req = req + "Host: " + host_port + CRLF
|
||||
req = req + "Connection: close" + CRLF
|
||||
if (headers != null) {
|
||||
keys = array(headers)
|
||||
i = 0
|
||||
while (i < length(keys)) {
|
||||
req = req + keys[i] + ": " + headers[keys[i]] + CRLF
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
if (length(body_str) > 0) {
|
||||
req = req + "Content-Length: " + text(length(body_str)) + CRLF
|
||||
}
|
||||
req = req + CRLF + body_str
|
||||
socket.send(fd, req)
|
||||
raw = text(socket.recv(fd, 65536))
|
||||
} disruption {
|
||||
raw = null
|
||||
}
|
||||
_do()
|
||||
socket.close(fd)
|
||||
if (raw == null) return null
|
||||
hdr_end = search(raw, CRLF + CRLF)
|
||||
if (hdr_end == null) return null
|
||||
|
||||
var header_text = text(raw, 0, hdr_end)
|
||||
var lines = array(header_text, CRLF)
|
||||
var status_parts = array(lines[0], " ")
|
||||
var status = number(status_parts[1])
|
||||
|
||||
var resp_headers = {}
|
||||
var hi = 1
|
||||
var colon = null
|
||||
while (hi < length(lines)) {
|
||||
colon = search(lines[hi], ": ")
|
||||
if (colon != null) {
|
||||
resp_headers[lower(text(lines[hi], 0, colon))] = text(lines[hi], colon + 2)
|
||||
}
|
||||
hi = hi + 1
|
||||
}
|
||||
|
||||
return {
|
||||
status: status,
|
||||
headers: resp_headers,
|
||||
body: text(raw, hdr_end + 4)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Requestor-based async fetch
|
||||
// ============================================================
|
||||
|
||||
// parse_url requestor — sync, extract {scheme, host, port, path} from URL
|
||||
var parse_url = function(callback, value) {
|
||||
var url = null
|
||||
var method = "GET"
|
||||
var req_headers = null
|
||||
var req_body = null
|
||||
log.console("value type=" + text(is_text(value)) + " val=" + text(value))
|
||||
if (is_text(value)) {
|
||||
url = value
|
||||
log.console("url after assign=" + text(is_text(url)) + " url=" + text(url))
|
||||
} else {
|
||||
url = value.url
|
||||
if (value.method != null) method = value.method
|
||||
if (value.headers != null) req_headers = value.headers
|
||||
if (value.body != null) req_body = value.body
|
||||
}
|
||||
|
||||
// strip scheme
|
||||
var scheme = "http"
|
||||
var rest = url
|
||||
var scheme_end = search(url, "://")
|
||||
log.console("A: url_type=" + text(is_text(url)) + " scheme_end=" + text(scheme_end))
|
||||
if (scheme_end != null) {
|
||||
scheme = lower(text(url, 0, scheme_end))
|
||||
rest = text(url, scheme_end + 3, length(url))
|
||||
log.console("B: scheme=" + scheme + " rest=" + rest + " rest_type=" + text(is_text(rest)))
|
||||
}
|
||||
|
||||
// split host from path
|
||||
var slash = search(rest, "/")
|
||||
var host_port = rest
|
||||
var path = "/"
|
||||
log.console("C: slash=" + text(slash))
|
||||
if (slash != null) {
|
||||
host_port = text(rest, 0, slash)
|
||||
path = text(rest, slash, length(rest))
|
||||
}
|
||||
// split host:port
|
||||
var hp = array(host_port, ":")
|
||||
var host = hp[0]
|
||||
var port = null
|
||||
if (length(hp) > 1) {
|
||||
port = number(hp[1])
|
||||
} else {
|
||||
port = scheme == "https" ? 443 : 80
|
||||
}
|
||||
|
||||
callback({
|
||||
scheme: scheme, host: host, port: port, path: path,
|
||||
host_port: host_port, method: method,
|
||||
req_headers: req_headers, req_body: req_body
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
// resolve_dns requestor — blocking getaddrinfo, swappable later
|
||||
var resolve_dns = function(callback, state) {
|
||||
var ok = true
|
||||
var addrs = null
|
||||
var _resolve = function() {
|
||||
addrs = socket.getaddrinfo(state.host, text(state.port))
|
||||
} disruption {
|
||||
ok = false
|
||||
}
|
||||
_resolve()
|
||||
if (!ok || addrs == null || length(addrs) == 0) {
|
||||
callback(null, "dns resolution failed for " + state.host)
|
||||
return null
|
||||
}
|
||||
callback(record(state, {address: addrs[0].address}))
|
||||
return null
|
||||
}
|
||||
|
||||
// open_connection requestor — non-blocking connect + optional TLS
|
||||
var open_connection = function(callback, state) {
|
||||
var fd = socket.socket("AF_INET", "SOCK_STREAM")
|
||||
var cancelled = false
|
||||
|
||||
var cancel = function() {
|
||||
var _close = null
|
||||
if (!cancelled) {
|
||||
cancelled = true
|
||||
_close = function() {
|
||||
socket.unwatch(fd)
|
||||
socket.close(fd)
|
||||
} disruption {}
|
||||
_close()
|
||||
}
|
||||
}
|
||||
|
||||
socket.setnonblock(fd)
|
||||
|
||||
var finish_connect = function(the_fd) {
|
||||
var ctx = null
|
||||
if (state.scheme == "https") {
|
||||
ctx = tls.wrap(the_fd, state.host)
|
||||
}
|
||||
callback(record(state, {fd: the_fd, tls: ctx}))
|
||||
}
|
||||
|
||||
// non-blocking connect — EINPROGRESS is expected
|
||||
var connect_err = false
|
||||
var _connect = function() {
|
||||
socket.connect(fd, {address: state.address, port: state.port})
|
||||
} disruption {
|
||||
connect_err = true
|
||||
}
|
||||
_connect()
|
||||
|
||||
// if connect succeeded immediately (localhost, etc)
|
||||
var _finish_immediate = null
|
||||
if (!connect_err && !cancelled) {
|
||||
_finish_immediate = function() {
|
||||
finish_connect(fd)
|
||||
} disruption {
|
||||
cancel()
|
||||
callback(null, "connection setup failed")
|
||||
}
|
||||
_finish_immediate()
|
||||
return cancel
|
||||
}
|
||||
|
||||
// wait for connect to complete
|
||||
socket.on_writable(fd, function() {
|
||||
if (cancelled) return
|
||||
var err = socket.getsockopt(fd, "SOL_SOCKET", "SO_ERROR")
|
||||
if (err != 0) {
|
||||
cancel()
|
||||
callback(null, "connect failed (errno " + text(err) + ")")
|
||||
return
|
||||
}
|
||||
var _finish = function() {
|
||||
finish_connect(fd)
|
||||
} disruption {
|
||||
cancel()
|
||||
callback(null, "connection setup failed")
|
||||
}
|
||||
_finish()
|
||||
})
|
||||
|
||||
return cancel
|
||||
}
|
||||
|
||||
// send_request requestor — format + send HTTP/1.1 request
|
||||
var send_request = function(callback, state) {
|
||||
var cancelled = false
|
||||
var cancel = function() {
|
||||
cancelled = true
|
||||
}
|
||||
|
||||
var _send = function() {
|
||||
var body_str = ""
|
||||
var keys = null
|
||||
var i = 0
|
||||
if (state.req_body != null) {
|
||||
if (is_text(state.req_body)) body_str = state.req_body
|
||||
else body_str = text(state.req_body)
|
||||
}
|
||||
|
||||
var req = state.method + " " + state.path + " HTTP/1.1" + CRLF
|
||||
req = req + "Host: " + state.host_port + CRLF
|
||||
req = req + "Connection: close" + CRLF
|
||||
req = req + "User-Agent: cell/1.0" + CRLF
|
||||
req = req + "Accept: */*" + CRLF
|
||||
|
||||
if (state.req_headers != null) {
|
||||
keys = array(state.req_headers)
|
||||
i = 0
|
||||
while (i < length(keys)) {
|
||||
req = req + keys[i] + ": " + state.req_headers[keys[i]] + CRLF
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
if (length(body_str) > 0) {
|
||||
req = req + "Content-Length: " + text(length(body_str)) + CRLF
|
||||
}
|
||||
|
||||
req = req + CRLF + body_str
|
||||
|
||||
if (state.tls != null) {
|
||||
tls.send(state.tls, req)
|
||||
} else {
|
||||
socket.send(state.fd, req)
|
||||
}
|
||||
} disruption {
|
||||
if (!cancelled) callback(null, "send request failed")
|
||||
return cancel
|
||||
}
|
||||
|
||||
_send()
|
||||
if (!cancelled) callback(state)
|
||||
return cancel
|
||||
}
|
||||
|
||||
// parse response headers from raw text
|
||||
function parse_headers(raw) {
|
||||
var hdr_end = search(raw, CRLF + CRLF)
|
||||
if (hdr_end == null) return null
|
||||
|
||||
var header_text = text(raw, 0, hdr_end)
|
||||
var lines = array(header_text, CRLF)
|
||||
var status_parts = array(lines[0], " ")
|
||||
var status_code = number(status_parts[1])
|
||||
|
||||
var headers = {}
|
||||
var i = 1
|
||||
var colon = null
|
||||
while (i < length(lines)) {
|
||||
colon = search(lines[i], ": ")
|
||||
if (colon != null) {
|
||||
headers[lower(text(lines[i], 0, colon))] = text(lines[i], colon + 2)
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
return {
|
||||
status: status_code, headers: headers,
|
||||
body_start: hdr_end + 4
|
||||
}
|
||||
}
|
||||
|
||||
// decode chunked transfer encoding
|
||||
function decode_chunked(body_text) {
|
||||
var result = ""
|
||||
var pos = 0
|
||||
var chunk_end = null
|
||||
var chunk_size = null
|
||||
while (pos < length(body_text)) {
|
||||
chunk_end = search(text(body_text, pos), CRLF)
|
||||
if (chunk_end == null) return result
|
||||
chunk_size = number(text(body_text, pos, pos + chunk_end), 16)
|
||||
if (chunk_size == null || chunk_size == 0) return result
|
||||
pos = pos + chunk_end + 2
|
||||
result = result + text(body_text, pos, pos + chunk_size)
|
||||
pos = pos + chunk_size + 2
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// receive_response requestor — async incremental receive
|
||||
var receive_response = function(callback, state) {
|
||||
var cancelled = false
|
||||
var buffer = ""
|
||||
var parsed = null
|
||||
var content_length = null
|
||||
var is_chunked = false
|
||||
var body_complete = false
|
||||
|
||||
var cancel = function() {
|
||||
var _cleanup = null
|
||||
if (!cancelled) {
|
||||
cancelled = true
|
||||
_cleanup = function() {
|
||||
if (state.tls != null) {
|
||||
tls.close(state.tls)
|
||||
} else {
|
||||
socket.unwatch(state.fd)
|
||||
socket.close(state.fd)
|
||||
}
|
||||
} disruption {}
|
||||
_cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
var finish = function() {
|
||||
if (cancelled) return
|
||||
var body_text = text(buffer, parsed.body_start)
|
||||
|
||||
if (is_chunked) {
|
||||
body_text = decode_chunked(body_text)
|
||||
}
|
||||
|
||||
// clean up connection
|
||||
var _cleanup = function() {
|
||||
if (state.tls != null) {
|
||||
tls.close(state.tls)
|
||||
} else {
|
||||
socket.close(state.fd)
|
||||
}
|
||||
} disruption {}
|
||||
_cleanup()
|
||||
|
||||
callback({
|
||||
status: parsed.status,
|
||||
headers: parsed.headers,
|
||||
body: body_text
|
||||
})
|
||||
}
|
||||
|
||||
var check_complete = function() {
|
||||
var te = null
|
||||
var cl = null
|
||||
var body_text = null
|
||||
// still waiting for headers
|
||||
if (parsed == null) {
|
||||
parsed = parse_headers(buffer)
|
||||
if (parsed == null) return false
|
||||
te = parsed.headers["transfer-encoding"]
|
||||
if (te != null && search(lower(te), "chunked") != null) {
|
||||
is_chunked = true
|
||||
}
|
||||
cl = parsed.headers["content-length"]
|
||||
if (cl != null) content_length = number(cl)
|
||||
}
|
||||
|
||||
body_text = text(buffer, parsed.body_start)
|
||||
|
||||
if (is_chunked) {
|
||||
// chunked: look for the terminating 0-length chunk
|
||||
if (search(body_text, CRLF + "0" + CRLF) != null) return true
|
||||
if (starts_with(body_text, "0" + CRLF)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
if (content_length != null) {
|
||||
return length(body_text) >= content_length
|
||||
}
|
||||
|
||||
// connection: close — we read until EOF (handled by recv returning 0 bytes)
|
||||
return false
|
||||
}
|
||||
|
||||
var on_data = function() {
|
||||
if (cancelled) return
|
||||
var chunk = null
|
||||
var got_data = false
|
||||
var eof = false
|
||||
|
||||
var _recv = function() {
|
||||
if (state.tls != null) {
|
||||
chunk = tls.recv(state.tls, 16384)
|
||||
} else {
|
||||
chunk = socket.recv(state.fd, 16384)
|
||||
}
|
||||
} disruption {
|
||||
// recv error — treat as EOF
|
||||
eof = true
|
||||
}
|
||||
_recv()
|
||||
|
||||
var chunk_text = null
|
||||
if (!eof && chunk != null) {
|
||||
stone(chunk)
|
||||
chunk_text = text(chunk)
|
||||
if (length(chunk_text) > 0) {
|
||||
buffer = buffer + chunk_text
|
||||
got_data = true
|
||||
} else {
|
||||
eof = true
|
||||
}
|
||||
}
|
||||
|
||||
if (got_data && check_complete()) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
if (eof) {
|
||||
// connection closed — if we have headers, deliver what we have
|
||||
if (parsed != null || parse_headers(buffer) != null) {
|
||||
if (parsed == null) parsed = parse_headers(buffer)
|
||||
finish()
|
||||
} else {
|
||||
cancel()
|
||||
callback(null, "connection closed before headers received")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// re-register for more data (one-shot watches)
|
||||
if (!cancelled) {
|
||||
if (state.tls != null) {
|
||||
tls.on_readable(state.tls, on_data)
|
||||
} else {
|
||||
socket.on_readable(state.fd, on_data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// start reading
|
||||
if (state.tls != null) {
|
||||
tls.on_readable(state.tls, on_data)
|
||||
} else {
|
||||
socket.on_readable(state.fd, on_data)
|
||||
}
|
||||
|
||||
return cancel
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// fetch — synchronous HTTP(S) GET, returns response body (stoned blob)
|
||||
// ============================================================
|
||||
|
||||
var fetch = function(url) {
|
||||
var scheme = "http"
|
||||
var rest = url
|
||||
var scheme_end = search(url, "://")
|
||||
var slash = null
|
||||
var host_port = null
|
||||
var path = "/"
|
||||
var hp = null
|
||||
var host = null
|
||||
var port = null
|
||||
var fd = null
|
||||
var ctx = null
|
||||
var buf = Blob()
|
||||
var raw_text = null
|
||||
var hdr_end = null
|
||||
var header_text = null
|
||||
var body_start_bits = null
|
||||
var body = null
|
||||
var addrs = null
|
||||
var address = null
|
||||
var ok = true
|
||||
|
||||
if (scheme_end != null) {
|
||||
scheme = lower(text(url, 0, scheme_end))
|
||||
rest = text(url, scheme_end + 3, length(url))
|
||||
}
|
||||
slash = search(rest, "/")
|
||||
host_port = rest
|
||||
if (slash != null) {
|
||||
host_port = text(rest, 0, slash)
|
||||
path = text(rest, slash, length(rest))
|
||||
}
|
||||
hp = array(host_port, ":")
|
||||
host = hp[0]
|
||||
port = length(hp) > 1 ? number(hp[1]) : (scheme == "https" ? 443 : 80)
|
||||
|
||||
addrs = socket.getaddrinfo(host, text(port))
|
||||
if (addrs == null || length(addrs) == 0) return null
|
||||
address = addrs[0].address
|
||||
|
||||
fd = socket.socket("AF_INET", "SOCK_STREAM")
|
||||
var _do = function() {
|
||||
var req = null
|
||||
var chunk = null
|
||||
socket.connect(fd, {address: address, port: port})
|
||||
if (scheme == "https") ctx = tls.wrap(fd, host)
|
||||
req = "GET " + path + " HTTP/1.1" + CRLF
|
||||
req = req + "Host: " + host_port + CRLF
|
||||
req = req + "Connection: close" + CRLF
|
||||
req = req + "User-Agent: cell/1.0" + CRLF
|
||||
req = req + "Accept: */*" + CRLF + CRLF
|
||||
if (ctx != null) tls.send(ctx, req)
|
||||
else socket.send(fd, req)
|
||||
while (true) {
|
||||
if (ctx != null) chunk = tls.recv(ctx, 16384)
|
||||
else chunk = socket.recv(fd, 16384)
|
||||
if (chunk == null) break
|
||||
stone(chunk)
|
||||
if (length(chunk) == 0) break
|
||||
buf.write_blob(chunk)
|
||||
}
|
||||
} disruption {
|
||||
ok = false
|
||||
}
|
||||
_do()
|
||||
var _cleanup = function() {
|
||||
if (ctx != null) tls.close(ctx)
|
||||
else socket.close(fd)
|
||||
} disruption {}
|
||||
_cleanup()
|
||||
if (!ok) return null
|
||||
stone(buf)
|
||||
raw_text = text(buf)
|
||||
hdr_end = search(raw_text, CRLF + CRLF)
|
||||
if (hdr_end == null) return null
|
||||
header_text = text(raw_text, 0, hdr_end)
|
||||
if (search(lower(header_text), "transfer-encoding: chunked") != null)
|
||||
return decode_chunked(text(raw_text, hdr_end + 4))
|
||||
// Headers are ASCII so char offset = byte offset
|
||||
body_start_bits = (hdr_end + 4) * 8
|
||||
body = buf.read_blob(body_start_bits, length(buf))
|
||||
stone(body)
|
||||
return body
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// fetch_requestor — async requestor pipeline for fetch
|
||||
// ============================================================
|
||||
|
||||
var fetch_requestor = sequence([
|
||||
parse_url,
|
||||
resolve_dns,
|
||||
open_connection,
|
||||
send_request,
|
||||
receive_response
|
||||
])
|
||||
|
||||
function close(fd) {
|
||||
socket.close(fd)
|
||||
}
|
||||
|
||||
return {
|
||||
// server
|
||||
serve: serve, accept: accept, on_request: on_request,
|
||||
respond: respond, close: close,
|
||||
sse_open: sse_open, sse_event: sse_event, sse_close: sse_close,
|
||||
// client
|
||||
fetch: fetch,
|
||||
fetch_requestor: fetch_requestor,
|
||||
request: request
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Minimal bootstrap — seeds the content-addressed cache
|
||||
// Only runs on cold start (C runtime couldn't find engine in cache)
|
||||
// Hidden vars: os, core_path, shop_path
|
||||
// Hidden vars: os, core_path, shop_path, native_mode (optional)
|
||||
var load_internal = os.load_internal
|
||||
function use_embed(name) {
|
||||
return load_internal("js_core_" + name + "_use")
|
||||
@@ -89,20 +89,190 @@ function compile_and_cache(name, source_path) {
|
||||
}
|
||||
}
|
||||
|
||||
// Seed the cache with everything engine needs
|
||||
var seed_files = [
|
||||
{name: "tokenize", path: "tokenize.cm"},
|
||||
{name: "parse", path: "parse.cm"},
|
||||
{name: "fold", path: "fold.cm"},
|
||||
{name: "mcode", path: "mcode.cm"},
|
||||
{name: "streamline", path: "streamline.cm"},
|
||||
{name: "engine", path: "internal/engine.cm"}
|
||||
]
|
||||
var _i = 0
|
||||
var entry = null
|
||||
while (_i < length(seed_files)) {
|
||||
entry = seed_files[_i]
|
||||
compile_and_cache(entry.name, core_path + '/' + entry.path)
|
||||
_i = _i + 1
|
||||
// --- Native compilation support ---
|
||||
|
||||
function detect_host_target() {
|
||||
var platform = os.platform()
|
||||
var arch = os.arch ? os.arch() : 'arm64'
|
||||
if (platform == 'macOS' || platform == 'darwin')
|
||||
return arch == 'x86_64' ? 'macos_x86_64' : 'macos_arm64'
|
||||
if (platform == 'Linux' || platform == 'linux')
|
||||
return arch == 'x86_64' ? 'linux' : 'linux_arm64'
|
||||
if (platform == 'Windows' || platform == 'windows')
|
||||
return 'windows'
|
||||
return null
|
||||
}
|
||||
|
||||
function detect_cc() {
|
||||
var platform = os.platform()
|
||||
if (platform == 'macOS') return 'clang'
|
||||
return 'cc'
|
||||
}
|
||||
|
||||
// Compute native dylib cache path matching build.cm's scheme:
|
||||
// cache_path(native_cache_content(src, target, ''), SALT_NATIVE)
|
||||
function native_dylib_cache_path(src, target) {
|
||||
var native_key = src + '\n' + target + '\nnative\n'
|
||||
var full_key = native_key + '\nnative'
|
||||
return cache_path(content_hash(full_key))
|
||||
}
|
||||
|
||||
// Compile a module to a native dylib and cache it
|
||||
var _qbe_mod = null
|
||||
var _qbe_emit_mod = null
|
||||
var _host_target = null
|
||||
var _cc = null
|
||||
var _is_darwin = false
|
||||
var _rt_compiled = false
|
||||
|
||||
function compile_native_cached(name, source_path) {
|
||||
var source_blob = fd.slurp(source_path)
|
||||
var src = text(source_blob)
|
||||
var dylib_path = native_dylib_cache_path(src, _host_target)
|
||||
var ast = null
|
||||
var compiled = null
|
||||
var il_parts = null
|
||||
var helpers_il = null
|
||||
var all_fns = null
|
||||
var full_il = null
|
||||
var asm_text = null
|
||||
var tmp = null
|
||||
var rc = null
|
||||
var rt_o = null
|
||||
var qbe_rt_path = null
|
||||
var link_cmd = null
|
||||
|
||||
if (dylib_path && fd.is_file(dylib_path)) {
|
||||
os.print("bootstrap: native cache hit: " + name + "\n")
|
||||
return
|
||||
}
|
||||
|
||||
var t0 = null
|
||||
var t1 = null
|
||||
|
||||
os.print("bootstrap: compiling native: " + name + "\n")
|
||||
t0 = os.now()
|
||||
ast = analyze(src, source_path)
|
||||
compiled = streamline_mod(mcode_mod(ast))
|
||||
t1 = os.now()
|
||||
os.print(" [" + name + "] pipeline (tok+parse+fold+mcode+streamline): " + text((t1 - t0) / 1000000) + "ms\n")
|
||||
|
||||
t0 = os.now()
|
||||
il_parts = _qbe_emit_mod(compiled, _qbe_mod, null)
|
||||
t1 = os.now()
|
||||
os.print(" [" + name + "] qbe_emit: " + text((t1 - t0) / 1000000) + "ms\n")
|
||||
|
||||
helpers_il = (il_parts.helpers && length(il_parts.helpers) > 0)
|
||||
? text(il_parts.helpers, "\n") : ""
|
||||
all_fns = text(il_parts.functions, "\n")
|
||||
full_il = il_parts.data + "\n\n" + helpers_il + "\n\n" + all_fns
|
||||
|
||||
t0 = os.now()
|
||||
asm_text = os.qbe(full_il)
|
||||
t1 = os.now()
|
||||
os.print(" [" + name + "] os.qbe (QBE compile): " + text((t1 - t0) / 1000000) + "ms\n")
|
||||
|
||||
tmp = '/tmp/cell_boot_' + name
|
||||
fd.slurpwrite(tmp + '.s', stone(blob(asm_text)))
|
||||
|
||||
t0 = os.now()
|
||||
rc = os.system(_cc + ' -c ' + tmp + '.s -o ' + tmp + '.o')
|
||||
t1 = os.now()
|
||||
os.print(" [" + name + "] clang -c: " + text((t1 - t0) / 1000000) + "ms\n")
|
||||
if (rc != 0) {
|
||||
os.print("error: assembly failed for " + name + "\n")
|
||||
disrupt
|
||||
}
|
||||
|
||||
// Compile QBE runtime stubs (once)
|
||||
rt_o = '/tmp/cell_qbe_rt.o'
|
||||
if (!_rt_compiled && !fd.is_file(rt_o)) {
|
||||
qbe_rt_path = core_path + '/src/qbe_rt.c'
|
||||
rc = os.system(_cc + ' -c ' + qbe_rt_path + ' -o ' + rt_o + ' -fPIC')
|
||||
if (rc != 0) {
|
||||
os.print("error: qbe_rt compilation failed\n")
|
||||
disrupt
|
||||
}
|
||||
_rt_compiled = true
|
||||
}
|
||||
|
||||
// Link dylib
|
||||
ensure_build_dir()
|
||||
link_cmd = _cc + ' -shared -fPIC'
|
||||
if (_is_darwin)
|
||||
link_cmd = link_cmd + ' -undefined dynamic_lookup'
|
||||
link_cmd = link_cmd + ' ' + tmp + '.o ' + rt_o + ' -o ' + dylib_path
|
||||
|
||||
t0 = os.now()
|
||||
rc = os.system(link_cmd)
|
||||
t1 = os.now()
|
||||
os.print(" [" + name + "] clang -shared (link): " + text((t1 - t0) / 1000000) + "ms\n")
|
||||
if (rc != 0) {
|
||||
os.print("error: linking failed for " + name + "\n")
|
||||
disrupt
|
||||
}
|
||||
}
|
||||
|
||||
// --- Main bootstrap logic ---
|
||||
|
||||
// Check if native_mode was passed from C runtime
|
||||
var _native = false
|
||||
var _check_nm = function() {
|
||||
if (native_mode) _native = true
|
||||
} disruption {}
|
||||
_check_nm()
|
||||
|
||||
var _targets = null
|
||||
var _ti = 0
|
||||
var _te = null
|
||||
|
||||
if (_native) {
|
||||
// Native path: compile everything to native dylibs
|
||||
_qbe_mod = boot_load("qbe")
|
||||
_qbe_emit_mod = boot_load("qbe_emit")
|
||||
_host_target = detect_host_target()
|
||||
_cc = detect_cc()
|
||||
_is_darwin = os.platform() == 'macOS'
|
||||
|
||||
if (!_host_target) {
|
||||
os.print("error: could not detect host target for native compilation\n")
|
||||
disrupt
|
||||
}
|
||||
|
||||
// Also seed bytecode cache for engine (so non-native path still works)
|
||||
compile_and_cache("engine", core_path + '/internal/engine.cm')
|
||||
|
||||
// Compile pipeline modules + qbe/qbe_emit + engine to native dylibs
|
||||
_targets = [
|
||||
{name: "tokenize", path: "tokenize.cm"},
|
||||
{name: "parse", path: "parse.cm"},
|
||||
{name: "fold", path: "fold.cm"},
|
||||
{name: "mcode", path: "mcode.cm"},
|
||||
{name: "streamline", path: "streamline.cm"},
|
||||
{name: "qbe", path: "qbe.cm"},
|
||||
{name: "qbe_emit", path: "qbe_emit.cm"},
|
||||
{name: "engine", path: "internal/engine.cm"}
|
||||
]
|
||||
_ti = 0
|
||||
while (_ti < length(_targets)) {
|
||||
_te = _targets[_ti]
|
||||
compile_native_cached(_te.name, core_path + '/' + _te.path)
|
||||
_ti = _ti + 1
|
||||
}
|
||||
} else {
|
||||
// Bytecode path: seed cache with everything engine needs
|
||||
_targets = [
|
||||
{name: "tokenize", path: "tokenize.cm"},
|
||||
{name: "parse", path: "parse.cm"},
|
||||
{name: "fold", path: "fold.cm"},
|
||||
{name: "mcode", path: "mcode.cm"},
|
||||
{name: "streamline", path: "streamline.cm"},
|
||||
{name: "engine", path: "internal/engine.cm"}
|
||||
]
|
||||
_ti = 0
|
||||
while (_ti < length(_targets)) {
|
||||
_te = _targets[_ti]
|
||||
compile_and_cache(_te.name, core_path + '/' + _te.path)
|
||||
_ti = _ti + 1
|
||||
}
|
||||
}
|
||||
os.print("bootstrap: cache seeded\n")
|
||||
|
||||
@@ -14,17 +14,12 @@ static void js_enet_host_finalizer(JSRuntime *rt, JSValue val)
|
||||
if (host) enet_host_destroy(host);
|
||||
}
|
||||
|
||||
//static void js_enet_peer_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func)
|
||||
//{
|
||||
// ENetPeer *peer = JS_GetOpaque(val, enet_peer_class_id);
|
||||
// JS_MarkValue(rt, *(JSValue*)peer->data, mark_func);
|
||||
//}
|
||||
|
||||
static void js_enet_peer_finalizer(JSRuntime *rt, JSValue val)
|
||||
{
|
||||
ENetPeer *peer = JS_GetOpaque(val, enet_peer_class_id);
|
||||
JS_FreeValueRT(rt, *(JSValue*)peer->data);
|
||||
free(peer->data);
|
||||
if (peer && peer->data) {
|
||||
free(peer->data);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the ENet library. Must be called before using any ENet functionality.
|
||||
@@ -34,8 +29,7 @@ static JSValue js_enet_initialize(JSContext *ctx, JSValueConst this_val, int arg
|
||||
return JS_NULL;
|
||||
}
|
||||
|
||||
// Deinitialize the ENet library, cleaning up all resources. Call this when you no longer
|
||||
// need any ENet functionality.
|
||||
// Deinitialize the ENet library, cleaning up all resources.
|
||||
static JSValue js_enet_deinitialize(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
|
||||
{
|
||||
enet_deinitialize();
|
||||
@@ -43,14 +37,7 @@ static JSValue js_enet_deinitialize(JSContext *ctx, JSValueConst this_val, int a
|
||||
}
|
||||
|
||||
// Create an ENet host for either a client-like unbound host or a server bound to a specific
|
||||
// address and port:
|
||||
//
|
||||
// - If no argument is provided, creates an unbound "client-like" host with default settings
|
||||
// (maximum 32 peers, 2 channels, unlimited bandwidth).
|
||||
// - If you pass an "ip:port" string (e.g. "127.0.0.1:7777"), it creates a server bound to
|
||||
// that address. The server supports up to 32 peers, 2 channels, and unlimited bandwidth.
|
||||
//
|
||||
// Throws an error if host creation fails for any reason.
|
||||
// address and port.
|
||||
static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
|
||||
{
|
||||
ENetHost *host;
|
||||
@@ -71,7 +58,6 @@ static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, int ar
|
||||
JSValue config_obj = argv[0];
|
||||
JSValue addr_val = JS_GetPropertyStr(ctx, config_obj, "address");
|
||||
const char *addr_str = JS_IsText(addr_val) ? JS_ToCString(ctx, addr_val) : NULL;
|
||||
JS_FreeValue(ctx, addr_val);
|
||||
|
||||
if (!addr_str)
|
||||
send = NULL;
|
||||
@@ -79,7 +65,6 @@ static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, int ar
|
||||
JSValue port_val = JS_GetPropertyStr(ctx, config_obj, "port");
|
||||
int32_t port32 = 0;
|
||||
JS_ToInt32(ctx, &port32, port_val);
|
||||
JS_FreeValue(ctx, port_val);
|
||||
|
||||
if (strcmp(addr_str, "any") == 0)
|
||||
address.host = ENET_HOST_ANY;
|
||||
@@ -98,15 +83,12 @@ static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, int ar
|
||||
|
||||
JSValue chan_val = JS_GetPropertyStr(ctx, config_obj, "channels");
|
||||
JS_ToUint32(ctx, &channel_limit, chan_val);
|
||||
JS_FreeValue(ctx, chan_val);
|
||||
|
||||
JSValue in_bw_val = JS_GetPropertyStr(ctx, config_obj, "incoming_bandwidth");
|
||||
JS_ToUint32(ctx, &incoming_bandwidth, in_bw_val);
|
||||
JS_FreeValue(ctx, in_bw_val);
|
||||
|
||||
JSValue out_bw_val = JS_GetPropertyStr(ctx, config_obj, "outgoing_bandwidth");
|
||||
JS_ToUint32(ctx, &outgoing_bandwidth, out_bw_val);
|
||||
JS_FreeValue(ctx, out_bw_val);
|
||||
|
||||
host = enet_host_create(send, peer_count, channel_limit, incoming_bandwidth, outgoing_bandwidth);
|
||||
if (!host) return JS_RaiseDisrupt(ctx, "Failed to create ENet host");
|
||||
@@ -129,71 +111,78 @@ static JSValue peer_get_value(JSContext *ctx, ENetPeer *peer)
|
||||
*(JSValue*)peer->data = JS_NewObjectClass(ctx, enet_peer_class_id);
|
||||
JS_SetOpaque(*(JSValue*)peer->data, peer);
|
||||
}
|
||||
return JS_DupValue(ctx, *(JSValue*)peer->data);
|
||||
return *(JSValue*)peer->data;
|
||||
}
|
||||
|
||||
// Poll for and process any available network events (connect, receive, disconnect, or none)
|
||||
// from this host, calling the provided callback for each event. This function loops until
|
||||
// no more events are available in the current timeframe.
|
||||
//
|
||||
// :param callback: A function called once for each available event, receiving an event
|
||||
// object as its single argument.
|
||||
// :param timeout: (optional) Timeout in milliseconds. Defaults to 0 (non-blocking).
|
||||
// :return: None
|
||||
// Poll for and process any available network events from this host,
|
||||
// calling the provided callback for each event.
|
||||
static JSValue js_enet_host_service(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
|
||||
{
|
||||
ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
|
||||
if (!host) return JS_EXCEPTION;
|
||||
|
||||
if (argc < 1 || !JS_IsFunction(argv[0])) return JS_RaiseDisrupt(ctx, "Expected a callback function as first argument");
|
||||
if (argc < 1 || !JS_IsFunction(argv[0]))
|
||||
return JS_RaiseDisrupt(ctx, "Expected a callback function as first argument");
|
||||
|
||||
double secs;
|
||||
JS_ToFloat64(ctx, &secs, argv[1]);
|
||||
enet_uint32 timeout_ms = 0;
|
||||
if (argc >= 2 && !JS_IsNull(argv[1])) {
|
||||
double secs = 0;
|
||||
JS_ToFloat64(ctx, &secs, argv[1]);
|
||||
if (secs > 0) timeout_ms = (enet_uint32)(secs * 1000.0);
|
||||
}
|
||||
|
||||
JS_FRAME(ctx);
|
||||
JSGCRef event_ref = { .val = JS_NULL, .prev = NULL };
|
||||
JS_PushGCRef(ctx, &event_ref);
|
||||
|
||||
ENetEvent event;
|
||||
while (enet_host_service(host, &event, secs*1000.0f) > 0) {
|
||||
JSValue event_obj = JS_NewObject(ctx);
|
||||
JS_SetPropertyStr(ctx, event_obj, "peer", peer_get_value(ctx, event.peer));
|
||||
while (enet_host_service(host, &event, timeout_ms) > 0) {
|
||||
event_ref.val = JS_NewObject(ctx);
|
||||
|
||||
JSValue peer_val = peer_get_value(ctx, event.peer);
|
||||
JS_SetPropertyStr(ctx, event_ref.val, "peer", peer_val);
|
||||
|
||||
switch (event.type) {
|
||||
case ENET_EVENT_TYPE_CONNECT:
|
||||
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "connect"));
|
||||
case ENET_EVENT_TYPE_CONNECT: {
|
||||
JSValue type_str = JS_NewString(ctx, "connect");
|
||||
JS_SetPropertyStr(ctx, event_ref.val, "type", type_str);
|
||||
break;
|
||||
case ENET_EVENT_TYPE_RECEIVE:
|
||||
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "receive"));
|
||||
JS_SetPropertyStr(ctx, event_obj, "channelID", JS_NewInt32(ctx, event.channelID));
|
||||
|
||||
// Pass raw data as string or ArrayBuffer
|
||||
}
|
||||
case ENET_EVENT_TYPE_RECEIVE: {
|
||||
JSValue type_str = JS_NewString(ctx, "receive");
|
||||
JS_SetPropertyStr(ctx, event_ref.val, "type", type_str);
|
||||
JS_SetPropertyStr(ctx, event_ref.val, "channelID", JS_NewInt32(ctx, event.channelID));
|
||||
if (event.packet->dataLength > 0) {
|
||||
JSValue data_val = js_new_blob_stoned_copy(ctx, event.packet->data, event.packet->dataLength);
|
||||
JS_SetPropertyStr(ctx, event_obj, "data", data_val);
|
||||
JS_SetPropertyStr(ctx, event_ref.val, "data", data_val);
|
||||
}
|
||||
enet_packet_destroy(event.packet);
|
||||
break;
|
||||
case ENET_EVENT_TYPE_DISCONNECT:
|
||||
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "disconnect"));
|
||||
}
|
||||
case ENET_EVENT_TYPE_DISCONNECT: {
|
||||
JSValue type_str = JS_NewString(ctx, "disconnect");
|
||||
JS_SetPropertyStr(ctx, event_ref.val, "type", type_str);
|
||||
break;
|
||||
case ENET_EVENT_TYPE_DISCONNECT_TIMEOUT:
|
||||
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "disconnect_timeout"));
|
||||
}
|
||||
case ENET_EVENT_TYPE_DISCONNECT_TIMEOUT: {
|
||||
JSValue type_str = JS_NewString(ctx, "disconnect_timeout");
|
||||
JS_SetPropertyStr(ctx, event_ref.val, "type", type_str);
|
||||
break;
|
||||
case ENET_EVENT_TYPE_NONE:
|
||||
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "none"));
|
||||
}
|
||||
case ENET_EVENT_TYPE_NONE: {
|
||||
JSValue type_str = JS_NewString(ctx, "none");
|
||||
JS_SetPropertyStr(ctx, event_ref.val, "type", type_str);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: raise exception?
|
||||
JS_FreeValue(ctx, event_obj);
|
||||
JS_Call(ctx, argv[0], JS_NULL, 1, &event_ref.val);
|
||||
}
|
||||
|
||||
return JS_NULL;
|
||||
JS_RETURN_NULL();
|
||||
}
|
||||
|
||||
// Initiate a connection from this host to a remote server. Throws an error if the
|
||||
// connection cannot be started.
|
||||
//
|
||||
// :param hostname: The hostname or IP address of the remote server (e.g. "example.com" or "127.0.0.1").
|
||||
// :param port: The port number to connect to.
|
||||
// :return: An ENetPeer object representing the connection.
|
||||
// Initiate a connection from this host to a remote server.
|
||||
static JSValue js_enet_host_connect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
|
||||
{
|
||||
ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
|
||||
@@ -218,8 +207,6 @@ static JSValue js_enet_host_connect(JSContext *ctx, JSValueConst this_val, int a
|
||||
}
|
||||
|
||||
// Flush all pending outgoing packets for this host immediately.
|
||||
//
|
||||
// :return: None
|
||||
static JSValue js_enet_host_flush(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
|
||||
{
|
||||
ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
|
||||
@@ -228,16 +215,13 @@ static JSValue js_enet_host_flush(JSContext *ctx, JSValueConst this_val, int arg
|
||||
return JS_NULL;
|
||||
}
|
||||
|
||||
// Broadcast a string or ArrayBuffer to all connected peers on channel 0.
|
||||
//
|
||||
// :param data: A string or ArrayBuffer to broadcast to all peers.
|
||||
// :return: None
|
||||
// Broadcast a string or blob to all connected peers on channel 0.
|
||||
static JSValue js_enet_host_broadcast(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
|
||||
{
|
||||
ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
|
||||
if (!host) return JS_EXCEPTION;
|
||||
|
||||
if (argc < 1) return JS_RaiseDisrupt(ctx, "Expected a string or ArrayBuffer to broadcast");
|
||||
if (argc < 1) return JS_RaiseDisrupt(ctx, "Expected a string or blob to broadcast");
|
||||
|
||||
const char *data_str = NULL;
|
||||
size_t data_len = 0;
|
||||
@@ -246,11 +230,11 @@ static JSValue js_enet_host_broadcast(JSContext *ctx, JSValueConst this_val, int
|
||||
if (JS_IsText(argv[0])) {
|
||||
data_str = JS_ToCStringLen(ctx, &data_len, argv[0]);
|
||||
if (!data_str) return JS_EXCEPTION;
|
||||
} else if (js_is_blob(ctx,argv[0])) {
|
||||
} else if (js_is_blob(ctx, argv[0])) {
|
||||
buf = js_get_blob_data(ctx, &data_len, argv[0]);
|
||||
if (!buf) return JS_EXCEPTION;
|
||||
} else {
|
||||
return JS_RaiseDisrupt(ctx, "broadcast() only accepts a string or ArrayBuffer");
|
||||
return JS_RaiseDisrupt(ctx, "broadcast() only accepts a string or blob");
|
||||
}
|
||||
|
||||
ENetPacket *packet = enet_packet_create(data_str ? (const void*)data_str : (const void*)buf, data_len, ENET_PACKET_FLAG_RELIABLE);
|
||||
@@ -261,30 +245,25 @@ static JSValue js_enet_host_broadcast(JSContext *ctx, JSValueConst this_val, int
|
||||
return JS_NULL;
|
||||
}
|
||||
|
||||
static JSValue js_enet_host_get_port(JSContext *js, JSValueConst self, int argc, JSValueConst *argv)
|
||||
// Host property getters
|
||||
static JSValue js_enet_host_get_port(JSContext *js, JSValueConst self)
|
||||
{
|
||||
ENetHost *host = JS_GetOpaque(self, enet_host_id);
|
||||
if (!host) return JS_EXCEPTION;
|
||||
return JS_NewInt32(js, host->address.port);
|
||||
}
|
||||
|
||||
static JSValue js_enet_host_get_address(JSContext *js, JSValueConst self, int argc, JSValueConst *argv)
|
||||
static JSValue js_enet_host_get_address(JSContext *js, JSValueConst self)
|
||||
{
|
||||
ENetHost *me = JS_GetOpaque(self, enet_host_id);
|
||||
if (!me) return JS_EXCEPTION;
|
||||
|
||||
char ip_str[128];
|
||||
if (enet_address_get_host_ip(&me->address, ip_str, sizeof(ip_str)) != 0)
|
||||
return JS_NULL;
|
||||
|
||||
return JS_NewString(js, ip_str);
|
||||
}
|
||||
|
||||
// Peer-level operations
|
||||
// Request a graceful disconnection from this peer. The connection will close after
|
||||
// pending data is sent.
|
||||
//
|
||||
// :return: None
|
||||
static JSValue js_enet_peer_disconnect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
|
||||
{
|
||||
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
|
||||
@@ -293,16 +272,12 @@ static JSValue js_enet_peer_disconnect(JSContext *ctx, JSValueConst this_val, in
|
||||
return JS_NULL;
|
||||
}
|
||||
|
||||
// Send a string or ArrayBuffer to this peer on channel 0.
|
||||
//
|
||||
// :param data: A string or ArrayBuffer to send.
|
||||
// :return: None
|
||||
static JSValue js_enet_peer_send(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
|
||||
{
|
||||
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
|
||||
if (!peer) return JS_EXCEPTION;
|
||||
|
||||
if (argc < 1) return JS_RaiseDisrupt(ctx, "Expected a string or ArrayBuffer to send");
|
||||
if (argc < 1) return JS_RaiseDisrupt(ctx, "Expected a string or blob to send");
|
||||
|
||||
const char *data_str = NULL;
|
||||
size_t data_len = 0;
|
||||
@@ -311,11 +286,11 @@ static JSValue js_enet_peer_send(JSContext *ctx, JSValueConst this_val, int argc
|
||||
if (JS_IsText(argv[0])) {
|
||||
data_str = JS_ToCStringLen(ctx, &data_len, argv[0]);
|
||||
if (!data_str) return JS_EXCEPTION;
|
||||
} else if (js_is_blob(ctx,argv[0])) {
|
||||
} else if (js_is_blob(ctx, argv[0])) {
|
||||
buf = js_get_blob_data(ctx, &data_len, argv[0]);
|
||||
if (!buf) return JS_EXCEPTION;
|
||||
} else {
|
||||
return JS_RaiseDisrupt(ctx, "send() only accepts a string or ArrayBuffer");
|
||||
return JS_RaiseDisrupt(ctx, "send() only accepts a string or blob");
|
||||
}
|
||||
|
||||
ENetPacket *packet = enet_packet_create(data_str ? (const void*)data_str : (const void*)buf, data_len, ENET_PACKET_FLAG_RELIABLE);
|
||||
@@ -326,9 +301,6 @@ static JSValue js_enet_peer_send(JSContext *ctx, JSValueConst this_val, int argc
|
||||
return JS_NULL;
|
||||
}
|
||||
|
||||
// Immediately terminate the connection to this peer, discarding any pending data.
|
||||
//
|
||||
// :return: None
|
||||
static JSValue js_enet_peer_disconnect_now(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
|
||||
{
|
||||
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
|
||||
@@ -337,9 +309,6 @@ static JSValue js_enet_peer_disconnect_now(JSContext *ctx, JSValueConst this_val
|
||||
return JS_NULL;
|
||||
}
|
||||
|
||||
// Request a disconnection from this peer after all queued packets are sent.
|
||||
//
|
||||
// :return: None
|
||||
static JSValue js_enet_peer_disconnect_later(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
|
||||
{
|
||||
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
|
||||
@@ -348,9 +317,6 @@ static JSValue js_enet_peer_disconnect_later(JSContext *ctx, JSValueConst this_v
|
||||
return JS_NULL;
|
||||
}
|
||||
|
||||
// Reset this peer's connection, immediately dropping it and clearing its internal state.
|
||||
//
|
||||
// :return: None
|
||||
static JSValue js_enet_peer_reset(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
|
||||
{
|
||||
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
|
||||
@@ -359,9 +325,6 @@ static JSValue js_enet_peer_reset(JSContext *ctx, JSValueConst this_val, int arg
|
||||
return JS_NULL;
|
||||
}
|
||||
|
||||
// Send a ping request to this peer to measure latency.
|
||||
//
|
||||
// :return: None
|
||||
static JSValue js_enet_peer_ping(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
|
||||
{
|
||||
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
|
||||
@@ -370,13 +333,6 @@ static JSValue js_enet_peer_ping(JSContext *ctx, JSValueConst this_val, int argc
|
||||
return JS_NULL;
|
||||
}
|
||||
|
||||
// Configure the throttling behavior for this peer, controlling how ENet adjusts its sending
|
||||
// rate based on packet loss or congestion.
|
||||
//
|
||||
// :param interval: The interval (ms) between throttle adjustments.
|
||||
// :param acceleration: The factor to increase sending speed when conditions improve.
|
||||
// :param deceleration: The factor to decrease sending speed when conditions worsen.
|
||||
// :return: None
|
||||
static JSValue js_enet_peer_throttle_configure(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
|
||||
{
|
||||
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
|
||||
@@ -412,12 +368,10 @@ static JSClassDef enet_host = {
|
||||
static JSClassDef enet_peer_class = {
|
||||
"ENetPeer",
|
||||
.finalizer = js_enet_peer_finalizer,
|
||||
// .gc_mark = js_enet_peer_mark
|
||||
};
|
||||
|
||||
JSValue js_enet_resolve_hostname(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
static JSValue js_enet_resolve_hostname(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
{
|
||||
// TODO: implement
|
||||
const char *hostname = JS_ToCString(js, argv[0]);
|
||||
JS_FreeCString(js, hostname);
|
||||
return JS_NULL;
|
||||
@@ -435,18 +389,19 @@ static const JSCFunctionListEntry js_enet_host_funcs[] = {
|
||||
JS_CFUNC_DEF("connect", 2, js_enet_host_connect),
|
||||
JS_CFUNC_DEF("flush", 0, js_enet_host_flush),
|
||||
JS_CFUNC_DEF("broadcast", 1, js_enet_host_broadcast),
|
||||
// JS_CGETSET_DEF("port", js_enet_host_get_port, NULL),
|
||||
// JS_CGETSET_DEF("address", js_enet_host_get_address, NULL),
|
||||
JS_CFUNC0_DEF("port", js_enet_host_get_port),
|
||||
JS_CFUNC0_DEF("address", js_enet_host_get_address),
|
||||
};
|
||||
|
||||
static JSValue js_enet_peer_get_rtt(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
|
||||
// Peer property getters (zero-arg methods)
|
||||
static JSValue js_enet_peer_get_rtt(JSContext *ctx, JSValueConst this_val)
|
||||
{
|
||||
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
|
||||
if (!peer) return JS_EXCEPTION;
|
||||
return JS_NewInt32(ctx, peer->roundTripTime);
|
||||
}
|
||||
|
||||
static JSValue js_enet_peer_get_incoming_bandwidth(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
|
||||
static JSValue js_enet_peer_get_incoming_bandwidth(JSContext *ctx, JSValueConst this_val)
|
||||
{
|
||||
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
|
||||
if (!peer) return JS_EXCEPTION;
|
||||
@@ -454,7 +409,7 @@ static JSValue js_enet_peer_get_incoming_bandwidth(JSContext *ctx, JSValueConst
|
||||
return JS_NewInt32(ctx, peer->incomingBandwidth);
|
||||
}
|
||||
|
||||
static JSValue js_enet_peer_get_outgoing_bandwidth(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
|
||||
static JSValue js_enet_peer_get_outgoing_bandwidth(JSContext *ctx, JSValueConst this_val)
|
||||
{
|
||||
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
|
||||
if (!peer) return JS_EXCEPTION;
|
||||
@@ -462,14 +417,14 @@ static JSValue js_enet_peer_get_outgoing_bandwidth(JSContext *ctx, JSValueConst
|
||||
return JS_NewInt32(ctx, peer->outgoingBandwidth);
|
||||
}
|
||||
|
||||
static JSValue js_enet_peer_get_last_send_time(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
|
||||
static JSValue js_enet_peer_get_last_send_time(JSContext *ctx, JSValueConst this_val)
|
||||
{
|
||||
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
|
||||
if (!peer) return JS_EXCEPTION;
|
||||
return JS_NewInt32(ctx, peer->lastSendTime);
|
||||
}
|
||||
|
||||
static JSValue js_enet_peer_get_last_receive_time(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
|
||||
static JSValue js_enet_peer_get_last_receive_time(JSContext *ctx, JSValueConst this_val)
|
||||
{
|
||||
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
|
||||
if (!peer) return JS_EXCEPTION;
|
||||
@@ -528,16 +483,17 @@ static JSValue js_enet_peer_get_reliable_data_in_transit(JSContext *ctx, JSValue
|
||||
static JSValue js_enet_peer_get_port(JSContext *js, JSValueConst self)
|
||||
{
|
||||
ENetPeer *peer = JS_GetOpaque(self, enet_peer_class_id);
|
||||
if (!peer) return JS_EXCEPTION;
|
||||
return JS_NewUint32(js, peer->address.port);
|
||||
}
|
||||
|
||||
static JSValue js_enet_peer_get_address(JSContext *js, JSValueConst self)
|
||||
{
|
||||
ENetPeer *peer = JS_GetOpaque(self, enet_peer_class_id);
|
||||
if (!peer) return JS_EXCEPTION;
|
||||
char ip_str[128];
|
||||
if (enet_address_get_host_ip(&peer->address, ip_str, sizeof(ip_str)) != 0)
|
||||
return JS_NULL;
|
||||
|
||||
return JS_NewString(js, ip_str);
|
||||
}
|
||||
|
||||
@@ -550,24 +506,26 @@ static const JSCFunctionListEntry js_enet_peer_funcs[] = {
|
||||
JS_CFUNC_DEF("ping", 0, js_enet_peer_ping),
|
||||
JS_CFUNC_DEF("throttle_configure", 3, js_enet_peer_throttle_configure),
|
||||
JS_CFUNC_DEF("timeout", 3, js_enet_peer_timeout),
|
||||
// JS_CGETSET_DEF("rtt", js_enet_peer_get_rtt, NULL),
|
||||
// JS_CGETSET_DEF("incoming_bandwidth", js_enet_peer_get_incoming_bandwidth, NULL),
|
||||
// JS_CGETSET_DEF("outgoing_bandwidth", js_enet_peer_get_outgoing_bandwidth, NULL),
|
||||
// JS_CGETSET_DEF("last_send_time", js_enet_peer_get_last_send_time, NULL),
|
||||
// JS_CGETSET_DEF("last_receive_time", js_enet_peer_get_last_receive_time, NULL),
|
||||
// JS_CGETSET_DEF("mtu", js_enet_peer_get_mtu, NULL),
|
||||
// JS_CGETSET_DEF("outgoing_data_total", js_enet_peer_get_outgoing_data_total, NULL),
|
||||
// JS_CGETSET_DEF("incoming_data_total", js_enet_peer_get_incoming_data_total, NULL),
|
||||
// JS_CGETSET_DEF("rtt_variance", js_enet_peer_get_rtt_variance, NULL),
|
||||
// JS_CGETSET_DEF("packet_loss", js_enet_peer_get_packet_loss, NULL),
|
||||
// JS_CGETSET_DEF("state", js_enet_peer_get_state, NULL),
|
||||
// JS_CGETSET_DEF("reliable_data_in_transit", js_enet_peer_get_reliable_data_in_transit, NULL),
|
||||
// JS_CGETSET_DEF("port", js_enet_peer_get_port, NULL),
|
||||
// JS_CGETSET_DEF("address", js_enet_peer_get_address, NULL),
|
||||
JS_CFUNC0_DEF("rtt", js_enet_peer_get_rtt),
|
||||
JS_CFUNC0_DEF("incoming_bandwidth", js_enet_peer_get_incoming_bandwidth),
|
||||
JS_CFUNC0_DEF("outgoing_bandwidth", js_enet_peer_get_outgoing_bandwidth),
|
||||
JS_CFUNC0_DEF("last_send_time", js_enet_peer_get_last_send_time),
|
||||
JS_CFUNC0_DEF("last_receive_time", js_enet_peer_get_last_receive_time),
|
||||
JS_CFUNC0_DEF("mtu", js_enet_peer_get_mtu),
|
||||
JS_CFUNC0_DEF("outgoing_data_total", js_enet_peer_get_outgoing_data_total),
|
||||
JS_CFUNC0_DEF("incoming_data_total", js_enet_peer_get_incoming_data_total),
|
||||
JS_CFUNC0_DEF("rtt_variance", js_enet_peer_get_rtt_variance),
|
||||
JS_CFUNC0_DEF("packet_loss", js_enet_peer_get_packet_loss),
|
||||
JS_CFUNC0_DEF("state", js_enet_peer_get_state),
|
||||
JS_CFUNC0_DEF("reliable_data_in_transit", js_enet_peer_get_reliable_data_in_transit),
|
||||
JS_CFUNC0_DEF("port", js_enet_peer_get_port),
|
||||
JS_CFUNC0_DEF("address", js_enet_peer_get_address),
|
||||
};
|
||||
|
||||
JSValue js_core_enet_use(JSContext *ctx)
|
||||
JSValue js_core_internal_enet_use(JSContext *ctx)
|
||||
{
|
||||
enet_initialize();
|
||||
|
||||
JS_FRAME(ctx);
|
||||
|
||||
JS_NewClassID(&enet_host_id);
|
||||
1016
internal/engine.cm
1016
internal/engine.cm
File diff suppressed because it is too large
Load Diff
@@ -223,12 +223,10 @@ JSC_SCALL(fd_rmdir,
|
||||
if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
|
||||
JSValue args[2] = { JS_NewString(js, full_path), JS_TRUE };
|
||||
JSValue result = js_fd_rmdir(js, JS_NULL, 2, args);
|
||||
JS_FreeValue(js, args[0]);
|
||||
if (JS_IsException(result)) {
|
||||
FindClose(hFind);
|
||||
return result;
|
||||
}
|
||||
JS_FreeValue(js, result);
|
||||
} else {
|
||||
if (unlink(full_path) != 0) {
|
||||
FindClose(hFind);
|
||||
@@ -252,12 +250,10 @@ JSC_SCALL(fd_rmdir,
|
||||
if (lstat(full_path, &st) == 0 && S_ISDIR(st.st_mode)) {
|
||||
JSValue args[2] = { JS_NewString(js, full_path), JS_TRUE };
|
||||
JSValue result = js_fd_rmdir(js, JS_NULL, 2, args);
|
||||
JS_FreeValue(js, args[0]);
|
||||
if (JS_IsException(result)) {
|
||||
closedir(dir);
|
||||
return result;
|
||||
}
|
||||
JS_FreeValue(js, result);
|
||||
} else {
|
||||
if (unlink(full_path) != 0) {
|
||||
closedir(dir);
|
||||
@@ -640,7 +636,8 @@ static void visit_directory(JSContext *js, JSValue *results, int *result_count,
|
||||
} else {
|
||||
strcpy(item_rel, ffd.cFileName);
|
||||
}
|
||||
JS_SetPropertyNumber(js, *results, (*result_count)++, JS_NewString(js, item_rel));
|
||||
JSValue name_str = JS_NewString(js, item_rel);
|
||||
JS_SetPropertyNumber(js, *results, (*result_count)++, name_str);
|
||||
|
||||
if (recurse) {
|
||||
struct stat st;
|
||||
@@ -665,7 +662,8 @@ static void visit_directory(JSContext *js, JSValue *results, int *result_count,
|
||||
} else {
|
||||
strcpy(item_rel, dir->d_name);
|
||||
}
|
||||
JS_SetPropertyNumber(js, *results, (*result_count)++, JS_NewString(js, item_rel));
|
||||
JSValue name_str = JS_NewString(js, item_rel);
|
||||
JS_SetPropertyNumber(js, *results, (*result_count)++, name_str);
|
||||
|
||||
if (recurse) {
|
||||
struct stat st;
|
||||
@@ -765,6 +763,22 @@ JSC_CCALL(fd_readlink,
|
||||
#endif
|
||||
)
|
||||
|
||||
JSC_CCALL(fd_on_readable,
|
||||
int fd = js2fd(js, argv[0]);
|
||||
if (fd < 0) return JS_EXCEPTION;
|
||||
if (!JS_IsFunction(argv[1]))
|
||||
return JS_RaiseDisrupt(js, "on_readable: callback must be a function");
|
||||
actor_watch_readable(js, fd, argv[1]);
|
||||
return JS_NULL;
|
||||
)
|
||||
|
||||
JSC_CCALL(fd_unwatch,
|
||||
int fd = js2fd(js, argv[0]);
|
||||
if (fd < 0) return JS_EXCEPTION;
|
||||
actor_unwatch(js, fd);
|
||||
return JS_NULL;
|
||||
)
|
||||
|
||||
static const JSCFunctionListEntry js_fd_funcs[] = {
|
||||
MIST_FUNC_DEF(fd, open, 2),
|
||||
MIST_FUNC_DEF(fd, write, 2),
|
||||
@@ -791,6 +805,8 @@ static const JSCFunctionListEntry js_fd_funcs[] = {
|
||||
MIST_FUNC_DEF(fd, symlink, 2),
|
||||
MIST_FUNC_DEF(fd, realpath, 1),
|
||||
MIST_FUNC_DEF(fd, readlink, 1),
|
||||
MIST_FUNC_DEF(fd, on_readable, 2),
|
||||
MIST_FUNC_DEF(fd, unwatch, 1),
|
||||
};
|
||||
|
||||
JSValue js_core_internal_fd_use(JSContext *js) {
|
||||
|
||||
@@ -326,7 +326,6 @@ JSC_SCALL(fd_readdir,
|
||||
|
||||
if (pd_file->listfiles(str, listfiles_cb, &ctx, 0) != 0) {
|
||||
const char* err = pd_file->geterr();
|
||||
JS_FreeValue(js, ret_arr);
|
||||
return JS_RaiseDisrupt(js, "listfiles failed: %s", err ? err : "unknown error");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#define NOTA_IMPLEMENTATION
|
||||
#include "quickjs-internal.h"
|
||||
#include "pit_internal.h"
|
||||
#include "cell.h"
|
||||
|
||||
static int nota_get_arr_len (JSContext *ctx, JSValue arr) {
|
||||
@@ -24,7 +24,7 @@ typedef struct NotaEncodeContext {
|
||||
static void nota_stack_push (NotaEncodeContext *enc, JSValueConst val) {
|
||||
NotaVisitedNode *node = (NotaVisitedNode *)sys_malloc (sizeof (NotaVisitedNode));
|
||||
JS_PushGCRef (enc->ctx, &node->ref);
|
||||
node->ref.val = JS_DupValue (enc->ctx, val);
|
||||
node->ref.val = val;
|
||||
node->next = enc->visited_list;
|
||||
enc->visited_list = node;
|
||||
}
|
||||
@@ -32,7 +32,6 @@ static void nota_stack_push (NotaEncodeContext *enc, JSValueConst val) {
|
||||
static void nota_stack_pop (NotaEncodeContext *enc) {
|
||||
NotaVisitedNode *node = enc->visited_list;
|
||||
enc->visited_list = node->next;
|
||||
JS_FreeValue (enc->ctx, node->ref.val);
|
||||
JS_PopGCRef (enc->ctx, &node->ref);
|
||||
sys_free (node);
|
||||
}
|
||||
@@ -48,14 +47,12 @@ static int nota_stack_has (NotaEncodeContext *enc, JSValueConst val) {
|
||||
}
|
||||
|
||||
static JSValue nota_apply_replacer (NotaEncodeContext *enc, JSValueConst holder, JSValueConst key, JSValueConst val) {
|
||||
if (!enc->replacer_ref || JS_IsNull (enc->replacer_ref->val)) return JS_DupValue (enc->ctx, val);
|
||||
if (!enc->replacer_ref || JS_IsNull (enc->replacer_ref->val)) return val;
|
||||
|
||||
JSValue args[2] = { JS_DupValue (enc->ctx, key), JS_DupValue (enc->ctx, val) };
|
||||
JSValue args[2] = { key, val };
|
||||
JSValue result = JS_Call (enc->ctx, enc->replacer_ref->val, holder, 2, args);
|
||||
JS_FreeValue (enc->ctx, args[0]);
|
||||
JS_FreeValue (enc->ctx, args[1]);
|
||||
|
||||
if (JS_IsException (result)) return JS_DupValue (enc->ctx, val);
|
||||
if (JS_IsException (result)) return val;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -140,15 +137,11 @@ static char *js_do_nota_decode (JSContext *js, JSValue *tmp, char *nota, JSValue
|
||||
}
|
||||
|
||||
if (!JS_IsNull (reviver)) {
|
||||
JSValue args[2] = { JS_DupValue (js, key), JS_DupValue (js, *tmp) };
|
||||
JSValue args[2] = { key, *tmp };
|
||||
JSValue revived = JS_Call (js, reviver, holder, 2, args);
|
||||
JS_FreeValue (js, args[0]);
|
||||
JS_FreeValue (js, args[1]);
|
||||
if (!JS_IsException (revived)) {
|
||||
JS_FreeValue (js, *tmp);
|
||||
*tmp = revived;
|
||||
} else {
|
||||
JS_FreeValue (js, revived);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,10 +222,8 @@ static void nota_encode_value (NotaEncodeContext *enc, JSValueConst val, JSValue
|
||||
if (!JS_IsNull (adata)) {
|
||||
nota_write_sym (&enc->nb, NOTA_PRIVATE);
|
||||
nota_encode_value (enc, adata, replaced_ref.val, JS_NULL);
|
||||
JS_FreeValue (ctx, adata);
|
||||
break;
|
||||
}
|
||||
JS_FreeValue (ctx, adata);
|
||||
if (nota_stack_has (enc, replaced_ref.val)) {
|
||||
enc->cycle = 1;
|
||||
break;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "cell.h"
|
||||
#include "cell_internal.h"
|
||||
#include "pit_internal.h"
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
@@ -308,12 +308,9 @@ JSC_SCALL(os_system,
|
||||
setting pause_flag = 2. Bump turn_gen so stale timer events are
|
||||
ignored, and clear the pause flag so the VM doesn't raise
|
||||
"interrupted" on the next backward branch. */
|
||||
cell_rt *crt = JS_GetContextOpaque(js);
|
||||
if (crt) {
|
||||
atomic_fetch_add_explicit(&crt->turn_gen, 1, memory_order_relaxed);
|
||||
JS_SetPauseFlag(js, 0);
|
||||
crt->turn_start_ns = cell_ns();
|
||||
}
|
||||
atomic_fetch_add_explicit(&js->turn_gen, 1, memory_order_relaxed);
|
||||
JS_SetPauseFlag(js, 0);
|
||||
js->turn_start_ns = cell_ns();
|
||||
ret = number2js(js, err);
|
||||
)
|
||||
|
||||
@@ -706,6 +703,27 @@ static JSValue js_os_stack(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
JS_RETURN(arr.val);
|
||||
}
|
||||
|
||||
static JSValue js_os_unstone(JSContext *js, JSValue self, int argc, JSValue *argv) {
|
||||
if (argc < 1) return JS_NULL;
|
||||
JSValue obj = argv[0];
|
||||
if (mist_is_blob(obj)) {
|
||||
JSBlob *bd = (JSBlob *)chase(obj);
|
||||
bd->mist_hdr = objhdr_set_s(bd->mist_hdr, false);
|
||||
return obj;
|
||||
}
|
||||
if (JS_IsArray(obj)) {
|
||||
JSArray *arr = JS_VALUE_GET_ARRAY(obj);
|
||||
arr->mist_hdr = objhdr_set_s(arr->mist_hdr, false);
|
||||
return obj;
|
||||
}
|
||||
if (mist_is_gc_object(obj)) {
|
||||
JSRecord *rec = JS_VALUE_GET_RECORD(obj);
|
||||
rec->mist_hdr = objhdr_set_s(rec->mist_hdr, false);
|
||||
return obj;
|
||||
}
|
||||
return JS_NULL;
|
||||
}
|
||||
|
||||
static const JSCFunctionListEntry js_os_funcs[] = {
|
||||
MIST_FUNC_DEF(os, platform, 0),
|
||||
MIST_FUNC_DEF(os, arch, 0),
|
||||
@@ -734,6 +752,7 @@ static const JSCFunctionListEntry js_os_funcs[] = {
|
||||
MIST_FUNC_DEF(os, getenv, 1),
|
||||
MIST_FUNC_DEF(os, qbe, 1),
|
||||
MIST_FUNC_DEF(os, stack, 1),
|
||||
MIST_FUNC_DEF(os, unstone, 1),
|
||||
};
|
||||
|
||||
JSValue js_core_internal_os_use(JSContext *js) {
|
||||
|
||||
263
internal/shop.cm
263
internal/shop.cm
@@ -13,8 +13,8 @@ var os = use('internal/os')
|
||||
var link = use('link')
|
||||
|
||||
// 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
|
||||
// analyze, run_ast_fn, core_json, use_cache, core_path, 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
|
||||
@@ -29,21 +29,18 @@ function safe_c_name(name) {
|
||||
|
||||
function pull_from_cache(content)
|
||||
{
|
||||
var path = hash_path(content)
|
||||
if (fd.is_file(path))
|
||||
var path = cache_path(content)
|
||||
if (fd.is_file(path)) {
|
||||
log.system('shop: cache hit')
|
||||
return fd.slurp(path)
|
||||
}
|
||||
}
|
||||
|
||||
function put_into_cache(content, obj)
|
||||
{
|
||||
var path = hash_path(content)
|
||||
var path = cache_path(content)
|
||||
fd.slurpwrite(path, obj)
|
||||
}
|
||||
|
||||
function hash_path(content, salt)
|
||||
{
|
||||
var s = salt || 'mach'
|
||||
return global_shop_path + '/build/' + content_hash(stone(blob(text(content) + '\n' + s)))
|
||||
log.system('shop: cached')
|
||||
}
|
||||
|
||||
var Shop = {}
|
||||
@@ -236,31 +233,34 @@ function safe_canonicalize(pkg) {
|
||||
// given a file, find the absolute path, package name, and import name
|
||||
Shop.file_info = function(file) {
|
||||
var info = {
|
||||
path: file,
|
||||
path: file,
|
||||
is_module: false,
|
||||
is_actor: false,
|
||||
package: null,
|
||||
name: null
|
||||
}
|
||||
|
||||
|
||||
if (ends_with(file, MOD_EXT))
|
||||
info.is_module = true
|
||||
else if (ends_with(file, ACTOR_EXT))
|
||||
info.is_actor = true
|
||||
|
||||
|
||||
// Find package directory and determine package name
|
||||
// find_package_dir resolves symlinks internally, so we must use the
|
||||
// resolved file path for substring math to get the correct name.
|
||||
var pkg_dir = pkg_tools.find_package_dir(file)
|
||||
var resolved_file = fd.realpath(file) || file
|
||||
if (pkg_dir) {
|
||||
info.package = abs_path_to_package(pkg_dir)
|
||||
|
||||
if (info.is_actor)
|
||||
info.name = text(file, length(pkg_dir) + 1, length(file) - length(ACTOR_EXT))
|
||||
info.name = text(resolved_file, length(pkg_dir) + 1, length(resolved_file) - length(ACTOR_EXT))
|
||||
else if (info.is_module)
|
||||
info.name = text(file, length(pkg_dir) + 1, length(file) - length(MOD_EXT))
|
||||
info.name = text(resolved_file, length(pkg_dir) + 1, length(resolved_file) - length(MOD_EXT))
|
||||
else
|
||||
info.name = text(file, length(pkg_dir) + 1)
|
||||
info.name = text(resolved_file, length(pkg_dir) + 1)
|
||||
}
|
||||
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
@@ -455,6 +455,7 @@ Shop.extract_commit_hash = function(pkg, response) {
|
||||
|
||||
var open_dls = {}
|
||||
var package_dylibs = {} // pkg -> [{file, symbol, dylib}, ...]
|
||||
var reload_hashes = {} // cache_key -> content hash for reload change detection
|
||||
|
||||
function open_dylib_cached(path) {
|
||||
var handle = open_dls[path]
|
||||
@@ -504,6 +505,11 @@ function try_native_mod_dylib(pkg, stem) {
|
||||
var handle = open_dylib_cached(build_path)
|
||||
if (!handle) return null
|
||||
var sym = Shop.c_symbol_for_file(pkg, stem)
|
||||
// Verify the symbol actually exists in the dylib before returning native descriptor
|
||||
if (sym && !os.dylib_has_symbol(handle, sym) && !os.dylib_has_symbol(handle, 'cell_main')) {
|
||||
log.shop('native dylib for ' + stem + ' (dylib=' + build_path + ') missing symbol ' + sym + ' and cell_main, falling back to bytecode')
|
||||
return null
|
||||
}
|
||||
return {_native: true, _handle: handle, _sym: sym}
|
||||
}
|
||||
|
||||
@@ -744,6 +750,8 @@ function resolve_mod_fn(path, pkg) {
|
||||
var dylib_path = null
|
||||
var handle = null
|
||||
var sym = null
|
||||
var _fi = null
|
||||
var _fi_pkg = null
|
||||
|
||||
policy = get_policy()
|
||||
|
||||
@@ -755,8 +763,8 @@ function resolve_mod_fn(path, pkg) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for native .cm dylib at deterministic path first
|
||||
if (policy.allow_dylib && pkg && _stem) {
|
||||
// Check for native .cm dylib at deterministic path first (only in native mode)
|
||||
if (policy.native && policy.allow_dylib && pkg && _stem) {
|
||||
native_result = try_native_mod_dylib(pkg, _stem)
|
||||
if (native_result != null) return native_result
|
||||
}
|
||||
@@ -769,7 +777,10 @@ function resolve_mod_fn(path, pkg) {
|
||||
if (dylib_path) {
|
||||
handle = os.dylib_open(dylib_path)
|
||||
if (handle) {
|
||||
sym = pkg && _stem ? Shop.c_symbol_for_file(pkg, _stem) : null
|
||||
// Derive symbol from file_info (authoritative package), not caller's pkg
|
||||
_fi = Shop.file_info(path)
|
||||
_fi_pkg = _fi.package || pkg
|
||||
sym = _fi_pkg ? Shop.c_symbol_for_file(_fi_pkg, (_fi.name ? _fi.name + (_fi.is_actor ? '.ce' : '.cm') : fd.basename(path))) : null
|
||||
return {_native: true, _handle: handle, _sym: sym}
|
||||
}
|
||||
}
|
||||
@@ -786,11 +797,11 @@ function resolve_mod_fn(path, pkg) {
|
||||
|
||||
// Check for cached mcode in content-addressed store
|
||||
if (policy.allow_compile) {
|
||||
cached_mcode_path = hash_path(content_key, 'mcode')
|
||||
cached_mcode_path = cache_path(content_key, 'mcode')
|
||||
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)
|
||||
if (!policy.native) put_into_cache(content_key, compiled)
|
||||
return compiled
|
||||
}
|
||||
}
|
||||
@@ -811,12 +822,14 @@ function resolve_mod_fn(path, pkg) {
|
||||
mcode_json = shop_json.encode(optimized)
|
||||
|
||||
// Cache mcode (architecture-independent) in content-addressed store
|
||||
fd.ensure_dir(global_shop_path + '/build')
|
||||
fd.slurpwrite(cached_mcode_path, stone(blob(mcode_json)))
|
||||
if (!policy.native) {
|
||||
fd.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)
|
||||
if (!policy.native) put_into_cache(content_key, compiled)
|
||||
|
||||
return compiled
|
||||
}
|
||||
@@ -825,6 +838,50 @@ function resolve_mod_fn(path, pkg) {
|
||||
disrupt
|
||||
}
|
||||
|
||||
// Resolve a module's bytecode only (skip native dylib check).
|
||||
// Used as fallback when a cached native dylib fails to load.
|
||||
function resolve_mod_fn_bytecode(path, pkg) {
|
||||
if (!fd.is_file(path)) return null
|
||||
|
||||
var content = text(fd.slurp(path))
|
||||
if (length(content) == 0) return null
|
||||
var content_key = stone(blob(content))
|
||||
var cached = null
|
||||
var cached_mcode_path = null
|
||||
var mcode_json = null
|
||||
var compiled = null
|
||||
|
||||
// Check cache for pre-compiled .mach blob
|
||||
cached = pull_from_cache(content_key)
|
||||
if (cached) return cached
|
||||
|
||||
// Check for cached mcode
|
||||
cached_mcode_path = cache_path(content_key, 'mcode')
|
||||
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 from source
|
||||
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) return null
|
||||
|
||||
var ast = analyze(content, path)
|
||||
var ir = _mcode_mod(ast)
|
||||
var optimized = _streamline_mod(ir)
|
||||
mcode_json = shop_json.encode(optimized)
|
||||
|
||||
fd.ensure_dir(global_shop_path + '/build')
|
||||
fd.slurpwrite(cache_path(content_key, 'mcode'), stone(blob(mcode_json)))
|
||||
|
||||
compiled = mach_compile_mcode_bin(path, mcode_json)
|
||||
put_into_cache(content_key, compiled)
|
||||
return compiled
|
||||
}
|
||||
|
||||
// given a path and a package context
|
||||
// return module info about where it was found
|
||||
// Resolve a module path to {path, scope, pkg} without compiling.
|
||||
@@ -884,7 +941,7 @@ function resolve_path(path, ctx)
|
||||
if (alias) {
|
||||
alias_path = get_packages_dir() + '/' + fd.safe_package_path(alias.package) + '/' + alias.path
|
||||
if (fd.is_file(alias_path))
|
||||
return {path: alias_path, scope: SCOPE_PACKAGE, pkg: ctx}
|
||||
return {path: alias_path, scope: SCOPE_PACKAGE, pkg: alias.package}
|
||||
}
|
||||
|
||||
package_path = get_packages_dir() + '/' + fd.safe_package_path(path)
|
||||
@@ -1292,6 +1349,8 @@ function execute_module(info)
|
||||
var inject = null
|
||||
var env = null
|
||||
var pkg = null
|
||||
var _native_load = null
|
||||
var _bc = null
|
||||
|
||||
if (mod_resolve.scope < 900) {
|
||||
// Check if native dylib was resolved (descriptor with _handle and _sym)
|
||||
@@ -1302,8 +1361,27 @@ function execute_module(info)
|
||||
pkg = file_info.package
|
||||
env.use = make_use_fn(pkg, true)
|
||||
env = stone(env)
|
||||
used = os.native_module_load_named(
|
||||
mod_resolve.symbol._handle, mod_resolve.symbol._sym, env)
|
||||
_native_load = function() {
|
||||
used = os.native_module_load_named(
|
||||
mod_resolve.symbol._handle, mod_resolve.symbol._sym, env)
|
||||
log.shop('loaded ' + info.cache_key + ' [native]')
|
||||
} disruption {
|
||||
// Native load failed — fall back to bytecode
|
||||
log.shop('native load failed for ' + info.cache_key + ' (sym=' + text(mod_resolve.symbol._sym || '') + '), falling back to bytecode')
|
||||
_bc = resolve_mod_fn_bytecode(mod_resolve.path, file_info.package)
|
||||
if (_bc) {
|
||||
// Build a fresh env for bytecode (env is stoned, can't modify)
|
||||
env = inject_env(inject)
|
||||
env.use = make_use_fn(pkg)
|
||||
env = stone(env)
|
||||
used = mach_load(_bc, env)
|
||||
log.shop('loaded ' + info.cache_key + ' [bytecode fallback]')
|
||||
} else {
|
||||
log.error('native load failed and bytecode fallback also failed for ' + info.cache_key)
|
||||
disrupt
|
||||
}
|
||||
}
|
||||
_native_load()
|
||||
} else {
|
||||
// Build env with runtime fns, capabilities, and use function
|
||||
file_info = Shop.file_info(mod_resolve.path)
|
||||
@@ -1315,6 +1393,7 @@ function execute_module(info)
|
||||
|
||||
// Load compiled bytecode with env
|
||||
used = mach_load(mod_resolve.symbol, env)
|
||||
log.shop('loaded ' + info.cache_key + ' [bytecode]')
|
||||
}
|
||||
} else if (c_resolve.scope < 900) {
|
||||
// C only
|
||||
@@ -1323,7 +1402,7 @@ function execute_module(info)
|
||||
log.shop(`Module could not be found (c_resolve scope=${info.c_resolve.scope}, mod_resolve scope=${info.mod_resolve.scope}, cache_key=${info.cache_key})`); disrupt
|
||||
}
|
||||
|
||||
if (!used) { log.error(`Module ${info} returned null`); disrupt }
|
||||
if (!used) { log.error('Module ' + text(info.cache_key || info) + ' returned null'); disrupt }
|
||||
|
||||
return used
|
||||
}
|
||||
@@ -1804,6 +1883,7 @@ Shop.sync_with_deps = function(pkg, opts) {
|
||||
if (visited[current]) continue
|
||||
visited[current] = true
|
||||
|
||||
log.console(' Fetching ' + current + '...')
|
||||
Shop.sync(current, opts)
|
||||
|
||||
_read_deps = function() {
|
||||
@@ -1903,19 +1983,27 @@ Shop.file_reload = function(file)
|
||||
}
|
||||
|
||||
Shop.module_reload = function(path, package) {
|
||||
if (!Shop.is_loaded(path,package)) return
|
||||
if (!Shop.is_loaded(path, package)) return false
|
||||
|
||||
// Clear the module info cache for this path
|
||||
var lookup_key = package ? package + ':' + path : ':' + path
|
||||
module_info_cache[lookup_key] = null
|
||||
var info = resolve_module_info(path, package)
|
||||
if (!info) return false
|
||||
|
||||
// Invalidate package dylib cache so next resolve triggers rebuild
|
||||
if (package) {
|
||||
package_dylibs[package] = null
|
||||
// Check if source actually changed
|
||||
var mod_path = null
|
||||
var source = null
|
||||
var new_hash = null
|
||||
if (info.mod_resolve) mod_path = info.mod_resolve.path
|
||||
if (mod_path && fd.is_file(mod_path)) {
|
||||
source = fd.slurp(mod_path)
|
||||
new_hash = content_hash(stone(blob(text(source))))
|
||||
if (reload_hashes[info.cache_key] == new_hash) return false
|
||||
reload_hashes[info.cache_key] = new_hash
|
||||
}
|
||||
|
||||
var info = resolve_module_info(path, package)
|
||||
if (!info) return
|
||||
// Clear caches
|
||||
module_info_cache[lookup_key] = null
|
||||
if (package) package_dylibs[package] = null
|
||||
|
||||
var cache_key = info.cache_key
|
||||
var old = use_cache[cache_key]
|
||||
@@ -1924,13 +2012,18 @@ Shop.module_reload = function(path, package) {
|
||||
var newmod = get_module(path, package)
|
||||
use_cache[cache_key] = newmod
|
||||
|
||||
// Smart update: unstone -> merge -> re-stone to preserve references
|
||||
if (old && is_object(old) && is_object(newmod)) {
|
||||
os.unstone(old)
|
||||
arrfor(array(newmod), function(k) { old[k] = newmod[k] })
|
||||
arrfor(array(old), function(k) {
|
||||
if (!(k in newmod)) old[k] = null
|
||||
})
|
||||
stone(old)
|
||||
use_cache[cache_key] = old
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function get_package_scripts(package)
|
||||
@@ -1994,6 +2087,12 @@ Shop.build_package_scripts = function(package)
|
||||
_try()
|
||||
})
|
||||
|
||||
if (length(errors) > 0) {
|
||||
log.console(' Compiling scripts (' + text(ok) + ' ok, ' + text(length(errors)) + ' errors)')
|
||||
} else if (ok > 0) {
|
||||
log.console(' Compiling scripts (' + text(ok) + ' ok)')
|
||||
}
|
||||
|
||||
return {ok: ok, errors: errors, total: length(scripts)}
|
||||
}
|
||||
|
||||
@@ -2070,6 +2169,7 @@ Shop.get_lib_dir = function() {
|
||||
Shop.ensure_dir = fd.ensure_dir
|
||||
Shop.install_zip = install_zip
|
||||
Shop.ensure_package_dylibs = ensure_package_dylibs
|
||||
Shop.resolve_path = resolve_path
|
||||
|
||||
Shop.get_local_dir = function() {
|
||||
return global_shop_path + "/local"
|
||||
@@ -2136,7 +2236,7 @@ Shop.load_as_mach = function(path, pkg) {
|
||||
|
||||
// Try cached mcode -> compile to mach
|
||||
if (!compiled) {
|
||||
cached_mcode_path = hash_path(content_key, 'mcode')
|
||||
cached_mcode_path = cache_path(content_key, 'mcode')
|
||||
if (fd.is_file(cached_mcode_path)) {
|
||||
mcode_json = text(fd.slurp(cached_mcode_path))
|
||||
compiled = mach_compile_mcode_bin(file_path, mcode_json)
|
||||
@@ -2156,7 +2256,7 @@ Shop.load_as_mach = function(path, pkg) {
|
||||
ir = _mcode_mod(ast)
|
||||
optimized = _streamline_mod(ir)
|
||||
mcode_json = shop_json.encode(optimized)
|
||||
cached_mcode_path = hash_path(content_key, 'mcode')
|
||||
cached_mcode_path = cache_path(content_key, 'mcode')
|
||||
fd.ensure_dir(global_shop_path + '/build')
|
||||
fd.slurpwrite(cached_mcode_path, stone(blob(mcode_json)))
|
||||
compiled = mach_compile_mcode_bin(file_path, mcode_json)
|
||||
@@ -2209,6 +2309,80 @@ Shop.load_as_dylib = function(path, pkg) {
|
||||
return os.native_module_load_named(result._handle, result._sym, env)
|
||||
}
|
||||
|
||||
// Trace all transitive module dependencies for a file.
|
||||
// Returns {scripts: [{path, package}], c_packages: [string]}
|
||||
Shop.trace_deps = function(file_path) {
|
||||
var visited = {}
|
||||
var scripts = []
|
||||
var c_packages = {}
|
||||
|
||||
function trace(fp) {
|
||||
if (visited[fp]) return
|
||||
visited[fp] = true
|
||||
var fi = Shop.file_info(fp)
|
||||
var file_pkg = fi.package
|
||||
var idx = null
|
||||
var j = 0
|
||||
var imp = null
|
||||
var rinfo = null
|
||||
if (ends_with(fp, '.cm'))
|
||||
scripts[] = {path: fp, package: file_pkg}
|
||||
var _trace = function() {
|
||||
idx = Shop.index_file(fp)
|
||||
if (!idx || !idx.imports) return
|
||||
j = 0
|
||||
while (j < length(idx.imports)) {
|
||||
imp = idx.imports[j]
|
||||
rinfo = Shop.resolve_import_info(imp.module_path, file_pkg)
|
||||
if (rinfo) {
|
||||
if (rinfo.type == 'script' && rinfo.resolved_path)
|
||||
trace(rinfo.resolved_path)
|
||||
else if (rinfo.type == 'native' && rinfo.package)
|
||||
c_packages[rinfo.package] = true
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
} disruption {}
|
||||
_trace()
|
||||
}
|
||||
|
||||
trace(file_path)
|
||||
return {scripts: scripts, c_packages: array(c_packages)}
|
||||
}
|
||||
|
||||
// Check if a C package has a build manifest (was previously built)
|
||||
Shop.has_c_manifest = function(pkg) {
|
||||
return fd.is_file(dylib_manifest_path(pkg))
|
||||
}
|
||||
|
||||
// Check if a .cm file has a cached bytecode artifact (mach or mcode)
|
||||
Shop.is_cached = function(path) {
|
||||
if (!fd.is_file(path)) return false
|
||||
var content_key = stone(blob(text(fd.slurp(path))))
|
||||
if (fd.is_file(cache_path(content_key, 'mach'))) return true
|
||||
if (fd.is_file(cache_path(content_key, 'mcode'))) return true
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if a .cm file has a cached native dylib artifact
|
||||
Shop.is_native_cached = function(path, pkg) {
|
||||
var build_mod = use_cache['core/build']
|
||||
if (!build_mod || !fd.is_file(path)) return false
|
||||
var src = text(fd.slurp(path))
|
||||
var host = detect_host_target()
|
||||
if (!host) return false
|
||||
var san_flags = build_mod.native_sanitize_flags ? build_mod.native_sanitize_flags() : ''
|
||||
var native_key = build_mod.native_cache_content ?
|
||||
build_mod.native_cache_content(src, host, san_flags) :
|
||||
(src + '\n' + host)
|
||||
return fd.is_file(build_mod.cache_path(native_key, build_mod.SALT_NATIVE))
|
||||
}
|
||||
|
||||
// Compile + cache a module without executing it
|
||||
Shop.precompile = function(path, pkg) {
|
||||
resolve_mod_fn(path, pkg)
|
||||
}
|
||||
|
||||
Shop.audit_packages = function() {
|
||||
var packages = Shop.list_packages()
|
||||
|
||||
@@ -2259,7 +2433,7 @@ Shop.use_native = function(path, package_context) {
|
||||
if (!starts_with(path, '/') && !fd.is_file(path)) {
|
||||
lookup = ends_with(path, '.cm') ? path : path + '.cm'
|
||||
locator = resolve_locator(lookup, package_context)
|
||||
if (!locator) { print('Module not found: ' + path); disrupt }
|
||||
if (!locator) { log.error('use_native: module not found: ' + path + ' (package: ' + text(package_context || '') + ')'); disrupt }
|
||||
src_path = locator.path
|
||||
} else if (!starts_with(path, '/')) {
|
||||
src_path = fd.realpath(path)
|
||||
@@ -2268,17 +2442,12 @@ Shop.use_native = function(path, package_context) {
|
||||
|
||||
var file_info = Shop.file_info(src_path)
|
||||
var pkg = file_info.package || (locator ? locator.pkg : package_context)
|
||||
var sym_stem = fd.basename(src_path)
|
||||
var pkg_dir = null
|
||||
var sym_stem = file_info.name ? file_info.name + (file_info.is_actor ? '.ce' : '.cm') : fd.basename(src_path)
|
||||
cache_key = 'native:' + text(pkg || '') + ':' + src_path
|
||||
if (use_cache[cache_key]) return use_cache[cache_key]
|
||||
|
||||
var sym_name = null
|
||||
if (pkg) {
|
||||
pkg_dir = get_packages_dir() + '/' + safe_package_path(pkg)
|
||||
if (starts_with(src_path, pkg_dir + '/')) {
|
||||
sym_stem = text(src_path, length(pkg_dir) + 1)
|
||||
}
|
||||
sym_name = Shop.c_symbol_for_file(pkg, sym_stem)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#define WOTA_IMPLEMENTATION
|
||||
#include "quickjs-internal.h"
|
||||
#include "pit_internal.h"
|
||||
#include "cell.h"
|
||||
|
||||
typedef struct ObjectRef {
|
||||
@@ -41,13 +41,11 @@ static void wota_stack_free (WotaEncodeContext *enc) {
|
||||
}
|
||||
|
||||
static JSValue wota_apply_replacer (WotaEncodeContext *enc, JSValueConst holder, JSValue key, JSValueConst val) {
|
||||
if (JS_IsNull (enc->replacer)) return JS_DupValue (enc->ctx, val);
|
||||
JSValue key_val = JS_IsNull (key) ? JS_NULL : JS_DupValue (enc->ctx, key);
|
||||
JSValue args[2] = { key_val, JS_DupValue (enc->ctx, val) };
|
||||
if (JS_IsNull (enc->replacer)) return val;
|
||||
JSValue key_val = JS_IsNull (key) ? JS_NULL : key;
|
||||
JSValue args[2] = { key_val, val };
|
||||
JSValue result = JS_Call (enc->ctx, enc->replacer, holder, 2, args);
|
||||
JS_FreeValue (enc->ctx, args[0]);
|
||||
JS_FreeValue (enc->ctx, args[1]);
|
||||
if (JS_IsException (result)) return JS_DupValue (enc->ctx, val);
|
||||
if (JS_IsException (result)) return val;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -60,20 +58,17 @@ static void encode_object_properties (WotaEncodeContext *enc, JSValueConst val,
|
||||
JSGCRef val_ref, keys_ref;
|
||||
JS_PushGCRef (ctx, &val_ref);
|
||||
JS_PushGCRef (ctx, &keys_ref);
|
||||
val_ref.val = JS_DupValue (ctx, val);
|
||||
val_ref.val = val;
|
||||
|
||||
keys_ref.val = JS_GetOwnPropertyNames (ctx, val_ref.val);
|
||||
if (JS_IsException (keys_ref.val)) {
|
||||
wota_write_sym (&enc->wb, WOTA_NULL);
|
||||
JS_FreeValue (ctx, val_ref.val);
|
||||
JS_PopGCRef (ctx, &keys_ref);
|
||||
JS_PopGCRef (ctx, &val_ref);
|
||||
return;
|
||||
}
|
||||
int64_t plen64;
|
||||
if (JS_GetLength (ctx, keys_ref.val, &plen64) < 0) {
|
||||
JS_FreeValue (ctx, keys_ref.val);
|
||||
JS_FreeValue (ctx, val_ref.val);
|
||||
wota_write_sym (&enc->wb, WOTA_NULL);
|
||||
JS_PopGCRef (ctx, &keys_ref);
|
||||
JS_PopGCRef (ctx, &val_ref);
|
||||
@@ -105,12 +100,9 @@ static void encode_object_properties (WotaEncodeContext *enc, JSValueConst val,
|
||||
prop_refs[non_function_count].val = prop_val;
|
||||
non_function_count++;
|
||||
} else {
|
||||
JS_FreeValue (ctx, prop_val);
|
||||
JS_FreeValue (ctx, key_refs[i].val);
|
||||
key_refs[i].val = JS_NULL;
|
||||
}
|
||||
}
|
||||
JS_FreeValue (ctx, keys_ref.val);
|
||||
wota_write_record (&enc->wb, non_function_count);
|
||||
for (uint32_t i = 0; i < non_function_count; i++) {
|
||||
size_t klen;
|
||||
@@ -118,8 +110,6 @@ static void encode_object_properties (WotaEncodeContext *enc, JSValueConst val,
|
||||
wota_write_text_len (&enc->wb, prop_name ? prop_name : "", prop_name ? klen : 0);
|
||||
wota_encode_value (enc, prop_refs[i].val, val_ref.val, key_refs[i].val);
|
||||
JS_FreeCString (ctx, prop_name);
|
||||
JS_FreeValue (ctx, prop_refs[i].val);
|
||||
JS_FreeValue (ctx, key_refs[i].val);
|
||||
}
|
||||
/* Pop all GC refs in reverse order */
|
||||
for (int i = plen - 1; i >= 0; i--) {
|
||||
@@ -128,7 +118,6 @@ static void encode_object_properties (WotaEncodeContext *enc, JSValueConst val,
|
||||
}
|
||||
sys_free (prop_refs);
|
||||
sys_free (key_refs);
|
||||
JS_FreeValue (ctx, val_ref.val);
|
||||
JS_PopGCRef (ctx, &keys_ref);
|
||||
JS_PopGCRef (ctx, &val_ref);
|
||||
}
|
||||
@@ -139,7 +128,7 @@ static void wota_encode_value (WotaEncodeContext *enc, JSValueConst val, JSValue
|
||||
if (!JS_IsNull (enc->replacer) && !JS_IsNull (key))
|
||||
replaced = wota_apply_replacer (enc, holder, key, val);
|
||||
else
|
||||
replaced = JS_DupValue (enc->ctx, val);
|
||||
replaced = val;
|
||||
|
||||
int tag = JS_VALUE_GET_TAG (replaced);
|
||||
switch (tag) {
|
||||
@@ -183,7 +172,6 @@ static void wota_encode_value (WotaEncodeContext *enc, JSValueConst val, JSValue
|
||||
size_t buf_len;
|
||||
void *buf_data = js_get_blob_data (ctx, &buf_len, replaced);
|
||||
if (buf_data == (void *)-1) {
|
||||
JS_FreeValue (ctx, replaced);
|
||||
return;
|
||||
}
|
||||
if (buf_len == 0) {
|
||||
@@ -205,7 +193,6 @@ static void wota_encode_value (WotaEncodeContext *enc, JSValueConst val, JSValue
|
||||
for (int64_t i = 0; i < arr_len; i++) {
|
||||
JSValue elem_val = JS_GetPropertyNumber (ctx, replaced, i);
|
||||
wota_encode_value (enc, elem_val, replaced, JS_NewInt32 (ctx, (int32_t)i));
|
||||
JS_FreeValue (ctx, elem_val);
|
||||
}
|
||||
wota_stack_pop (enc);
|
||||
break;
|
||||
@@ -218,10 +205,8 @@ static void wota_encode_value (WotaEncodeContext *enc, JSValueConst val, JSValue
|
||||
if (!JS_IsNull (adata)) {
|
||||
wota_write_sym (&enc->wb, WOTA_PRIVATE);
|
||||
wota_encode_value (enc, adata, replaced, JS_NULL);
|
||||
JS_FreeValue (ctx, adata);
|
||||
break;
|
||||
}
|
||||
JS_FreeValue (ctx, adata);
|
||||
if (wota_stack_has (enc, replaced)) {
|
||||
enc->cycle = 1;
|
||||
break;
|
||||
@@ -230,16 +215,13 @@ static void wota_encode_value (WotaEncodeContext *enc, JSValueConst val, JSValue
|
||||
JSValue to_json = JS_GetPropertyStr (ctx, replaced, "toJSON");
|
||||
if (JS_IsFunction (to_json)) {
|
||||
JSValue result = JS_Call (ctx, to_json, replaced, 0, NULL);
|
||||
JS_FreeValue (ctx, to_json);
|
||||
if (!JS_IsException (result)) {
|
||||
wota_encode_value (enc, result, holder, key);
|
||||
JS_FreeValue (ctx, result);
|
||||
} else
|
||||
wota_write_sym (&enc->wb, WOTA_NULL);
|
||||
wota_stack_pop (enc);
|
||||
break;
|
||||
}
|
||||
JS_FreeValue (ctx, to_json);
|
||||
encode_object_properties (enc, replaced, holder);
|
||||
wota_stack_pop (enc);
|
||||
break;
|
||||
@@ -248,7 +230,6 @@ static void wota_encode_value (WotaEncodeContext *enc, JSValueConst val, JSValue
|
||||
wota_write_sym (&enc->wb, WOTA_NULL);
|
||||
break;
|
||||
}
|
||||
JS_FreeValue (ctx, replaced);
|
||||
}
|
||||
|
||||
static char *decode_wota_value (JSContext *ctx, char *data_ptr, JSValue *out_val, JSValue holder, JSValue key, JSValue reviver) {
|
||||
@@ -355,16 +336,12 @@ static char *decode_wota_value (JSContext *ctx, char *data_ptr, JSValue *out_val
|
||||
break;
|
||||
}
|
||||
if (!JS_IsNull (reviver)) {
|
||||
JSValue key_val = JS_IsNull (key) ? JS_NULL : JS_DupValue (ctx, key);
|
||||
JSValue args[2] = { key_val, JS_DupValue (ctx, *out_val) };
|
||||
JSValue key_val = JS_IsNull (key) ? JS_NULL : key;
|
||||
JSValue args[2] = { key_val, *out_val };
|
||||
JSValue revived = JS_Call (ctx, reviver, holder, 2, args);
|
||||
JS_FreeValue (ctx, args[0]);
|
||||
JS_FreeValue (ctx, args[1]);
|
||||
if (!JS_IsException (revived)) {
|
||||
JS_FreeValue (ctx, *out_val);
|
||||
*out_val = revived;
|
||||
} else
|
||||
JS_FreeValue (ctx, revived);
|
||||
}
|
||||
}
|
||||
return data_ptr;
|
||||
}
|
||||
|
||||
494
log.ce
494
log.ce
@@ -1,15 +1,17 @@
|
||||
// cell log - Manage and read log sinks
|
||||
// cell log - Manage log sink configuration
|
||||
//
|
||||
// Usage:
|
||||
// cell log list List configured sinks
|
||||
// cell log list Show sinks and channel routing
|
||||
// cell log channels List channels with status
|
||||
// cell log enable <channel> Enable a channel on terminal
|
||||
// cell log disable <channel> Disable a channel on terminal
|
||||
// cell log add <name> console [opts] Add a console sink
|
||||
// cell log add <name> file <path> [opts] Add a file sink
|
||||
// cell log remove <name> Remove a sink
|
||||
// cell log read <sink> [opts] Read from a file sink
|
||||
// cell log tail <sink> [--lines=N] Follow a file sink
|
||||
//
|
||||
// The --stack option controls which channels capture a stack trace.
|
||||
// Default: --stack=error (errors always show a stack trace).
|
||||
// cell log route <channel> <sink> Route a channel to a sink
|
||||
// cell log unroute <channel> <sink> Remove a channel from a sink
|
||||
// cell log stack <channel> Enable stack traces on a channel
|
||||
// cell log unstack <channel> Disable stack traces on a channel
|
||||
|
||||
var toml = use('toml')
|
||||
var fd = use('fd')
|
||||
@@ -18,9 +20,8 @@ var json = use('json')
|
||||
var log_path = shop_path + '/log.toml'
|
||||
|
||||
function load_config() {
|
||||
if (fd.is_file(log_path)) {
|
||||
if (fd.is_file(log_path))
|
||||
return toml.decode(text(fd.slurp(log_path)))
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -45,23 +46,24 @@ function print_help() {
|
||||
log.console("Usage: cell log <command> [options]")
|
||||
log.console("")
|
||||
log.console("Commands:")
|
||||
log.console(" list List configured sinks")
|
||||
log.console(" list Show sinks and channel routing")
|
||||
log.console(" channels List channels with status")
|
||||
log.console(" enable <channel> Enable a channel on terminal")
|
||||
log.console(" disable <channel> Disable a channel on terminal")
|
||||
log.console(" add <name> console [opts] Add a console sink")
|
||||
log.console(" add <name> file <path> [opts] Add a file sink")
|
||||
log.console(" remove <name> Remove a sink")
|
||||
log.console(" read <sink> [opts] Read from a file sink")
|
||||
log.console(" tail <sink> [--lines=N] Follow a file sink")
|
||||
log.console(" route <channel> <sink> Route a channel to a sink")
|
||||
log.console(" unroute <channel> <sink> Remove a channel from a sink")
|
||||
log.console(" stack <channel> Enable stack traces on a channel")
|
||||
log.console(" unstack <channel> Disable stack traces on a channel")
|
||||
log.console("")
|
||||
log.console("Options for add:")
|
||||
log.console(" --format=pretty|bare|json Output format (default: pretty for console, json for file)")
|
||||
log.console(" --channels=ch1,ch2 Channels to subscribe (default: console,error,system)")
|
||||
log.console(" --exclude=ch1,ch2 Channels to exclude (for wildcard sinks)")
|
||||
log.console(" --stack=ch1,ch2 Channels that capture a stack trace (default: error)")
|
||||
log.console("")
|
||||
log.console("Options for read:")
|
||||
log.console(" --lines=N Show last N lines (default: all)")
|
||||
log.console(" --channel=X Filter by channel")
|
||||
log.console(" --since=timestamp Only show entries after timestamp")
|
||||
log.console(" --channels=ch1,ch2 Channels to subscribe (default: *)")
|
||||
log.console(" --exclude=ch1,ch2 Channels to exclude")
|
||||
log.console(" --mode=append|overwrite File write mode (default: append)")
|
||||
log.console(" --max_size=N Max file size in bytes before truncation")
|
||||
}
|
||||
|
||||
function parse_opt(arg, prefix) {
|
||||
@@ -71,36 +73,85 @@ function parse_opt(arg, prefix) {
|
||||
return null
|
||||
}
|
||||
|
||||
function format_entry(entry) {
|
||||
var aid = text(entry.actor_id, 0, 5)
|
||||
var src = ""
|
||||
var ev = null
|
||||
if (entry.source && entry.source.file)
|
||||
src = entry.source.file + ":" + text(entry.source.line)
|
||||
ev = is_text(entry.event) ? entry.event : json.encode(entry.event)
|
||||
return "[" + aid + "] [" + entry.channel + "] " + src + " " + ev
|
||||
// Collect all stack channels across all sinks
|
||||
function collect_stack_channels(config) {
|
||||
var stack_chs = {}
|
||||
var names = array(config.sink)
|
||||
arrfor(names, function(n) {
|
||||
var s = config.sink[n]
|
||||
if (is_array(s.stack)) {
|
||||
arrfor(s.stack, function(ch) { stack_chs[ch] = true })
|
||||
}
|
||||
})
|
||||
return stack_chs
|
||||
}
|
||||
|
||||
// Find which sinks a stack channel is declared on (for modification)
|
||||
function find_stack_sink(config, channel) {
|
||||
var names = array(config.sink)
|
||||
var found = null
|
||||
arrfor(names, function(n) {
|
||||
if (found) return
|
||||
var s = config.sink[n]
|
||||
if (is_array(s.stack)) {
|
||||
arrfor(s.stack, function(ch) {
|
||||
if (ch == channel) found = n
|
||||
})
|
||||
}
|
||||
})
|
||||
return found
|
||||
}
|
||||
|
||||
function do_list() {
|
||||
var config = load_config()
|
||||
var names = null
|
||||
var channel_routing = {}
|
||||
var stack_chs = null
|
||||
names = (config && config.sink) ? array(config.sink) : []
|
||||
if (length(names) == 0) {
|
||||
log.console("No log sinks configured.")
|
||||
log.console("Default: console pretty for console/error/system (stack traces on error)")
|
||||
return
|
||||
}
|
||||
|
||||
// Show sinks
|
||||
log.console("Sinks:")
|
||||
arrfor(names, function(n) {
|
||||
var s = config.sink[n]
|
||||
var ch = is_array(s.channels) ? text(s.channels, ', ') : '(none)'
|
||||
var ex = is_array(s.exclude) ? " exclude=" + text(s.exclude, ',') : ""
|
||||
var stk = is_array(s.stack) ? " stack=" + text(s.stack, ',') : ""
|
||||
var fmt = s.format || (s.type == 'file' ? 'json' : 'pretty')
|
||||
var mode = s.mode ? " mode=" + s.mode : ""
|
||||
var maxsz = s.max_size ? " max_size=" + text(s.max_size) : ""
|
||||
var ex = is_array(s.exclude) ? " exclude=" + text(s.exclude, ',') : ""
|
||||
if (s.type == 'file')
|
||||
log.console(" " + n + ": " + s.type + " -> " + s.path + " [" + ch + "] format=" + fmt + ex + stk)
|
||||
log.console(" " + n + ": file -> " + s.path + " format=" + fmt + mode + maxsz)
|
||||
else
|
||||
log.console(" " + n + ": " + s.type + " [" + ch + "] format=" + fmt + ex + stk)
|
||||
log.console(" " + n + ": console format=" + fmt + ex)
|
||||
})
|
||||
|
||||
// Build channel -> sinks map
|
||||
arrfor(names, function(n) {
|
||||
var s = config.sink[n]
|
||||
var chs = is_array(s.channels) ? s.channels : []
|
||||
arrfor(chs, function(ch) {
|
||||
if (!channel_routing[ch]) channel_routing[ch] = []
|
||||
channel_routing[ch][] = n
|
||||
})
|
||||
})
|
||||
|
||||
// Show routing
|
||||
log.console("")
|
||||
log.console("Routing:")
|
||||
var channels = array(channel_routing)
|
||||
arrfor(channels, function(ch) {
|
||||
log.console(" " + ch + " -> " + text(channel_routing[ch], ', '))
|
||||
})
|
||||
|
||||
// Show stack traces
|
||||
stack_chs = collect_stack_channels(config)
|
||||
var stack_list = array(stack_chs)
|
||||
if (length(stack_list) > 0) {
|
||||
log.console("")
|
||||
log.console("Stack traces on: " + text(stack_list, ', '))
|
||||
}
|
||||
}
|
||||
|
||||
function do_add() {
|
||||
@@ -108,14 +159,15 @@ function do_add() {
|
||||
var sink_type = null
|
||||
var path = null
|
||||
var format = null
|
||||
var channels = ["console", "error", "system"]
|
||||
var channels = ["*"]
|
||||
var exclude = null
|
||||
var stack_chs = ["error"]
|
||||
var mode = null
|
||||
var max_size = null
|
||||
var config = null
|
||||
var val = null
|
||||
var i = 0
|
||||
if (length(args) < 3) {
|
||||
log.error("Usage: cell log add <name> console|file [path] [options]")
|
||||
log.console("Usage: cell log add <name> console|file [path] [options]")
|
||||
return
|
||||
}
|
||||
name = args[1]
|
||||
@@ -123,7 +175,7 @@ function do_add() {
|
||||
|
||||
if (sink_type == 'file') {
|
||||
if (length(args) < 4) {
|
||||
log.error("Usage: cell log add <name> file <path> [options]")
|
||||
log.console("Usage: cell log add <name> file <path> [options]")
|
||||
return
|
||||
}
|
||||
path = args[3]
|
||||
@@ -133,7 +185,7 @@ function do_add() {
|
||||
format = "pretty"
|
||||
i = 3
|
||||
} else {
|
||||
log.error("Unknown sink type: " + sink_type + " (use 'console' or 'file')")
|
||||
log.console("Unknown sink type: " + sink_type + " (use 'console' or 'file')")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -144,17 +196,21 @@ function do_add() {
|
||||
if (val) { channels = array(val, ','); continue }
|
||||
val = parse_opt(args[i], 'exclude')
|
||||
if (val) { exclude = array(val, ','); continue }
|
||||
val = parse_opt(args[i], 'stack')
|
||||
if (val) { stack_chs = array(val, ','); continue }
|
||||
val = parse_opt(args[i], 'mode')
|
||||
if (val) { mode = val; continue }
|
||||
val = parse_opt(args[i], 'max_size')
|
||||
if (val) { max_size = number(val); continue }
|
||||
}
|
||||
|
||||
config = load_config()
|
||||
if (!config) config = {}
|
||||
if (!config.sink) config.sink = {}
|
||||
|
||||
config.sink[name] = {type: sink_type, format: format, channels: channels, stack: stack_chs}
|
||||
config.sink[name] = {type: sink_type, format: format, channels: channels}
|
||||
if (path) config.sink[name].path = path
|
||||
if (exclude) config.sink[name].exclude = exclude
|
||||
if (mode) config.sink[name].mode = mode
|
||||
if (max_size) config.sink[name].max_size = max_size
|
||||
|
||||
save_config(config)
|
||||
log.console("Added sink: " + name)
|
||||
@@ -164,13 +220,13 @@ function do_remove() {
|
||||
var name = null
|
||||
var config = null
|
||||
if (length(args) < 2) {
|
||||
log.error("Usage: cell log remove <name>")
|
||||
log.console("Usage: cell log remove <name>")
|
||||
return
|
||||
}
|
||||
name = args[1]
|
||||
config = load_config()
|
||||
if (!config || !config.sink || !config.sink[name]) {
|
||||
log.error("Sink not found: " + name)
|
||||
log.console("Sink not found: " + name)
|
||||
return
|
||||
}
|
||||
delete config.sink[name]
|
||||
@@ -178,154 +234,242 @@ function do_remove() {
|
||||
log.console("Removed sink: " + name)
|
||||
}
|
||||
|
||||
function do_read() {
|
||||
var name = null
|
||||
var max_lines = 0
|
||||
var filter_channel = null
|
||||
var since = 0
|
||||
function do_route() {
|
||||
var channel = null
|
||||
var sink_name = null
|
||||
var config = null
|
||||
var sink = null
|
||||
var content = null
|
||||
var lines = null
|
||||
var entries = []
|
||||
var entry = null
|
||||
var val = null
|
||||
var i = 0
|
||||
|
||||
if (length(args) < 2) {
|
||||
log.error("Usage: cell log read <sink_name> [options]")
|
||||
var already = false
|
||||
if (length(args) < 3) {
|
||||
log.console("Usage: cell log route <channel> <sink>")
|
||||
return
|
||||
}
|
||||
name = args[1]
|
||||
|
||||
for (i = 2; i < length(args); i++) {
|
||||
val = parse_opt(args[i], 'lines')
|
||||
if (val) { max_lines = number(val); continue }
|
||||
val = parse_opt(args[i], 'channel')
|
||||
if (val) { filter_channel = val; continue }
|
||||
val = parse_opt(args[i], 'since')
|
||||
if (val) { since = number(val); continue }
|
||||
}
|
||||
|
||||
channel = args[1]
|
||||
sink_name = args[2]
|
||||
config = load_config()
|
||||
if (!config || !config.sink || !config.sink[name]) {
|
||||
log.error("Sink not found: " + name)
|
||||
if (!config || !config.sink || !config.sink[sink_name]) {
|
||||
log.console("Sink not found: " + sink_name)
|
||||
return
|
||||
}
|
||||
sink = config.sink[name]
|
||||
if (sink.type != 'file') {
|
||||
log.error("Can only read from file sinks")
|
||||
return
|
||||
}
|
||||
if (!fd.is_file(sink.path)) {
|
||||
log.console("Log file does not exist yet: " + sink.path)
|
||||
return
|
||||
}
|
||||
|
||||
content = text(fd.slurp(sink.path))
|
||||
lines = array(content, '\n')
|
||||
|
||||
arrfor(lines, function(line) {
|
||||
var parse_fn = null
|
||||
if (length(line) == 0) return
|
||||
parse_fn = function() {
|
||||
entry = json.decode(line)
|
||||
} disruption {
|
||||
entry = null
|
||||
}
|
||||
parse_fn()
|
||||
if (!entry) return
|
||||
if (filter_channel && entry.channel != filter_channel) return
|
||||
if (since > 0 && entry.timestamp < since) return
|
||||
entries[] = entry
|
||||
})
|
||||
|
||||
if (max_lines > 0 && length(entries) > max_lines)
|
||||
entries = array(entries, length(entries) - max_lines, length(entries))
|
||||
|
||||
arrfor(entries, function(e) {
|
||||
log.console(format_entry(e))
|
||||
sink = config.sink[sink_name]
|
||||
if (!is_array(sink.channels)) sink.channels = []
|
||||
arrfor(sink.channels, function(ch) {
|
||||
if (ch == channel) already = true
|
||||
})
|
||||
if (already) {
|
||||
log.console(channel + " already routed to " + sink_name)
|
||||
return
|
||||
}
|
||||
sink.channels[] = channel
|
||||
save_config(config)
|
||||
log.console(channel + " -> " + sink_name)
|
||||
}
|
||||
|
||||
function do_tail() {
|
||||
var name = null
|
||||
var tail_lines = 10
|
||||
function do_unroute() {
|
||||
var channel = null
|
||||
var sink_name = null
|
||||
var config = null
|
||||
var sink = null
|
||||
var last_size = 0
|
||||
var val = null
|
||||
var i = 0
|
||||
|
||||
if (length(args) < 2) {
|
||||
log.error("Usage: cell log tail <sink_name> [--lines=N]")
|
||||
var found = false
|
||||
if (length(args) < 3) {
|
||||
log.console("Usage: cell log unroute <channel> <sink>")
|
||||
return
|
||||
}
|
||||
name = args[1]
|
||||
|
||||
for (i = 2; i < length(args); i++) {
|
||||
val = parse_opt(args[i], 'lines')
|
||||
if (val) { tail_lines = number(val); continue }
|
||||
}
|
||||
|
||||
channel = args[1]
|
||||
sink_name = args[2]
|
||||
config = load_config()
|
||||
if (!config || !config.sink || !config.sink[name]) {
|
||||
log.error("Sink not found: " + name)
|
||||
if (!config || !config.sink || !config.sink[sink_name]) {
|
||||
log.console("Sink not found: " + sink_name)
|
||||
return
|
||||
}
|
||||
sink = config.sink[name]
|
||||
if (sink.type != 'file') {
|
||||
log.error("Can only tail file sinks")
|
||||
sink = config.sink[sink_name]
|
||||
if (!is_array(sink.channels)) sink.channels = []
|
||||
sink.channels = filter(sink.channels, function(ch) { return ch != channel })
|
||||
save_config(config)
|
||||
log.console(channel + " removed from " + sink_name)
|
||||
}
|
||||
|
||||
function do_stack() {
|
||||
var channel = null
|
||||
var config = null
|
||||
var names = null
|
||||
var added = false
|
||||
if (length(args) < 2) {
|
||||
log.console("Usage: cell log stack <channel>")
|
||||
return
|
||||
}
|
||||
if (!fd.is_file(sink.path))
|
||||
log.console("Waiting for log file: " + sink.path)
|
||||
|
||||
function poll() {
|
||||
var st = null
|
||||
var poll_content = null
|
||||
var poll_lines = null
|
||||
var start = 0
|
||||
var poll_entry = null
|
||||
var old_line_count = 0
|
||||
var idx = 0
|
||||
var parse_fn = null
|
||||
if (!fd.is_file(sink.path)) {
|
||||
$delay(poll, 1)
|
||||
return
|
||||
channel = args[1]
|
||||
config = load_config()
|
||||
if (!config || !config.sink) {
|
||||
log.console("No sinks configured")
|
||||
return
|
||||
}
|
||||
// Add to first sink that already has a stack array, or first sink overall
|
||||
names = array(config.sink)
|
||||
arrfor(names, function(n) {
|
||||
var s = config.sink[n]
|
||||
var already = false
|
||||
if (added) return
|
||||
if (is_array(s.stack)) {
|
||||
arrfor(s.stack, function(ch) { if (ch == channel) already = true })
|
||||
if (!already) s.stack[] = channel
|
||||
added = true
|
||||
}
|
||||
st = fd.stat(sink.path)
|
||||
if (st.size == last_size) {
|
||||
$delay(poll, 1)
|
||||
return
|
||||
})
|
||||
if (!added && length(names) > 0) {
|
||||
config.sink[names[0]].stack = [channel]
|
||||
added = true
|
||||
}
|
||||
if (added) {
|
||||
save_config(config)
|
||||
log.console("Stack traces enabled on: " + channel)
|
||||
}
|
||||
}
|
||||
|
||||
function do_unstack() {
|
||||
var channel = null
|
||||
var config = null
|
||||
var names = null
|
||||
if (length(args) < 2) {
|
||||
log.console("Usage: cell log unstack <channel>")
|
||||
return
|
||||
}
|
||||
channel = args[1]
|
||||
config = load_config()
|
||||
if (!config || !config.sink) {
|
||||
log.console("No sinks configured")
|
||||
return
|
||||
}
|
||||
names = array(config.sink)
|
||||
arrfor(names, function(n) {
|
||||
var s = config.sink[n]
|
||||
if (is_array(s.stack))
|
||||
s.stack = filter(s.stack, function(ch) { return ch != channel })
|
||||
})
|
||||
save_config(config)
|
||||
log.console("Stack traces disabled on: " + channel)
|
||||
}
|
||||
|
||||
var known_channels = ["console", "error", "warn", "system", "build", "shop", "compile", "test"]
|
||||
|
||||
function find_terminal_sink(config) {
|
||||
var names = null
|
||||
var found = null
|
||||
if (!config || !config.sink) return null
|
||||
names = array(config.sink)
|
||||
if (config.sink.terminal) return config.sink.terminal
|
||||
arrfor(names, function(n) {
|
||||
if (!found && config.sink[n].type == "console")
|
||||
found = config.sink[n]
|
||||
})
|
||||
return found
|
||||
}
|
||||
|
||||
function do_enable() {
|
||||
var channel = null
|
||||
var config = null
|
||||
var sink = null
|
||||
var i = 0
|
||||
var already = false
|
||||
if (length(args) < 2) {
|
||||
log.error("Usage: cell log enable <channel>")
|
||||
return
|
||||
}
|
||||
channel = args[1]
|
||||
config = load_config()
|
||||
if (!config) config = {sink: {}}
|
||||
if (!config.sink) config.sink = {}
|
||||
sink = find_terminal_sink(config)
|
||||
if (!sink) {
|
||||
config.sink.terminal = {type: "console", format: "clean", channels: ["console", "error", channel], stack: ["error"]}
|
||||
save_config(config)
|
||||
log.console("Enabled channel: " + channel)
|
||||
return
|
||||
}
|
||||
if (is_array(sink.channels) && length(sink.channels) == 1 && sink.channels[0] == "*") {
|
||||
if (is_array(sink.exclude)) {
|
||||
var new_exclude = []
|
||||
arrfor(sink.exclude, function(ex) {
|
||||
if (ex != channel) push(new_exclude, ex)
|
||||
})
|
||||
sink.exclude = new_exclude
|
||||
}
|
||||
} else {
|
||||
if (!is_array(sink.channels)) sink.channels = ["console", "error"]
|
||||
arrfor(sink.channels, function(ch) {
|
||||
if (ch == channel) already = true
|
||||
})
|
||||
if (!already) sink.channels[] = channel
|
||||
}
|
||||
save_config(config)
|
||||
log.console("Enabled channel: " + channel)
|
||||
}
|
||||
|
||||
poll_content = text(fd.slurp(sink.path))
|
||||
poll_lines = array(poll_content, '\n')
|
||||
|
||||
if (last_size == 0 && length(poll_lines) > tail_lines) {
|
||||
start = length(poll_lines) - tail_lines
|
||||
} else if (last_size > 0) {
|
||||
old_line_count = length(array(text(poll_content, 0, last_size), '\n'))
|
||||
start = old_line_count
|
||||
function do_disable() {
|
||||
var channel = null
|
||||
var config = null
|
||||
var sink = null
|
||||
var i = 0
|
||||
var new_channels = []
|
||||
if (length(args) < 2) {
|
||||
log.error("Usage: cell log disable <channel>")
|
||||
return
|
||||
}
|
||||
channel = args[1]
|
||||
config = load_config()
|
||||
if (!config || !config.sink) {
|
||||
log.error("No log configuration found")
|
||||
return
|
||||
}
|
||||
sink = find_terminal_sink(config)
|
||||
if (!sink) {
|
||||
log.error("No terminal sink found")
|
||||
return
|
||||
}
|
||||
if (is_array(sink.channels) && length(sink.channels) == 1 && sink.channels[0] == "*") {
|
||||
if (!is_array(sink.exclude)) sink.exclude = []
|
||||
var already_excluded = false
|
||||
arrfor(sink.exclude, function(ex) {
|
||||
if (ex == channel) already_excluded = true
|
||||
})
|
||||
if (!already_excluded) sink.exclude[] = channel
|
||||
} else {
|
||||
if (is_array(sink.channels)) {
|
||||
arrfor(sink.channels, function(ch) {
|
||||
if (ch != channel) push(new_channels, ch)
|
||||
})
|
||||
sink.channels = new_channels
|
||||
}
|
||||
}
|
||||
save_config(config)
|
||||
log.console("Disabled channel: " + channel)
|
||||
}
|
||||
|
||||
last_size = st.size
|
||||
for (idx = start; idx < length(poll_lines); idx++) {
|
||||
if (length(poll_lines[idx]) == 0) continue
|
||||
parse_fn = function() {
|
||||
poll_entry = json.decode(poll_lines[idx])
|
||||
} disruption {
|
||||
poll_entry = null
|
||||
function do_channels() {
|
||||
var config = load_config()
|
||||
var sink = null
|
||||
var is_wildcard = false
|
||||
var active = {}
|
||||
if (config) sink = find_terminal_sink(config)
|
||||
if (sink) {
|
||||
if (is_array(sink.channels) && length(sink.channels) == 1 && sink.channels[0] == "*") {
|
||||
is_wildcard = true
|
||||
arrfor(known_channels, function(ch) { active[ch] = true })
|
||||
if (is_array(sink.exclude)) {
|
||||
arrfor(sink.exclude, function(ex) { active[ex] = false })
|
||||
}
|
||||
parse_fn()
|
||||
if (!poll_entry) continue
|
||||
os.print(format_entry(poll_entry) + "\n")
|
||||
} else if (is_array(sink.channels)) {
|
||||
arrfor(sink.channels, function(ch) { active[ch] = true })
|
||||
}
|
||||
$delay(poll, 1)
|
||||
} else {
|
||||
active.console = true
|
||||
active.error = true
|
||||
}
|
||||
|
||||
poll()
|
||||
log.console("Channels:")
|
||||
arrfor(known_channels, function(ch) {
|
||||
var status = active[ch] ? "enabled" : "disabled"
|
||||
log.console(" " + ch + ": " + status)
|
||||
})
|
||||
}
|
||||
|
||||
// Main dispatch
|
||||
@@ -335,16 +479,26 @@ if (length(args) == 0) {
|
||||
print_help()
|
||||
} else if (args[0] == 'list') {
|
||||
do_list()
|
||||
} else if (args[0] == 'channels') {
|
||||
do_channels()
|
||||
} else if (args[0] == 'enable') {
|
||||
do_enable()
|
||||
} else if (args[0] == 'disable') {
|
||||
do_disable()
|
||||
} else if (args[0] == 'add') {
|
||||
do_add()
|
||||
} else if (args[0] == 'remove') {
|
||||
do_remove()
|
||||
} else if (args[0] == 'read') {
|
||||
do_read()
|
||||
} else if (args[0] == 'tail') {
|
||||
do_tail()
|
||||
} else if (args[0] == 'route') {
|
||||
do_route()
|
||||
} else if (args[0] == 'unroute') {
|
||||
do_unroute()
|
||||
} else if (args[0] == 'stack') {
|
||||
do_stack()
|
||||
} else if (args[0] == 'unstack') {
|
||||
do_unstack()
|
||||
} else {
|
||||
log.error("Unknown command: " + args[0])
|
||||
log.console("Unknown command: " + args[0])
|
||||
print_help()
|
||||
}
|
||||
|
||||
|
||||
605
mcode.cm
605
mcode.cm
@@ -38,6 +38,11 @@ var mcode = function(ast) {
|
||||
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",
|
||||
is_blob: "is_blob", is_data: "is_data",
|
||||
is_true: "is_true", is_false: "is_false", is_fit: "is_fit",
|
||||
is_character: "is_char", is_digit: "is_digit", is_letter: "is_letter",
|
||||
is_lower: "is_lower", is_upper: "is_upper", is_whitespace: "is_ws",
|
||||
is_actor: "is_actor",
|
||||
length: "length"
|
||||
}
|
||||
|
||||
@@ -89,6 +94,8 @@ var mcode = function(ast) {
|
||||
var s_filename = null
|
||||
var s_has_disruption = false
|
||||
var s_slot_types = {}
|
||||
var s_num_err_label = null
|
||||
var s_num_err_emitted = false
|
||||
|
||||
// Shared closure vars for binop helpers (avoids >4 param functions)
|
||||
var _bp_dest = 0
|
||||
@@ -118,7 +125,9 @@ var mcode = function(ast) {
|
||||
cur_line: s_cur_line,
|
||||
cur_col: s_cur_col,
|
||||
has_disruption: s_has_disruption,
|
||||
slot_types: s_slot_types
|
||||
slot_types: s_slot_types,
|
||||
num_err_label: s_num_err_label,
|
||||
num_err_emitted: s_num_err_emitted
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +150,8 @@ var mcode = function(ast) {
|
||||
s_cur_col = saved.cur_col
|
||||
s_has_disruption = saved.has_disruption
|
||||
s_slot_types = saved.slot_types
|
||||
s_num_err_label = saved.num_err_label
|
||||
s_num_err_emitted = saved.num_err_emitted
|
||||
}
|
||||
|
||||
// Slot allocation
|
||||
@@ -351,6 +362,8 @@ var mcode = function(ast) {
|
||||
s_slot_types[text(dest)] = s_slot_types[text(src)]
|
||||
}
|
||||
|
||||
var emit_numeric_binop = null
|
||||
|
||||
// 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() {
|
||||
@@ -410,32 +423,40 @@ var mcode = function(ast) {
|
||||
|
||||
// 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(op_str) {
|
||||
emit_numeric_binop = function(op_str) {
|
||||
var left_known = is_known_number(_bp_ln) || slot_is_num(_bp_left)
|
||||
var right_known = is_known_number(_bp_rn) || slot_is_num(_bp_right)
|
||||
var t0 = null
|
||||
var done = null
|
||||
if (left_known && right_known) {
|
||||
emit_3(op_str, _bp_dest, _bp_left, _bp_right)
|
||||
mark_slot(_bp_dest, "num")
|
||||
return null
|
||||
}
|
||||
var t0 = alloc_slot()
|
||||
var err = gen_label("num_err")
|
||||
var done = gen_label("num_done")
|
||||
if (s_num_err_label == null) {
|
||||
s_num_err_label = gen_label("num_err")
|
||||
}
|
||||
t0 = alloc_slot()
|
||||
if (!left_known) {
|
||||
emit_2("is_num", t0, _bp_left)
|
||||
emit_jump_cond("jump_false", t0, err)
|
||||
emit_jump_cond("jump_false", t0, s_num_err_label)
|
||||
mark_slot(_bp_left, "num")
|
||||
}
|
||||
if (!right_known) {
|
||||
emit_2("is_num", t0, _bp_right)
|
||||
emit_jump_cond("jump_false", t0, err)
|
||||
emit_jump_cond("jump_false", t0, s_num_err_label)
|
||||
mark_slot(_bp_right, "num")
|
||||
}
|
||||
emit_3(op_str, _bp_dest, _bp_left, _bp_right)
|
||||
emit_jump(done)
|
||||
|
||||
emit_label(err)
|
||||
emit_log_error("cannot apply '" + _bp_op_sym + "': operands must be numbers")
|
||||
emit_0("disrupt")
|
||||
emit_label(done)
|
||||
if (!s_num_err_emitted) {
|
||||
done = gen_label("num_done")
|
||||
emit_jump(done)
|
||||
emit_label(s_num_err_label)
|
||||
emit_log_error("operands must be numbers")
|
||||
emit_0("disrupt")
|
||||
emit_label(done)
|
||||
s_num_err_emitted = true
|
||||
}
|
||||
mark_slot(_bp_dest, "num")
|
||||
return null
|
||||
}
|
||||
@@ -460,23 +481,30 @@ var mcode = function(ast) {
|
||||
|
||||
// emit_neg_decomposed: emit type-guarded negate
|
||||
var emit_neg_decomposed = function(dest, src, src_node) {
|
||||
var t0 = null
|
||||
var done = null
|
||||
if (is_known_number(src_node) || slot_is_num(src)) {
|
||||
emit_2("negate", dest, src)
|
||||
mark_slot(dest, "num")
|
||||
return null
|
||||
}
|
||||
var t0 = alloc_slot()
|
||||
var err = gen_label("neg_err")
|
||||
var done = gen_label("neg_done")
|
||||
if (s_num_err_label == null) {
|
||||
s_num_err_label = gen_label("num_err")
|
||||
}
|
||||
t0 = alloc_slot()
|
||||
emit_2("is_num", t0, src)
|
||||
emit_jump_cond("jump_false", t0, err)
|
||||
emit_jump_cond("jump_false", t0, s_num_err_label)
|
||||
mark_slot(src, "num")
|
||||
emit_2("negate", dest, src)
|
||||
emit_jump(done)
|
||||
|
||||
emit_label(err)
|
||||
emit_log_error("cannot negate: operand must be a number")
|
||||
emit_0("disrupt")
|
||||
emit_label(done)
|
||||
if (!s_num_err_emitted) {
|
||||
done = gen_label("num_done")
|
||||
emit_jump(done)
|
||||
emit_label(s_num_err_label)
|
||||
emit_log_error("operands must be numbers")
|
||||
emit_0("disrupt")
|
||||
emit_label(done)
|
||||
s_num_err_emitted = true
|
||||
}
|
||||
mark_slot(dest, "num")
|
||||
return null
|
||||
}
|
||||
@@ -852,9 +880,124 @@ var mcode = function(ast) {
|
||||
var inline_every = true
|
||||
var inline_some = true
|
||||
var inline_reduce = true
|
||||
var inline_map = false
|
||||
var inline_find = true
|
||||
|
||||
// --- Helper: emit arity-dispatched callback invocation ---
|
||||
// ctx = {fn, fn_arity, result, null_s, frame, zero, one, az, ao, prefix,
|
||||
// known_arity (optional — compile-time arity of callback literal)}
|
||||
// args = [slot_for_arg1, slot_for_arg2] — data args (not this)
|
||||
// max_args = 1 or 2 — how many data args to support
|
||||
var emit_arity_call = function(ctx, args, max_args) {
|
||||
var call_one = null
|
||||
var call_two = null
|
||||
var call_done = null
|
||||
var ka = ctx.known_arity
|
||||
// When callback arity is known at compile time, emit only the matching
|
||||
// call path. This avoids dead branches where parameters are nulled,
|
||||
// which confuse the type checker after inlining (e.g. push on null).
|
||||
if (ka != null) {
|
||||
if (ka >= max_args) {
|
||||
ka = max_args
|
||||
}
|
||||
if (ka == 0) {
|
||||
emit_3("frame", ctx.frame, ctx.fn, 0)
|
||||
emit_3("setarg", ctx.frame, 0, ctx.null_s)
|
||||
emit_2("invoke", ctx.frame, ctx.result)
|
||||
} else if (ka == 1 || max_args < 2) {
|
||||
emit_3("frame", ctx.frame, ctx.fn, 1)
|
||||
emit_3("setarg", ctx.frame, 0, ctx.null_s)
|
||||
emit_3("setarg", ctx.frame, 1, args[0])
|
||||
emit_2("invoke", ctx.frame, ctx.result)
|
||||
} else {
|
||||
emit_3("frame", ctx.frame, ctx.fn, 2)
|
||||
emit_3("setarg", ctx.frame, 0, ctx.null_s)
|
||||
emit_3("setarg", ctx.frame, 1, args[0])
|
||||
emit_3("setarg", ctx.frame, 2, args[1])
|
||||
emit_2("invoke", ctx.frame, ctx.result)
|
||||
}
|
||||
return null
|
||||
}
|
||||
call_one = gen_label(ctx.prefix + "_c1")
|
||||
call_two = gen_label(ctx.prefix + "_c2")
|
||||
call_done = gen_label(ctx.prefix + "_cd")
|
||||
emit_3("eq", ctx.az, ctx.fn_arity, ctx.zero)
|
||||
emit_jump_cond("jump_false", ctx.az, call_one)
|
||||
emit_3("frame", ctx.frame, ctx.fn, 0)
|
||||
emit_3("setarg", ctx.frame, 0, ctx.null_s)
|
||||
emit_2("invoke", ctx.frame, ctx.result)
|
||||
emit_jump(call_done)
|
||||
emit_label(call_one)
|
||||
if (max_args >= 2) {
|
||||
emit_3("eq", ctx.ao, ctx.fn_arity, ctx.one)
|
||||
emit_jump_cond("jump_false", ctx.ao, call_two)
|
||||
}
|
||||
emit_3("frame", ctx.frame, ctx.fn, 1)
|
||||
emit_3("setarg", ctx.frame, 0, ctx.null_s)
|
||||
emit_3("setarg", ctx.frame, 1, args[0])
|
||||
emit_2("invoke", ctx.frame, ctx.result)
|
||||
if (max_args < 2) {
|
||||
emit_label(call_done)
|
||||
return null
|
||||
}
|
||||
emit_jump(call_done)
|
||||
emit_label(call_two)
|
||||
emit_3("frame", ctx.frame, ctx.fn, 2)
|
||||
emit_3("setarg", ctx.frame, 0, ctx.null_s)
|
||||
emit_3("setarg", ctx.frame, 1, args[0])
|
||||
emit_3("setarg", ctx.frame, 2, args[1])
|
||||
emit_2("invoke", ctx.frame, ctx.result)
|
||||
emit_label(call_done)
|
||||
return null
|
||||
}
|
||||
|
||||
// --- Helper: guard that reverse param is logical (or null) ---
|
||||
// Disrupts if rev_slot is not null and not a boolean.
|
||||
var emit_reverse_guard = function(rev_slot, msg) {
|
||||
var ok_label = gen_label("rev_ok")
|
||||
var g = alloc_slot()
|
||||
emit_jump_cond("jump_null", rev_slot, ok_label)
|
||||
emit_2("is_bool", g, rev_slot)
|
||||
emit_jump_cond("jump_true", g, ok_label)
|
||||
emit_log_error(msg)
|
||||
emit_0("disrupt")
|
||||
emit_label(ok_label)
|
||||
}
|
||||
|
||||
// --- Helper: forward loop scaffolding ---
|
||||
// L = {arr, len, i, check, item, one, loop_label, done_label}
|
||||
// body_fn(L) — called between element load and increment
|
||||
var emit_forward_loop = function(L, body_fn) {
|
||||
emit_2("int", L.i, 0)
|
||||
emit_label(L.loop_label)
|
||||
emit_3("lt", L.check, L.i, L.len)
|
||||
emit_jump_cond("jump_false", L.check, L.done_label)
|
||||
emit_3("load_index", L.item, L.arr, L.i)
|
||||
body_fn(L)
|
||||
emit_3("add", L.i, L.i, L.one)
|
||||
emit_jump(L.loop_label)
|
||||
emit_label(L.done_label)
|
||||
return null
|
||||
}
|
||||
|
||||
// --- Helper: reverse loop scaffolding ---
|
||||
var emit_reverse_loop = function(L, body_fn) {
|
||||
var zero = alloc_slot()
|
||||
emit_2("int", zero, 0)
|
||||
emit_3("subtract", L.i, L.len, L.one)
|
||||
emit_label(L.loop_label)
|
||||
emit_3("ge", L.check, L.i, zero)
|
||||
emit_jump_cond("jump_false", L.check, L.done_label)
|
||||
emit_3("load_index", L.item, L.arr, L.i)
|
||||
body_fn(L)
|
||||
emit_3("subtract", L.i, L.i, L.one)
|
||||
emit_jump(L.loop_label)
|
||||
emit_label(L.done_label)
|
||||
return null
|
||||
}
|
||||
|
||||
// --- Helper: emit a reduce loop body ---
|
||||
// r = {acc, i, arr, fn, len, fn_arity}; emits loop updating acc in-place.
|
||||
// r = {acc, i, arr, fn, len, fn_arity, known_arity}; 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
|
||||
@@ -868,13 +1011,13 @@ var mcode = function(ast) {
|
||||
var null_s = alloc_slot()
|
||||
var one = alloc_slot()
|
||||
var zero = alloc_slot()
|
||||
var arity_is_zero = alloc_slot()
|
||||
var arity_is_one = alloc_slot()
|
||||
var az = alloc_slot()
|
||||
var ao = alloc_slot()
|
||||
var f = alloc_slot()
|
||||
var loop_label = gen_label("reduce_loop")
|
||||
var call_one_label = gen_label("reduce_call_one")
|
||||
var call_two_label = gen_label("reduce_call_two")
|
||||
var call_done_label = gen_label("reduce_call_done")
|
||||
var ctx = {fn: fn_slot, fn_arity: fn_arity, result: acc, null_s: null_s,
|
||||
frame: f, zero: zero, one: one, az: az, ao: ao, prefix: "reduce",
|
||||
known_arity: r.known_arity}
|
||||
emit_2("int", one, 1)
|
||||
emit_2("int", zero, 0)
|
||||
emit_1("null", null_s)
|
||||
@@ -886,27 +1029,7 @@ var mcode = function(ast) {
|
||||
}
|
||||
emit_jump_cond("jump_false", check, done_label)
|
||||
emit_3("load_index", item, arr_slot, i)
|
||||
emit_3("eq", arity_is_zero, fn_arity, zero)
|
||||
emit_jump_cond("jump_false", arity_is_zero, call_one_label)
|
||||
emit_3("frame", f, fn_slot, 0)
|
||||
emit_3("setarg", f, 0, null_s)
|
||||
emit_2("invoke", f, acc)
|
||||
emit_jump(call_done_label)
|
||||
emit_label(call_one_label)
|
||||
emit_3("eq", arity_is_one, fn_arity, one)
|
||||
emit_jump_cond("jump_false", arity_is_one, call_two_label)
|
||||
emit_3("frame", f, fn_slot, 1)
|
||||
emit_3("setarg", f, 0, null_s)
|
||||
emit_3("setarg", f, 1, acc)
|
||||
emit_2("invoke", f, acc)
|
||||
emit_jump(call_done_label)
|
||||
emit_label(call_two_label)
|
||||
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)
|
||||
emit_label(call_done_label)
|
||||
emit_arity_call(ctx, [acc, item], 2)
|
||||
if (forward) {
|
||||
emit_3("add", i, i, one)
|
||||
} else {
|
||||
@@ -915,60 +1038,64 @@ var mcode = function(ast) {
|
||||
emit_jump(loop_label)
|
||||
}
|
||||
|
||||
// --- Inline expansion: arrfor(arr, fn) ---
|
||||
var expand_inline_arrfor = function(dest, arr_slot, fn_slot) {
|
||||
// --- Inline expansion: arrfor(arr, fn[, rev[, exit]]) ---
|
||||
var expand_inline_arrfor = function(dest, args, nargs) {
|
||||
var arr_slot = args.arr
|
||||
var fn_slot = args.fn
|
||||
var len = alloc_slot()
|
||||
var i = alloc_slot()
|
||||
var check = alloc_slot()
|
||||
var item = alloc_slot()
|
||||
var fn_arity = alloc_slot()
|
||||
var arity_is_zero = alloc_slot()
|
||||
var arity_is_one = alloc_slot()
|
||||
var az = alloc_slot()
|
||||
var ao = alloc_slot()
|
||||
var null_s = alloc_slot()
|
||||
var zero = 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")
|
||||
var call_one_label = gen_label("arrfor_call_one")
|
||||
var call_two_label = gen_label("arrfor_call_two")
|
||||
var call_done_label = gen_label("arrfor_call_done")
|
||||
var val = alloc_slot()
|
||||
var eq_check = alloc_slot()
|
||||
var early_exit = gen_label("arrfor_exit")
|
||||
var done_final = gen_label("arrfor_final")
|
||||
var rev_label = gen_label("arrfor_rev")
|
||||
var final_label = gen_label("arrfor_fwd_done")
|
||||
var fwd_L = {arr: arr_slot, len: len, i: i, check: check, item: item, one: one,
|
||||
loop_label: gen_label("arrfor_fwd"), done_label: gen_label("arrfor_fwd_d")}
|
||||
var rev_L = {arr: arr_slot, len: len, i: i, check: check, item: item, one: one,
|
||||
loop_label: gen_label("arrfor_rev_l"), done_label: gen_label("arrfor_rev_d")}
|
||||
var ctx = {fn: fn_slot, fn_arity: fn_arity, result: val, null_s: null_s,
|
||||
frame: f, zero: zero, one: one, az: az, ao: ao, prefix: "arrfor"}
|
||||
var body_fn = function(L) {
|
||||
emit_arity_call(ctx, [L.item, L.i], 2)
|
||||
if (nargs >= 4 && args.exit >= 0) {
|
||||
emit_3("eq", eq_check, val, args.exit)
|
||||
emit_jump_cond("jump_true", eq_check, early_exit)
|
||||
}
|
||||
return null
|
||||
}
|
||||
emit_2("length", len, arr_slot)
|
||||
emit_2("int", i, 0)
|
||||
emit_2("int", zero, 0)
|
||||
emit_2("int", one, 1)
|
||||
emit_1("null", null_s)
|
||||
emit_2("length", fn_arity, fn_slot)
|
||||
emit_label(loop_label)
|
||||
emit_3("lt", check, i, len)
|
||||
emit_jump_cond("jump_false", check, done_label)
|
||||
emit_3("load_index", item, arr_slot, i)
|
||||
emit_3("eq", arity_is_zero, fn_arity, zero)
|
||||
emit_jump_cond("jump_false", arity_is_zero, call_one_label)
|
||||
emit_3("frame", f, fn_slot, 0)
|
||||
emit_3("setarg", f, 0, null_s)
|
||||
emit_2("invoke", f, discard)
|
||||
emit_jump(call_done_label)
|
||||
emit_label(call_one_label)
|
||||
emit_3("eq", arity_is_one, fn_arity, one)
|
||||
emit_jump_cond("jump_false", arity_is_one, call_two_label)
|
||||
emit_3("frame", f, fn_slot, 1)
|
||||
emit_3("setarg", f, 0, null_s)
|
||||
emit_3("setarg", f, 1, item)
|
||||
emit_2("invoke", f, discard)
|
||||
emit_jump(call_done_label)
|
||||
emit_label(call_two_label)
|
||||
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_label(call_done_label)
|
||||
emit_3("add", i, i, one)
|
||||
emit_jump(loop_label)
|
||||
emit_label(done_label)
|
||||
if (nargs <= 2) {
|
||||
emit_forward_loop(fwd_L, body_fn)
|
||||
} else {
|
||||
emit_reverse_guard(args.rev, "arrfor: reverse must be a logical")
|
||||
emit_jump_cond("wary_true", args.rev, rev_label)
|
||||
emit_forward_loop(fwd_L, body_fn)
|
||||
emit_jump(final_label)
|
||||
emit_label(rev_label)
|
||||
emit_reverse_loop(rev_L, body_fn)
|
||||
emit_label(final_label)
|
||||
}
|
||||
emit_1("null", dest)
|
||||
emit_jump(done_final)
|
||||
if (nargs >= 4 && args.exit >= 0) {
|
||||
emit_label(early_exit)
|
||||
emit_2("move", dest, val)
|
||||
}
|
||||
emit_label(done_final)
|
||||
return dest
|
||||
}
|
||||
|
||||
@@ -1013,7 +1140,7 @@ var mcode = function(ast) {
|
||||
emit_3("setarg", f, 1, item)
|
||||
emit_2("invoke", f, val)
|
||||
emit_label(call_done_label)
|
||||
emit_jump_cond("jump_false", val, ret_false)
|
||||
emit_jump_cond("wary_false", val, ret_false)
|
||||
emit_3("add", i, i, one)
|
||||
emit_jump(loop_label)
|
||||
emit_label(ret_true)
|
||||
@@ -1066,7 +1193,7 @@ var mcode = function(ast) {
|
||||
emit_3("setarg", f, 1, item)
|
||||
emit_2("invoke", f, val)
|
||||
emit_label(call_done_label)
|
||||
emit_jump_cond("jump_true", val, ret_true)
|
||||
emit_jump_cond("wary_true", val, ret_true)
|
||||
emit_3("add", i, i, one)
|
||||
emit_jump(loop_label)
|
||||
emit_label(ret_true)
|
||||
@@ -1080,6 +1207,171 @@ var mcode = function(ast) {
|
||||
|
||||
// --- 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 fn_arity = alloc_slot()
|
||||
var az = alloc_slot()
|
||||
var ao = alloc_slot()
|
||||
var null_s = alloc_slot()
|
||||
var zero = alloc_slot()
|
||||
var one = alloc_slot()
|
||||
var f = alloc_slot()
|
||||
var val = alloc_slot()
|
||||
var skip = gen_label("filter_skip")
|
||||
var bail = gen_label("filter_bail")
|
||||
var bool_check = alloc_slot()
|
||||
var ctx = {fn: fn_slot, fn_arity: fn_arity, result: val, null_s: null_s,
|
||||
frame: f, zero: zero, one: one, az: az, ao: ao, prefix: "filter"}
|
||||
var L = {arr: arr_slot, len: len, i: i, check: check, item: item, one: one,
|
||||
loop_label: gen_label("filter_loop"), done_label: gen_label("filter_done")}
|
||||
add_instr(["array", result, 0])
|
||||
emit_2("length", len, arr_slot)
|
||||
emit_2("int", zero, 0)
|
||||
emit_2("int", one, 1)
|
||||
emit_1("null", null_s)
|
||||
emit_2("length", fn_arity, fn_slot)
|
||||
emit_forward_loop(L, function(L) {
|
||||
emit_arity_call(ctx, [L.item, L.i], 2)
|
||||
emit_2("is_bool", bool_check, val)
|
||||
emit_jump_cond("jump_false", bool_check, bail)
|
||||
emit_jump_cond("jump_false", val, skip)
|
||||
emit_2("push", result, L.item)
|
||||
emit_label(skip)
|
||||
return null
|
||||
})
|
||||
emit_2("move", dest, result)
|
||||
var end = gen_label("filter_end")
|
||||
emit_jump(end)
|
||||
emit_label(bail)
|
||||
emit_1("null", dest)
|
||||
emit_label(end)
|
||||
return dest
|
||||
}
|
||||
|
||||
// --- Inline expansion: find(arr, target[, rev[, from]]) ---
|
||||
var expand_inline_find = function(dest, args, nargs) {
|
||||
var arr_slot = args.arr
|
||||
var target = args.target
|
||||
var len = alloc_slot()
|
||||
var i = alloc_slot()
|
||||
var check = alloc_slot()
|
||||
var item = alloc_slot()
|
||||
var fn_arity = alloc_slot()
|
||||
var az = alloc_slot()
|
||||
var ao = alloc_slot()
|
||||
var null_s = alloc_slot()
|
||||
var zero = alloc_slot()
|
||||
var one = alloc_slot()
|
||||
var f = alloc_slot()
|
||||
var val = alloc_slot()
|
||||
var is_fn = alloc_slot()
|
||||
var eq_check = alloc_slot()
|
||||
var fn_mode_label = gen_label("find_fn")
|
||||
var found_label = gen_label("find_found")
|
||||
var not_found_label = gen_label("find_nf")
|
||||
var final_label = gen_label("find_final")
|
||||
var vrev = gen_label("find_vrev")
|
||||
var vdone = gen_label("find_vdone")
|
||||
var frev = gen_label("find_frev")
|
||||
var fdone = gen_label("find_fdone")
|
||||
var vL = {arr: arr_slot, len: len, i: i, check: check, item: item, one: one,
|
||||
loop_label: gen_label("find_vl"), done_label: gen_label("find_vd")}
|
||||
var vrL = {arr: arr_slot, len: len, i: i, check: check, item: item, one: one,
|
||||
loop_label: gen_label("find_vrl"), done_label: gen_label("find_vrd")}
|
||||
var fL = {arr: arr_slot, len: len, i: i, check: check, item: item, one: one,
|
||||
loop_label: gen_label("find_fl"), done_label: gen_label("find_fd")}
|
||||
var ffL = {arr: arr_slot, len: len, i: i, check: check, item: item, one: one,
|
||||
loop_label: gen_label("find_ffl"), done_label: gen_label("find_ffd")}
|
||||
var frL = {arr: arr_slot, len: len, i: i, check: check, item: item, one: one,
|
||||
loop_label: gen_label("find_frl"), done_label: gen_label("find_frd")}
|
||||
var ctx = {fn: target, fn_arity: fn_arity, result: val, null_s: null_s,
|
||||
frame: f, zero: zero, one: one, az: az, ao: ao, prefix: "find"}
|
||||
var val_body = function(L) {
|
||||
emit_3("eq", eq_check, L.item, target)
|
||||
emit_jump_cond("jump_true", eq_check, found_label)
|
||||
return null
|
||||
}
|
||||
var fn_body = function(L) {
|
||||
emit_arity_call(ctx, [L.item, L.i], 2)
|
||||
emit_jump_cond("wary_true", val, found_label)
|
||||
return null
|
||||
}
|
||||
emit_2("length", len, arr_slot)
|
||||
emit_2("int", zero, 0)
|
||||
emit_2("int", one, 1)
|
||||
emit_1("null", null_s)
|
||||
if (nargs > 2) {
|
||||
emit_reverse_guard(args.rev, "find: reverse must be a logical")
|
||||
}
|
||||
emit_2("is_func", is_fn, target)
|
||||
emit_jump_cond("jump_true", is_fn, fn_mode_label)
|
||||
// === Value mode ===
|
||||
if (nargs <= 2) {
|
||||
emit_forward_loop(vL, val_body)
|
||||
} else {
|
||||
emit_jump_cond("wary_true", args.rev, vrev)
|
||||
if (nargs >= 4 && args.from >= 0) {
|
||||
emit_2("move", i, args.from)
|
||||
}
|
||||
if (nargs >= 4 && args.from >= 0) {
|
||||
emit_label(vL.loop_label)
|
||||
emit_3("lt", vL.check, vL.i, vL.len)
|
||||
emit_jump_cond("jump_false", vL.check, vL.done_label)
|
||||
emit_3("load_index", vL.item, vL.arr, vL.i)
|
||||
val_body(vL)
|
||||
emit_3("add", vL.i, vL.i, vL.one)
|
||||
emit_jump(vL.loop_label)
|
||||
emit_label(vL.done_label)
|
||||
} else {
|
||||
emit_forward_loop(vL, val_body)
|
||||
}
|
||||
emit_jump(vdone)
|
||||
emit_label(vrev)
|
||||
emit_reverse_loop(vrL, val_body)
|
||||
emit_label(vdone)
|
||||
}
|
||||
emit_jump(not_found_label)
|
||||
// === Function mode ===
|
||||
emit_label(fn_mode_label)
|
||||
emit_2("length", fn_arity, target)
|
||||
if (nargs <= 2) {
|
||||
emit_forward_loop(fL, fn_body)
|
||||
} else {
|
||||
emit_jump_cond("wary_true", args.rev, frev)
|
||||
if (nargs >= 4 && args.from >= 0) {
|
||||
emit_2("move", i, args.from)
|
||||
}
|
||||
if (nargs >= 4 && args.from >= 0) {
|
||||
emit_label(ffL.loop_label)
|
||||
emit_3("lt", ffL.check, ffL.i, ffL.len)
|
||||
emit_jump_cond("jump_false", ffL.check, ffL.done_label)
|
||||
emit_3("load_index", ffL.item, ffL.arr, ffL.i)
|
||||
fn_body(ffL)
|
||||
emit_3("add", ffL.i, ffL.i, ffL.one)
|
||||
emit_jump(ffL.loop_label)
|
||||
emit_label(ffL.done_label)
|
||||
} else {
|
||||
emit_forward_loop(ffL, fn_body)
|
||||
}
|
||||
emit_jump(fdone)
|
||||
emit_label(frev)
|
||||
emit_reverse_loop(frL, fn_body)
|
||||
emit_label(fdone)
|
||||
}
|
||||
emit_label(not_found_label)
|
||||
emit_1("null", dest)
|
||||
emit_jump(final_label)
|
||||
emit_label(found_label)
|
||||
emit_2("move", dest, i)
|
||||
emit_label(final_label)
|
||||
return dest
|
||||
}
|
||||
|
||||
// --- Inline expansion: array(arr, fn) → map ---
|
||||
var expand_inline_map = function(dest, arr_slot, fn_slot) {
|
||||
var result = alloc_slot()
|
||||
var len = alloc_slot()
|
||||
var i = alloc_slot()
|
||||
@@ -1093,12 +1385,11 @@ var mcode = function(ast) {
|
||||
var one = alloc_slot()
|
||||
var f = alloc_slot()
|
||||
var val = alloc_slot()
|
||||
var loop_label = gen_label("filter_loop")
|
||||
var call_one_label = gen_label("filter_call_one")
|
||||
var call_two_label = gen_label("filter_call_two")
|
||||
var call_done_label = gen_label("filter_call_done")
|
||||
var skip_label = gen_label("filter_skip")
|
||||
var done_label = gen_label("filter_done")
|
||||
var loop_label = gen_label("map_loop")
|
||||
var call_one_label = gen_label("map_call_one")
|
||||
var call_two_label = gen_label("map_call_two")
|
||||
var call_done_label = gen_label("map_call_done")
|
||||
var done_label = gen_label("map_done")
|
||||
add_instr(["array", result, 0])
|
||||
emit_2("length", len, arr_slot)
|
||||
emit_2("int", i, 0)
|
||||
@@ -1131,9 +1422,37 @@ var mcode = function(ast) {
|
||||
emit_3("setarg", f, 2, i)
|
||||
emit_2("invoke", f, val)
|
||||
emit_label(call_done_label)
|
||||
emit_jump_cond("jump_false", val, skip_label)
|
||||
emit_2("push", result, item)
|
||||
emit_label(skip_label)
|
||||
emit_2("push", result, val)
|
||||
emit_3("add", i, i, one)
|
||||
emit_jump(loop_label)
|
||||
emit_label(done_label)
|
||||
emit_2("move", dest, result)
|
||||
return dest
|
||||
}
|
||||
|
||||
// --- Inline expansion: array(arr, intrinsic_op) → map with direct opcode ---
|
||||
var expand_inline_map_intrinsic = function(dest, arr_slot, opcode) {
|
||||
var result = alloc_slot()
|
||||
var len = alloc_slot()
|
||||
var i = alloc_slot()
|
||||
var check = alloc_slot()
|
||||
var item = alloc_slot()
|
||||
var val = alloc_slot()
|
||||
var zero = alloc_slot()
|
||||
var one = alloc_slot()
|
||||
var loop_label = gen_label("mapi_loop")
|
||||
var done_label = gen_label("mapi_done")
|
||||
add_instr(["array", result, 0])
|
||||
emit_2("length", len, arr_slot)
|
||||
emit_2("int", i, 0)
|
||||
emit_2("int", zero, 0)
|
||||
emit_2("int", one, 1)
|
||||
emit_label(loop_label)
|
||||
emit_3("lt", check, i, len)
|
||||
emit_jump_cond("jump_false", check, done_label)
|
||||
emit_3("load_index", item, arr_slot, i)
|
||||
emit_2(opcode, val, item)
|
||||
emit_2("push", result, val)
|
||||
emit_3("add", i, i, one)
|
||||
emit_jump(loop_label)
|
||||
emit_label(done_label)
|
||||
@@ -1168,7 +1487,8 @@ var mcode = function(ast) {
|
||||
emit_2("length", fn_arity, fn_slot)
|
||||
emit_2("int", zero, 0)
|
||||
emit_2("int", one, 1)
|
||||
r = {acc: acc, i: i, arr: arr_slot, fn: fn_slot, len: len, fn_arity: fn_arity}
|
||||
r = {acc: acc, i: i, arr: arr_slot, fn: fn_slot, len: len, fn_arity: fn_arity,
|
||||
known_arity: args.fn_known_arity}
|
||||
if (nargs == 2) {
|
||||
null_label = gen_label("reduce_null")
|
||||
d1 = gen_label("reduce_d1")
|
||||
@@ -1220,12 +1540,13 @@ var mcode = function(ast) {
|
||||
d2 = gen_label("reduce_d2")
|
||||
d3 = gen_label("reduce_d3")
|
||||
d4 = gen_label("reduce_d4")
|
||||
emit_reverse_guard(rev_slot, "reduce: reverse must be a logical")
|
||||
emit_2("is_null", check, init_slot)
|
||||
emit_jump_cond("jump_false", check, has_init)
|
||||
// No initial
|
||||
emit_3("lt", check, zero, len)
|
||||
emit_jump_cond("jump_false", check, null_label)
|
||||
emit_jump_cond("jump_true", rev_slot, no_init_rev)
|
||||
emit_jump_cond("wary_true", rev_slot, no_init_rev)
|
||||
// No initial, forward
|
||||
emit_3("load_index", acc, arr_slot, zero)
|
||||
emit_2("move", i, one)
|
||||
@@ -1247,7 +1568,7 @@ var mcode = function(ast) {
|
||||
emit_jump(final_label)
|
||||
// Has initial
|
||||
emit_label(has_init)
|
||||
emit_jump_cond("jump_true", rev_slot, init_rev)
|
||||
emit_jump_cond("wary_true", rev_slot, init_rev)
|
||||
// Has initial, forward
|
||||
emit_2("move", acc, init_slot)
|
||||
emit_2("int", i, 0)
|
||||
@@ -1294,7 +1615,7 @@ var mcode = function(ast) {
|
||||
left_slot = gen_expr(left, -1)
|
||||
dest = alloc_slot()
|
||||
emit_2("move", dest, left_slot)
|
||||
emit_jump_cond("jump_false", dest, end_label)
|
||||
emit_jump_cond("wary_false", dest, end_label)
|
||||
right_slot = gen_expr(right, -1)
|
||||
emit_2("move", dest, right_slot)
|
||||
emit_label(end_label)
|
||||
@@ -1306,7 +1627,7 @@ var mcode = function(ast) {
|
||||
left_slot = gen_expr(left, -1)
|
||||
dest = alloc_slot()
|
||||
emit_2("move", dest, left_slot)
|
||||
emit_jump_cond("jump_true", dest, end_label)
|
||||
emit_jump_cond("wary_true", dest, end_label)
|
||||
right_slot = gen_expr(right, -1)
|
||||
emit_2("move", dest, right_slot)
|
||||
emit_label(end_label)
|
||||
@@ -1617,6 +1938,8 @@ var mcode = function(ast) {
|
||||
var guard_t = 0
|
||||
var guard_err = null
|
||||
var guard_done = null
|
||||
var cb_known = null
|
||||
var cb_p = null
|
||||
|
||||
if (expr == null) {
|
||||
return -1
|
||||
@@ -1875,12 +2198,22 @@ var mcode = function(ast) {
|
||||
emit_label(guard_done)
|
||||
return a1
|
||||
}
|
||||
// Callback intrinsics → inline mcode loops
|
||||
if (nargs == 2 && fname == "arrfor" && inline_arrfor) {
|
||||
// apply(fn, arr) → direct opcode
|
||||
if (nargs == 2 && fname == "apply") {
|
||||
a0 = gen_expr(args_list[0], -1)
|
||||
a1 = gen_expr(args_list[1], -1)
|
||||
d = alloc_slot()
|
||||
return expand_inline_arrfor(d, a0, a1)
|
||||
emit_3("apply", d, a0, a1)
|
||||
return d
|
||||
}
|
||||
// Callback intrinsics → inline mcode loops
|
||||
if (fname == "arrfor" && nargs >= 2 && nargs <= 4 && inline_arrfor) {
|
||||
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_arrfor(d, {arr: a0, fn: a1, rev: a2, exit: a3}, nargs)
|
||||
}
|
||||
if (nargs == 2 && fname == "every" && inline_every) {
|
||||
a0 = gen_expr(args_list[0], -1)
|
||||
@@ -1900,14 +2233,30 @@ var mcode = function(ast) {
|
||||
d = alloc_slot()
|
||||
return expand_inline_filter(d, a0, a1)
|
||||
}
|
||||
if (fname == "find" && nargs >= 2 && nargs <= 4 && inline_find) {
|
||||
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_find(d, {arr: a0, target: a1, rev: a2, from: a3}, nargs)
|
||||
}
|
||||
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)
|
||||
cb_known = null
|
||||
if (args_list[1].kind == "function") {
|
||||
cb_p = args_list[1].list
|
||||
if (cb_p == null) cb_p = args_list[1].parameters
|
||||
cb_known = cb_p != null ? length(cb_p) : 0
|
||||
}
|
||||
return expand_inline_reduce(d, {arr: a0, fn: a1, init: a2, rev: a3,
|
||||
fn_known_arity: cb_known}, nargs)
|
||||
}
|
||||
// array(arr, fn) inline expansion removed — array() is too complex to inline
|
||||
}
|
||||
|
||||
// Collect arg slots
|
||||
@@ -2043,7 +2392,7 @@ var mcode = function(ast) {
|
||||
obj = operand.left
|
||||
prop = operand.right
|
||||
obj_slot = gen_expr(obj, -1)
|
||||
push(s_instructions, ["delete", slot, obj_slot, prop])
|
||||
add_instr(["delete", slot, obj_slot, prop])
|
||||
} else if (operand_kind == "[") {
|
||||
obj = operand.left
|
||||
idx = operand.right
|
||||
@@ -2064,7 +2413,7 @@ var mcode = function(ast) {
|
||||
else_label = gen_label("tern_else")
|
||||
end_label = gen_label("tern_end")
|
||||
cond_slot = gen_expr(cond, -1)
|
||||
emit_jump_cond("jump_false", cond_slot, else_label)
|
||||
emit_jump_cond("wary_false", cond_slot, else_label)
|
||||
dest = alloc_slot()
|
||||
then_slot = gen_expr(then_expr, -1)
|
||||
emit_2("move", dest, then_slot)
|
||||
@@ -2289,7 +2638,7 @@ var mcode = function(ast) {
|
||||
else_label = gen_label("if_else")
|
||||
end_label = gen_label("if_end")
|
||||
cond_slot = gen_expr(cond, -1)
|
||||
emit_jump_cond("jump_false", cond_slot, else_label)
|
||||
emit_jump_cond("wary_false", cond_slot, else_label)
|
||||
_i = 0
|
||||
while (_i < length(then_stmts)) {
|
||||
gen_statement(then_stmts[_i])
|
||||
@@ -2330,7 +2679,7 @@ var mcode = function(ast) {
|
||||
}
|
||||
emit_label(start_label)
|
||||
cond_slot = gen_expr(cond, -1)
|
||||
emit_jump_cond("jump_false", cond_slot, end_label)
|
||||
emit_jump_cond("wary_false", cond_slot, end_label)
|
||||
_i = 0
|
||||
while (_i < length(stmts)) {
|
||||
gen_statement(stmts[_i])
|
||||
@@ -2365,7 +2714,7 @@ var mcode = function(ast) {
|
||||
}
|
||||
emit_label(cond_label)
|
||||
cond_slot = gen_expr(cond, -1)
|
||||
emit_jump_cond("jump_true", cond_slot, start_label)
|
||||
emit_jump_cond("wary_true", cond_slot, start_label)
|
||||
emit_label(end_label)
|
||||
s_loop_break = old_break
|
||||
s_loop_continue = old_continue
|
||||
@@ -2399,7 +2748,7 @@ var mcode = function(ast) {
|
||||
emit_label(start_label)
|
||||
if (test != null) {
|
||||
test_slot = gen_expr(test, -1)
|
||||
emit_jump_cond("jump_false", test_slot, end_label)
|
||||
emit_jump_cond("wary_false", test_slot, end_label)
|
||||
}
|
||||
_i = 0
|
||||
while (_i < length(stmts)) {
|
||||
@@ -2605,6 +2954,8 @@ var mcode = function(ast) {
|
||||
s_vars = []
|
||||
s_intrinsic_cache = []
|
||||
s_slot_types = {}
|
||||
s_num_err_label = null
|
||||
s_num_err_emitted = false
|
||||
s_loop_break = null
|
||||
s_loop_continue = null
|
||||
s_label_map = {}
|
||||
@@ -2781,7 +3132,6 @@ var mcode = function(ast) {
|
||||
var local_slot = 0
|
||||
var dest = 0
|
||||
var statements = ast.statements
|
||||
var last_expr_slot = -1
|
||||
var stmt = null
|
||||
var kind = null
|
||||
var null_slot = 0
|
||||
@@ -2804,6 +3154,8 @@ var mcode = function(ast) {
|
||||
s_label_counter = 0
|
||||
s_func_counter = 0
|
||||
s_slot_types = {}
|
||||
s_num_err_label = null
|
||||
s_num_err_emitted = false
|
||||
s_loop_break = null
|
||||
s_loop_continue = null
|
||||
s_label_map = {}
|
||||
@@ -2846,21 +3198,18 @@ var mcode = function(ast) {
|
||||
kind = stmt.kind
|
||||
if (kind != null) {
|
||||
if (kind == "call") {
|
||||
last_expr_slot = gen_expr(stmt.expression, -1)
|
||||
gen_expr(stmt.expression, -1)
|
||||
} else if (kind == "return" || kind == "disrupt" ||
|
||||
kind == "break" || kind == "continue") {
|
||||
gen_statement(stmt)
|
||||
last_expr_slot = -1
|
||||
} else if (kind == "var" || kind == "def" ||
|
||||
kind == "break" || kind == "continue" ||
|
||||
kind == "var" || kind == "def" ||
|
||||
kind == "var_list" || kind == "def_list" ||
|
||||
kind == "function" || kind == "block" ||
|
||||
kind == "if" || kind == "while" ||
|
||||
kind == "do" || kind == "for" ||
|
||||
kind == "switch") {
|
||||
gen_statement(stmt)
|
||||
last_expr_slot = -1
|
||||
} else {
|
||||
last_expr_slot = gen_expr(stmt, -1)
|
||||
gen_expr(stmt, -1)
|
||||
}
|
||||
} else {
|
||||
gen_statement(stmt)
|
||||
@@ -2868,13 +3217,9 @@ var mcode = function(ast) {
|
||||
_i = _i + 1
|
||||
}
|
||||
|
||||
if (last_expr_slot >= 0) {
|
||||
emit_1("return", last_expr_slot)
|
||||
} else {
|
||||
null_slot = alloc_slot()
|
||||
emit_1("null", null_slot)
|
||||
emit_1("return", null_slot)
|
||||
}
|
||||
null_slot = alloc_slot()
|
||||
emit_1("null", null_slot)
|
||||
emit_1("return", null_slot)
|
||||
|
||||
result = {}
|
||||
result.name = filename != null ? filename : "<eval>"
|
||||
|
||||
12
meson.build
12
meson.build
@@ -34,6 +34,7 @@ if host_machine.system() == 'darwin'
|
||||
fworks = [
|
||||
'CoreFoundation',
|
||||
'CFNetwork',
|
||||
'Security',
|
||||
]
|
||||
foreach fkit : fworks
|
||||
deps += dependency('appleframeworks', modules: fkit)
|
||||
@@ -82,7 +83,9 @@ scripts = [
|
||||
'internal/os.c',
|
||||
'internal/fd.c',
|
||||
'net/http.c',
|
||||
'net/enet.c',
|
||||
'net/tls.c',
|
||||
'net/socket.c',
|
||||
'internal/enet.c',
|
||||
'archive/miniz.c',
|
||||
'source/cJSON.c'
|
||||
]
|
||||
@@ -156,9 +159,12 @@ qbe_lib = static_library('qbe',
|
||||
if host_machine.system() == 'windows'
|
||||
exe_ext = '.exe'
|
||||
link += '-Wl,--export-all-symbols'
|
||||
else
|
||||
elif host_machine.system() == 'darwin'
|
||||
exe_ext = ''
|
||||
link += '-Wl,-export_dynamic'
|
||||
else
|
||||
exe_ext = ''
|
||||
link += '-Wl,--export-dynamic'
|
||||
endif
|
||||
|
||||
cell_so = shared_library(
|
||||
@@ -193,5 +199,3 @@ cell_exe = executable('cell',
|
||||
|
||||
# Install headers for building dynamic libraries using Cell
|
||||
install_headers('source/cell.h')
|
||||
install_headers('source/quickjs.h')
|
||||
install_headers('source/wota.h')
|
||||
|
||||
@@ -321,7 +321,7 @@ static const JSCFunctionListEntry js_http_funcs[] = {
|
||||
JS_CFUNC_DEF("fetch", 2, js_fetch_picoparser),
|
||||
};
|
||||
|
||||
JSValue js_core_http_use(JSContext *js) {
|
||||
JSValue js_core_net_http_use(JSContext *js) {
|
||||
JS_FRAME(js);
|
||||
par_easycurl_init(0); // Initialize platform HTTP backend
|
||||
JS_ROOT(mod, JS_NewObject(js));
|
||||
|
||||
116
net/socket.c
116
net/socket.c
@@ -24,6 +24,9 @@
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#ifndef _WIN32
|
||||
#include <fcntl.h>
|
||||
#endif
|
||||
|
||||
// Helper to convert JS value to file descriptor
|
||||
static int js2fd(JSContext *ctx, JSValueConst val)
|
||||
@@ -64,7 +67,6 @@ JSC_CCALL(socket_getaddrinfo,
|
||||
else if (strcmp(family, "AF_INET6") == 0) hints.ai_family = AF_INET6;
|
||||
JS_FreeCString(js, family);
|
||||
}
|
||||
JS_FreeValue(js, val);
|
||||
|
||||
val = JS_GetPropertyStr(js, argv[2], "socktype");
|
||||
if (!JS_IsNull(val)) {
|
||||
@@ -73,19 +75,16 @@ JSC_CCALL(socket_getaddrinfo,
|
||||
else if (strcmp(socktype, "SOCK_DGRAM") == 0) hints.ai_socktype = SOCK_DGRAM;
|
||||
JS_FreeCString(js, socktype);
|
||||
}
|
||||
JS_FreeValue(js, val);
|
||||
|
||||
val = JS_GetPropertyStr(js, argv[2], "flags");
|
||||
if (!JS_IsNull(val)) {
|
||||
hints.ai_flags = js2number(js, val);
|
||||
}
|
||||
JS_FreeValue(js, val);
|
||||
|
||||
val = JS_GetPropertyStr(js, argv[2], "passive");
|
||||
if (JS_ToBool(js, val)) {
|
||||
hints.ai_flags |= AI_PASSIVE;
|
||||
}
|
||||
JS_FreeValue(js, val);
|
||||
}
|
||||
|
||||
int status = getaddrinfo(node, service, &hints, &res);
|
||||
@@ -563,10 +562,107 @@ JSC_CCALL(socket_setsockopt,
|
||||
JSC_CCALL(socket_close,
|
||||
int sockfd = js2fd(js, argv[0]);
|
||||
if (sockfd < 0) return JS_EXCEPTION;
|
||||
|
||||
|
||||
if (close(sockfd) != 0)
|
||||
return JS_RaiseDisrupt(js, "close failed: %s", strerror(errno));
|
||||
|
||||
|
||||
return JS_NULL;
|
||||
)
|
||||
|
||||
JSC_CCALL(socket_on_readable,
|
||||
int sockfd = js2fd(js, argv[0]);
|
||||
if (sockfd < 0) return JS_EXCEPTION;
|
||||
if (!JS_IsFunction(argv[1]))
|
||||
return JS_RaiseDisrupt(js, "on_readable: callback must be a function");
|
||||
actor_watch_readable(js, sockfd, argv[1]);
|
||||
return JS_NULL;
|
||||
)
|
||||
|
||||
JSC_CCALL(socket_unwatch,
|
||||
int sockfd = js2fd(js, argv[0]);
|
||||
if (sockfd < 0) return JS_EXCEPTION;
|
||||
actor_unwatch(js, sockfd);
|
||||
return JS_NULL;
|
||||
)
|
||||
|
||||
JSC_CCALL(socket_on_writable,
|
||||
int sockfd = js2fd(js, argv[0]);
|
||||
if (sockfd < 0) return JS_EXCEPTION;
|
||||
if (!JS_IsFunction(argv[1]))
|
||||
return JS_RaiseDisrupt(js, "on_writable: callback must be a function");
|
||||
actor_watch_writable(js, sockfd, argv[1]);
|
||||
return JS_NULL;
|
||||
)
|
||||
|
||||
JSC_CCALL(socket_setnonblock,
|
||||
int sockfd = js2fd(js, argv[0]);
|
||||
if (sockfd < 0) return JS_EXCEPTION;
|
||||
#ifdef _WIN32
|
||||
u_long mode = 1;
|
||||
if (ioctlsocket(sockfd, FIONBIO, &mode) != 0)
|
||||
return JS_RaiseDisrupt(js, "setnonblock failed");
|
||||
#else
|
||||
int flags = fcntl(sockfd, F_GETFL, 0);
|
||||
if (flags < 0 || fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) < 0)
|
||||
return JS_RaiseDisrupt(js, "setnonblock failed: %s", strerror(errno));
|
||||
#endif
|
||||
return JS_NULL;
|
||||
)
|
||||
|
||||
JSC_CCALL(socket_getsockopt,
|
||||
int sockfd = js2fd(js, argv[0]);
|
||||
if (sockfd < 0) return JS_EXCEPTION;
|
||||
|
||||
int level = SOL_SOCKET;
|
||||
int optname = 0;
|
||||
|
||||
// Parse level
|
||||
if (JS_IsText(argv[1])) {
|
||||
const char *level_str = JS_ToCString(js, argv[1]);
|
||||
if (strcmp(level_str, "SOL_SOCKET") == 0) level = SOL_SOCKET;
|
||||
else if (strcmp(level_str, "IPPROTO_TCP") == 0) level = IPPROTO_TCP;
|
||||
else if (strcmp(level_str, "IPPROTO_IP") == 0) level = IPPROTO_IP;
|
||||
else if (strcmp(level_str, "IPPROTO_IPV6") == 0) level = IPPROTO_IPV6;
|
||||
JS_FreeCString(js, level_str);
|
||||
} else {
|
||||
level = js2number(js, argv[1]);
|
||||
}
|
||||
|
||||
// Parse option name
|
||||
if (JS_IsText(argv[2])) {
|
||||
const char *opt_str = JS_ToCString(js, argv[2]);
|
||||
if (strcmp(opt_str, "SO_ERROR") == 0) optname = SO_ERROR;
|
||||
else if (strcmp(opt_str, "SO_REUSEADDR") == 0) optname = SO_REUSEADDR;
|
||||
else if (strcmp(opt_str, "SO_KEEPALIVE") == 0) optname = SO_KEEPALIVE;
|
||||
else if (strcmp(opt_str, "SO_BROADCAST") == 0) optname = SO_BROADCAST;
|
||||
JS_FreeCString(js, opt_str);
|
||||
} else {
|
||||
optname = js2number(js, argv[2]);
|
||||
}
|
||||
|
||||
int optval = 0;
|
||||
socklen_t optlen = sizeof(optval);
|
||||
if (getsockopt(sockfd, level, optname, &optval, &optlen) < 0)
|
||||
return JS_RaiseDisrupt(js, "getsockopt failed: %s", strerror(errno));
|
||||
|
||||
return JS_NewInt32(js, optval);
|
||||
)
|
||||
|
||||
JSC_CCALL(socket_send_self,
|
||||
if (argc < 1 || !JS_IsText(argv[0]))
|
||||
return JS_RaiseDisrupt(js, "send_self: expects a text argument");
|
||||
const char *msg = JS_ToCString(js, argv[0]);
|
||||
WotaBuffer wb;
|
||||
wota_buffer_init(&wb, 16);
|
||||
wota_write_record(&wb, 1);
|
||||
wota_write_text(&wb, "text");
|
||||
wota_write_text(&wb, msg);
|
||||
JS_FreeCString(js, msg);
|
||||
const char *err = JS_SendMessage(js, &wb);
|
||||
if (err) {
|
||||
wota_buffer_free(&wb);
|
||||
return JS_RaiseDisrupt(js, "send_self failed: %s", err);
|
||||
}
|
||||
return JS_NULL;
|
||||
)
|
||||
|
||||
@@ -587,6 +683,12 @@ static const JSCFunctionListEntry js_socket_funcs[] = {
|
||||
MIST_FUNC_DEF(socket, gai_strerror, 1),
|
||||
MIST_FUNC_DEF(socket, setsockopt, 4),
|
||||
MIST_FUNC_DEF(socket, close, 1),
|
||||
MIST_FUNC_DEF(socket, on_readable, 2),
|
||||
MIST_FUNC_DEF(socket, on_writable, 2),
|
||||
MIST_FUNC_DEF(socket, unwatch, 1),
|
||||
MIST_FUNC_DEF(socket, setnonblock, 1),
|
||||
MIST_FUNC_DEF(socket, getsockopt, 3),
|
||||
MIST_FUNC_DEF(socket, send_self, 1),
|
||||
};
|
||||
|
||||
JSValue js_core_socket_use(JSContext *js) {
|
||||
@@ -611,6 +713,8 @@ JSValue js_core_socket_use(JSContext *js) {
|
||||
|
||||
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_SetPropertyStr(js, mod.val, "SO_ERROR", JS_NewInt32(js, SO_ERROR));
|
||||
JS_SetPropertyStr(js, mod.val, "SO_KEEPALIVE", JS_NewInt32(js, SO_KEEPALIVE));
|
||||
|
||||
JS_RETURN(mod.val);
|
||||
}
|
||||
|
||||
238
net/tls.c
Normal file
238
net/tls.c
Normal file
@@ -0,0 +1,238 @@
|
||||
#include "cell.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#if defined(__APPLE__)
|
||||
/* SecureTransport — deprecated but functional, no external deps */
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
#include <Security/Security.h>
|
||||
#include <Security/SecureTransport.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#include <poll.h>
|
||||
|
||||
typedef struct {
|
||||
SSLContextRef ssl;
|
||||
int fd;
|
||||
} tls_ctx;
|
||||
|
||||
static void tls_ctx_free(JSRuntime *rt, tls_ctx *ctx) {
|
||||
if (!ctx) return;
|
||||
if (ctx->ssl) {
|
||||
SSLClose(ctx->ssl);
|
||||
CFRelease(ctx->ssl);
|
||||
}
|
||||
if (ctx->fd >= 0)
|
||||
close(ctx->fd);
|
||||
free(ctx);
|
||||
}
|
||||
|
||||
QJSCLASS(tls_ctx,)
|
||||
|
||||
static OSStatus tls_read_cb(SSLConnectionRef conn, void *data, size_t *len) {
|
||||
int fd = *(const int *)conn;
|
||||
size_t requested = *len;
|
||||
size_t total = 0;
|
||||
while (total < requested) {
|
||||
ssize_t n = read(fd, (char *)data + total, requested - total);
|
||||
if (n > 0) {
|
||||
total += n;
|
||||
} else if (n == 0) {
|
||||
*len = total;
|
||||
return errSSLClosedGraceful;
|
||||
} else {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
*len = total;
|
||||
return (total > 0) ? noErr : errSSLWouldBlock;
|
||||
}
|
||||
*len = total;
|
||||
return errSSLClosedAbort;
|
||||
}
|
||||
}
|
||||
*len = total;
|
||||
return noErr;
|
||||
}
|
||||
|
||||
static OSStatus tls_write_cb(SSLConnectionRef conn, const void *data, size_t *len) {
|
||||
int fd = *(const int *)conn;
|
||||
size_t requested = *len;
|
||||
size_t total = 0;
|
||||
while (total < requested) {
|
||||
ssize_t n = write(fd, (const char *)data + total, requested - total);
|
||||
if (n > 0) {
|
||||
total += n;
|
||||
} else if (n == 0) {
|
||||
*len = total;
|
||||
return errSSLClosedGraceful;
|
||||
} else {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
*len = total;
|
||||
return (total > 0) ? noErr : errSSLWouldBlock;
|
||||
}
|
||||
*len = total;
|
||||
return errSSLClosedAbort;
|
||||
}
|
||||
}
|
||||
*len = total;
|
||||
return noErr;
|
||||
}
|
||||
|
||||
/* tls.wrap(fd, hostname) -> ctx */
|
||||
JSC_CCALL(tls_wrap,
|
||||
int fd = -1;
|
||||
if (JS_ToInt32(js, &fd, argv[0]) < 0)
|
||||
return JS_RaiseDisrupt(js, "tls.wrap: fd must be a number");
|
||||
const char *hostname = JS_ToCString(js, argv[1]);
|
||||
if (!hostname)
|
||||
return JS_RaiseDisrupt(js, "tls.wrap: hostname must be a string");
|
||||
|
||||
tls_ctx *ctx = calloc(1, sizeof(tls_ctx));
|
||||
ctx->fd = fd;
|
||||
ctx->ssl = SSLCreateContext(NULL, kSSLClientSide, kSSLStreamType);
|
||||
if (!ctx->ssl) {
|
||||
free(ctx);
|
||||
JS_FreeCString(js, hostname);
|
||||
return JS_RaiseDisrupt(js, "tls.wrap: SSLCreateContext failed");
|
||||
}
|
||||
|
||||
SSLSetIOFuncs(ctx->ssl, tls_read_cb, tls_write_cb);
|
||||
SSLSetConnection(ctx->ssl, &ctx->fd);
|
||||
SSLSetPeerDomainName(ctx->ssl, hostname, strlen(hostname));
|
||||
JS_FreeCString(js, hostname);
|
||||
|
||||
/* Retry handshake on non-blocking sockets (errSSLWouldBlock) */
|
||||
OSStatus status;
|
||||
for (int attempts = 0; attempts < 200; attempts++) {
|
||||
status = SSLHandshake(ctx->ssl);
|
||||
if (status == noErr) break;
|
||||
if (status != errSSLWouldBlock) break;
|
||||
struct pollfd pfd = { .fd = ctx->fd, .events = POLLIN | POLLOUT };
|
||||
poll(&pfd, 1, 50);
|
||||
}
|
||||
if (status != noErr) {
|
||||
CFRelease(ctx->ssl);
|
||||
ctx->ssl = NULL;
|
||||
ctx->fd = -1; /* don't close caller's fd */
|
||||
free(ctx);
|
||||
return JS_RaiseDisrupt(js, "tls.wrap: handshake failed (status %d)", (int)status);
|
||||
}
|
||||
|
||||
return tls_ctx2js(js, ctx);
|
||||
)
|
||||
|
||||
/* tls.send(ctx, data) -> bytes_sent */
|
||||
JSC_CCALL(tls_send,
|
||||
tls_ctx *ctx = js2tls_ctx(js, argv[0]);
|
||||
if (!ctx || !ctx->ssl)
|
||||
return JS_RaiseDisrupt(js, "tls.send: invalid context");
|
||||
|
||||
size_t len;
|
||||
size_t written = 0;
|
||||
OSStatus status;
|
||||
|
||||
if (JS_IsText(argv[1])) {
|
||||
const char *data = JS_ToCStringLen(js, &len, argv[1]);
|
||||
status = SSLWrite(ctx->ssl, data, len, &written);
|
||||
JS_FreeCString(js, data);
|
||||
} else {
|
||||
unsigned char *data = js_get_blob_data(js, &len, argv[1]);
|
||||
if (!data)
|
||||
return JS_RaiseDisrupt(js, "tls.send: invalid data");
|
||||
status = SSLWrite(ctx->ssl, data, len, &written);
|
||||
}
|
||||
|
||||
if (status != noErr && status != errSSLWouldBlock)
|
||||
return JS_RaiseDisrupt(js, "tls.send: write failed (status %d)", (int)status);
|
||||
|
||||
return JS_NewInt64(js, (int64_t)written);
|
||||
)
|
||||
|
||||
/* tls.recv(ctx, len) -> blob */
|
||||
JSC_CCALL(tls_recv,
|
||||
tls_ctx *ctx = js2tls_ctx(js, argv[0]);
|
||||
if (!ctx || !ctx->ssl)
|
||||
return JS_RaiseDisrupt(js, "tls.recv: invalid context");
|
||||
|
||||
size_t len = 4096;
|
||||
if (argc > 1) len = js2number(js, argv[1]);
|
||||
|
||||
void *out;
|
||||
ret = js_new_blob_alloc(js, len, &out);
|
||||
if (JS_IsException(ret)) return ret;
|
||||
|
||||
size_t received = 0;
|
||||
OSStatus status = SSLRead(ctx->ssl, out, len, &received);
|
||||
|
||||
if (status != noErr && status != errSSLWouldBlock &&
|
||||
status != errSSLClosedGraceful) {
|
||||
return JS_RaiseDisrupt(js, "tls.recv: read failed (status %d)", (int)status);
|
||||
}
|
||||
|
||||
js_blob_stone(ret, received);
|
||||
return ret;
|
||||
)
|
||||
|
||||
/* tls.close(ctx) -> null */
|
||||
JSC_CCALL(tls_close,
|
||||
tls_ctx *ctx = js2tls_ctx(js, argv[0]);
|
||||
if (!ctx) return JS_NULL;
|
||||
if (ctx->ssl) {
|
||||
SSLClose(ctx->ssl);
|
||||
CFRelease(ctx->ssl);
|
||||
ctx->ssl = NULL;
|
||||
}
|
||||
if (ctx->fd >= 0) {
|
||||
close(ctx->fd);
|
||||
ctx->fd = -1;
|
||||
}
|
||||
return JS_NULL;
|
||||
)
|
||||
|
||||
/* tls.fd(ctx) -> number — get underlying fd for on_readable */
|
||||
JSC_CCALL(tls_fd,
|
||||
tls_ctx *ctx = js2tls_ctx(js, argv[0]);
|
||||
if (!ctx)
|
||||
return JS_RaiseDisrupt(js, "tls.fd: invalid context");
|
||||
return JS_NewInt32(js, ctx->fd);
|
||||
)
|
||||
|
||||
/* tls.on_readable(ctx, callback) -> null */
|
||||
JSC_CCALL(tls_on_readable,
|
||||
tls_ctx *ctx = js2tls_ctx(js, argv[0]);
|
||||
if (!ctx)
|
||||
return JS_RaiseDisrupt(js, "tls.on_readable: invalid context");
|
||||
if (!JS_IsFunction(argv[1]))
|
||||
return JS_RaiseDisrupt(js, "tls.on_readable: callback must be a function");
|
||||
actor_watch_readable(js, ctx->fd, argv[1]);
|
||||
return JS_NULL;
|
||||
)
|
||||
|
||||
static const JSCFunctionListEntry js_tls_funcs[] = {
|
||||
MIST_FUNC_DEF(tls, wrap, 2),
|
||||
MIST_FUNC_DEF(tls, send, 2),
|
||||
MIST_FUNC_DEF(tls, recv, 2),
|
||||
MIST_FUNC_DEF(tls, close, 1),
|
||||
MIST_FUNC_DEF(tls, fd, 1),
|
||||
MIST_FUNC_DEF(tls, on_readable, 2),
|
||||
};
|
||||
|
||||
JSValue js_core_net_tls_use(JSContext *js) {
|
||||
JS_FRAME(js);
|
||||
QJSCLASSPREP_NO_FUNCS(tls_ctx);
|
||||
JS_ROOT(mod, JS_NewObject(js));
|
||||
JS_SetPropertyFunctionList(js, mod.val, js_tls_funcs, countof(js_tls_funcs));
|
||||
JS_RETURN(mod.val);
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
#else
|
||||
/* Stub for non-Apple platforms — TLS not yet implemented */
|
||||
JSValue js_core_net_tls_use(JSContext *js) {
|
||||
return JS_RaiseDisrupt(js, "TLS not available on this platform");
|
||||
}
|
||||
#endif
|
||||
64
parse.cm
64
parse.cm
@@ -1420,6 +1420,7 @@ var parse = function(tokens, src, filename, tokenizer) {
|
||||
var sem_errors = []
|
||||
var scopes_array = []
|
||||
var intrinsics = []
|
||||
var hoisted_fn_refs = []
|
||||
|
||||
var sem_error = function(node, msg) {
|
||||
var err = {message: msg}
|
||||
@@ -1441,14 +1442,17 @@ var parse = function(tokens, src, filename, tokenizer) {
|
||||
}
|
||||
|
||||
var sem_add_var = function(scope, name, make_opts) {
|
||||
push(scope.vars, {
|
||||
var entry = {
|
||||
name: name,
|
||||
is_const: make_opts.is_const == true,
|
||||
make: make_opts.make,
|
||||
function_nr: make_opts.fn_nr,
|
||||
nr_uses: 0,
|
||||
closure: 0
|
||||
})
|
||||
}
|
||||
if (make_opts.reached == false) entry.reached = false
|
||||
if (make_opts.decl_line != null) entry.decl_line = make_opts.decl_line
|
||||
push(scope.vars, entry)
|
||||
}
|
||||
|
||||
var sem_lookup_var = function(scope, name) {
|
||||
@@ -1567,39 +1571,17 @@ var parse = function(tokens, src, filename, tokenizer) {
|
||||
var sem_check_expr = null
|
||||
var sem_check_stmt = null
|
||||
|
||||
var sem_predeclare_vars = function(scope, stmts) {
|
||||
var sem_predeclare_fns = function(scope, stmts) {
|
||||
var i = 0
|
||||
var stmt = null
|
||||
var kind = null
|
||||
var name = null
|
||||
var item = null
|
||||
var ik = null
|
||||
var j = 0
|
||||
while (i < length(stmts)) {
|
||||
stmt = stmts[i]
|
||||
kind = stmt.kind
|
||||
if (kind == "function") {
|
||||
if (stmt.kind == "function") {
|
||||
name = stmt.name
|
||||
if (name != null && sem_find_var(scope, name) == null) {
|
||||
sem_add_var(scope, name, {make: "function", fn_nr: scope.function_nr})
|
||||
}
|
||||
} else if (kind == "var") {
|
||||
name = stmt.left.name
|
||||
if (name != null && sem_find_var(scope, name) == null) {
|
||||
sem_add_var(scope, name, {make: "var", fn_nr: scope.function_nr})
|
||||
}
|
||||
} else if (kind == "var_list") {
|
||||
j = 0
|
||||
while (j < length(stmt.list)) {
|
||||
item = stmt.list[j]
|
||||
ik = item.kind
|
||||
if (ik == "var") {
|
||||
name = item.left.name
|
||||
if (name != null && sem_find_var(scope, name) == null) {
|
||||
sem_add_var(scope, name, {make: "var", fn_nr: scope.function_nr})
|
||||
}
|
||||
}
|
||||
j = j + 1
|
||||
sem_add_var(scope, name, {make: "function", fn_nr: scope.function_nr,
|
||||
decl_line: stmt.from_row != null ? stmt.from_row + 1 : null, reached: false})
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
@@ -1831,7 +1813,7 @@ var parse = function(tokens, src, filename, tokenizer) {
|
||||
i = i + 1
|
||||
}
|
||||
if (expr.statements != null) {
|
||||
sem_predeclare_vars(fn_scope, expr.statements)
|
||||
sem_predeclare_fns(fn_scope, expr.statements)
|
||||
i = 0
|
||||
while (i < length(expr.statements)) {
|
||||
sem_check_stmt(fn_scope, expr.statements[i])
|
||||
@@ -1875,6 +1857,11 @@ var parse = function(tokens, src, filename, tokenizer) {
|
||||
expr.function_nr = r.def_function_nr
|
||||
r.v.nr_uses = r.v.nr_uses + 1
|
||||
if (r.level > 0) r.v.closure = 1
|
||||
if (r.v.reached == false && r.v.decl_line != null && expr.from_row != null && expr.from_row + 1 < r.v.decl_line) {
|
||||
push(hoisted_fn_refs, {name: name, line: expr.from_row + 1,
|
||||
col: expr.from_column != null ? expr.from_column + 1 : null,
|
||||
decl_line: r.v.decl_line})
|
||||
}
|
||||
} else {
|
||||
expr.level = -1
|
||||
expr.intrinsic = true
|
||||
@@ -2088,7 +2075,14 @@ var parse = function(tokens, src, filename, tokenizer) {
|
||||
enclosing = sem_find_func_scope(scope)
|
||||
if (enclosing != null) enclosing.has_inner_func = true
|
||||
name = stmt.name
|
||||
if (name != null && sem_find_var(scope, name) == null) sem_add_var(scope, name, {make: "function", fn_nr: scope.function_nr})
|
||||
if (name != null) {
|
||||
existing = sem_find_var(scope, name)
|
||||
if (existing != null) {
|
||||
existing.reached = true
|
||||
} else {
|
||||
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})
|
||||
@@ -2102,7 +2096,7 @@ var parse = function(tokens, src, filename, tokenizer) {
|
||||
if (def_val != null) sem_check_expr(fn_scope, def_val)
|
||||
i = i + 1
|
||||
}
|
||||
sem_predeclare_vars(fn_scope, stmt.statements)
|
||||
sem_predeclare_fns(fn_scope, stmt.statements)
|
||||
i = 0
|
||||
while (i < length(stmt.statements)) {
|
||||
sem_check_stmt(fn_scope, stmt.statements[i])
|
||||
@@ -2124,6 +2118,7 @@ var parse = function(tokens, src, filename, tokenizer) {
|
||||
}
|
||||
|
||||
var semantic_check = function(ast) {
|
||||
hoisted_fn_refs = []
|
||||
var global_scope = make_scope(null, 0, {is_func: true})
|
||||
var i = 0
|
||||
var stmt = null
|
||||
@@ -2134,7 +2129,11 @@ var parse = function(tokens, src, filename, tokenizer) {
|
||||
i = 0
|
||||
while (i < length(ast.functions)) {
|
||||
name = ast.functions[i].name
|
||||
if (name != null) sem_add_var(global_scope, name, {make: "function", fn_nr: 0})
|
||||
if (name != null) {
|
||||
sem_add_var(global_scope, name, {make: "function", fn_nr: 0,
|
||||
decl_line: ast.functions[i].from_row != null ? ast.functions[i].from_row + 1 : null,
|
||||
reached: false})
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
@@ -2161,6 +2160,7 @@ var parse = function(tokens, src, filename, tokenizer) {
|
||||
|
||||
ast.scopes = scopes_array
|
||||
ast.intrinsics = intrinsics
|
||||
if (length(hoisted_fn_refs) > 0) ast._hoisted_fns = hoisted_fn_refs
|
||||
if (length(sem_errors) > 0) {
|
||||
ast.errors = sem_errors
|
||||
}
|
||||
|
||||
338
plan.md
338
plan.md
@@ -1,338 +0,0 @@
|
||||
# Cell/QuickJS Refactoring Plan: Remove Atoms, Shapes, and Dual-Encoding
|
||||
|
||||
## Overview
|
||||
|
||||
Refactor `source/quickjs.c` to match `docs/memory.md` specification:
|
||||
- Remove JSAtom system (171 references → ~41 remaining)
|
||||
- Remove JSShape system (94 references) ✓
|
||||
- Remove IC caches (shape-based inline caches) ✓
|
||||
- Remove `is_wide_char` dual-encoding (18 locations) ✓
|
||||
- Use JSValue texts directly as property keys
|
||||
- Reference: `mquickjs.c` shows the target pattern
|
||||
|
||||
## Completed Phases
|
||||
|
||||
### Phase 1: Remove is_wide_char Remnants ✓
|
||||
### Phase 2: Remove IC Caches ✓
|
||||
### Phase 3: Remove JSShape System ✓
|
||||
### Phase 4: Complete Property Access with JSValue Keys ✓
|
||||
|
||||
Completed:
|
||||
- Removed JS_GC_OBJ_TYPE_JS_OBJECT fallbacks from OP_get_field
|
||||
- Removed JS_GC_OBJ_TYPE_JS_OBJECT fallbacks from OP_put_field
|
||||
- Removed JS_GC_OBJ_TYPE_JS_OBJECT fallbacks from OP_define_field
|
||||
- Created emit_key() function that adds JSValue to cpool and emits index
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Convert JSAtom to JSValue Text (IN PROGRESS)
|
||||
|
||||
This is the core transformation. All identifier handling moves from atoms to JSValue.
|
||||
|
||||
### Completed Items
|
||||
|
||||
**Token and Parser Infrastructure:**
|
||||
- [x] Change JSToken.u.ident.atom to JSToken.u.ident.str (JSValue)
|
||||
- [x] Change parse_ident() to return JSValue
|
||||
- [x] Create emit_key() function (cpool-based)
|
||||
- [x] Create JS_KEY_* macros for common names (lines ~279-335 in quickjs.c)
|
||||
- [x] Update all token.u.ident.atom references to .str
|
||||
- [x] Create keyword lookup table (js_keywords[]) with string comparison
|
||||
- [x] Rewrite update_token_ident() to use js_keyword_lookup()
|
||||
- [x] Rewrite is_strict_future_keyword() to use JSValue
|
||||
- [x] Update token_is_pseudo_keyword() to use JSValue and js_key_equal()
|
||||
|
||||
**Function Declaration Parsing:**
|
||||
- [x] Update js_parse_function_decl() signature to use JSValue func_name
|
||||
- [x] Update js_parse_function_decl2() to use JSValue func_name throughout
|
||||
- [x] Update js_parse_function_check_names() to use JSValue
|
||||
- [x] Convert JS_DupAtom/JS_FreeAtom to JS_DupValue/JS_FreeValue in function parsing
|
||||
|
||||
**Variable Definition and Lookup:**
|
||||
- [x] Update find_global_var() to use JSValue and js_key_equal()
|
||||
- [x] Update find_lexical_global_var() to use JSValue
|
||||
- [x] Update find_lexical_decl() to use JSValue and js_key_equal()
|
||||
- [x] Update js_define_var() to use JSValue
|
||||
- [x] Update js_parse_check_duplicate_parameter() to use JSValue and js_key_equal()
|
||||
- [x] Update js_parse_destructuring_var() to return JSValue
|
||||
- [x] Update js_parse_var() to use JSValue for variable names
|
||||
|
||||
**Comparison Helpers:**
|
||||
- [x] Create js_key_equal_str() for comparing JSValue with C string literals
|
||||
- [x] Update is_var_in_arg_scope() to use js_key_equal/js_key_equal_str
|
||||
- [x] Update has_with_scope() to use js_key_equal_str
|
||||
- [x] Update closure variable comparisons (cv->var_name) to use js_key_equal_str
|
||||
|
||||
**Property Access:**
|
||||
- [x] Fix JS_GetPropertyStr to create proper JSValue keys
|
||||
- [x] Fix JS_SetPropertyInternal callers to use JS_KEY_* instead of JS_ATOM_*
|
||||
|
||||
### JS_KEY_* Macros Added
|
||||
|
||||
Compile-time immediate ASCII string constants (≤7 chars):
|
||||
```c
|
||||
JS_KEY_empty, JS_KEY_name, JS_KEY_message, JS_KEY_stack,
|
||||
JS_KEY_errors, JS_KEY_Error, JS_KEY_cause, JS_KEY_length,
|
||||
JS_KEY_value, JS_KEY_get, JS_KEY_set, JS_KEY_raw,
|
||||
JS_KEY_flags, JS_KEY_source, JS_KEY_exec, JS_KEY_toJSON,
|
||||
JS_KEY_eval, JS_KEY_this, JS_KEY_true, JS_KEY_false,
|
||||
JS_KEY_null, JS_KEY_NaN, JS_KEY_default, JS_KEY_index,
|
||||
JS_KEY_input, JS_KEY_groups, JS_KEY_indices, JS_KEY_let,
|
||||
JS_KEY_var, JS_KEY_new, JS_KEY_of, JS_KEY_yield,
|
||||
JS_KEY_async, JS_KEY_target, JS_KEY_from, JS_KEY_meta,
|
||||
JS_KEY_as, JS_KEY_with
|
||||
```
|
||||
|
||||
Runtime macro for strings >7 chars:
|
||||
```c
|
||||
#define JS_KEY_STR(ctx, str) JS_NewStringLen((ctx), (str), sizeof(str) - 1)
|
||||
```
|
||||
|
||||
Helper function for comparing JSValue with C string literals:
|
||||
```c
|
||||
static JS_BOOL js_key_equal_str(JSValue a, const char *str);
|
||||
```
|
||||
|
||||
### Remaining Work
|
||||
|
||||
#### 5.3 Update js_parse_property_name() ✓
|
||||
- [x] Change return type from JSAtom* to JSValue*
|
||||
- [x] Update all callers (js_parse_object_literal, etc.)
|
||||
- [x] Updated get_lvalue(), put_lvalue(), js_parse_destructuring_element()
|
||||
|
||||
#### 5.4 Replace remaining emit_atom() calls with emit_key() ✓
|
||||
- [x] Removed emit_atom wrapper function
|
||||
- [x] Changed last emit_atom(JS_ATOM_this) to emit_key(JS_KEY_this)
|
||||
|
||||
#### 5.5 Update Variable Opcode Format in quickjs-opcode.h
|
||||
- [ ] Change `atom` format opcodes to `key` format
|
||||
- [ ] Change `atom_u8` and `atom_u16` to `key_u8` and `key_u16`
|
||||
|
||||
#### 5.6 Update VM Opcode Handlers ✓
|
||||
These now read cpool indices and look up JSValue:
|
||||
- [x] OP_check_var, OP_get_var_undef, OP_get_var
|
||||
- [x] OP_put_var, OP_put_var_init, OP_put_var_strict
|
||||
- [x] OP_set_name, OP_make_var_ref, OP_delete_var
|
||||
- [x] OP_define_var, OP_define_func, OP_throw_error
|
||||
- [x] OP_make_loc_ref, OP_make_arg_ref
|
||||
- [x] OP_define_method, OP_define_method_computed
|
||||
|
||||
#### 5.7 Update resolve_scope_var() ✓
|
||||
- [x] Changed signature to use JSValue var_name
|
||||
- [x] Updated all comparisons to use js_key_equal()/js_key_equal_str()
|
||||
- [x] Updated var_object_test() to use JSValue
|
||||
- [x] Updated optimize_scope_make_global_ref() to use JSValue
|
||||
- [x] Updated resolve_variables() callers to read from cpool
|
||||
|
||||
#### 5.8 Convert Remaining JS_ATOM_* Usages
|
||||
Categories remaining:
|
||||
- Some debug/print functions still use JSAtom
|
||||
- Some function signatures not yet converted
|
||||
- Will be addressed in Phase 7 cleanup
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Update Bytecode Serialization ✓
|
||||
|
||||
### 6.1 JS_WriteObjectTag Changes ✓
|
||||
- [x] Changed JS_WriteObjectTag to use bc_put_key() directly for property keys
|
||||
- [x] Removed JS_ValueToAtom/bc_put_atom path (was broken anyway)
|
||||
- [x] cpool values serialized via JS_WriteObjectRec()
|
||||
|
||||
### 6.2 JS_ReadObject Changes ✓
|
||||
- [x] Changed JS_ReadObjectTag to use bc_get_key() for property keys
|
||||
- [x] Uses JS_SetPropertyInternal with JSValue keys
|
||||
|
||||
### 6.3 Opcode Format Updates ✓
|
||||
- [x] Added OP_FMT_key_u8, OP_FMT_key_u16, OP_FMT_key_label_u16 formats
|
||||
- [x] Updated variable opcodes to use key formats instead of atom formats
|
||||
- [x] Updated bc_byte_swap() to handle new key formats
|
||||
- [x] Updated JS_WriteFunctionBytecode() to skip key format opcodes
|
||||
- [x] Updated JS_ReadFunctionBytecode() to skip key format opcodes
|
||||
|
||||
### 6.4 Version Bump ✓
|
||||
- [x] Incremented BC_VERSION from 5 to 6
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Final Cleanup ✓
|
||||
|
||||
### 7.1 Additional Parser/Compiler Fixes ✓
|
||||
- [x] Fixed TOK_IDENT case to use JSValue name, JS_DupValue, emit_key
|
||||
- [x] Fixed TOK_TRY catch clause to use JSValue name
|
||||
- [x] Fixed js_parse_statement_or_decl label_name to use JSValue
|
||||
- [x] Fixed OP_scope_get_var "eval" check to use js_key_equal_str
|
||||
- [x] Fixed js_parse_delete to use JSValue for name comparison
|
||||
- [x] Fixed JSON parsing to use js_key_from_string for property names
|
||||
- [x] Added js_key_from_string() helper function
|
||||
- [x] Added JS_KEY__ret_, JS_KEY__eval_, JS_KEY__var_ for internal names
|
||||
- [x] Updated add_closure_var, get_closure_var2, get_closure_var to use JSValue var_name
|
||||
- [x] Updated set_closure_from_var to use JS_DupValue
|
||||
- [x] Updated add_closure_variables to use JS_DupValue
|
||||
- [x] Updated instantiate_hoisted_definitions to use fd_cpool_add for keys
|
||||
- [x] Updated resolve_variables to use js_key_equal and fd_cpool_add
|
||||
|
||||
### 7.1.1 Property Access and Runtime Fixes ✓
|
||||
- [x] Fixed JS_GetPropertyValue to use js_key_from_string instead of JS_ValueToAtom
|
||||
- [x] Fixed JS_GetPropertyKey to use js_key_from_string for string keys
|
||||
- [x] Fixed JS_SetPropertyKey to use js_key_from_string for string keys
|
||||
- [x] Fixed JS_HasPropertyKey to use js_key_from_string for string keys
|
||||
- [x] Fixed JS_DeletePropertyKey to use js_key_from_string for string keys
|
||||
- [x] Updated JS_HasProperty signature to take JSValue prop
|
||||
- [x] Fixed OP_get_ref_value handler to use JSValue key
|
||||
- [x] Fixed OP_put_ref_value handler to use JSValue key
|
||||
- [x] Updated free_func_def to use JS_FreeValue for JSValue fields
|
||||
|
||||
### 7.2 Remove JSAtom Type and Functions ✓
|
||||
- [x] Removed most JS_ATOM_* constants (kept JS_ATOM_NULL, JS_ATOM_END for BC compat)
|
||||
- [x] JS_NewAtomString now returns JSValue using js_key_new
|
||||
- [x] JS_FreeAtom, JS_DupAtom are stubs (no-op for backward compat)
|
||||
- [x] JS_AtomToValue, JS_ValueToAtom are stubs (minimal BC compat)
|
||||
- [x] Replaced JS_ATOM_* usages with JS_KEY_* or JS_GetPropertyStr
|
||||
|
||||
### 7.3 Additional Runtime Fixes ✓
|
||||
- [x] Fixed free_function_bytecode to use JS_FreeValueRT for JSValue fields
|
||||
- [x] Fixed JS_SetPropertyFunctionList to use JSValue keys via find_key()
|
||||
- [x] Fixed JS_InstantiateFunctionListItem to use JSValue keys
|
||||
- [x] Fixed internalize_json_property to use JSValue names
|
||||
- [x] Fixed emit_break and push_break_entry to use JSValue label_name
|
||||
- [x] Implemented JS_Invoke to use JSValue method key
|
||||
|
||||
### 7.4 Remaining Stubs (kept for bytecode backward compatibility)
|
||||
- JSAtom typedef (uint32_t) - used in BC serialization
|
||||
- JS_ATOM_NULL, JS_ATOM_END - bytecode format markers
|
||||
- JS_FreeAtom, JS_DupAtom - no-op stubs
|
||||
- JS_FreeAtomRT, JS_DupAtomRT - no-op stubs
|
||||
- Legacy BC reader (idx_to_atom array) - for reading old bytecode
|
||||
|
||||
---
|
||||
|
||||
## Current Build Status
|
||||
|
||||
**Build: SUCCEEDS** with warnings (unused variables, labels)
|
||||
|
||||
**Statistics:**
|
||||
- JS_ATOM_* usages: Minimal (only BC serialization compat)
|
||||
- Property access uses JS_KEY_* macros or JS_GetPropertyStr
|
||||
- BC_VERSION: 6 (updated for new key-based format)
|
||||
|
||||
**What Works:**
|
||||
- All property access via JSValue keys
|
||||
- Keyword detection via string comparison
|
||||
- Function declaration parsing with JSValue names
|
||||
- Variable definition with JSValue names
|
||||
- Closure variable tracking with JSValue names
|
||||
- VM opcode handlers read cpool indices and look up JSValue
|
||||
- resolve_scope_var() uses JSValue throughout
|
||||
- js_parse_property_name() returns JSValue
|
||||
- Bytecode serialization uses bc_put_key/bc_get_key for property keys
|
||||
- Variable opcodes use key format (cpool indices)
|
||||
- JSON parsing uses JSValue keys
|
||||
- Internal variable names use JS_KEY__ret_, JS_KEY__eval_, JS_KEY__var_
|
||||
- JS_SetPropertyFunctionList uses JSValue keys
|
||||
- JS_Invoke uses JSValue method keys
|
||||
- break/continue labels use JSValue
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Migrate to New Tagging System (IN PROGRESS)
|
||||
|
||||
**Problem**: `JS_VALUE_GET_TAG` returns `JS_TAG_PTR` for all pointers, but ~200 places check for obsolete tags like `JS_TAG_OBJECT`, `JS_TAG_STRING`, `JS_TAG_FUNCTION`, etc., which are never returned. This causes crashes.
|
||||
|
||||
**Target Design** (from memory.md):
|
||||
- JSValue tags: Only `JS_TAG_INT`, `JS_TAG_PTR`, `JS_TAG_SHORT_FLOAT`, `JS_TAG_SPECIAL`
|
||||
- Pointer types determined by `objhdr_t` header at offset 8 in heap objects
|
||||
- mist_obj_type: `OBJ_ARRAY(0)`, `OBJ_BLOB(1)`, `OBJ_TEXT(2)`, `OBJ_RECORD(3)`, `OBJ_FUNCTION(4)`, etc.
|
||||
|
||||
### 8.1 Unified Heap Object Layout ✓
|
||||
- [x] Updated mist_text structure to have objhdr_t at offset 8:
|
||||
```c
|
||||
typedef struct mist_text {
|
||||
JSRefCountHeader _dummy_header; /* unused, for offset alignment */
|
||||
uint32_t _pad; /* padding to align objhdr_t to offset 8 */
|
||||
objhdr_t hdr; /* NOW at offset 8, like JSString */
|
||||
word_t length;
|
||||
word_t packed[];
|
||||
} mist_text;
|
||||
```
|
||||
- [x] JSString already has objhdr_t at offset 8
|
||||
|
||||
### 8.2 Type-Checking Helper Functions ✓
|
||||
Added lowercase internal helpers (to avoid conflict with quickjs.h declarations):
|
||||
```c
|
||||
static inline JS_BOOL js_is_gc_object(JSValue v) { return JS_IsPtr(v); }
|
||||
static inline JSGCObjectTypeEnum js_get_gc_type(JSValue v) {
|
||||
return ((JSGCObjectHeader *)JS_VALUE_GET_PTR(v))->gc_obj_type;
|
||||
}
|
||||
static inline JS_BOOL js_is_record(JSValue v) {
|
||||
if (!JS_IsPtr(v)) return FALSE;
|
||||
return js_get_gc_type(v) == JS_GC_OBJ_TYPE_RECORD;
|
||||
}
|
||||
static inline JS_BOOL js_is_array(JSValue v) {
|
||||
if (!JS_IsPtr(v)) return FALSE;
|
||||
return js_get_gc_type(v) == JS_GC_OBJ_TYPE_ARRAY;
|
||||
}
|
||||
static inline JS_BOOL js_is_function(JSValue v) {
|
||||
if (!JS_IsPtr(v)) return FALSE;
|
||||
return js_get_gc_type(v) == JS_GC_OBJ_TYPE_FUNCTION;
|
||||
}
|
||||
static inline JS_BOOL js_is_object(JSValue v) {
|
||||
if (!JS_IsPtr(v)) return FALSE;
|
||||
JSGCObjectTypeEnum t = js_get_gc_type(v);
|
||||
return t == JS_GC_OBJ_TYPE_RECORD || t == JS_GC_OBJ_TYPE_ARRAY;
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 Updated Core Functions ✓
|
||||
- [x] Updated JS_IsString to read objhdr_t from offset 8
|
||||
- [x] Updated js_key_hash to read objhdr_t from offset 8
|
||||
- [x] Updated js_key_equal to read objhdr_t from offset 8
|
||||
- [x] Updated __JS_FreeValueRT to use objhdr_type for type dispatch
|
||||
- [x] Updated JS_MarkValue, JS_MarkValueEdgeEx for GC
|
||||
- [x] Added JS_SetPropertyValue function
|
||||
- [x] Changed quickjs.h JS_IsFunction/JS_IsObject from inline to extern declarations
|
||||
|
||||
### 8.4 Tag Check Migration (PARTIAL)
|
||||
Updated some critical tag checks:
|
||||
- [x] Some JS_TAG_OBJECT checks → js_is_object() or js_is_record()
|
||||
- [ ] Many more JS_TAG_OBJECT checks remain (~200 total)
|
||||
- [ ] JS_TAG_FUNCTION checks → js_is_function()
|
||||
- [ ] JS_TAG_STRING checks (some already use JS_IsString)
|
||||
|
||||
### 8.5 Remaining Work
|
||||
- [ ] Fix ASAN memory corruption error (attempting free on address not malloc'd)
|
||||
- Crash occurs in js_def_realloc during js_realloc_array
|
||||
- Address is 112 bytes inside a JSFunctionDef allocation
|
||||
- [ ] Complete remaining ~200 tag check migrations
|
||||
- [ ] Add mist_hdr to JSFunction (optional, gc_obj_type already works)
|
||||
- [ ] Remove obsolete tag definitions from quickjs.h:
|
||||
- JS_TAG_STRING = -8
|
||||
- JS_TAG_ARRAY = -6
|
||||
- JS_TAG_FUNCTION = -5
|
||||
- JS_TAG_FUNCTION_BYTECODE = -2
|
||||
- JS_TAG_OBJECT = -1
|
||||
|
||||
### Current Status
|
||||
|
||||
**Build: SUCCEEDS** with warnings
|
||||
|
||||
**Runtime: CRASHES** with ASAN error:
|
||||
```
|
||||
==16122==ERROR: AddressSanitizer: attempting free on address which was not malloc()-ed
|
||||
```
|
||||
The crash occurs during test execution in `js_def_realloc` called from `js_realloc_array`.
|
||||
Root cause not yet identified - likely a pointer being passed to realloc that wasn't allocated with malloc.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- JSVarDef.var_name is JSValue
|
||||
- JSClosureVar.var_name is JSValue
|
||||
- JSGlobalVar.var_name is JSValue
|
||||
- JSFunctionDef.func_name is JSValue
|
||||
- BlockEnv.label_name is JSValue
|
||||
- OP_get_field/put_field/define_field already use cpool index format
|
||||
- JSRecord with open addressing is fully implemented
|
||||
- js_key_hash and js_key_equal work with both immediate and heap text
|
||||
- js_key_equal_str enables comparison with C string literals for internal names
|
||||
@@ -135,7 +135,6 @@ JSC_SCALL(file_listfiles,
|
||||
JSValue arr = JS_NewArray(js);
|
||||
struct listfiles_ctx ctx = { js, arr, 0 };
|
||||
if (pd_file->listfiles(str, listfiles_cb, &ctx, showhidden) != 0) {
|
||||
JS_FreeValue(js, arr);
|
||||
ret = JS_NULL;
|
||||
} else {
|
||||
ret = arr;
|
||||
|
||||
@@ -112,9 +112,6 @@ static void encode_js_object(json_encoder *enc, JSContext *js, JSValue obj) {
|
||||
JSValue val = JS_GetProperty(js, obj, props[i].atom);
|
||||
enc->addTableMember(enc, key, strlen(key));
|
||||
encode_js_value(enc, js, val);
|
||||
JS_FreeValue(js, val);
|
||||
JS_FreeCString(js, key);
|
||||
JS_FreeAtom(js, props[i].atom);
|
||||
}
|
||||
js_free_rt(props);
|
||||
}
|
||||
@@ -125,12 +122,10 @@ static void encode_js_array(json_encoder *enc, JSContext *js, JSValue arr) {
|
||||
enc->startArray(enc);
|
||||
JSValue lenVal = JS_GetPropertyStr(js, arr, "length");
|
||||
int len = (int)js2number(js, lenVal);
|
||||
JS_FreeValue(js, lenVal);
|
||||
for (int i = 0; i < len; i++) {
|
||||
enc->addArrayMember(enc);
|
||||
JSValue val = JS_GetPropertyNumber(js, arr, i);
|
||||
encode_js_value(enc, js, val);
|
||||
JS_FreeValue(js, val);
|
||||
}
|
||||
enc->endArray(enc);
|
||||
}
|
||||
@@ -149,7 +144,6 @@ static void encode_js_value(json_encoder *enc, JSContext *js, JSValue val) {
|
||||
size_t len;
|
||||
const char *str = JS_ToCStringLen(js, &len, val);
|
||||
enc->writeString(enc, str, len);
|
||||
JS_FreeCString(js, str);
|
||||
} else if (JS_IsArray(val)) {
|
||||
encode_js_array(enc, js, val);
|
||||
} else if (JS_IsObject(val)) {
|
||||
|
||||
@@ -30,9 +30,6 @@ static void add_score_cb(PDScore *score, const char *errorMessage) {
|
||||
args[0] = score_to_js(g_scoreboard_js, score);
|
||||
args[1] = errorMessage ? JS_NewString(g_scoreboard_js, errorMessage) : JS_NULL;
|
||||
JSValue ret = JS_Call(g_scoreboard_js, g_add_score_callback, JS_NULL, 2, args);
|
||||
JS_FreeValue(g_scoreboard_js, ret);
|
||||
JS_FreeValue(g_scoreboard_js, args[0]);
|
||||
JS_FreeValue(g_scoreboard_js, args[1]);
|
||||
}
|
||||
|
||||
static void personal_best_cb(PDScore *score, const char *errorMessage) {
|
||||
@@ -41,9 +38,6 @@ static void personal_best_cb(PDScore *score, const char *errorMessage) {
|
||||
args[0] = score_to_js(g_scoreboard_js, score);
|
||||
args[1] = errorMessage ? JS_NewString(g_scoreboard_js, errorMessage) : JS_NULL;
|
||||
JSValue ret = JS_Call(g_scoreboard_js, g_personal_best_callback, JS_NULL, 2, args);
|
||||
JS_FreeValue(g_scoreboard_js, ret);
|
||||
JS_FreeValue(g_scoreboard_js, args[0]);
|
||||
JS_FreeValue(g_scoreboard_js, args[1]);
|
||||
}
|
||||
|
||||
static void boards_list_cb(PDBoardsList *boards, const char *errorMessage) {
|
||||
@@ -65,9 +59,6 @@ static void boards_list_cb(PDBoardsList *boards, const char *errorMessage) {
|
||||
}
|
||||
args[1] = errorMessage ? JS_NewString(g_scoreboard_js, errorMessage) : JS_NULL;
|
||||
JSValue ret = JS_Call(g_scoreboard_js, g_boards_list_callback, JS_NULL, 2, args);
|
||||
JS_FreeValue(g_scoreboard_js, ret);
|
||||
JS_FreeValue(g_scoreboard_js, args[0]);
|
||||
JS_FreeValue(g_scoreboard_js, args[1]);
|
||||
}
|
||||
|
||||
static void scores_cb(PDScoresList *scores, const char *errorMessage) {
|
||||
@@ -92,9 +83,6 @@ static void scores_cb(PDScoresList *scores, const char *errorMessage) {
|
||||
}
|
||||
args[1] = errorMessage ? JS_NewString(g_scoreboard_js, errorMessage) : JS_NULL;
|
||||
JSValue ret = JS_Call(g_scoreboard_js, g_scores_callback, JS_NULL, 2, args);
|
||||
JS_FreeValue(g_scoreboard_js, ret);
|
||||
JS_FreeValue(g_scoreboard_js, args[0]);
|
||||
JS_FreeValue(g_scoreboard_js, args[1]);
|
||||
}
|
||||
|
||||
// --- API Functions ---
|
||||
@@ -104,8 +92,7 @@ JSC_SCALL(scoreboards_addScore,
|
||||
uint32_t value = (uint32_t)js2number(js, argv[1]);
|
||||
if (argc > 2 && JS_IsFunction(argv[2])) {
|
||||
g_scoreboard_js = js;
|
||||
JS_FreeValue(js, g_add_score_callback);
|
||||
g_add_score_callback = JS_DupValue(js, argv[2]);
|
||||
g_add_score_callback = argv[2];
|
||||
}
|
||||
ret = JS_NewBool(js, pd_scoreboards->addScore(str, value, add_score_cb));
|
||||
)
|
||||
@@ -114,8 +101,7 @@ JSC_SCALL(scoreboards_getPersonalBest,
|
||||
if (!pd_scoreboards) return JS_RaiseDisrupt(js, "scoreboards not initialized");
|
||||
if (argc > 1 && JS_IsFunction(argv[1])) {
|
||||
g_scoreboard_js = js;
|
||||
JS_FreeValue(js, g_personal_best_callback);
|
||||
g_personal_best_callback = JS_DupValue(js, argv[1]);
|
||||
g_personal_best_callback = argv[1];
|
||||
}
|
||||
ret = JS_NewBool(js, pd_scoreboards->getPersonalBest(str, personal_best_cb));
|
||||
)
|
||||
@@ -131,8 +117,7 @@ JSC_CCALL(scoreboards_getScoreboards,
|
||||
if (!pd_scoreboards) return JS_RaiseDisrupt(js, "scoreboards not initialized");
|
||||
if (argc > 0 && JS_IsFunction(argv[0])) {
|
||||
g_scoreboard_js = js;
|
||||
JS_FreeValue(js, g_boards_list_callback);
|
||||
g_boards_list_callback = JS_DupValue(js, argv[0]);
|
||||
g_boards_list_callback = argv[0];
|
||||
}
|
||||
return JS_NewBool(js, pd_scoreboards->getScoreboards(boards_list_cb));
|
||||
)
|
||||
@@ -147,8 +132,7 @@ JSC_SCALL(scoreboards_getScores,
|
||||
if (!pd_scoreboards) return JS_RaiseDisrupt(js, "scoreboards not initialized");
|
||||
if (argc > 1 && JS_IsFunction(argv[1])) {
|
||||
g_scoreboard_js = js;
|
||||
JS_FreeValue(js, g_scores_callback);
|
||||
g_scores_callback = JS_DupValue(js, argv[1]);
|
||||
g_scores_callback = argv[1];
|
||||
}
|
||||
ret = JS_NewBool(js, pd_scoreboards->getScores(str, scores_cb));
|
||||
)
|
||||
|
||||
124
probe.ce
Normal file
124
probe.ce
Normal file
@@ -0,0 +1,124 @@
|
||||
// cell probe - Query a running probe server
|
||||
//
|
||||
// Usage:
|
||||
// cell probe List all targets and probes
|
||||
// cell probe <target> <name> Query a probe
|
||||
// cell probe <target> <name> k=v ... Query with arguments
|
||||
// cell probe --port=8080 game state Use a different port
|
||||
|
||||
var http = use('http')
|
||||
var json = use('json')
|
||||
|
||||
var port = 9000
|
||||
var base = null
|
||||
|
||||
function print_targets(targets) {
|
||||
var keys = array(targets)
|
||||
var j = 0
|
||||
var p = 0
|
||||
var probes = null
|
||||
while (j < length(keys)) {
|
||||
probes = targets[keys[j]]
|
||||
log.console(keys[j])
|
||||
p = 0
|
||||
while (p < length(probes)) {
|
||||
log.console(" " + probes[p])
|
||||
p = p + 1
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
}
|
||||
|
||||
function run() {
|
||||
var target = null
|
||||
var name = null
|
||||
var probe_args = {}
|
||||
var i = 0
|
||||
var eq = null
|
||||
var k = null
|
||||
var v = null
|
||||
var n = null
|
||||
|
||||
for (i = 0; i < length(args); i++) {
|
||||
if (args[i] == '--help' || args[i] == '-h') {
|
||||
log.console("Usage: cell probe [target] [name] [key=value ...]")
|
||||
log.console("")
|
||||
log.console(" cell probe List all targets and probes")
|
||||
log.console(" cell probe game state Query game/state")
|
||||
log.console(" cell probe game entity id=1 Query with arguments")
|
||||
log.console("")
|
||||
log.console("Options:")
|
||||
log.console(" --port=N Connect to port N (default 9000)")
|
||||
return
|
||||
} else if (starts_with(args[i], '--port=')) {
|
||||
port = number(text(args[i], 7))
|
||||
} else if (target == null) {
|
||||
target = args[i]
|
||||
} else if (name == null) {
|
||||
name = args[i]
|
||||
} else {
|
||||
eq = search(args[i], "=")
|
||||
if (eq != null) {
|
||||
k = text(args[i], 0, eq)
|
||||
v = text(args[i], eq + 1)
|
||||
n = number(v)
|
||||
if (n != null) {
|
||||
v = n
|
||||
} else if (v == "true") {
|
||||
v = true
|
||||
} else if (v == "false") {
|
||||
v = false
|
||||
} else if (v == "null") {
|
||||
v = null
|
||||
}
|
||||
probe_args[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
base = "http://127.0.0.1:" + text(port)
|
||||
|
||||
var resp = null
|
||||
var body = null
|
||||
var data = null
|
||||
|
||||
if (target == null) {
|
||||
resp = http.request("GET", base + "/discover", null, null)
|
||||
} else {
|
||||
body = {target: target, name: name}
|
||||
if (length(array(probe_args)) > 0) body.args = probe_args
|
||||
resp = http.request("POST", base + "/probe",
|
||||
{"content-type": "application/json"}, json.encode(body, false))
|
||||
}
|
||||
|
||||
if (resp == null) {
|
||||
log.error("could not connect to probe server on port " + text(port))
|
||||
return
|
||||
}
|
||||
|
||||
var _parse = function() {
|
||||
data = json.decode(resp.body)
|
||||
} disruption {
|
||||
data = null
|
||||
}
|
||||
_parse()
|
||||
|
||||
if (data == null) {
|
||||
log.error("invalid response from server")
|
||||
return
|
||||
}
|
||||
|
||||
if (!data.ok) {
|
||||
log.error(data.error)
|
||||
return
|
||||
}
|
||||
|
||||
if (target == null) {
|
||||
print_targets(data.targets)
|
||||
} else {
|
||||
log.console(json.encode(data.result, 2))
|
||||
}
|
||||
}
|
||||
run()
|
||||
|
||||
$stop()
|
||||
124
probe.cm
Normal file
124
probe.cm
Normal file
@@ -0,0 +1,124 @@
|
||||
var http = use('http')
|
||||
var json = use('json')
|
||||
|
||||
var registry = {}
|
||||
var server_fd = null
|
||||
var port = 9000
|
||||
|
||||
function handle_request(req) {
|
||||
var result = null
|
||||
var _try = null
|
||||
|
||||
if (req.method == "GET" && req.path == "/discover") {
|
||||
result = discover()
|
||||
http.respond(req._conn, 200, {"content-type": "application/json"},
|
||||
json.encode(result))
|
||||
return
|
||||
}
|
||||
|
||||
if (req.method == "POST" && req.path == "/probe") {
|
||||
_try = function() {
|
||||
result = handle_probe(req)
|
||||
} disruption {
|
||||
result = {ok: false, error: "probe failed"}
|
||||
}
|
||||
_try()
|
||||
http.respond(req._conn, 200, {"content-type": "application/json"},
|
||||
json.encode(result))
|
||||
return
|
||||
}
|
||||
|
||||
if (req.method == "POST" && req.path == "/snapshot") {
|
||||
_try = function() {
|
||||
result = handle_snapshot(req)
|
||||
} disruption {
|
||||
result = {ok: false, error: "snapshot failed"}
|
||||
}
|
||||
_try()
|
||||
http.respond(req._conn, 200, {"content-type": "application/json"},
|
||||
json.encode(result))
|
||||
return
|
||||
}
|
||||
|
||||
http.respond(req._conn, 404, {"content-type": "application/json"},
|
||||
json.encode({ok: false, error: "not found"}))
|
||||
}
|
||||
|
||||
function discover() {
|
||||
var targets = {}
|
||||
var target_keys = array(registry)
|
||||
var i = 0
|
||||
while (i < length(target_keys)) {
|
||||
targets[target_keys[i]] = array(registry[target_keys[i]])
|
||||
i = i + 1
|
||||
}
|
||||
return {ok: true, targets: targets}
|
||||
}
|
||||
|
||||
function handle_probe(req) {
|
||||
var body = json.decode(req.body)
|
||||
var target = body.target
|
||||
var name = body.name
|
||||
var args = body.args
|
||||
|
||||
if (target == null || name == null) {
|
||||
return {ok: false, error: "missing target or name"}
|
||||
}
|
||||
|
||||
var target_probes = registry[target]
|
||||
if (target_probes == null) {
|
||||
return {ok: false, error: "unknown target: " + target}
|
||||
}
|
||||
|
||||
var probe_fn = target_probes[name]
|
||||
if (probe_fn == null) {
|
||||
return {ok: false, error: "unknown probe: " + target + "/" + name}
|
||||
}
|
||||
|
||||
if (args == null) args = {}
|
||||
var result = probe_fn(args)
|
||||
return {ok: true, result: result}
|
||||
}
|
||||
|
||||
function handle_snapshot(req) {
|
||||
var body = json.decode(req.body)
|
||||
var probes = body.probes
|
||||
if (probes == null) {
|
||||
return {ok: false, error: "missing probes array"}
|
||||
}
|
||||
|
||||
var results = {}
|
||||
var i = 0
|
||||
var p = null
|
||||
var target_probes = null
|
||||
var probe_fn = null
|
||||
var key = null
|
||||
while (i < length(probes)) {
|
||||
p = probes[i]
|
||||
key = p.target + "/" + p.name
|
||||
target_probes = registry[p.target]
|
||||
if (target_probes != null) {
|
||||
probe_fn = target_probes[p.name]
|
||||
if (probe_fn != null) {
|
||||
results[key] = probe_fn(p.args != null ? p.args : {})
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return {ok: true, results: results}
|
||||
}
|
||||
|
||||
function start_server() {
|
||||
server_fd = http.serve(port)
|
||||
http.on_request(server_fd, handle_request)
|
||||
}
|
||||
|
||||
function register(target, probes) {
|
||||
registry[target] = probes
|
||||
if (server_fd == null) start_server()
|
||||
}
|
||||
|
||||
return {
|
||||
register: register,
|
||||
port: port
|
||||
}
|
||||
3313
qbe_emit.cm
3313
qbe_emit.cm
File diff suppressed because it is too large
Load Diff
2
seed.ce
2
seed.ce
@@ -42,7 +42,7 @@ for (i = 0; i < length(args); i++) {
|
||||
|
||||
var core_dir = shop.get_package_dir('core')
|
||||
var boot_dir = core_dir + '/boot'
|
||||
var pipeline_modules = ['tokenize', 'parse', 'fold', 'mcode', 'streamline']
|
||||
var pipeline_modules = ['tokenize', 'parse', 'fold', 'mcode', 'streamline', 'qbe', 'qbe_emit']
|
||||
var generated = 0
|
||||
var name = null
|
||||
var src_path = null
|
||||
|
||||
257
source/cell.c
257
source/cell.c
@@ -8,8 +8,7 @@
|
||||
#include "stb_ds.h"
|
||||
|
||||
#include "cell.h"
|
||||
#include "cell_internal.h"
|
||||
#include "cJSON.h"
|
||||
#include "pit_internal.h"
|
||||
|
||||
#define BOOTSTRAP_MCODE "boot/bootstrap.cm.mcode"
|
||||
#define ENGINE_SRC "internal/engine.cm"
|
||||
@@ -20,19 +19,20 @@
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <dlfcn.h>
|
||||
#include "monocypher.h"
|
||||
|
||||
/* Test suite declarations */
|
||||
int run_c_test_suite(JSContext *ctx);
|
||||
static int run_test_suite(size_t heap_size);
|
||||
|
||||
cell_rt *root_cell = NULL;
|
||||
static JSContext *root_ctx = NULL;
|
||||
static char *shop_path = NULL;
|
||||
volatile JSContext *g_crash_ctx = NULL;
|
||||
static char *core_path = NULL;
|
||||
static int native_mode = 0;
|
||||
static int warn_mode = 1;
|
||||
static JSRuntime *g_runtime = NULL;
|
||||
JSRuntime *g_runtime = NULL;
|
||||
|
||||
// Compute blake2b hash of data and return hex string (caller must free)
|
||||
static char *compute_blake2_hex(const char *data, size_t size) {
|
||||
@@ -249,50 +249,81 @@ static char *try_engine_cache(size_t *out_size) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static const char *detect_host_target(void) {
|
||||
#if defined(__APPLE__) && defined(__aarch64__)
|
||||
return "macos_arm64";
|
||||
#elif defined(__APPLE__) && defined(__x86_64__)
|
||||
return "macos_x86_64";
|
||||
#elif defined(__linux__) && defined(__x86_64__)
|
||||
return "linux";
|
||||
#elif defined(__linux__) && defined(__aarch64__)
|
||||
return "linux_arm64";
|
||||
#elif defined(_WIN32)
|
||||
return "windows";
|
||||
#else
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
static char *try_engine_native_cache(void) {
|
||||
const char *target = detect_host_target();
|
||||
if (!target) return NULL;
|
||||
size_t src_size;
|
||||
char *src = load_core_file(ENGINE_SRC, &src_size);
|
||||
if (!src) return NULL;
|
||||
size_t target_len = strlen(target);
|
||||
size_t key_len = src_size + 1 + target_len + 8 + 7;
|
||||
char *key = malloc(key_len + 1);
|
||||
if (!key) { free(src); return NULL; }
|
||||
memcpy(key, src, src_size);
|
||||
size_t pos = src_size;
|
||||
key[pos++] = '\n';
|
||||
memcpy(key + pos, target, target_len);
|
||||
pos += target_len;
|
||||
memcpy(key + pos, "\nnative\n", 8);
|
||||
pos += 8;
|
||||
memcpy(key + pos, "\nnative", 7);
|
||||
pos += 7;
|
||||
key[pos] = '\0';
|
||||
free(src);
|
||||
char *hex = compute_blake2_hex(key, pos);
|
||||
free(key);
|
||||
if (!hex) return NULL;
|
||||
char *cpath = build_cache_path(hex);
|
||||
free(hex);
|
||||
if (!cpath) return NULL;
|
||||
struct stat st;
|
||||
if (stat(cpath, &st) != 0) { free(cpath); return NULL; }
|
||||
return cpath;
|
||||
}
|
||||
|
||||
// Get the core path for use by scripts
|
||||
const char* cell_get_core_path(void) {
|
||||
return core_path;
|
||||
}
|
||||
|
||||
void actor_disrupt(cell_rt *crt)
|
||||
void actor_disrupt(JSContext *ctx)
|
||||
{
|
||||
crt->disrupt = 1;
|
||||
JS_SetPauseFlag(crt->context, 2);
|
||||
if (crt->state != ACTOR_RUNNING)
|
||||
actor_free(crt);
|
||||
ctx->disrupt = 1;
|
||||
JS_SetPauseFlag(ctx, 2);
|
||||
if (ctx->state != ACTOR_RUNNING)
|
||||
actor_free(ctx);
|
||||
}
|
||||
|
||||
JSValue js_core_internal_os_use(JSContext *js);
|
||||
JSValue js_core_json_use(JSContext *js);
|
||||
|
||||
void script_startup(cell_rt *prt)
|
||||
void script_startup(JSContext *js)
|
||||
{
|
||||
if (!g_runtime) {
|
||||
g_runtime = JS_NewRuntime();
|
||||
}
|
||||
JSContext *js = JS_NewContext(g_runtime);
|
||||
|
||||
JS_SetContextOpaque(js, prt);
|
||||
JS_SetGCScanExternal(js, actor_gc_scan);
|
||||
prt->context = js;
|
||||
|
||||
/* Set per-actor heap memory limit */
|
||||
js->actor_label = js->name; /* may be NULL; updated when name is set */
|
||||
JS_SetHeapMemoryLimit(js, ACTOR_MEMORY_LIMIT);
|
||||
|
||||
/* Register all GCRef fields so the Cheney GC can relocate them. */
|
||||
JS_AddGCRef(js, &prt->idx_buffer_ref);
|
||||
JS_AddGCRef(js, &prt->on_exception_ref);
|
||||
JS_AddGCRef(js, &prt->message_handle_ref);
|
||||
JS_AddGCRef(js, &prt->unneeded_ref);
|
||||
JS_AddGCRef(js, &prt->actor_sym_ref);
|
||||
prt->idx_buffer_ref.val = JS_NULL;
|
||||
prt->on_exception_ref.val = JS_NULL;
|
||||
prt->message_handle_ref.val = JS_NULL;
|
||||
prt->unneeded_ref.val = JS_NULL;
|
||||
prt->actor_sym_ref.val = JS_NULL;
|
||||
|
||||
cell_rt *crt = JS_GetContextOpaque(js);
|
||||
JS_FreeValue(js, js_core_blob_use(js));
|
||||
|
||||
// Try engine fast-path: load engine.cm from source-hash cache
|
||||
size_t bin_size;
|
||||
@@ -327,14 +358,16 @@ void script_startup(cell_rt *prt)
|
||||
}
|
||||
btmp = shop_path ? JS_NewString(js, shop_path) : JS_NULL;
|
||||
JS_SetPropertyStr(js, boot_env_ref.val, "shop_path", btmp);
|
||||
if (native_mode)
|
||||
JS_SetPropertyStr(js, boot_env_ref.val, "native_mode", JS_NewBool(js, 1));
|
||||
JSValue boot_env = JS_Stone(js, boot_env_ref.val);
|
||||
JS_DeleteGCRef(js, &boot_env_ref);
|
||||
|
||||
crt->state = ACTOR_RUNNING;
|
||||
js->state = ACTOR_RUNNING;
|
||||
JSValue bv = JS_RunMachBin(js, (const uint8_t *)boot_bin, boot_bin_size, boot_env);
|
||||
free(boot_bin);
|
||||
uncaught_exception(js, bv);
|
||||
crt->state = ACTOR_IDLE;
|
||||
js->state = ACTOR_IDLE;
|
||||
|
||||
// Retry engine from cache
|
||||
bin_data = try_engine_cache(&bin_size);
|
||||
@@ -354,20 +387,20 @@ void script_startup(cell_rt *prt)
|
||||
tmp = js_core_json_use(js);
|
||||
JS_SetPropertyStr(js, env_ref.val, "json", tmp);
|
||||
|
||||
crt->actor_sym_ref.val = JS_NewObject(js);
|
||||
JS_CellStone(js, crt->actor_sym_ref.val);
|
||||
JS_SetActorSym(js, JS_DupValue(js, crt->actor_sym_ref.val));
|
||||
JS_SetPropertyStr(js, env_ref.val, "actorsym", JS_DupValue(js, crt->actor_sym_ref.val));
|
||||
js->actor_sym_ref.val = JS_NewObject(js);
|
||||
JS_CellStone(js, js->actor_sym_ref.val);
|
||||
JS_SetActorSym(js, js->actor_sym_ref.val);
|
||||
JS_SetPropertyStr(js, env_ref.val, "actorsym", js->actor_sym_ref.val);
|
||||
|
||||
// Always set init (even if null)
|
||||
if (crt->init_wota) {
|
||||
if (js->init_wota) {
|
||||
JSGCRef init_ref;
|
||||
JS_PushGCRef(js, &init_ref);
|
||||
init_ref.val = wota2value(js, crt->init_wota);
|
||||
init_ref.val = wota2value(js, js->init_wota);
|
||||
JS_SetPropertyStr(js, env_ref.val, "init", init_ref.val);
|
||||
JS_PopGCRef(js, &init_ref);
|
||||
free(crt->init_wota);
|
||||
crt->init_wota = NULL;
|
||||
free(js->init_wota);
|
||||
js->init_wota = NULL;
|
||||
} else {
|
||||
JS_SetPropertyStr(js, env_ref.val, "init", JS_NULL);
|
||||
}
|
||||
@@ -387,12 +420,12 @@ void script_startup(cell_rt *prt)
|
||||
JS_DeleteGCRef(js, &env_ref);
|
||||
|
||||
// Run engine from binary
|
||||
crt->state = ACTOR_RUNNING;
|
||||
js->state = ACTOR_RUNNING;
|
||||
JSValue v = JS_RunMachBin(js, (const uint8_t *)bin_data, bin_size, hidden_env);
|
||||
free(bin_data);
|
||||
uncaught_exception(js, v);
|
||||
crt->state = ACTOR_IDLE;
|
||||
set_actor_state(crt);
|
||||
js->state = ACTOR_IDLE;
|
||||
set_actor_state(js);
|
||||
}
|
||||
|
||||
static void signal_handler(int sig)
|
||||
@@ -455,6 +488,7 @@ static void print_usage(const char *prog)
|
||||
printf(" --native Use AOT native code instead of bytecode\n");
|
||||
printf(" --heap <size> Initial heap size (e.g. 256MB, 1GB)\n");
|
||||
printf(" --test [heap_size] Run C test suite\n");
|
||||
printf(" -e <code> Evaluate code string as a program\n");
|
||||
printf(" -h, --help Show this help message\n");
|
||||
printf("\nEnvironment:\n");
|
||||
printf(" CELL_CORE Core path (default: <shop>/packages/core)\n");
|
||||
@@ -464,6 +498,8 @@ static void print_usage(const char *prog)
|
||||
printf("Run the 'help' script like 'cell help' to see available scripts\n");
|
||||
}
|
||||
|
||||
JSValue cell_rt_native_module_load_named(JSContext *ctx, void *dl_handle, const char *sym_name, JSValue env);
|
||||
|
||||
int cell_init(int argc, char **argv)
|
||||
{
|
||||
/* Check for --help flag */
|
||||
@@ -490,6 +526,7 @@ int cell_init(int argc, char **argv)
|
||||
size_t heap_size = 1024 * 1024; /* 1MB default */
|
||||
const char *shop_override = NULL;
|
||||
const char *core_override = NULL;
|
||||
const char *eval_script = NULL;
|
||||
|
||||
// Parse flags (order-independent)
|
||||
while (arg_start < argc && argv[arg_start][0] == '-') {
|
||||
@@ -536,12 +573,19 @@ int cell_init(int argc, char **argv)
|
||||
} else if (strcmp(argv[arg_start], "--no-warn") == 0) {
|
||||
warn_mode = 0;
|
||||
arg_start++;
|
||||
} else if (strcmp(argv[arg_start], "-e") == 0) {
|
||||
if (arg_start + 1 >= argc) {
|
||||
printf("ERROR: -e requires a code string argument\n");
|
||||
return 1;
|
||||
}
|
||||
eval_script = argv[arg_start + 1];
|
||||
arg_start += 2;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (arg_start >= argc) {
|
||||
if (arg_start >= argc && !eval_script) {
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
@@ -562,45 +606,41 @@ int cell_init(int argc, char **argv)
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Create a cell_rt for the CLI context so JS-C bridge functions work */
|
||||
cell_rt *cli_rt = calloc(sizeof(*cli_rt), 1);
|
||||
cli_rt->mutex = malloc(sizeof(pthread_mutex_t));
|
||||
/* Set up mutexes on the CLI context */
|
||||
ctx->mutex = malloc(sizeof(pthread_mutex_t));
|
||||
pthread_mutexattr_t mattr;
|
||||
pthread_mutexattr_init(&mattr);
|
||||
pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_RECURSIVE);
|
||||
pthread_mutex_init(cli_rt->mutex, &mattr);
|
||||
cli_rt->msg_mutex = malloc(sizeof(pthread_mutex_t));
|
||||
pthread_mutex_init(cli_rt->msg_mutex, &mattr);
|
||||
pthread_mutex_init(ctx->mutex, &mattr);
|
||||
ctx->msg_mutex = malloc(sizeof(pthread_mutex_t));
|
||||
pthread_mutex_init(ctx->msg_mutex, &mattr);
|
||||
pthread_mutexattr_destroy(&mattr);
|
||||
|
||||
cli_rt->context = ctx;
|
||||
JS_SetContextOpaque(ctx, cli_rt);
|
||||
JS_SetGCScanExternal(ctx, actor_gc_scan);
|
||||
|
||||
JS_AddGCRef(ctx, &cli_rt->idx_buffer_ref);
|
||||
JS_AddGCRef(ctx, &cli_rt->on_exception_ref);
|
||||
JS_AddGCRef(ctx, &cli_rt->message_handle_ref);
|
||||
JS_AddGCRef(ctx, &cli_rt->unneeded_ref);
|
||||
JS_AddGCRef(ctx, &cli_rt->actor_sym_ref);
|
||||
cli_rt->idx_buffer_ref.val = JS_NULL;
|
||||
cli_rt->on_exception_ref.val = JS_NULL;
|
||||
cli_rt->message_handle_ref.val = JS_NULL;
|
||||
cli_rt->unneeded_ref.val = JS_NULL;
|
||||
cli_rt->actor_sym_ref.val = JS_NewObject(ctx);
|
||||
JS_CellStone(ctx, cli_rt->actor_sym_ref.val);
|
||||
JS_SetActorSym(ctx, JS_DupValue(ctx, cli_rt->actor_sym_ref.val));
|
||||
ctx->actor_sym_ref.val = JS_NewObject(ctx);
|
||||
JS_CellStone(ctx, ctx->actor_sym_ref.val);
|
||||
JS_SetActorSym(ctx, ctx->actor_sym_ref.val);
|
||||
|
||||
root_cell = cli_rt;
|
||||
root_ctx = ctx;
|
||||
|
||||
JS_FreeValue(ctx, js_core_blob_use(ctx));
|
||||
|
||||
int exit_code = 0;
|
||||
int use_native_engine = 0;
|
||||
char *native_dylib_path = NULL;
|
||||
void *native_handle = NULL;
|
||||
|
||||
// Native mode: try native engine cache first
|
||||
if (native_mode)
|
||||
native_dylib_path = try_engine_native_cache();
|
||||
|
||||
// Try engine fast-path: load engine.cm from source-hash cache
|
||||
size_t bin_size;
|
||||
char *bin_data = try_engine_cache(&bin_size);
|
||||
char *bin_data = NULL;
|
||||
if (!native_dylib_path)
|
||||
bin_data = try_engine_cache(&bin_size);
|
||||
|
||||
if (!bin_data) {
|
||||
if (!native_dylib_path && !bin_data) {
|
||||
// Cold path: run bootstrap to seed cache, then retry
|
||||
size_t boot_size;
|
||||
char *boot_data = load_core_file(BOOTSTRAP_MCODE, &boot_size);
|
||||
@@ -628,9 +668,11 @@ int cell_init(int argc, char **argv)
|
||||
JS_SetPropertyStr(ctx, boot_env_ref.val, "core_path", btmp);
|
||||
btmp = shop_path ? JS_NewString(ctx, shop_path) : JS_NULL;
|
||||
JS_SetPropertyStr(ctx, boot_env_ref.val, "shop_path", btmp);
|
||||
JS_SetPropertyStr(ctx, boot_env_ref.val, "actorsym", JS_DupValue(ctx, cli_rt->actor_sym_ref.val));
|
||||
JS_SetPropertyStr(ctx, boot_env_ref.val, "actorsym", ctx->actor_sym_ref.val);
|
||||
btmp = js_core_json_use(ctx);
|
||||
JS_SetPropertyStr(ctx, boot_env_ref.val, "json", btmp);
|
||||
if (native_mode)
|
||||
JS_SetPropertyStr(ctx, boot_env_ref.val, "native_mode", JS_NewBool(ctx, 1));
|
||||
JS_SetPropertyStr(ctx, boot_env_ref.val, "init", JS_NULL);
|
||||
JSGCRef boot_args_ref;
|
||||
JS_AddGCRef(ctx, &boot_args_ref);
|
||||
@@ -652,14 +694,32 @@ int cell_init(int argc, char **argv)
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Retry engine from cache (new-style bootstrap seeds it)
|
||||
bin_data = try_engine_cache(&bin_size);
|
||||
if (!bin_data) {
|
||||
// Old-style bootstrap already ran the program — skip engine load
|
||||
goto check_actors;
|
||||
// After bootstrap, retry cache
|
||||
if (native_mode)
|
||||
native_dylib_path = try_engine_native_cache();
|
||||
if (!native_dylib_path) {
|
||||
bin_data = try_engine_cache(&bin_size);
|
||||
if (!bin_data) {
|
||||
// Old-style bootstrap already ran the program — skip engine load
|
||||
goto check_actors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Open native dylib if we have a path
|
||||
if (native_dylib_path) {
|
||||
native_handle = dlopen(native_dylib_path, RTLD_NOW | RTLD_GLOBAL);
|
||||
if (native_handle) {
|
||||
use_native_engine = 1;
|
||||
} else {
|
||||
// Fall back to bytecode
|
||||
if (!bin_data)
|
||||
bin_data = try_engine_cache(&bin_size);
|
||||
}
|
||||
free(native_dylib_path);
|
||||
native_dylib_path = NULL;
|
||||
}
|
||||
|
||||
{
|
||||
// Build engine environment
|
||||
JSGCRef env_ref;
|
||||
@@ -672,10 +732,10 @@ int cell_init(int argc, char **argv)
|
||||
JS_SetPropertyStr(ctx, env_ref.val, "core_path", tmp);
|
||||
tmp = shop_path ? JS_NewString(ctx, shop_path) : JS_NULL;
|
||||
JS_SetPropertyStr(ctx, env_ref.val, "shop_path", tmp);
|
||||
JS_SetPropertyStr(ctx, env_ref.val, "actorsym", JS_DupValue(ctx, cli_rt->actor_sym_ref.val));
|
||||
JS_SetPropertyStr(ctx, env_ref.val, "actorsym", ctx->actor_sym_ref.val);
|
||||
tmp = js_core_json_use(ctx);
|
||||
JS_SetPropertyStr(ctx, env_ref.val, "json", tmp);
|
||||
if (native_mode || !warn_mode) {
|
||||
if (native_mode || !warn_mode || eval_script) {
|
||||
JSGCRef init_ref;
|
||||
JS_AddGCRef(ctx, &init_ref);
|
||||
init_ref.val = JS_NewObject(ctx);
|
||||
@@ -683,6 +743,10 @@ int cell_init(int argc, char **argv)
|
||||
JS_SetPropertyStr(ctx, init_ref.val, "native_mode", JS_NewBool(ctx, 1));
|
||||
if (!warn_mode)
|
||||
JS_SetPropertyStr(ctx, init_ref.val, "no_warn", JS_NewBool(ctx, 1));
|
||||
if (eval_script) {
|
||||
JSValue es = JS_NewString(ctx, eval_script);
|
||||
JS_SetPropertyStr(ctx, init_ref.val, "eval_script", es);
|
||||
}
|
||||
JS_SetPropertyStr(ctx, env_ref.val, "init", init_ref.val);
|
||||
JS_DeleteGCRef(ctx, &init_ref);
|
||||
} else {
|
||||
@@ -700,10 +764,15 @@ int cell_init(int argc, char **argv)
|
||||
JSValue hidden_env = JS_Stone(ctx, env_ref.val);
|
||||
|
||||
g_crash_ctx = ctx;
|
||||
JSValue result = JS_RunMachBin(ctx, (const uint8_t *)bin_data, bin_size, hidden_env);
|
||||
JSValue result;
|
||||
if (use_native_engine) {
|
||||
result = cell_rt_native_module_load_named(ctx, native_handle, "cell_main", hidden_env);
|
||||
} else {
|
||||
result = JS_RunMachBin(ctx, (const uint8_t *)bin_data, bin_size, hidden_env);
|
||||
free(bin_data);
|
||||
}
|
||||
g_crash_ctx = NULL;
|
||||
JS_DeleteGCRef(ctx, &env_ref);
|
||||
free(bin_data);
|
||||
|
||||
if (JS_IsException(result)) {
|
||||
JS_GetException(ctx);
|
||||
@@ -726,18 +795,11 @@ check_actors:
|
||||
}
|
||||
|
||||
/* No actors spawned — clean up CLI context */
|
||||
JS_DeleteGCRef(ctx, &cli_rt->idx_buffer_ref);
|
||||
JS_DeleteGCRef(ctx, &cli_rt->on_exception_ref);
|
||||
JS_DeleteGCRef(ctx, &cli_rt->message_handle_ref);
|
||||
JS_DeleteGCRef(ctx, &cli_rt->unneeded_ref);
|
||||
JS_DeleteGCRef(ctx, &cli_rt->actor_sym_ref);
|
||||
|
||||
pthread_mutex_destroy(cli_rt->mutex);
|
||||
free(cli_rt->mutex);
|
||||
pthread_mutex_destroy(cli_rt->msg_mutex);
|
||||
free(cli_rt->msg_mutex);
|
||||
free(cli_rt);
|
||||
root_cell = NULL;
|
||||
pthread_mutex_destroy(ctx->mutex);
|
||||
free(ctx->mutex);
|
||||
pthread_mutex_destroy(ctx->msg_mutex);
|
||||
free(ctx->msg_mutex);
|
||||
root_ctx = NULL;
|
||||
|
||||
JS_FreeContext(ctx);
|
||||
JS_FreeRuntime(g_runtime);
|
||||
@@ -776,9 +838,21 @@ double cell_random() {
|
||||
return (double)buf / 9007199254740992.0;
|
||||
}
|
||||
|
||||
static cell_hook g_cell_trace_hook = NULL;
|
||||
|
||||
void cell_trace_sethook(cell_hook hook)
|
||||
{
|
||||
(void)hook;
|
||||
g_cell_trace_hook = hook;
|
||||
}
|
||||
|
||||
cell_hook cell_trace_gethook(void)
|
||||
{
|
||||
return g_cell_trace_hook;
|
||||
}
|
||||
|
||||
void cell_rt_set_trace_hook(JSContext *ctx, cell_hook hook)
|
||||
{
|
||||
ctx->actor_trace_hook = hook;
|
||||
}
|
||||
|
||||
int uncaught_exception(JSContext *js, JSValue v)
|
||||
@@ -791,13 +865,12 @@ int uncaught_exception(JSContext *js, JSValue v)
|
||||
by JS_ThrowError2 / print_backtrace. Just clear the flag. */
|
||||
if (has_exc)
|
||||
JS_GetException(js);
|
||||
cell_rt *crt = JS_GetContextOpaque(js);
|
||||
if (crt && !JS_IsNull(crt->on_exception_ref.val)) {
|
||||
if (!JS_IsNull(js->on_exception_ref.val)) {
|
||||
/* Disable interruption so actor_die can send messages
|
||||
without being re-interrupted. */
|
||||
JS_SetPauseFlag(js, 0);
|
||||
JSValue err = JS_NewString(js, "interrupted");
|
||||
JS_Call(js, crt->on_exception_ref.val, JS_NULL, 1, &err);
|
||||
JS_Call(js, js->on_exception_ref.val, JS_NULL, 1, &err);
|
||||
/* Clear any secondary exception from the callback. */
|
||||
if (JS_HasException(js))
|
||||
JS_GetException(js);
|
||||
|
||||
974
source/cell.h
974
source/cell.h
File diff suppressed because it is too large
Load Diff
@@ -1,112 +0,0 @@
|
||||
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
/* Letter type for unified message queue */
|
||||
typedef enum {
|
||||
LETTER_BLOB, /* Blob message */
|
||||
LETTER_CALLBACK /* JSValue callback function */
|
||||
} letter_type;
|
||||
|
||||
typedef struct letter {
|
||||
letter_type type;
|
||||
union {
|
||||
blob *blob_data; /* For LETTER_BLOB */
|
||||
JSValue callback; /* For LETTER_CALLBACK */
|
||||
};
|
||||
} letter;
|
||||
|
||||
#define ACTOR_IDLE 0 // Actor not doing anything
|
||||
#define ACTOR_READY 1 // Actor ready for a turn
|
||||
#define ACTOR_RUNNING 2 // Actor taking a turn
|
||||
#define ACTOR_EXHAUSTED 3 // Actor waiting for GC
|
||||
#define ACTOR_RECLAIMING 4 // Actor running GC
|
||||
#define ACTOR_SLOW 5 // Actor going slowly; deprioritize
|
||||
#define ACTOR_REFRESHED 6 // GC finished, ready to resume
|
||||
|
||||
// #define ACTOR_TRACE
|
||||
|
||||
#define ACTOR_FAST_TIMER_NS (10ULL * 1000000) // 10ms per turn
|
||||
#define ACTOR_SLOW_TIMER_NS (60000ULL * 1000000) // 60s for slow actors
|
||||
#define ACTOR_SLOW_STRIKES_MAX 3 // consecutive slow turns -> kill
|
||||
#define ACTOR_MEMORY_LIMIT (16ULL * 1024 * 1024) // 16MB heap cap
|
||||
|
||||
typedef struct cell_rt {
|
||||
JSContext *context;
|
||||
|
||||
/* JSValues on the GC heap — each paired with a JSGCRef so the
|
||||
Cheney GC can relocate them during compaction. */
|
||||
JSGCRef idx_buffer_ref;
|
||||
JSGCRef on_exception_ref;
|
||||
JSGCRef message_handle_ref;
|
||||
JSGCRef unneeded_ref;
|
||||
JSGCRef actor_sym_ref;
|
||||
|
||||
void *init_wota;
|
||||
|
||||
/* Protects JSContext usage */
|
||||
pthread_mutex_t *mutex; /* for everything else */
|
||||
pthread_mutex_t *msg_mutex; /* For message queue and timers queue */
|
||||
|
||||
char *id;
|
||||
|
||||
int idx_count;
|
||||
|
||||
/* The "mailbox" for incoming messages + a dedicated lock for it: */
|
||||
letter *letters;
|
||||
|
||||
/* CHANGED FOR EVENTS: a separate lock for the actor->events queue */
|
||||
struct { uint32_t key; JSValue value; } *timers;
|
||||
|
||||
int state;
|
||||
uint32_t ar; // timer for unneeded
|
||||
double ar_secs; // time for unneeded
|
||||
|
||||
int disrupt;
|
||||
int is_quiescent; // tracked by scheduler for quiescence detection
|
||||
int main_thread_only;
|
||||
int affinity;
|
||||
|
||||
uint64_t turn_start_ns; // cell_ns() when turn began
|
||||
_Atomic uint32_t turn_gen; // incremented each turn start
|
||||
int slow_strikes; // consecutive slow-completed turns
|
||||
int vm_suspended; // 1 if VM is paused mid-turn
|
||||
|
||||
const char *name; // human friendly name
|
||||
cell_hook trace_hook;
|
||||
} cell_rt;
|
||||
|
||||
/* Set by actor_turn/CLI before entering the VM, cleared after.
|
||||
Read by signal_handler to print JS stack on crash. */
|
||||
extern volatile JSContext *g_crash_ctx;
|
||||
|
||||
cell_rt *create_actor(void *wota);
|
||||
const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar);
|
||||
void actor_disrupt(cell_rt *actor);
|
||||
|
||||
const char *send_message(const char *id, void *msg);
|
||||
const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar);
|
||||
void actor_unneeded(cell_rt *actor, JSValue fn, double seconds);
|
||||
void script_startup(cell_rt *rt);
|
||||
int uncaught_exception(JSContext *js, JSValue v);
|
||||
int actor_exists(const char *id);
|
||||
|
||||
void set_actor_state(cell_rt *actor);
|
||||
void enqueue_actor_priority(cell_rt *actor);
|
||||
void actor_clock(cell_rt *actor, JSValue fn);
|
||||
uint32_t actor_delay(cell_rt *actor, JSValue fn, double seconds);
|
||||
JSValue actor_remove_timer(cell_rt *actor, uint32_t timer_id);
|
||||
void exit_handler(void);
|
||||
void actor_loop();
|
||||
void actor_initialize(void);
|
||||
void actor_free(cell_rt *actor);
|
||||
void actor_gc_scan(JSContext *ctx, uint8_t *fb, uint8_t *fe,
|
||||
uint8_t *tb, uint8_t **tf, uint8_t *te);
|
||||
int scheduler_actor_count(void);
|
||||
void scheduler_enable_quiescence(void);
|
||||
|
||||
JSValue JS_ResumeRegisterVM(JSContext *ctx);
|
||||
|
||||
uint64_t cell_ns();
|
||||
void cell_sleep(double seconds);
|
||||
int randombytes(void *buf, size_t n);
|
||||
452
source/mach.c
452
source/mach.c
@@ -1,29 +1,5 @@
|
||||
/*
|
||||
* QuickJS Javascript Engine
|
||||
*
|
||||
* Copyright (c) 2017-2025 Fabrice Bellard
|
||||
* Copyright (c) 2017-2025 Charlie Gordon
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "quickjs-internal.h"
|
||||
#include "pit_internal.h"
|
||||
#include <assert.h>
|
||||
|
||||
/* ============================================================
|
||||
Mach VM instruction definitions (private to mach.c)
|
||||
@@ -88,7 +64,7 @@
|
||||
[P] IS_IDENTICAL, IS_INT, IS_NUM, IS_TEXT, IS_BOOL, IS_NULL
|
||||
[P] IS_ARRAY, IS_FUNC, IS_RECORD, IS_STONE, IS_PROXY
|
||||
[P] NOT, AND, OR, BITNOT, BITAND, BITOR, BITXOR
|
||||
[P] JMP, JMPTRUE, JMPFALSE, JMPNULL, JMPNOTNULL
|
||||
[P] JMP, JMPTRUE, JMPFALSE, JMPNULL, JMPNOTNULL, WARYTRUE, WARYFALSE, JMPEMPTY
|
||||
[P] RETURN, RETNIL, SETARG, GETUP, SETUP, DISRUPT, THROW
|
||||
[P] LENGTH (array + imm-ASCII fast path only; text/blob fallback is [G])
|
||||
[N] EQ_TEXT..GE_TEXT (js_string_compare_value — no allocation)
|
||||
@@ -269,6 +245,22 @@ typedef enum MachOpcode {
|
||||
MACH_IS_STONE, /* R(A) = is_stone(R(B)) */
|
||||
MACH_LENGTH, /* R(A) = length(R(B)) — array/text/blob length */
|
||||
MACH_IS_PROXY, /* R(A) = is_function(R(B)) && R(B).length == 2 */
|
||||
MACH_IS_BLOB, /* R(A) = is_blob(R(B)) */
|
||||
MACH_IS_DATA, /* R(A) = is_data(R(B)) — not function or null */
|
||||
MACH_IS_TRUE, /* R(A) = (R(B) === true) */
|
||||
MACH_IS_FALSE, /* R(A) = (R(B) === false) */
|
||||
MACH_IS_FIT, /* R(A) = is_fit(R(B)) — safe integer */
|
||||
MACH_IS_CHAR, /* R(A) = is_character(R(B)) — single char text */
|
||||
MACH_IS_DIGIT, /* R(A) = is_digit(R(B)) */
|
||||
MACH_IS_LETTER, /* R(A) = is_letter(R(B)) */
|
||||
MACH_IS_LOWER, /* R(A) = is_lower(R(B)) */
|
||||
MACH_IS_UPPER, /* R(A) = is_upper(R(B)) */
|
||||
MACH_IS_WS, /* R(A) = is_whitespace(R(B)) */
|
||||
MACH_IS_ACTOR, /* R(A) = is_actor(R(B)) — has actor_sym property */
|
||||
MACH_APPLY, /* R(A) = apply(R(B), R(C)) — call fn with args from array (ABC) */
|
||||
MACH_WARYTRUE, /* if toBool(R(A)): pc += sBx — coercing (iAsBx) */
|
||||
MACH_WARYFALSE, /* if !toBool(R(A)): pc += sBx — coercing (iAsBx) */
|
||||
MACH_JMPEMPTY, /* if R(A)==empty_text: pc += sBx (iAsBx) */
|
||||
|
||||
MACH_OP_COUNT
|
||||
} MachOpcode;
|
||||
@@ -381,6 +373,22 @@ static const char *mach_opcode_names[MACH_OP_COUNT] = {
|
||||
[MACH_IS_STONE] = "is_stone",
|
||||
[MACH_LENGTH] = "length",
|
||||
[MACH_IS_PROXY] = "is_proxy",
|
||||
[MACH_IS_BLOB] = "is_blob",
|
||||
[MACH_IS_DATA] = "is_data",
|
||||
[MACH_IS_TRUE] = "is_true",
|
||||
[MACH_IS_FALSE] = "is_false",
|
||||
[MACH_IS_FIT] = "is_fit",
|
||||
[MACH_IS_CHAR] = "is_char",
|
||||
[MACH_IS_DIGIT] = "is_digit",
|
||||
[MACH_IS_LETTER] = "is_letter",
|
||||
[MACH_IS_LOWER] = "is_lower",
|
||||
[MACH_IS_UPPER] = "is_upper",
|
||||
[MACH_IS_WS] = "is_ws",
|
||||
[MACH_IS_ACTOR] = "is_actor",
|
||||
[MACH_APPLY] = "apply",
|
||||
[MACH_WARYTRUE] = "wary_true",
|
||||
[MACH_WARYFALSE] = "wary_false",
|
||||
[MACH_JMPEMPTY] = "jump_empty",
|
||||
};
|
||||
|
||||
/* ---- Compile-time constant pool entry ---- */
|
||||
@@ -932,7 +940,7 @@ JSValue js_new_register_function(JSContext *ctx, JSCodeRegister *code, JSValue e
|
||||
return out;
|
||||
}
|
||||
|
||||
JSValue js_new_native_function_with_code(JSContext *ctx, JSValue code_obj, int arity, JSValue outer_frame) {
|
||||
JSValue js_new_native_function_with_code(JSContext *ctx, JSValue code_obj, int arity, JSValue outer_frame, JSValue env_record) {
|
||||
JSGCRef frame_ref;
|
||||
JSGCRef fn_ref;
|
||||
JSFunction *fn;
|
||||
@@ -955,7 +963,7 @@ JSValue js_new_native_function_with_code(JSContext *ctx, JSValue code_obj, int a
|
||||
fn->name = JS_NULL;
|
||||
fn = JS_VALUE_GET_FUNCTION(fn_ref.val);
|
||||
fn->u.cell.code = code_obj;
|
||||
fn->u.cell.env_record = JS_NULL;
|
||||
fn->u.cell.env_record = env_record;
|
||||
fn->u.cell.outer_frame = frame_ref.val;
|
||||
|
||||
JSValue out = fn_ref.val;
|
||||
@@ -966,11 +974,11 @@ JSValue js_new_native_function_with_code(JSContext *ctx, JSValue code_obj, int a
|
||||
|
||||
/* Create a native (QBE-compiled) function */
|
||||
JSValue js_new_native_function(JSContext *ctx, void *fn_ptr, void *dl_handle,
|
||||
uint16_t nr_slots, int arity, JSValue outer_frame) {
|
||||
uint16_t nr_slots, int arity, JSValue outer_frame, JSValue env) {
|
||||
JSValue code_obj = js_new_native_code(ctx, fn_ptr, dl_handle, nr_slots, arity);
|
||||
if (JS_IsException(code_obj))
|
||||
return JS_EXCEPTION;
|
||||
return js_new_native_function_with_code(ctx, code_obj, arity, outer_frame);
|
||||
return js_new_native_function_with_code(ctx, code_obj, arity, outer_frame, env);
|
||||
}
|
||||
|
||||
/* Binary operations helper */
|
||||
@@ -1312,6 +1320,20 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
|
||||
|
||||
pc = code->entry_point;
|
||||
result = JS_NULL;
|
||||
|
||||
/* Fire trace hook for top-level register function entry */
|
||||
if (unlikely(ctx->trace_hook) && (ctx->trace_type & JS_HOOK_CALL)) {
|
||||
js_debug dbg = {0};
|
||||
if (code->name_cstr)
|
||||
snprintf(dbg.name, sizeof(dbg.name), "%s", code->name_cstr);
|
||||
if (code->filename_cstr)
|
||||
snprintf(dbg.filename, sizeof(dbg.filename), "%s", code->filename_cstr);
|
||||
if (code->line_table)
|
||||
dbg.line = code->line_table[code->entry_point].line;
|
||||
dbg.param_n = code->arity;
|
||||
dbg.unique = (int)(uintptr_t)code;
|
||||
ctx->trace_hook(ctx, JS_HOOK_CALL, &dbg, ctx->trace_data);
|
||||
}
|
||||
} /* end normal path block */
|
||||
|
||||
vm_dispatch:
|
||||
@@ -1396,6 +1418,14 @@ vm_dispatch:
|
||||
DT(MACH_IS_ARRAY), DT(MACH_IS_FUNC),
|
||||
DT(MACH_IS_RECORD), DT(MACH_IS_STONE),
|
||||
DT(MACH_LENGTH), DT(MACH_IS_PROXY),
|
||||
DT(MACH_IS_BLOB), DT(MACH_IS_DATA),
|
||||
DT(MACH_IS_TRUE), DT(MACH_IS_FALSE),
|
||||
DT(MACH_IS_FIT), DT(MACH_IS_CHAR),
|
||||
DT(MACH_IS_DIGIT), DT(MACH_IS_LETTER),
|
||||
DT(MACH_IS_LOWER), DT(MACH_IS_UPPER),
|
||||
DT(MACH_IS_WS), DT(MACH_IS_ACTOR),
|
||||
DT(MACH_APPLY),
|
||||
DT(MACH_WARYTRUE), DT(MACH_WARYFALSE), DT(MACH_JMPEMPTY),
|
||||
};
|
||||
#pragma GCC diagnostic pop
|
||||
#undef DT
|
||||
@@ -1988,21 +2018,12 @@ vm_dispatch:
|
||||
int depth = b;
|
||||
JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function);
|
||||
JSFrameRegister *target = (JSFrameRegister *)JS_VALUE_GET_PTR(fn->u.cell.outer_frame);
|
||||
if (!target) {
|
||||
fprintf(stderr, "GETUP: NULL outer_frame at depth 0! pc=%d a=%d depth=%d slot=%d nr_slots=%d instr=0x%08x\n",
|
||||
pc-1, a, depth, c, code->nr_slots, instr);
|
||||
result = JS_RaiseDisrupt(ctx, "GETUP: NULL outer_frame");
|
||||
goto disrupt;
|
||||
}
|
||||
assert(depth > 0);
|
||||
assert(target != NULL);
|
||||
for (int d = 1; d < depth; d++) {
|
||||
fn = JS_VALUE_GET_FUNCTION(target->function);
|
||||
JSFrameRegister *next = (JSFrameRegister *)JS_VALUE_GET_PTR(fn->u.cell.outer_frame);
|
||||
if (!next) {
|
||||
fprintf(stderr, "GETUP: NULL outer_frame at depth %d! pc=%d a=%d depth=%d slot=%d nr_slots=%d instr=0x%08x\n",
|
||||
d, pc-1, a, depth, c, code->nr_slots, instr);
|
||||
result = JS_RaiseDisrupt(ctx, "GETUP: NULL outer_frame at depth %d", d);
|
||||
goto disrupt;
|
||||
}
|
||||
assert(next != NULL);
|
||||
target = next;
|
||||
}
|
||||
stone_mutable_text(target->slots[c]);
|
||||
@@ -2015,22 +2036,14 @@ vm_dispatch:
|
||||
int depth = b;
|
||||
JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function);
|
||||
JSFrameRegister *target = (JSFrameRegister *)JS_VALUE_GET_PTR(fn->u.cell.outer_frame);
|
||||
assert(depth > 0);
|
||||
assert(target != NULL);
|
||||
for (int d = 1; d < depth; d++) {
|
||||
fn = JS_VALUE_GET_FUNCTION(target->function);
|
||||
target = (JSFrameRegister *)JS_VALUE_GET_PTR(fn->u.cell.outer_frame);
|
||||
assert(target != NULL);
|
||||
}
|
||||
{
|
||||
uint64_t tcap = objhdr_cap56(target->header);
|
||||
if ((unsigned)c >= tcap) {
|
||||
fprintf(stderr, "MACH_SETUP OOB: slot=%d >= target_cap=%llu depth=%d "
|
||||
"cur_fn=%s (%s) pc=%u\n",
|
||||
c, (unsigned long long)tcap, depth,
|
||||
code->name_cstr ? code->name_cstr : "?",
|
||||
code->filename_cstr ? code->filename_cstr : "?", pc - 1);
|
||||
fflush(stderr);
|
||||
VM_BREAK();
|
||||
}
|
||||
}
|
||||
assert((unsigned)c < objhdr_cap56(target->header));
|
||||
target->slots[c] = frame->slots[a];
|
||||
VM_BREAK();
|
||||
}
|
||||
@@ -2055,12 +2068,7 @@ vm_dispatch:
|
||||
}
|
||||
|
||||
VM_CASE(MACH_JMPTRUE): {
|
||||
JSValue v = frame->slots[a];
|
||||
int cond;
|
||||
if (v == JS_TRUE) cond = 1;
|
||||
else if (v == JS_FALSE || v == JS_NULL) cond = 0;
|
||||
else cond = JS_ToBool(ctx, v);
|
||||
if (cond) {
|
||||
if (frame->slots[a] == JS_TRUE) {
|
||||
int offset = MACH_GET_sBx(instr);
|
||||
pc = (uint32_t)((int32_t)pc + offset);
|
||||
if (offset < 0) {
|
||||
@@ -2081,12 +2089,7 @@ vm_dispatch:
|
||||
}
|
||||
|
||||
VM_CASE(MACH_JMPFALSE): {
|
||||
JSValue v = frame->slots[a];
|
||||
int cond;
|
||||
if (v == JS_TRUE) cond = 1;
|
||||
else if (v == JS_FALSE || v == JS_NULL) cond = 0;
|
||||
else cond = JS_ToBool(ctx, v);
|
||||
if (!cond) {
|
||||
if (frame->slots[a] == JS_FALSE) {
|
||||
int offset = MACH_GET_sBx(instr);
|
||||
pc = (uint32_t)((int32_t)pc + offset);
|
||||
if (offset < 0) {
|
||||
@@ -2118,6 +2121,8 @@ vm_dispatch:
|
||||
stone_mutable_text(frame->slots[a]);
|
||||
result = frame->slots[a];
|
||||
if (!JS_IsPtr(frame->caller)) goto done;
|
||||
if (unlikely(ctx->trace_hook) && (ctx->trace_type & JS_HOOK_RET))
|
||||
ctx->trace_hook(ctx, JS_HOOK_RET, NULL, ctx->trace_data);
|
||||
{
|
||||
#ifdef VALIDATE_GC
|
||||
const char *callee_name = "?";
|
||||
@@ -2163,6 +2168,8 @@ vm_dispatch:
|
||||
VM_CASE(MACH_RETNIL):
|
||||
result = JS_NULL;
|
||||
if (!JS_IsPtr(frame->caller)) goto done;
|
||||
if (unlikely(ctx->trace_hook) && (ctx->trace_type & JS_HOOK_RET))
|
||||
ctx->trace_hook(ctx, JS_HOOK_RET, NULL, ctx->trace_data);
|
||||
{
|
||||
JSFrameRegister *caller = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->caller);
|
||||
frame->caller = JS_NULL;
|
||||
@@ -2386,6 +2393,182 @@ vm_dispatch:
|
||||
frame->slots[a] = JS_NewBool(ctx, is_proxy);
|
||||
VM_BREAK();
|
||||
}
|
||||
VM_CASE(MACH_IS_BLOB):
|
||||
frame->slots[a] = JS_NewBool(ctx, mist_is_blob(frame->slots[b]));
|
||||
VM_BREAK();
|
||||
VM_CASE(MACH_IS_DATA): {
|
||||
JSValue v = frame->slots[b];
|
||||
/* data is text, number, logical, array, blob, or record — not function, null */
|
||||
int result = (v != JS_NULL && !mist_is_function(v));
|
||||
frame->slots[a] = JS_NewBool(ctx, result);
|
||||
VM_BREAK();
|
||||
}
|
||||
VM_CASE(MACH_IS_TRUE):
|
||||
frame->slots[a] = JS_NewBool(ctx, frame->slots[b] == JS_TRUE);
|
||||
VM_BREAK();
|
||||
VM_CASE(MACH_IS_FALSE):
|
||||
frame->slots[a] = JS_NewBool(ctx, frame->slots[b] == JS_FALSE);
|
||||
VM_BREAK();
|
||||
VM_CASE(MACH_IS_FIT): {
|
||||
JSValue v = frame->slots[b];
|
||||
int result = 0;
|
||||
if (JS_IsInt(v)) {
|
||||
result = 1;
|
||||
} else if (JS_IsShortFloat(v)) {
|
||||
double d = JS_VALUE_GET_FLOAT64(v);
|
||||
result = (isfinite(d) && trunc(d) == d && fabs(d) <= 9007199254740992.0);
|
||||
}
|
||||
frame->slots[a] = JS_NewBool(ctx, result);
|
||||
VM_BREAK();
|
||||
}
|
||||
VM_CASE(MACH_IS_CHAR): {
|
||||
JSValue v = frame->slots[b];
|
||||
int result = 0;
|
||||
if (MIST_IsImmediateASCII(v))
|
||||
result = (MIST_GetImmediateASCIILen(v) == 1);
|
||||
else if (mist_is_text(v))
|
||||
result = (js_string_value_len(v) == 1);
|
||||
frame->slots[a] = JS_NewBool(ctx, result);
|
||||
VM_BREAK();
|
||||
}
|
||||
VM_CASE(MACH_IS_DIGIT): {
|
||||
JSValue v = frame->slots[b];
|
||||
int result = 0;
|
||||
if (MIST_IsImmediateASCII(v) && MIST_GetImmediateASCIILen(v) == 1) {
|
||||
int ch = MIST_GetImmediateASCIIChar(v, 0);
|
||||
result = (ch >= '0' && ch <= '9');
|
||||
} else if (mist_is_text(v) && js_string_value_len(v) == 1) {
|
||||
uint32_t ch = js_string_value_get(v, 0);
|
||||
result = (ch >= '0' && ch <= '9');
|
||||
}
|
||||
frame->slots[a] = JS_NewBool(ctx, result);
|
||||
VM_BREAK();
|
||||
}
|
||||
VM_CASE(MACH_IS_LETTER): {
|
||||
JSValue v = frame->slots[b];
|
||||
int result = 0;
|
||||
if (MIST_IsImmediateASCII(v) && MIST_GetImmediateASCIILen(v) == 1) {
|
||||
int ch = MIST_GetImmediateASCIIChar(v, 0);
|
||||
result = ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'));
|
||||
} else if (mist_is_text(v) && js_string_value_len(v) == 1) {
|
||||
uint32_t ch = js_string_value_get(v, 0);
|
||||
result = ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'));
|
||||
}
|
||||
frame->slots[a] = JS_NewBool(ctx, result);
|
||||
VM_BREAK();
|
||||
}
|
||||
VM_CASE(MACH_IS_LOWER): {
|
||||
JSValue v = frame->slots[b];
|
||||
int result = 0;
|
||||
if (MIST_IsImmediateASCII(v) && MIST_GetImmediateASCIILen(v) == 1) {
|
||||
int ch = MIST_GetImmediateASCIIChar(v, 0);
|
||||
result = (ch >= 'a' && ch <= 'z');
|
||||
} else if (mist_is_text(v) && js_string_value_len(v) == 1) {
|
||||
uint32_t ch = js_string_value_get(v, 0);
|
||||
result = (ch >= 'a' && ch <= 'z');
|
||||
}
|
||||
frame->slots[a] = JS_NewBool(ctx, result);
|
||||
VM_BREAK();
|
||||
}
|
||||
VM_CASE(MACH_IS_UPPER): {
|
||||
JSValue v = frame->slots[b];
|
||||
int result = 0;
|
||||
if (MIST_IsImmediateASCII(v) && MIST_GetImmediateASCIILen(v) == 1) {
|
||||
int ch = MIST_GetImmediateASCIIChar(v, 0);
|
||||
result = (ch >= 'A' && ch <= 'Z');
|
||||
} else if (mist_is_text(v) && js_string_value_len(v) == 1) {
|
||||
uint32_t ch = js_string_value_get(v, 0);
|
||||
result = (ch >= 'A' && ch <= 'Z');
|
||||
}
|
||||
frame->slots[a] = JS_NewBool(ctx, result);
|
||||
VM_BREAK();
|
||||
}
|
||||
VM_CASE(MACH_IS_WS): {
|
||||
JSValue v = frame->slots[b];
|
||||
int result = 0;
|
||||
if (MIST_IsImmediateASCII(v)) {
|
||||
int len = MIST_GetImmediateASCIILen(v);
|
||||
if (len > 0) {
|
||||
result = 1;
|
||||
for (int i = 0; i < len; i++) {
|
||||
int ch = MIST_GetImmediateASCIIChar(v, i);
|
||||
if (!(ch == ' ' || ch == '\t' || ch == '\n'
|
||||
|| ch == '\r' || ch == '\f' || ch == '\v')) {
|
||||
result = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (mist_is_text(v)) {
|
||||
int len = js_string_value_len(v);
|
||||
if (len > 0) {
|
||||
result = 1;
|
||||
for (int i = 0; i < len; i++) {
|
||||
uint32_t ch = js_string_value_get(v, i);
|
||||
if (!(ch == ' ' || ch == '\t' || ch == '\n'
|
||||
|| ch == '\r' || ch == '\f' || ch == '\v')) {
|
||||
result = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
frame->slots[a] = JS_NewBool(ctx, result);
|
||||
VM_BREAK();
|
||||
}
|
||||
VM_CASE(MACH_IS_ACTOR): {
|
||||
JSValue v = frame->slots[b];
|
||||
int result = 0;
|
||||
if (mist_is_record(v) && !JS_IsNull(ctx->actor_sym)) {
|
||||
result = JS_HasPropertyKey(ctx, v, ctx->actor_sym) > 0;
|
||||
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
|
||||
}
|
||||
frame->slots[a] = JS_NewBool(ctx, result);
|
||||
VM_BREAK();
|
||||
}
|
||||
VM_CASE(MACH_APPLY): {
|
||||
/* A=dest, B=fn, C=arr_or_val */
|
||||
JSValue fn_val = frame->slots[b];
|
||||
JSValue arg_val = frame->slots[c];
|
||||
if (!mist_is_function(fn_val)) {
|
||||
frame->slots[a] = fn_val;
|
||||
VM_BREAK();
|
||||
}
|
||||
JSFunction *fn = JS_VALUE_GET_FUNCTION(fn_val);
|
||||
JSValue ret;
|
||||
ctx->reg_current_frame = frame_ref.val;
|
||||
ctx->current_register_pc = pc > 0 ? pc - 1 : 0;
|
||||
ctx->vm_call_depth++;
|
||||
if (!mist_is_array(arg_val)) {
|
||||
/* Non-array: use as single argument */
|
||||
if (!mach_check_call_arity(ctx, fn, 1)) {
|
||||
ctx->vm_call_depth--;
|
||||
goto disrupt;
|
||||
}
|
||||
ret = JS_CallInternal(ctx, fn_val, JS_NULL, 1, &arg_val, 0);
|
||||
} else {
|
||||
JSArray *arr = JS_VALUE_GET_ARRAY(arg_val);
|
||||
int len = arr->len;
|
||||
if (!mach_check_call_arity(ctx, fn, len)) {
|
||||
ctx->vm_call_depth--;
|
||||
goto disrupt;
|
||||
}
|
||||
if (len == 0) {
|
||||
ret = JS_CallInternal(ctx, fn_val, JS_NULL, 0, NULL, 0);
|
||||
} else {
|
||||
JSValue *args = alloca(sizeof(JSValue) * len);
|
||||
for (int i = 0; i < len; i++)
|
||||
args[i] = arr->values[i];
|
||||
ret = JS_CallInternal(ctx, fn_val, JS_NULL, len, args, 0);
|
||||
}
|
||||
}
|
||||
ctx->vm_call_depth--;
|
||||
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
|
||||
ctx->reg_current_frame = JS_NULL;
|
||||
if (JS_IsException(ret)) goto disrupt;
|
||||
frame->slots[a] = ret;
|
||||
VM_BREAK();
|
||||
}
|
||||
/* Logical */
|
||||
VM_CASE(MACH_NOT): {
|
||||
int bval = JS_ToBool(ctx, frame->slots[b]);
|
||||
@@ -2571,6 +2754,19 @@ vm_dispatch:
|
||||
if (fn->kind == JS_FUNC_KIND_REGISTER) {
|
||||
/* Register function: FRAME already allocated nr_slots — just switch */
|
||||
JSCodeRegister *fn_code = JS_VALUE_GET_CODE(fn->u.cell.code)->u.reg.code;
|
||||
/* Fire trace hook for register-to-register call */
|
||||
if (unlikely(ctx->trace_hook) && (ctx->trace_type & JS_HOOK_CALL)) {
|
||||
js_debug dbg = {0};
|
||||
if (fn_code->name_cstr)
|
||||
snprintf(dbg.name, sizeof(dbg.name), "%s", fn_code->name_cstr);
|
||||
if (fn_code->filename_cstr)
|
||||
snprintf(dbg.filename, sizeof(dbg.filename), "%s", fn_code->filename_cstr);
|
||||
if (fn_code->line_table)
|
||||
dbg.line = fn_code->line_table[fn_code->entry_point].line;
|
||||
dbg.param_n = fn_code->arity;
|
||||
dbg.unique = (int)(uintptr_t)fn_code;
|
||||
ctx->trace_hook(ctx, JS_HOOK_CALL, &dbg, ctx->trace_data);
|
||||
}
|
||||
/* Save return info */
|
||||
frame->address = JS_NewInt32(ctx, (pc << 16) | b);
|
||||
fr->caller = JS_MKPTR(frame);
|
||||
@@ -2626,6 +2822,23 @@ vm_dispatch:
|
||||
|
||||
if (fn->kind == JS_FUNC_KIND_REGISTER) {
|
||||
JSCodeRegister *fn_code = JS_VALUE_GET_CODE(FN_READ_CODE(fn))->u.reg.code;
|
||||
/* Tail call: fire RET for current, CALL for new */
|
||||
if (unlikely(ctx->trace_hook)) {
|
||||
if (ctx->trace_type & JS_HOOK_RET)
|
||||
ctx->trace_hook(ctx, JS_HOOK_RET, NULL, ctx->trace_data);
|
||||
if (ctx->trace_type & JS_HOOK_CALL) {
|
||||
js_debug dbg = {0};
|
||||
if (fn_code->name_cstr)
|
||||
snprintf(dbg.name, sizeof(dbg.name), "%s", fn_code->name_cstr);
|
||||
if (fn_code->filename_cstr)
|
||||
snprintf(dbg.filename, sizeof(dbg.filename), "%s", fn_code->filename_cstr);
|
||||
if (fn_code->line_table)
|
||||
dbg.line = fn_code->line_table[fn_code->entry_point].line;
|
||||
dbg.param_n = fn_code->arity;
|
||||
dbg.unique = (int)(uintptr_t)fn_code;
|
||||
ctx->trace_hook(ctx, JS_HOOK_CALL, &dbg, ctx->trace_data);
|
||||
}
|
||||
}
|
||||
int current_slots = (int)objhdr_cap56(frame->header);
|
||||
|
||||
if (fn_code->nr_slots <= current_slots) {
|
||||
@@ -2695,6 +2908,67 @@ vm_dispatch:
|
||||
VM_BREAK();
|
||||
}
|
||||
|
||||
/* Wary jumps — coerce via JS_ToBool (old JMPTRUE/JMPFALSE behavior) */
|
||||
VM_CASE(MACH_WARYTRUE): {
|
||||
JSValue v = frame->slots[a];
|
||||
int cond;
|
||||
if (v == JS_TRUE) cond = 1;
|
||||
else if (v == JS_FALSE || v == JS_NULL) cond = 0;
|
||||
else cond = JS_ToBool(ctx, v);
|
||||
if (cond) {
|
||||
int offset = MACH_GET_sBx(instr);
|
||||
pc = (uint32_t)((int32_t)pc + offset);
|
||||
if (offset < 0) {
|
||||
int pf = atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed);
|
||||
if (pf == 2) {
|
||||
result = JS_RaiseDisrupt(ctx, "interrupted");
|
||||
goto done;
|
||||
}
|
||||
if (pf == 1) {
|
||||
if (ctx->vm_call_depth > 0)
|
||||
atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed);
|
||||
else
|
||||
goto suspend;
|
||||
}
|
||||
}
|
||||
}
|
||||
VM_BREAK();
|
||||
}
|
||||
|
||||
VM_CASE(MACH_WARYFALSE): {
|
||||
JSValue v = frame->slots[a];
|
||||
int cond;
|
||||
if (v == JS_TRUE) cond = 1;
|
||||
else if (v == JS_FALSE || v == JS_NULL) cond = 0;
|
||||
else cond = JS_ToBool(ctx, v);
|
||||
if (!cond) {
|
||||
int offset = MACH_GET_sBx(instr);
|
||||
pc = (uint32_t)((int32_t)pc + offset);
|
||||
if (offset < 0) {
|
||||
int pf = atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed);
|
||||
if (pf == 2) {
|
||||
result = JS_RaiseDisrupt(ctx, "interrupted");
|
||||
goto done;
|
||||
}
|
||||
if (pf == 1) {
|
||||
if (ctx->vm_call_depth > 0)
|
||||
atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed);
|
||||
else
|
||||
goto suspend;
|
||||
}
|
||||
}
|
||||
}
|
||||
VM_BREAK();
|
||||
}
|
||||
|
||||
VM_CASE(MACH_JMPEMPTY): {
|
||||
if (frame->slots[a] == JS_EMPTY_TEXT) {
|
||||
int offset = MACH_GET_sBx(instr);
|
||||
pc = (uint32_t)((int32_t)pc + offset);
|
||||
}
|
||||
VM_BREAK();
|
||||
}
|
||||
|
||||
/* Disrupt (mcode alias) */
|
||||
VM_CASE(MACH_DISRUPT):
|
||||
goto disrupt;
|
||||
@@ -2768,6 +3042,9 @@ suspend:
|
||||
return result;
|
||||
|
||||
done:
|
||||
/* Fire trace hook for top-level register function return */
|
||||
if (unlikely(ctx->trace_hook) && (ctx->trace_type & JS_HOOK_RET))
|
||||
ctx->trace_hook(ctx, JS_HOOK_RET, NULL, ctx->trace_data);
|
||||
#ifdef HAVE_ASAN
|
||||
__asan_js_ctx = NULL;
|
||||
#endif
|
||||
@@ -3031,6 +3308,19 @@ static MachCode *mcode_lower_func(cJSON *fobj, const char *filename) {
|
||||
else if (strcmp(op, "is_stone") == 0) { AB2(MACH_IS_STONE); }
|
||||
else if (strcmp(op, "length") == 0) { AB2(MACH_LENGTH); }
|
||||
else if (strcmp(op, "is_proxy") == 0) { AB2(MACH_IS_PROXY); }
|
||||
else if (strcmp(op, "is_blob") == 0) { AB2(MACH_IS_BLOB); }
|
||||
else if (strcmp(op, "is_data") == 0) { AB2(MACH_IS_DATA); }
|
||||
else if (strcmp(op, "is_true") == 0) { AB2(MACH_IS_TRUE); }
|
||||
else if (strcmp(op, "is_false") == 0) { AB2(MACH_IS_FALSE); }
|
||||
else if (strcmp(op, "is_fit") == 0) { AB2(MACH_IS_FIT); }
|
||||
else if (strcmp(op, "is_char") == 0) { AB2(MACH_IS_CHAR); }
|
||||
else if (strcmp(op, "is_digit") == 0) { AB2(MACH_IS_DIGIT); }
|
||||
else if (strcmp(op, "is_letter") == 0) { AB2(MACH_IS_LETTER); }
|
||||
else if (strcmp(op, "is_lower") == 0) { AB2(MACH_IS_LOWER); }
|
||||
else if (strcmp(op, "is_upper") == 0) { AB2(MACH_IS_UPPER); }
|
||||
else if (strcmp(op, "is_ws") == 0) { AB2(MACH_IS_WS); }
|
||||
else if (strcmp(op, "is_actor") == 0) { AB2(MACH_IS_ACTOR); }
|
||||
else if (strcmp(op, "apply") == 0) { ABC3(MACH_APPLY); }
|
||||
/* Logical */
|
||||
else if (strcmp(op, "not") == 0) { AB2(MACH_NOT); }
|
||||
else if (strcmp(op, "and") == 0) { ABC3(MACH_AND); }
|
||||
@@ -3181,6 +3471,34 @@ static MachCode *mcode_lower_func(cJSON *fobj, const char *filename) {
|
||||
EM(MACH_AsBx(MACH_JMPNOTNULL, reg, 0));
|
||||
ml_patch(&s, pc_now, lbl, 0, reg);
|
||||
}
|
||||
else if (strcmp(op, "jump_null") == 0) {
|
||||
int reg = A1;
|
||||
const char *lbl = cJSON_GetArrayItem(it, 2)->valuestring;
|
||||
int pc_now = s.code_count;
|
||||
EM(MACH_AsBx(MACH_JMPNULL, reg, 0));
|
||||
ml_patch(&s, pc_now, lbl, 0, reg);
|
||||
}
|
||||
else if (strcmp(op, "wary_true") == 0) {
|
||||
int reg = A1;
|
||||
const char *lbl = cJSON_GetArrayItem(it, 2)->valuestring;
|
||||
int pc_now = s.code_count;
|
||||
EM(MACH_AsBx(MACH_WARYTRUE, reg, 0));
|
||||
ml_patch(&s, pc_now, lbl, 0, reg);
|
||||
}
|
||||
else if (strcmp(op, "wary_false") == 0) {
|
||||
int reg = A1;
|
||||
const char *lbl = cJSON_GetArrayItem(it, 2)->valuestring;
|
||||
int pc_now = s.code_count;
|
||||
EM(MACH_AsBx(MACH_WARYFALSE, reg, 0));
|
||||
ml_patch(&s, pc_now, lbl, 0, reg);
|
||||
}
|
||||
else if (strcmp(op, "jump_empty") == 0) {
|
||||
int reg = A1;
|
||||
const char *lbl = cJSON_GetArrayItem(it, 2)->valuestring;
|
||||
int pc_now = s.code_count;
|
||||
EM(MACH_AsBx(MACH_JMPEMPTY, reg, 0));
|
||||
ml_patch(&s, pc_now, lbl, 0, reg);
|
||||
}
|
||||
/* Return / error */
|
||||
else if (strcmp(op, "return") == 0) {
|
||||
EM(MACH_ABC(MACH_RETURN, A1, 0, 0));
|
||||
|
||||
@@ -1,30 +1,6 @@
|
||||
#ifndef QUICKJS_INTERNAL_H
|
||||
#define QUICKJS_INTERNAL_H
|
||||
|
||||
/*
|
||||
* QuickJS Javascript Engine
|
||||
*
|
||||
* Copyright (c) 2017-2025 Fabrice Bellard
|
||||
* Copyright (c) 2017-2025 Charlie Gordon
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
#include <assert.h>
|
||||
#include <ctype.h>
|
||||
#include <inttypes.h>
|
||||
@@ -36,6 +12,9 @@
|
||||
#include <stdatomic.h>
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
#ifndef TARGET_PLAYDATE
|
||||
#include <pthread.h>
|
||||
#endif
|
||||
#if defined(__APPLE__)
|
||||
#include <malloc/malloc.h>
|
||||
#elif defined(__linux__) || defined(__GLIBC__)
|
||||
@@ -45,16 +24,176 @@
|
||||
#endif
|
||||
|
||||
#include "cutils.h"
|
||||
|
||||
#include "dtoa.h"
|
||||
#include "libregexp.h"
|
||||
#include "libunicode.h"
|
||||
#include "list.h"
|
||||
#include "quickjs.h"
|
||||
#include "cell.h"
|
||||
#include "cJSON.h"
|
||||
#include "blob.h"
|
||||
#include "nota.h"
|
||||
#include "wota.h"
|
||||
|
||||
/* ============================================================
|
||||
Internal API — not for C module authors
|
||||
============================================================ */
|
||||
|
||||
/* Object header types */
|
||||
enum mist_obj_type {
|
||||
OBJ_ARRAY = 0,
|
||||
OBJ_BLOB = 1,
|
||||
OBJ_TEXT = 2,
|
||||
OBJ_RECORD = 3, // js style objects
|
||||
OBJ_FUNCTION = 4,
|
||||
OBJ_CODE = 5,
|
||||
OBJ_FRAME = 6,
|
||||
OBJ_FORWARD = 7
|
||||
};
|
||||
|
||||
/* Object header bits */
|
||||
#define OBJHDR_S_BIT 3u
|
||||
#define OBJHDR_P_BIT 4u
|
||||
#define OBJHDR_A_BIT 5u
|
||||
#define OBJHDR_R_BIT 7u
|
||||
|
||||
#define OBJHDR_FLAG(bit) ((objhdr_t)1ull << (bit))
|
||||
#define OBJHDR_S_MASK OBJHDR_FLAG (OBJHDR_S_BIT)
|
||||
#define OBJHDR_P_MASK OBJHDR_FLAG (OBJHDR_P_BIT)
|
||||
#define OBJHDR_A_MASK OBJHDR_FLAG (OBJHDR_A_BIT)
|
||||
#define OBJHDR_R_MASK OBJHDR_FLAG (OBJHDR_R_BIT)
|
||||
|
||||
typedef uint64_t word_t; // one actor-memory word
|
||||
typedef uint64_t objhdr_t; // header word
|
||||
typedef uint64_t objref_t; // 56-bit word address (0 = null)
|
||||
|
||||
static inline uint8_t objhdr_type (objhdr_t h) { return (uint8_t)(h & 7u); }
|
||||
static inline int objhdr_s (objhdr_t h) { return (h & OBJHDR_S_MASK) != 0; }
|
||||
|
||||
/* Word size constant */
|
||||
#define JSW 8
|
||||
|
||||
/* Runtime / Context lifecycle */
|
||||
JSRuntime *JS_NewRuntime (void);
|
||||
void JS_FreeRuntime (JSRuntime *rt);
|
||||
void JS_SetMemoryLimit (JSRuntime *rt, size_t limit);
|
||||
void JS_SetPoolSize (JSRuntime *rt, size_t initial, size_t cap);
|
||||
|
||||
JSContext *JS_NewContext (JSRuntime *rt);
|
||||
JSContext *JS_NewContextWithHeapSize (JSRuntime *rt, size_t heap_size);
|
||||
void JS_FreeContext (JSContext *s);
|
||||
|
||||
typedef void (*JS_GCScanFn)(JSContext *ctx,
|
||||
uint8_t *from_base, uint8_t *from_end,
|
||||
uint8_t *to_base, uint8_t **to_free, uint8_t *to_end);
|
||||
void JS_SetGCScanExternal(JSContext *ctx, JS_GCScanFn fn);
|
||||
|
||||
void JS_SetActorSym (JSContext *ctx, JSValue sym);
|
||||
JSValue JS_GetActorSym (JSContext *ctx);
|
||||
JSRuntime *JS_GetRuntime (JSContext *ctx);
|
||||
void JS_SetMaxStackSize (JSContext *ctx, size_t stack_size);
|
||||
void JS_UpdateStackTop (JSContext *ctx);
|
||||
int JS_GetVMCallDepth(JSContext *ctx);
|
||||
void JS_SetHeapMemoryLimit(JSContext *ctx, size_t limit);
|
||||
void JS_SetPauseFlag(JSContext *ctx, int value);
|
||||
JS_BOOL JS_IsLiveObject (JSRuntime *rt, JSValue obj);
|
||||
|
||||
/* Suspended state */
|
||||
#define JS_TAG_SUSPENDED 0x13 /* 10011 - distinct special tag */
|
||||
#define JS_SUSPENDED ((JSValue)JS_TAG_SUSPENDED)
|
||||
|
||||
static inline JS_BOOL JS_IsSuspended(JSValue v) {
|
||||
return JS_VALUE_GET_TAG(v) == JS_TAG_SUSPENDED;
|
||||
}
|
||||
|
||||
#ifndef JS_DEFAULT_STACK_SIZE
|
||||
#define JS_DEFAULT_STACK_SIZE (1024 * 1024)
|
||||
#endif
|
||||
|
||||
/* Internal compile flags */
|
||||
#define JS_EVAL_FLAG_COMPILE_ONLY (1 << 5)
|
||||
|
||||
/* Compilation and MachCode */
|
||||
struct cJSON;
|
||||
typedef struct MachCode MachCode;
|
||||
|
||||
void JS_FreeMachCode(MachCode *mc);
|
||||
uint8_t *JS_SerializeMachCode(MachCode *mc, size_t *out_size);
|
||||
MachCode *JS_DeserializeMachCode(const uint8_t *data, size_t size);
|
||||
struct JSCodeRegister *JS_LoadMachCode(JSContext *ctx, MachCode *mc, JSValue env);
|
||||
JSValue JS_RunMachBin(JSContext *ctx, const uint8_t *data, size_t size, JSValue env);
|
||||
JSValue JS_RunMachMcode(JSContext *ctx, const char *json_str, size_t len, JSValue env);
|
||||
void JS_DumpMachBin(JSContext *ctx, const uint8_t *data, size_t size, JSValue env);
|
||||
MachCode *mach_compile_mcode(struct cJSON *mcode_json);
|
||||
|
||||
/* Debug / Dump utilities */
|
||||
typedef struct JSMemoryUsage {
|
||||
int64_t malloc_size, malloc_limit, memory_used_size;
|
||||
int64_t malloc_count;
|
||||
int64_t memory_used_count;
|
||||
int64_t str_count, str_size;
|
||||
int64_t obj_count, obj_size;
|
||||
int64_t prop_count, prop_size;
|
||||
int64_t shape_count, shape_size;
|
||||
int64_t js_func_count, js_func_size, js_func_code_size;
|
||||
int64_t js_func_pc2line_count, js_func_pc2line_size;
|
||||
int64_t c_func_count, array_count;
|
||||
int64_t fast_array_count, fast_array_elements;
|
||||
int64_t binary_object_count, binary_object_size;
|
||||
} JSMemoryUsage;
|
||||
|
||||
void JS_ComputeMemoryUsage (JSRuntime *rt, JSMemoryUsage *s);
|
||||
void JS_DumpMemoryUsage (FILE *fp, const JSMemoryUsage *s, JSRuntime *rt);
|
||||
|
||||
typedef struct {
|
||||
JS_BOOL show_hidden : 8;
|
||||
JS_BOOL raw_dump : 8;
|
||||
uint32_t max_depth;
|
||||
uint32_t max_string_length;
|
||||
uint32_t max_item_count;
|
||||
} JSPrintValueOptions;
|
||||
|
||||
typedef void JSPrintValueWrite (void *opaque, const char *buf, size_t len);
|
||||
|
||||
void JS_PrintValueSetDefaultOptions (JSPrintValueOptions *options);
|
||||
void JS_PrintValueRT (JSRuntime *rt, JSPrintValueWrite *write_func,
|
||||
void *write_opaque, JSValue val,
|
||||
const JSPrintValueOptions *options);
|
||||
void JS_PrintValue (JSContext *ctx, JSPrintValueWrite *write_func,
|
||||
void *write_opaque, JSValue val,
|
||||
const JSPrintValueOptions *options);
|
||||
|
||||
void js_debug_info (JSContext *js, JSValue fn, js_debug *dbg);
|
||||
|
||||
uint32_t js_debugger_stack_depth (JSContext *ctx);
|
||||
JSValue js_debugger_backtrace_fns (JSContext *ctx);
|
||||
JSValue js_debugger_closure_variables (JSContext *ctx, JSValue fn);
|
||||
JSValue js_debugger_local_variables (JSContext *ctx, int stack_index);
|
||||
void js_debugger_set_closure_variable (JSContext *js, JSValue fn,
|
||||
JSValue var_name, JSValue val);
|
||||
JSValue js_debugger_build_backtrace (JSContext *ctx);
|
||||
JSValue js_debugger_fn_info (JSContext *ctx, JSValue fn);
|
||||
JSValue js_debugger_fn_bytecode (JSContext *js, JSValue fn);
|
||||
void *js_debugger_val_address (JSContext *js, JSValue val);
|
||||
|
||||
/* Stack trace */
|
||||
JSValue JS_GetStack (JSContext *ctx);
|
||||
void JS_CrashPrintStack(JSContext *ctx);
|
||||
|
||||
/* Serialization (internal) */
|
||||
JSValue wota2value(JSContext *js, void *v);
|
||||
void *value2wota(JSContext *js, JSValue v, JSValue replacer, size_t *bytes);
|
||||
JSValue nota2value(JSContext *js, void *nota);
|
||||
void *value2nota(JSContext *js, JSValue v);
|
||||
|
||||
/* Internal module init (called during context init) */
|
||||
JSValue js_core_blob_use(JSContext *js);
|
||||
JSValue js_core_json_use(JSContext *js);
|
||||
|
||||
/* ============================================================
|
||||
End internal API declarations
|
||||
============================================================ */
|
||||
|
||||
void *js_malloc (JSContext *ctx, size_t size);
|
||||
void *js_mallocz (JSContext *ctx, size_t size);
|
||||
|
||||
@@ -702,6 +841,46 @@ typedef struct CCallRoot {
|
||||
struct CCallRoot *prev; /* stack for nesting (C -> JS -> C -> ...) */
|
||||
} CCallRoot;
|
||||
|
||||
/* ============================================================
|
||||
Actor Types (merged from cell_internal.h)
|
||||
============================================================ */
|
||||
|
||||
/* Letter type for unified message queue */
|
||||
typedef enum {
|
||||
LETTER_BLOB,
|
||||
LETTER_CALLBACK
|
||||
} letter_type;
|
||||
|
||||
typedef struct letter {
|
||||
letter_type type;
|
||||
union {
|
||||
blob *blob_data;
|
||||
JSValue callback;
|
||||
};
|
||||
} letter;
|
||||
|
||||
/* I/O watch entry — one per watched file descriptor */
|
||||
typedef struct {
|
||||
int fd;
|
||||
short events; /* POLLIN, POLLOUT */
|
||||
JSContext *actor;
|
||||
JSValue callback;
|
||||
} io_watch;
|
||||
|
||||
/* Actor state machine constants */
|
||||
#define ACTOR_IDLE 0
|
||||
#define ACTOR_READY 1
|
||||
#define ACTOR_RUNNING 2
|
||||
#define ACTOR_EXHAUSTED 3
|
||||
#define ACTOR_RECLAIMING 4
|
||||
#define ACTOR_SLOW 5
|
||||
#define ACTOR_REFRESHED 6
|
||||
|
||||
#define ACTOR_FAST_TIMER_NS (10ULL * 1000000)
|
||||
#define ACTOR_SLOW_TIMER_NS (60000ULL * 1000000)
|
||||
#define ACTOR_SLOW_STRIKES_MAX 3
|
||||
#define ACTOR_MEMORY_LIMIT (1024ULL * 1024 * 1024)
|
||||
|
||||
struct JSContext {
|
||||
JSRuntime *rt;
|
||||
|
||||
@@ -757,7 +936,6 @@ struct JSContext {
|
||||
|
||||
/* if NULL, RegExp compilation is not supported */
|
||||
JSValue (*compile_regexp) (JSContext *ctx, JSValue pattern, JSValue flags);
|
||||
void *user_opaque;
|
||||
|
||||
/* GC callback to scan external C-side roots (actor letters, timers) */
|
||||
void (*gc_scan_external)(JSContext *ctx,
|
||||
@@ -778,6 +956,7 @@ struct JSContext {
|
||||
uint32_t suspended_pc; /* saved PC for resume */
|
||||
int vm_call_depth; /* 0 = pure bytecode, >0 = C frames on stack */
|
||||
size_t heap_memory_limit; /* 0 = no limit, else max heap bytes */
|
||||
const char *actor_label; /* human-readable label for OOM diagnostics */
|
||||
|
||||
JSValue current_exception;
|
||||
|
||||
@@ -794,6 +973,44 @@ struct JSContext {
|
||||
// todo: want this, but should be a simple increment/decrement counter while frames are pushed
|
||||
size_t stack_depth;
|
||||
size_t stack_limit;
|
||||
|
||||
/* === Actor fields (merged from cell_rt) === */
|
||||
|
||||
JSGCRef idx_buffer_ref;
|
||||
JSGCRef on_exception_ref;
|
||||
JSGCRef message_handle_ref;
|
||||
JSGCRef unneeded_ref;
|
||||
JSGCRef actor_sym_ref;
|
||||
|
||||
void *init_wota;
|
||||
|
||||
#ifndef TARGET_PLAYDATE
|
||||
pthread_mutex_t *mutex;
|
||||
pthread_mutex_t *msg_mutex;
|
||||
#endif
|
||||
|
||||
char *id;
|
||||
int idx_count;
|
||||
|
||||
letter *letters;
|
||||
struct { uint32_t key; JSValue value; } *timers;
|
||||
|
||||
int state;
|
||||
uint32_t ar;
|
||||
double ar_secs;
|
||||
|
||||
int disrupt;
|
||||
int is_quiescent;
|
||||
int main_thread_only;
|
||||
int affinity;
|
||||
|
||||
uint64_t turn_start_ns;
|
||||
_Atomic uint32_t turn_gen;
|
||||
int slow_strikes;
|
||||
int vm_suspended;
|
||||
|
||||
const char *name;
|
||||
cell_hook actor_trace_hook;
|
||||
};
|
||||
|
||||
/* ============================================================
|
||||
@@ -817,6 +1034,48 @@ static inline const char *JS_KeyGetStr (JSContext *ctx, char *buf, size_t buf_si
|
||||
}
|
||||
|
||||
|
||||
/* ============================================================
|
||||
Actor / Scheduler Declarations (merged from cell_internal.h)
|
||||
============================================================ */
|
||||
|
||||
/* Set by actor_turn/CLI before entering the VM, cleared after.
|
||||
Read by signal_handler to print JS stack on crash. */
|
||||
extern volatile JSContext *g_crash_ctx;
|
||||
|
||||
JSContext *create_actor(void *wota);
|
||||
const char *register_actor(const char *id, JSContext *actor, int mainthread, double ar);
|
||||
void actor_disrupt(JSContext *actor);
|
||||
|
||||
const char *send_message(const char *id, void *msg);
|
||||
void actor_unneeded(JSContext *actor, JSValue fn, double seconds);
|
||||
void script_startup(JSContext *ctx);
|
||||
int uncaught_exception(JSContext *js, JSValue v);
|
||||
int actor_exists(const char *id);
|
||||
|
||||
void set_actor_state(JSContext *actor);
|
||||
void enqueue_actor_priority(JSContext *actor);
|
||||
void actor_clock(JSContext *actor, JSValue fn);
|
||||
uint32_t actor_delay(JSContext *actor, JSValue fn, double seconds);
|
||||
JSValue actor_remove_timer(JSContext *actor, uint32_t timer_id);
|
||||
void actor_watch(JSContext *actor, int fd, short events, JSValue fn);
|
||||
void actor_watch_readable(JSContext *actor, int fd, JSValue fn);
|
||||
void actor_watch_writable(JSContext *actor, int fd, JSValue fn);
|
||||
void actor_unwatch(JSContext *actor, int fd);
|
||||
void exit_handler(void);
|
||||
void actor_loop(void);
|
||||
void actor_initialize(void);
|
||||
void actor_free(JSContext *actor);
|
||||
void actor_gc_scan(JSContext *ctx, uint8_t *fb, uint8_t *fe,
|
||||
uint8_t *tb, uint8_t **tf, uint8_t *te);
|
||||
int scheduler_actor_count(void);
|
||||
void scheduler_enable_quiescence(void);
|
||||
|
||||
JSValue JS_ResumeRegisterVM(JSContext *ctx);
|
||||
|
||||
uint64_t cell_ns(void);
|
||||
void cell_sleep(double seconds);
|
||||
int randombytes(void *buf, size_t n);
|
||||
|
||||
/* ============================================================
|
||||
Constant Text Pool Functions
|
||||
============================================================ */
|
||||
@@ -1287,8 +1546,8 @@ JSValue js_key_from_string (JSContext *ctx, JSValue val);
|
||||
|
||||
/* mach.c exports */
|
||||
JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, JSValue this_obj, int argc, JSValue *argv, JSValue env, JSValue outer_frame);
|
||||
JSValue js_new_native_function(JSContext *ctx, void *fn_ptr, void *dl_handle, uint16_t nr_slots, int arity, JSValue outer_frame);
|
||||
JSValue js_new_native_function_with_code(JSContext *ctx, JSValue code_obj, int arity, JSValue outer_frame);
|
||||
JSValue js_new_native_function(JSContext *ctx, void *fn_ptr, void *dl_handle, uint16_t nr_slots, int arity, JSValue outer_frame, JSValue env);
|
||||
JSValue js_new_native_function_with_code(JSContext *ctx, JSValue code_obj, int arity, JSValue outer_frame, JSValue env_record);
|
||||
JSFrameRegister *alloc_frame_register(JSContext *ctx, int slot_count);
|
||||
void cell_rt_free_native_state(JSContext *ctx);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
/* QBE headers */
|
||||
#include "all.h"
|
||||
@@ -154,6 +155,8 @@ JSValue js_os_qbe(JSContext *js, JSValue self, int argc, JSValue *argv) {
|
||||
}
|
||||
|
||||
/* Run the QBE pipeline */
|
||||
struct timespec t0, t1;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||
parse(inf, "<ir>", qbe_dbgfile, qbe_data, qbe_func);
|
||||
fclose(inf);
|
||||
|
||||
@@ -162,6 +165,11 @@ JSValue js_os_qbe(JSContext *js, JSValue self, int argc, JSValue *argv) {
|
||||
fflush(qbe_outf);
|
||||
fclose(qbe_outf);
|
||||
qbe_outf = NULL;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t1);
|
||||
double ms = (t1.tv_sec - t0.tv_sec) * 1000.0 +
|
||||
(t1.tv_nsec - t0.tv_nsec) / 1e6;
|
||||
fprintf(stderr, "[qbe_backend] os.qbe: %.1fms (%zu bytes IL -> %zu bytes asm)\n",
|
||||
ms, ir_len, out_len);
|
||||
|
||||
JS_FreeCString(js, ir);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* string comparison, bitwise ops on floats, and boolean conversion.
|
||||
*/
|
||||
|
||||
#include "quickjs-internal.h"
|
||||
#include "pit_internal.h"
|
||||
#include <math.h>
|
||||
#include <pthread.h>
|
||||
#include <stdlib.h>
|
||||
@@ -17,10 +17,6 @@ JSValue qbe_new_float64(JSContext *ctx, double d) {
|
||||
return __JS_NewFloat64(ctx, d);
|
||||
}
|
||||
|
||||
JSValue qbe_new_string(JSContext *ctx, const char *str) {
|
||||
return JS_NewString(ctx, str);
|
||||
}
|
||||
|
||||
/* Comparison op IDs (must match qbe.cm float_cmp_op_id values) */
|
||||
enum {
|
||||
QBE_CMP_EQ = 0,
|
||||
@@ -31,128 +27,89 @@ enum {
|
||||
QBE_CMP_GE = 5
|
||||
};
|
||||
|
||||
/* ============================================================
|
||||
Float binary arithmetic
|
||||
============================================================ */
|
||||
|
||||
static inline JSValue qbe_float_binop(JSContext *ctx, JSValue a, JSValue b,
|
||||
double (*op)(double, double)) {
|
||||
double da, db;
|
||||
JS_ToFloat64(ctx, &da, a);
|
||||
JS_ToFloat64(ctx, &db, b);
|
||||
double r = op(da, db);
|
||||
if (!isfinite(r))
|
||||
return JS_NULL;
|
||||
return JS_NewFloat64(ctx, r);
|
||||
}
|
||||
|
||||
static double op_add(double a, double b) { return a + b; }
|
||||
static double op_sub(double a, double b) { return a - b; }
|
||||
static double op_mul(double a, double b) { return a * b; }
|
||||
|
||||
JSValue qbe_float_add(JSContext *ctx, JSValue a, JSValue b) {
|
||||
return qbe_float_binop(ctx, a, b, op_add);
|
||||
}
|
||||
|
||||
/* Generic add: concat if both text, float add if both numeric, else type error */
|
||||
JSValue cell_rt_add(JSContext *ctx, JSValue a, JSValue b) {
|
||||
if (JS_IsText(a) && JS_IsText(b))
|
||||
return JS_ConcatString(ctx, a, b);
|
||||
if (JS_IsNumber(a) && JS_IsNumber(b))
|
||||
return qbe_float_binop(ctx, a, b, op_add);
|
||||
JS_RaiseDisrupt(ctx, "cannot add incompatible types");
|
||||
return JS_NULL;
|
||||
}
|
||||
|
||||
JSValue qbe_float_sub(JSContext *ctx, JSValue a, JSValue b) {
|
||||
return qbe_float_binop(ctx, a, b, op_sub);
|
||||
}
|
||||
|
||||
JSValue qbe_float_mul(JSContext *ctx, JSValue a, JSValue b) {
|
||||
return qbe_float_binop(ctx, a, b, op_mul);
|
||||
}
|
||||
|
||||
JSValue qbe_float_div(JSContext *ctx, JSValue a, JSValue b) {
|
||||
double da, db;
|
||||
JS_ToFloat64(ctx, &da, a);
|
||||
JS_ToFloat64(ctx, &db, b);
|
||||
if (db == 0.0)
|
||||
return JS_NULL;
|
||||
double r = da / db;
|
||||
if (!isfinite(r))
|
||||
return JS_NULL;
|
||||
return JS_NewFloat64(ctx, r);
|
||||
}
|
||||
|
||||
JSValue qbe_float_mod(JSContext *ctx, JSValue a, JSValue b) {
|
||||
double da, db;
|
||||
JS_ToFloat64(ctx, &da, a);
|
||||
JS_ToFloat64(ctx, &db, b);
|
||||
if (db == 0.0)
|
||||
return JS_NULL;
|
||||
double r = fmod(da, db);
|
||||
if (!isfinite(r))
|
||||
return JS_NULL;
|
||||
return JS_NewFloat64(ctx, r);
|
||||
}
|
||||
|
||||
JSValue qbe_float_pow(JSContext *ctx, JSValue a, JSValue b) {
|
||||
double da, db;
|
||||
JS_ToFloat64(ctx, &da, a);
|
||||
JS_ToFloat64(ctx, &db, b);
|
||||
double r = pow(da, db);
|
||||
if (!isfinite(r) && isfinite(da) && isfinite(db))
|
||||
return JS_NULL;
|
||||
return JS_NewFloat64(ctx, r);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Float unary ops
|
||||
============================================================ */
|
||||
|
||||
JSValue qbe_float_neg(JSContext *ctx, JSValue v) {
|
||||
double d;
|
||||
JS_ToFloat64(ctx, &d, v);
|
||||
return JS_NewFloat64(ctx, -d);
|
||||
}
|
||||
|
||||
JSValue qbe_float_inc(JSContext *ctx, JSValue v) {
|
||||
double d;
|
||||
JS_ToFloat64(ctx, &d, v);
|
||||
return JS_NewFloat64(ctx, d + 1);
|
||||
}
|
||||
|
||||
JSValue qbe_float_dec(JSContext *ctx, JSValue v) {
|
||||
double d;
|
||||
JS_ToFloat64(ctx, &d, v);
|
||||
return JS_NewFloat64(ctx, d - 1);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Float comparison — returns 0 or 1 for QBE branching
|
||||
============================================================ */
|
||||
|
||||
int qbe_float_cmp(JSContext *ctx, int op, JSValue a, JSValue b) {
|
||||
double da, db;
|
||||
JS_ToFloat64(ctx, &da, a);
|
||||
JS_ToFloat64(ctx, &db, b);
|
||||
switch (op) {
|
||||
case QBE_CMP_EQ: return da == db;
|
||||
case QBE_CMP_NE: return da != db;
|
||||
case QBE_CMP_LT: return da < db;
|
||||
case QBE_CMP_LE: return da <= db;
|
||||
case QBE_CMP_GT: return da > db;
|
||||
case QBE_CMP_GE: return da >= db;
|
||||
default: return 0;
|
||||
/* Generic comparison helper matching MACH eq/ne/lt/le/gt/ge semantics. */
|
||||
JSValue cell_rt_cmp(JSContext *ctx, int op, JSValue a, JSValue b) {
|
||||
if (JS_VALUE_IS_BOTH_INT(a, b)) {
|
||||
int32_t ia = JS_VALUE_GET_INT(a);
|
||||
int32_t ib = JS_VALUE_GET_INT(b);
|
||||
switch (op) {
|
||||
case QBE_CMP_EQ: return JS_NewBool(ctx, ia == ib);
|
||||
case QBE_CMP_NE: return JS_NewBool(ctx, ia != ib);
|
||||
case QBE_CMP_LT: return JS_NewBool(ctx, ia < ib);
|
||||
case QBE_CMP_LE: return JS_NewBool(ctx, ia <= ib);
|
||||
case QBE_CMP_GT: return JS_NewBool(ctx, ia > ib);
|
||||
case QBE_CMP_GE: return JS_NewBool(ctx, ia >= ib);
|
||||
default: return JS_NewBool(ctx, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Boolean conversion wrapper
|
||||
============================================================ */
|
||||
/* Fast path: identity after chasing forward pointers (matches MACH). */
|
||||
{
|
||||
JSValue ca = JS_IsPtr(a) ? JS_MKPTR(chase(a)) : a;
|
||||
JSValue cb = JS_IsPtr(b) ? JS_MKPTR(chase(b)) : b;
|
||||
if (ca == cb) {
|
||||
if (op == QBE_CMP_EQ || op == QBE_CMP_LE || op == QBE_CMP_GE)
|
||||
return JS_TRUE;
|
||||
if (op == QBE_CMP_NE)
|
||||
return JS_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
int qbe_to_bool(JSContext *ctx, JSValue v) {
|
||||
return JS_ToBool(ctx, v);
|
||||
if (JS_IsNumber(a) && JS_IsNumber(b)) {
|
||||
double da, db;
|
||||
JS_ToFloat64(ctx, &da, a);
|
||||
JS_ToFloat64(ctx, &db, b);
|
||||
switch (op) {
|
||||
case QBE_CMP_EQ: return JS_NewBool(ctx, da == db);
|
||||
case QBE_CMP_NE: return JS_NewBool(ctx, da != db);
|
||||
case QBE_CMP_LT: return JS_NewBool(ctx, da < db);
|
||||
case QBE_CMP_LE: return JS_NewBool(ctx, da <= db);
|
||||
case QBE_CMP_GT: return JS_NewBool(ctx, da > db);
|
||||
case QBE_CMP_GE: return JS_NewBool(ctx, da >= db);
|
||||
default: return JS_NewBool(ctx, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (mist_is_text(a) && mist_is_text(b)) {
|
||||
int cmp = js_string_compare_value(ctx, a, b, FALSE);
|
||||
switch (op) {
|
||||
case QBE_CMP_EQ: return JS_NewBool(ctx, cmp == 0);
|
||||
case QBE_CMP_NE: return JS_NewBool(ctx, cmp != 0);
|
||||
case QBE_CMP_LT: return JS_NewBool(ctx, cmp < 0);
|
||||
case QBE_CMP_LE: return JS_NewBool(ctx, cmp <= 0);
|
||||
case QBE_CMP_GT: return JS_NewBool(ctx, cmp > 0);
|
||||
case QBE_CMP_GE: return JS_NewBool(ctx, cmp >= 0);
|
||||
default: return JS_NewBool(ctx, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (JS_IsNull(a) && JS_IsNull(b)) {
|
||||
if (op == QBE_CMP_EQ || op == QBE_CMP_LE || op == QBE_CMP_GE)
|
||||
return JS_TRUE;
|
||||
return JS_FALSE;
|
||||
}
|
||||
|
||||
if (JS_IsBool(a) && JS_IsBool(b)) {
|
||||
int ba = JS_VALUE_GET_BOOL(a);
|
||||
int bb = JS_VALUE_GET_BOOL(b);
|
||||
switch (op) {
|
||||
case QBE_CMP_EQ: return JS_NewBool(ctx, ba == bb);
|
||||
case QBE_CMP_NE: return JS_NewBool(ctx, ba != bb);
|
||||
case QBE_CMP_LT: return JS_NewBool(ctx, ba < bb);
|
||||
case QBE_CMP_LE: return JS_NewBool(ctx, ba <= bb);
|
||||
case QBE_CMP_GT: return JS_NewBool(ctx, ba > bb);
|
||||
case QBE_CMP_GE: return JS_NewBool(ctx, ba >= bb);
|
||||
default: return JS_NewBool(ctx, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (op == QBE_CMP_EQ)
|
||||
return JS_NewBool(ctx, 0);
|
||||
if (op == QBE_CMP_NE)
|
||||
return JS_NewBool(ctx, 1);
|
||||
|
||||
JS_RaiseDisrupt(ctx, "cannot compare: operands must be same type");
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
@@ -190,29 +147,33 @@ JSValue qbe_bitwise_xor(JSContext *ctx, JSValue a, JSValue b) {
|
||||
return JS_NewInt32(ctx, ia ^ ib);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Shift ops on floats (convert to int32, shift, re-tag)
|
||||
============================================================ */
|
||||
/* Concat helper matching MACH_CONCAT semantics exactly. */
|
||||
JSValue cell_rt_concat(JSContext *ctx, JSValue left, JSValue right, int self_assign) {
|
||||
if (self_assign) {
|
||||
if (JS_IsPtr(left)) {
|
||||
JSText *s = (JSText *)chase(left);
|
||||
int slen = (int)s->length;
|
||||
int rlen = js_string_value_len(right);
|
||||
int cap = (int)objhdr_cap56(s->hdr);
|
||||
if (objhdr_type(s->hdr) == OBJ_TEXT
|
||||
&& !(s->hdr & OBJHDR_S_MASK)
|
||||
&& slen + rlen <= cap) {
|
||||
for (int i = 0; i < rlen; i++)
|
||||
string_put(s, slen + i, js_string_value_get(right, i));
|
||||
s->length = slen + rlen;
|
||||
return left;
|
||||
}
|
||||
}
|
||||
JSValue res = JS_ConcatStringGrow(ctx, left, right);
|
||||
if (JS_IsException(res))
|
||||
return JS_EXCEPTION;
|
||||
return res;
|
||||
}
|
||||
|
||||
JSValue qbe_shift_shl(JSContext *ctx, JSValue a, JSValue b) {
|
||||
int32_t ia, ib;
|
||||
JS_ToInt32(ctx, &ia, a);
|
||||
JS_ToInt32(ctx, &ib, b);
|
||||
return JS_NewInt32(ctx, ia << (ib & 31));
|
||||
}
|
||||
|
||||
JSValue qbe_shift_sar(JSContext *ctx, JSValue a, JSValue b) {
|
||||
int32_t ia, ib;
|
||||
JS_ToInt32(ctx, &ia, a);
|
||||
JS_ToInt32(ctx, &ib, b);
|
||||
return JS_NewInt32(ctx, ia >> (ib & 31));
|
||||
}
|
||||
|
||||
JSValue qbe_shift_shr(JSContext *ctx, JSValue a, JSValue b) {
|
||||
int32_t ia, ib;
|
||||
JS_ToInt32(ctx, &ia, a);
|
||||
JS_ToInt32(ctx, &ib, b);
|
||||
return JS_NewInt32(ctx, (uint32_t)ia >> (ib & 31));
|
||||
JSValue res = JS_ConcatString(ctx, left, right);
|
||||
if (JS_IsException(res))
|
||||
return JS_EXCEPTION;
|
||||
return res;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
@@ -251,9 +212,6 @@ typedef struct {
|
||||
AOTCodeCacheEntry *code_cache;
|
||||
int code_cache_count;
|
||||
int code_cache_cap;
|
||||
JSGCRef native_env_ref;
|
||||
int has_native_env;
|
||||
int native_env_ref_inited;
|
||||
AOTGCRefChunk **gc_ref_chunks;
|
||||
int gc_ref_chunk_count;
|
||||
int aot_depth;
|
||||
@@ -381,13 +339,6 @@ static JSValue cell_rt_load_field_key(JSContext *ctx, JSValue obj, JSValue key)
|
||||
return JS_GetProperty(ctx, obj, key);
|
||||
}
|
||||
|
||||
JSValue cell_rt_load_field(JSContext *ctx, JSValue obj, const char *name) {
|
||||
JSValue key = aot_key_from_cstr(ctx, name);
|
||||
if (JS_IsException(key))
|
||||
return JS_EXCEPTION;
|
||||
return cell_rt_load_field_key(ctx, obj, key);
|
||||
}
|
||||
|
||||
JSValue cell_rt_load_field_lit(JSContext *ctx, JSValue obj, int64_t lit_idx) {
|
||||
JSValue key = aot_lit_from_index(ctx, lit_idx);
|
||||
if (JS_IsException(key))
|
||||
@@ -395,29 +346,12 @@ JSValue cell_rt_load_field_lit(JSContext *ctx, JSValue obj, int64_t lit_idx) {
|
||||
return cell_rt_load_field_key(ctx, obj, key);
|
||||
}
|
||||
|
||||
/* Like cell_rt_load_field but without the function guard.
|
||||
Used by load_dynamic when the key happens to be a static string. */
|
||||
JSValue cell_rt_load_prop_str(JSContext *ctx, JSValue obj, const char *name) {
|
||||
JSValue key = aot_key_from_cstr(ctx, name);
|
||||
if (JS_IsException(key))
|
||||
return JS_EXCEPTION;
|
||||
return JS_GetProperty(ctx, obj, key);
|
||||
}
|
||||
|
||||
static int cell_rt_store_field_key(JSContext *ctx, JSValue val, JSValue obj,
|
||||
JSValue key) {
|
||||
int ret = JS_SetProperty(ctx, obj, key, val);
|
||||
return (ret < 0 || JS_HasException(ctx)) ? 0 : 1;
|
||||
}
|
||||
|
||||
int cell_rt_store_field(JSContext *ctx, JSValue val, JSValue obj,
|
||||
const char *name) {
|
||||
JSValue key = aot_key_from_cstr(ctx, name);
|
||||
if (JS_IsException(key))
|
||||
return 0;
|
||||
return cell_rt_store_field_key(ctx, val, obj, key);
|
||||
}
|
||||
|
||||
int cell_rt_store_field_lit(JSContext *ctx, JSValue val, JSValue obj,
|
||||
int64_t lit_idx) {
|
||||
JSValue key = aot_lit_from_index(ctx, lit_idx);
|
||||
@@ -455,70 +389,22 @@ int cell_rt_store_dynamic(JSContext *ctx, JSValue val, JSValue obj,
|
||||
}
|
||||
}
|
||||
|
||||
JSValue cell_rt_load_index(JSContext *ctx, JSValue arr, JSValue idx) {
|
||||
if (JS_IsInt(idx))
|
||||
return JS_GetPropertyNumber(ctx, arr, (uint32_t)JS_VALUE_GET_INT(idx));
|
||||
return JS_GetProperty(ctx, arr, idx);
|
||||
}
|
||||
/* --- Env-based variable lookup (env at frame slot 0) --- */
|
||||
|
||||
int cell_rt_store_index(JSContext *ctx, JSValue val, JSValue arr,
|
||||
JSValue idx) {
|
||||
int ret = 0;
|
||||
JSValue nr = JS_NULL;
|
||||
if (JS_IsInt(idx))
|
||||
nr = JS_SetPropertyNumber(ctx, arr, (uint32_t)JS_VALUE_GET_INT(idx), val);
|
||||
else
|
||||
ret = JS_SetProperty(ctx, arr, idx, val);
|
||||
if (JS_IsInt(idx))
|
||||
return JS_IsException(nr) ? 0 : 1;
|
||||
return (ret < 0 || JS_HasException(ctx)) ? 0 : 1;
|
||||
}
|
||||
|
||||
/* --- Intrinsic/global lookup --- */
|
||||
|
||||
void cell_rt_set_native_env(JSContext *ctx, JSValue env) {
|
||||
NativeRTState *st = native_state(ctx);
|
||||
if (!st) return;
|
||||
if (!JS_IsNull(env) && !JS_IsStone(env)) {
|
||||
fprintf(stderr, "cell_rt_set_native_env: ERROR env not stone\n");
|
||||
abort();
|
||||
JSValue cell_rt_access_env(JSContext *ctx, JSValue env, int64_t lit_idx) {
|
||||
JSValue key = aot_lit_from_index(ctx, lit_idx);
|
||||
if (JS_IsException(key))
|
||||
return JS_EXCEPTION;
|
||||
/* Try env first (linear scan for stoned records) */
|
||||
if (!JS_IsNull(env) && JS_IsRecord(env)) {
|
||||
JSRecord *rec = (JSRecord *)chase(env);
|
||||
uint64_t mask = objhdr_cap56(rec->mist_hdr);
|
||||
for (uint64_t i = 1; i <= mask; i++) {
|
||||
if (js_key_equal(rec->slots[i].key, key))
|
||||
return rec->slots[i].val;
|
||||
}
|
||||
}
|
||||
/* Drop module literal pool roots before switching native env/module. */
|
||||
aot_clear_lit_pool(ctx, st);
|
||||
|
||||
/* Native module boundary: clear per-actor key cache so stale keys
|
||||
cannot survive across context/module lifetimes. */
|
||||
free(st->key_cache);
|
||||
st->key_cache = NULL;
|
||||
st->key_cache_count = 0;
|
||||
st->key_cache_cap = 0;
|
||||
|
||||
if (st->has_native_env && st->native_env_ref_inited) {
|
||||
JS_DeleteGCRef(ctx, &st->native_env_ref);
|
||||
st->native_env_ref_inited = 0;
|
||||
}
|
||||
if (!JS_IsNull(env)) {
|
||||
JS_AddGCRef(ctx, &st->native_env_ref);
|
||||
st->native_env_ref_inited = 1;
|
||||
st->native_env_ref.val = env;
|
||||
st->has_native_env = 1;
|
||||
} else {
|
||||
st->has_native_env = 0;
|
||||
st->native_env_ref.val = JS_NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static JSValue cell_rt_get_intrinsic_key(JSContext *ctx, JSValue key) {
|
||||
NativeRTState *st = native_state(ctx);
|
||||
if (!st) return JS_EXCEPTION;
|
||||
/* Check native env first (runtime-provided functions like log) */
|
||||
if (st->has_native_env) {
|
||||
JSValue v = JS_GetProperty(ctx, st->native_env_ref.val, key);
|
||||
if (!JS_IsNull(v))
|
||||
return v;
|
||||
}
|
||||
/* Linear scan of global object — avoids hash mismatch issues with
|
||||
stoned records whose keys may be in cold storage */
|
||||
/* Fallback to global object */
|
||||
JSValue gobj = ctx->global_obj;
|
||||
if (JS_IsRecord(gobj)) {
|
||||
JSRecord *rec = (JSRecord *)chase(gobj);
|
||||
@@ -528,70 +414,12 @@ static JSValue cell_rt_get_intrinsic_key(JSContext *ctx, JSValue key) {
|
||||
return rec->slots[i].val;
|
||||
}
|
||||
}
|
||||
JS_RaiseDisrupt(ctx, "name is not defined");
|
||||
const char *kstr = JS_ToCString(ctx, key);
|
||||
JS_RaiseDisrupt(ctx, "'%s' is not defined", kstr ? kstr : "?");
|
||||
if (kstr) JS_FreeCString(ctx, kstr);
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
|
||||
JSValue cell_rt_get_intrinsic(JSContext *ctx, const char *name) {
|
||||
JSValue key = aot_key_from_cstr(ctx, name);
|
||||
if (JS_IsException(key))
|
||||
return JS_EXCEPTION;
|
||||
return cell_rt_get_intrinsic_key(ctx, key);
|
||||
}
|
||||
|
||||
JSValue cell_rt_get_intrinsic_lit(JSContext *ctx, int64_t lit_idx) {
|
||||
JSValue key = aot_lit_from_index(ctx, lit_idx);
|
||||
if (JS_IsException(key))
|
||||
return JS_EXCEPTION;
|
||||
return cell_rt_get_intrinsic_key(ctx, key);
|
||||
}
|
||||
|
||||
/* --- Closure access ---
|
||||
Walk the outer_frame chain on JSFunction (JS_FUNC_KIND_NATIVE).
|
||||
The frame's function field links to the JSFunction, whose
|
||||
u.native.outer_frame points to the enclosing frame.
|
||||
GC traces outer_frame naturally — no registry needed. */
|
||||
|
||||
/* Get the outer frame's slots from a frame pointer.
|
||||
The frame's function must be JS_FUNC_KIND_NATIVE. */
|
||||
static JSValue *get_outer_frame_slots(JSValue *fp) {
|
||||
/* fp points to frame->slots[0]; frame header is before it */
|
||||
JSFrameRegister *frame = (JSFrameRegister *)((char *)fp - offsetof(JSFrameRegister, slots));
|
||||
if (JS_IsNull(frame->function))
|
||||
return NULL;
|
||||
JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function);
|
||||
if (fn->kind != JS_FUNC_KIND_NATIVE)
|
||||
return NULL;
|
||||
JSValue outer = fn->u.cell.outer_frame;
|
||||
if (JS_IsNull(outer))
|
||||
return NULL;
|
||||
JSFrameRegister *outer_frame = (JSFrameRegister *)JS_VALUE_GET_PTR(outer);
|
||||
return (JSValue *)outer_frame->slots;
|
||||
}
|
||||
|
||||
JSValue cell_rt_get_closure(JSContext *ctx, void *fp, int64_t depth,
|
||||
int64_t slot) {
|
||||
(void)ctx;
|
||||
JSValue *frame = (JSValue *)fp;
|
||||
for (int64_t d = 0; d < depth; d++) {
|
||||
frame = get_outer_frame_slots(frame);
|
||||
if (!frame)
|
||||
return JS_NULL;
|
||||
}
|
||||
return frame[slot];
|
||||
}
|
||||
|
||||
void cell_rt_put_closure(JSContext *ctx, void *fp, JSValue val, int64_t depth,
|
||||
int64_t slot) {
|
||||
(void)ctx;
|
||||
JSValue *frame = (JSValue *)fp;
|
||||
for (int64_t d = 0; d < depth; d++) {
|
||||
frame = get_outer_frame_slots(frame);
|
||||
if (!frame) return;
|
||||
}
|
||||
frame[slot] = val;
|
||||
}
|
||||
|
||||
/* --- GC-managed AOT frame stack ---
|
||||
Each native dispatch loop pushes a GC ref so the GC can find and
|
||||
update the current frame pointer when it moves objects.
|
||||
@@ -740,18 +568,6 @@ typedef JSValue (*cell_compiled_fn)(JSContext *ctx, void *fp);
|
||||
to call another function (instead of recursing via C stack).
|
||||
============================================================ */
|
||||
|
||||
/* Poll pause state on taken backward jumps (AOT backedges).
|
||||
MACH can suspend/resume a register VM frame at pc granularity; native AOT
|
||||
does not currently have an equivalent resume point, so we acknowledge timer
|
||||
pauses by clearing pause_flag and continuing the current turn. */
|
||||
int cell_rt_check_backedge(JSContext *ctx) {
|
||||
int pf = atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed);
|
||||
if (pf >= 1) {
|
||||
atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void cell_rt_signal_call(JSContext *ctx, void *fp, int64_t frame_slot) {
|
||||
NativeRTState *st = native_state(ctx);
|
||||
if (!st) return;
|
||||
@@ -779,6 +595,30 @@ static int cell_check_call_arity(JSContext *ctx, JSFunction *fn, int argc) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
JSValue cell_rt_apply(JSContext *ctx, JSValue fn_val, JSValue arg_val) {
|
||||
if (!mist_is_function(fn_val))
|
||||
return fn_val;
|
||||
|
||||
JSFunction *fn = JS_VALUE_GET_FUNCTION(fn_val);
|
||||
if (!mist_is_array(arg_val)) {
|
||||
if (!cell_check_call_arity(ctx, fn, 1))
|
||||
return JS_EXCEPTION;
|
||||
return JS_CallInternal(ctx, fn_val, JS_NULL, 1, &arg_val, 0);
|
||||
}
|
||||
|
||||
JSArray *arr = JS_VALUE_GET_ARRAY(arg_val);
|
||||
int len = arr->len;
|
||||
if (!cell_check_call_arity(ctx, fn, len))
|
||||
return JS_EXCEPTION;
|
||||
if (len == 0)
|
||||
return JS_CallInternal(ctx, fn_val, JS_NULL, 0, NULL, 0);
|
||||
|
||||
JSValue args[len];
|
||||
for (int i = 0; i < len; i++)
|
||||
args[i] = arr->values[i];
|
||||
return JS_CallInternal(ctx, fn_val, JS_NULL, len, args, 0);
|
||||
}
|
||||
|
||||
static inline void cell_copy_args_0_4(JSValue *fp, JSValue *argv, int copy) {
|
||||
/* fp[0] is `this`; copy args into fp[1..4] */
|
||||
switch (copy) {
|
||||
@@ -895,6 +735,12 @@ JSValue cell_native_dispatch(JSContext *ctx, JSValue func_obj,
|
||||
|
||||
if (!JS_IsFunction(callee_fn_val)) {
|
||||
JS_RaiseDisrupt(ctx, "not a function");
|
||||
{
|
||||
int ret_info = JS_VALUE_GET_INT(frame->address);
|
||||
int ret_slot = ret_info & 0xFFFF;
|
||||
if (ret_slot != 0xFFFF)
|
||||
fp[ret_slot] = JS_EXCEPTION;
|
||||
}
|
||||
/* Resume caller with exception pending */
|
||||
JSFunction *exc_fn = JS_VALUE_GET_FUNCTION(frame->function);
|
||||
fn = (cell_compiled_fn)JS_VALUE_GET_CODE(FN_READ_CODE(exc_fn))->u.native.fn_ptr;
|
||||
@@ -905,6 +751,10 @@ JSValue cell_native_dispatch(JSContext *ctx, JSValue func_obj,
|
||||
|
||||
JSFunction *callee_fn = JS_VALUE_GET_FUNCTION(callee_fn_val);
|
||||
if (!cell_check_call_arity(ctx, callee_fn, callee_argc)) {
|
||||
int ret_info = JS_VALUE_GET_INT(frame->address);
|
||||
int ret_slot = ret_info & 0xFFFF;
|
||||
if (ret_slot != 0xFFFF)
|
||||
fp[ret_slot] = JS_EXCEPTION;
|
||||
JSFunction *exc_fn = JS_VALUE_GET_FUNCTION(frame->function);
|
||||
fn = (cell_compiled_fn)JS_VALUE_GET_CODE(FN_READ_CODE(exc_fn))->u.native.fn_ptr;
|
||||
cell_sync_dl_from_native_fn(st, exc_fn);
|
||||
@@ -1152,7 +1002,6 @@ static JSValue aot_get_or_create_native_code(JSContext *ctx, NativeRTState *st,
|
||||
Called from QBE-generated code during function creation. */
|
||||
JSValue cell_rt_make_function(JSContext *ctx, int64_t fn_idx, void *outer_fp,
|
||||
int64_t nr_args, int64_t nr_slots) {
|
||||
(void)outer_fp;
|
||||
NativeRTState *st = native_state(ctx);
|
||||
if (!st) return JS_EXCEPTION;
|
||||
if (!st->current_dl_handle)
|
||||
@@ -1168,7 +1017,18 @@ JSValue cell_rt_make_function(JSContext *ctx, int64_t fn_idx, void *outer_fp,
|
||||
if (st->aot_depth > 0)
|
||||
outer_frame = aot_gc_ref_at(st, st->aot_depth - 1)->val;
|
||||
|
||||
return js_new_native_function_with_code(ctx, code_obj, (int)nr_args, outer_frame);
|
||||
/* Read env from the parent frame's function object */
|
||||
JSValue env = JS_NULL;
|
||||
if (outer_fp) {
|
||||
JSFrameRegister *parent = (JSFrameRegister *)((char *)outer_fp - offsetof(JSFrameRegister, slots));
|
||||
if (JS_IsFunction(parent->function)) {
|
||||
JSFunction *parent_fn = JS_VALUE_GET_FUNCTION(parent->function);
|
||||
if (parent_fn->kind == JS_FUNC_KIND_NATIVE)
|
||||
env = parent_fn->u.cell.env_record;
|
||||
}
|
||||
}
|
||||
|
||||
return js_new_native_function_with_code(ctx, code_obj, (int)nr_args, outer_frame, env);
|
||||
}
|
||||
|
||||
/* --- Frame-based function calling ---
|
||||
@@ -1191,56 +1051,10 @@ JSValue cell_rt_frame(JSContext *ctx, JSValue fn, int64_t nargs) {
|
||||
return JS_MKPTR(new_frame);
|
||||
}
|
||||
|
||||
void cell_rt_setarg(JSValue frame_val, int64_t idx, JSValue val) {
|
||||
if (frame_val == JS_EXCEPTION || frame_val == JS_NULL) return;
|
||||
JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val);
|
||||
fr->slots[idx] = val;
|
||||
}
|
||||
|
||||
/* cell_rt_invoke — still used for non-dispatch-loop paths (e.g. old code) */
|
||||
JSValue cell_rt_invoke(JSContext *ctx, JSValue frame_val) {
|
||||
if (frame_val == JS_EXCEPTION) return JS_EXCEPTION;
|
||||
JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val);
|
||||
int c_argc = JS_VALUE_GET_INT(fr->address);
|
||||
if (c_argc < 0) c_argc = 0;
|
||||
JSValue fn_val = fr->function;
|
||||
|
||||
if (!JS_IsFunction(fn_val)) {
|
||||
JS_RaiseDisrupt(ctx, "not a function");
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
|
||||
JSFunction *fn = JS_VALUE_GET_FUNCTION(fn_val);
|
||||
JSValue result;
|
||||
if (!cell_check_call_arity(ctx, fn, c_argc))
|
||||
return JS_EXCEPTION;
|
||||
|
||||
if (fn->kind == JS_FUNC_KIND_C) {
|
||||
result = js_call_c_function(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]);
|
||||
} else if (fn->kind == JS_FUNC_KIND_NATIVE) {
|
||||
result = cell_native_dispatch(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]);
|
||||
} else {
|
||||
JSValue args[c_argc > 0 ? c_argc : 1];
|
||||
for (int i = 0; i < c_argc; i++)
|
||||
args[i] = fr->slots[i + 1];
|
||||
result = JS_CallInternal(ctx, fn_val, fr->slots[0], c_argc, args, 0);
|
||||
}
|
||||
|
||||
if (JS_IsException(result))
|
||||
return JS_EXCEPTION;
|
||||
if (JS_HasException(ctx))
|
||||
JS_GetException(ctx);
|
||||
return result;
|
||||
}
|
||||
|
||||
JSValue cell_rt_goframe(JSContext *ctx, JSValue fn, int64_t nargs) {
|
||||
return cell_rt_frame(ctx, fn, nargs);
|
||||
}
|
||||
|
||||
JSValue cell_rt_goinvoke(JSContext *ctx, JSValue frame_val) {
|
||||
return cell_rt_invoke(ctx, frame_val);
|
||||
}
|
||||
|
||||
/* --- Array push/pop --- */
|
||||
|
||||
JSValue cell_rt_push(JSContext *ctx, JSValue arr, JSValue val) {
|
||||
@@ -1268,13 +1082,6 @@ static JSValue cell_rt_delete_key(JSContext *ctx, JSValue obj, JSValue key) {
|
||||
return JS_NewBool(ctx, ret >= 0);
|
||||
}
|
||||
|
||||
JSValue cell_rt_delete_str(JSContext *ctx, JSValue obj, const char *name) {
|
||||
JSValue key = aot_key_from_cstr(ctx, name);
|
||||
if (JS_IsException(key))
|
||||
return JS_EXCEPTION;
|
||||
return cell_rt_delete_key(ctx, obj, key);
|
||||
}
|
||||
|
||||
JSValue cell_rt_delete_lit(JSContext *ctx, JSValue obj, int64_t lit_idx) {
|
||||
JSValue key = aot_lit_from_index(ctx, lit_idx);
|
||||
if (JS_IsException(key))
|
||||
@@ -1282,49 +1089,6 @@ JSValue cell_rt_delete_lit(JSContext *ctx, JSValue obj, int64_t lit_idx) {
|
||||
return cell_rt_delete_key(ctx, obj, key);
|
||||
}
|
||||
|
||||
/* --- Typeof --- */
|
||||
|
||||
JSValue cell_rt_typeof(JSContext *ctx, JSValue val) {
|
||||
if (JS_IsNull(val)) return JS_NewString(ctx, "null");
|
||||
if (JS_IsInt(val) || JS_IsNumber(val)) return JS_NewString(ctx, "number");
|
||||
if (JS_IsBool(val)) return JS_NewString(ctx, "logical");
|
||||
if (JS_IsText(val)) return JS_NewString(ctx, "text");
|
||||
if (JS_IsFunction(val)) return JS_NewString(ctx, "function");
|
||||
if (JS_IsArray(val)) return JS_NewString(ctx, "array");
|
||||
if (JS_IsRecord(val)) return JS_NewString(ctx, "object");
|
||||
return JS_NewString(ctx, "unknown");
|
||||
}
|
||||
|
||||
/* --- Text comparison stubs (called from QBE type-dispatch branches) --- */
|
||||
|
||||
JSValue cell_rt_lt_text(JSContext *ctx, JSValue a, JSValue b) {
|
||||
const char *sa = JS_ToCString(ctx, a);
|
||||
const char *sb = JS_ToCString(ctx, b);
|
||||
int r = (sa && sb) ? strcmp(sa, sb) < 0 : 0;
|
||||
return JS_NewBool(ctx, r);
|
||||
}
|
||||
|
||||
JSValue cell_rt_gt_text(JSContext *ctx, JSValue a, JSValue b) {
|
||||
const char *sa = JS_ToCString(ctx, a);
|
||||
const char *sb = JS_ToCString(ctx, b);
|
||||
int r = (sa && sb) ? strcmp(sa, sb) > 0 : 0;
|
||||
return JS_NewBool(ctx, r);
|
||||
}
|
||||
|
||||
JSValue cell_rt_le_text(JSContext *ctx, JSValue a, JSValue b) {
|
||||
const char *sa = JS_ToCString(ctx, a);
|
||||
const char *sb = JS_ToCString(ctx, b);
|
||||
int r = (sa && sb) ? strcmp(sa, sb) <= 0 : 0;
|
||||
return JS_NewBool(ctx, r);
|
||||
}
|
||||
|
||||
JSValue cell_rt_ge_text(JSContext *ctx, JSValue a, JSValue b) {
|
||||
const char *sa = JS_ToCString(ctx, a);
|
||||
const char *sb = JS_ToCString(ctx, b);
|
||||
int r = (sa && sb) ? strcmp(sa, sb) >= 0 : 0;
|
||||
return JS_NewBool(ctx, r);
|
||||
}
|
||||
|
||||
static int cell_rt_tol_eq_inner(JSContext *ctx, JSValue a, JSValue b,
|
||||
JSValue tol) {
|
||||
if (JS_IsNumber(a) && JS_IsNumber(b) && JS_IsNumber(tol)) {
|
||||
@@ -1358,31 +1122,11 @@ JSValue cell_rt_ne_tol(JSContext *ctx, JSValue a, JSValue b, JSValue tol) {
|
||||
return JS_NewBool(ctx, !cell_rt_tol_eq_inner(ctx, a, b, tol));
|
||||
}
|
||||
|
||||
/* --- Type check: is_proxy (function with arity 2) --- */
|
||||
|
||||
int cell_rt_is_proxy(JSContext *ctx, JSValue v) {
|
||||
(void)ctx;
|
||||
if (!JS_IsFunction(v)) return 0;
|
||||
JSFunction *fn = JS_VALUE_GET_FUNCTION(v);
|
||||
return fn->length == 2;
|
||||
}
|
||||
|
||||
/* --- Identity check (chases forwarding pointers) --- */
|
||||
|
||||
JSValue cell_rt_is_identical(JSContext *ctx, JSValue a, JSValue b) {
|
||||
if (JS_IsPtr(a)) a = JS_MKPTR(chase(a));
|
||||
if (JS_IsPtr(b)) b = JS_MKPTR(chase(b));
|
||||
return JS_NewBool(ctx, a == b);
|
||||
}
|
||||
|
||||
/* --- Short-circuit and/or (non-allocating) --- */
|
||||
|
||||
JSValue cell_rt_and(JSContext *ctx, JSValue left, JSValue right) {
|
||||
return JS_ToBool(ctx, left) ? right : left;
|
||||
}
|
||||
|
||||
JSValue cell_rt_or(JSContext *ctx, JSValue left, JSValue right) {
|
||||
return JS_ToBool(ctx, left) ? left : right;
|
||||
int cell_rt_is_actor(JSContext *ctx, JSValue v) {
|
||||
int result = 0;
|
||||
if (mist_is_record(v) && !JS_IsNull(ctx->actor_sym))
|
||||
result = JS_HasPropertyKey(ctx, v, ctx->actor_sym) > 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
/* --- Exception checking ---
|
||||
@@ -1431,15 +1175,16 @@ JSValue cell_rt_regexp(JSContext *ctx, const char *pattern, const char *flags) {
|
||||
Creates a temporary JS_FUNC_KIND_NATIVE function so that the full
|
||||
dispatch loop (tail calls, closures, etc.) works for module-level code. */
|
||||
static JSValue native_module_run(JSContext *ctx, void *dl_handle,
|
||||
cell_compiled_fn entry, int nr_slots) {
|
||||
cell_compiled_fn entry, int nr_slots,
|
||||
JSValue env) {
|
||||
NativeRTState *st = native_state(ctx);
|
||||
if (!st) return JS_EXCEPTION;
|
||||
void *prev_handle = st->current_dl_handle;
|
||||
st->current_dl_handle = dl_handle;
|
||||
|
||||
/* Create a native function object for the entry point */
|
||||
/* Create a native function object for the entry point, with env on the function */
|
||||
JSValue func_obj = js_new_native_function(ctx, (void *)entry, dl_handle,
|
||||
(uint16_t)nr_slots, 0, JS_NULL);
|
||||
(uint16_t)nr_slots, 0, JS_NULL, env);
|
||||
if (JS_IsException(func_obj)) {
|
||||
st->current_dl_handle = prev_handle;
|
||||
return JS_EXCEPTION;
|
||||
@@ -1459,14 +1204,11 @@ JSValue cell_rt_native_module_load(JSContext *ctx, void *dl_handle, JSValue env)
|
||||
if (!fn)
|
||||
return JS_RaiseDisrupt(ctx, "cell_main not found in native module dylib");
|
||||
|
||||
/* Make env available for cell_rt_get_intrinsic lookups */
|
||||
cell_rt_set_native_env(ctx, env);
|
||||
|
||||
/* Try to read nr_slots from the module (exported by emitter) */
|
||||
int *slots_ptr = (int *)dlsym(dl_handle, "cell_main_nr_slots");
|
||||
int nr_slots = slots_ptr ? *slots_ptr : 512;
|
||||
|
||||
return native_module_run(ctx, dl_handle, fn, nr_slots);
|
||||
return native_module_run(ctx, dl_handle, fn, nr_slots, env);
|
||||
}
|
||||
|
||||
/* Load a native module from a dylib handle, trying a named symbol first.
|
||||
@@ -1482,11 +1224,11 @@ JSValue cell_rt_native_module_load_named(JSContext *ctx, void *dl_handle, const
|
||||
fn = (cell_compiled_fn)dlsym(dl_handle, "cell_main");
|
||||
used_name = "cell_main";
|
||||
}
|
||||
if (!fn)
|
||||
return JS_RaiseDisrupt(ctx, "symbol not found in native module dylib");
|
||||
|
||||
/* Make env available for cell_rt_get_intrinsic lookups */
|
||||
cell_rt_set_native_env(ctx, env);
|
||||
if (!fn) {
|
||||
return JS_RaiseDisrupt(ctx,
|
||||
"symbol '%s' (and fallback 'cell_main') not found in native module dylib",
|
||||
sym_name ? sym_name : "(null)");
|
||||
}
|
||||
|
||||
/* Try to read nr_slots from the module */
|
||||
char slots_sym[128];
|
||||
@@ -1494,7 +1236,7 @@ JSValue cell_rt_native_module_load_named(JSContext *ctx, void *dl_handle, const
|
||||
int *slots_ptr = (int *)dlsym(dl_handle, slots_sym);
|
||||
int nr_slots = slots_ptr ? *slots_ptr : 512;
|
||||
|
||||
return native_module_run(ctx, dl_handle, fn, nr_slots);
|
||||
return native_module_run(ctx, dl_handle, fn, nr_slots, env);
|
||||
}
|
||||
|
||||
void cell_rt_free_native_state(JSContext *ctx) {
|
||||
@@ -1503,12 +1245,6 @@ void cell_rt_free_native_state(JSContext *ctx) {
|
||||
|
||||
aot_clear_lit_pool(ctx, st);
|
||||
|
||||
if (st->has_native_env && st->native_env_ref_inited) {
|
||||
JS_DeleteGCRef(ctx, &st->native_env_ref);
|
||||
st->native_env_ref_inited = 0;
|
||||
st->native_env_ref.val = JS_NULL;
|
||||
}
|
||||
|
||||
for (int ci = 0; ci < st->gc_ref_chunk_count; ci++) {
|
||||
AOTGCRefChunk *chunk = st->gc_ref_chunks[ci];
|
||||
if (!chunk) continue;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
#include "cell.h"
|
||||
#include "cell_internal.h"
|
||||
#include "quickjs-internal.h"
|
||||
#include "pit_internal.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
JSC_CCALL(os_createactor,
|
||||
cell_rt *rt = JS_GetContextOpaque(js);
|
||||
if (rt->disrupt)
|
||||
if (js->disrupt)
|
||||
return JS_RaiseDisrupt(js, "Can't start a new actor while disrupting.");
|
||||
|
||||
void *startup = value2wota(js, argv[0], JS_NULL, NULL);
|
||||
@@ -57,14 +55,12 @@ JSC_CCALL(os_mailbox_push,
|
||||
)
|
||||
|
||||
JSC_CCALL(os_register_actor,
|
||||
cell_rt *rt = JS_GetContextOpaque(js);
|
||||
const char *id = JS_ToCString(js, argv[0]);
|
||||
double ar;
|
||||
JS_ToFloat64(js, &ar, argv[3]);
|
||||
const char *err = register_actor(id, rt, JS_ToBool(js, argv[2]), ar);
|
||||
const char *err = register_actor(id, js, JS_ToBool(js, argv[2]), ar);
|
||||
if (err) return JS_RaiseDisrupt(js, "Could not register actor: %s", err);
|
||||
rt->message_handle_ref.val = argv[1];
|
||||
rt->context = js;
|
||||
js->message_handle_ref.val = argv[1];
|
||||
JS_FreeCString(js, id);
|
||||
)
|
||||
|
||||
@@ -77,56 +73,50 @@ JSC_CCALL(os_mailbox_exist,
|
||||
)
|
||||
|
||||
JSC_CCALL(os_unneeded,
|
||||
cell_rt *actor = JS_GetContextOpaque(js);
|
||||
double secs;
|
||||
JS_ToFloat64(js, &secs, argv[1]);
|
||||
actor_unneeded(actor, argv[0], secs);
|
||||
actor_unneeded(js, argv[0], secs);
|
||||
)
|
||||
|
||||
JSC_CCALL(actor_disrupt,
|
||||
cell_rt *crt = JS_GetContextOpaque(js);
|
||||
actor_disrupt(crt);
|
||||
actor_disrupt(js);
|
||||
)
|
||||
|
||||
JSC_SCALL(actor_setname,
|
||||
cell_rt *rt = JS_GetContextOpaque(js);
|
||||
rt->name = strdup(str);
|
||||
js->name = strdup(str);
|
||||
js->actor_label = js->name;
|
||||
)
|
||||
|
||||
JSC_CCALL(actor_on_exception,
|
||||
cell_rt *rt = JS_GetContextOpaque(js);
|
||||
rt->on_exception_ref.val = argv[0];
|
||||
js->on_exception_ref.val = argv[0];
|
||||
)
|
||||
|
||||
JSC_CCALL(actor_clock,
|
||||
if (!JS_IsFunction(argv[0]))
|
||||
return JS_RaiseDisrupt(js, "Argument must be a function.");
|
||||
|
||||
cell_rt *actor = JS_GetContextOpaque(js);
|
||||
actor_clock(actor, argv[0]);
|
||||
|
||||
actor_clock(js, argv[0]);
|
||||
)
|
||||
|
||||
JSC_CCALL(actor_delay,
|
||||
if (!JS_IsFunction(argv[0]))
|
||||
return JS_RaiseDisrupt(js, "Argument must be a function.");
|
||||
|
||||
cell_rt *actor = JS_GetContextOpaque(js);
|
||||
double seconds;
|
||||
JS_ToFloat64(js, &seconds, argv[1]);
|
||||
if (seconds <= 0) {
|
||||
actor_clock(actor, argv[0]);
|
||||
actor_clock(js, argv[0]);
|
||||
return JS_NULL;
|
||||
}
|
||||
|
||||
uint32_t id = actor_delay(actor, argv[0], seconds);
|
||||
uint32_t id = actor_delay(js, argv[0], seconds);
|
||||
return JS_NewUint32(js, id);
|
||||
)
|
||||
|
||||
JSC_CCALL(actor_removetimer,
|
||||
cell_rt *actor = JS_GetContextOpaque(js);
|
||||
uint32_t timer_id;
|
||||
JS_ToUint32(js, &timer_id, argv[0]);
|
||||
(void)actor_remove_timer(actor, timer_id);
|
||||
(void)actor_remove_timer(js, timer_id);
|
||||
)
|
||||
|
||||
/* Log callback bridge: called from JS_Log, calls ƿit log(channel, [msg, stack])
|
||||
@@ -143,7 +133,8 @@ static void js_log_callback(JSContext *ctx, const char *channel, const char *msg
|
||||
JS_FRAME(ctx);
|
||||
JS_ROOT(stack, JS_GetStack(ctx));
|
||||
JS_ROOT(args_array, JS_NewArray(ctx));
|
||||
JS_SetPropertyNumber(ctx, args_array.val, 0, JS_NewString(ctx, msg));
|
||||
JSValue msg_str = JS_NewString(ctx, msg);
|
||||
JS_SetPropertyNumber(ctx, args_array.val, 0, msg_str);
|
||||
JS_SetPropertyNumber(ctx, args_array.val, 1, stack.val);
|
||||
JSValue argv[2];
|
||||
argv[0] = JS_NewString(ctx, channel);
|
||||
|
||||
@@ -1,265 +0,0 @@
|
||||
/*
|
||||
* QuickJS atom definitions
|
||||
*
|
||||
* Copyright (c) 2017-2018 Fabrice Bellard
|
||||
* Copyright (c) 2017-2018 Charlie Gordon
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifdef DEF
|
||||
|
||||
/* Note: first atoms are considered as keywords in the parser */
|
||||
DEF(null, "null") /* must be first */
|
||||
DEF(false, "false")
|
||||
DEF(true, "true")
|
||||
DEF(if, "if")
|
||||
DEF(else, "else")
|
||||
DEF(return, "return")
|
||||
DEF(go, "go")
|
||||
DEF(var, "var")
|
||||
DEF(def, "def")
|
||||
DEF(this, "this")
|
||||
DEF(delete, "delete")
|
||||
DEF(void, "void")
|
||||
DEF(new, "new")
|
||||
DEF(in, "in")
|
||||
DEF(do, "do")
|
||||
DEF(while, "while")
|
||||
DEF(for, "for")
|
||||
DEF(break, "break")
|
||||
DEF(continue, "continue")
|
||||
DEF(switch, "switch")
|
||||
DEF(case, "case")
|
||||
DEF(default, "default")
|
||||
DEF(throw, "throw")
|
||||
DEF(try, "try")
|
||||
DEF(catch, "catch")
|
||||
DEF(finally, "finally")
|
||||
DEF(function, "function")
|
||||
DEF(debugger, "debugger")
|
||||
DEF(with, "with")
|
||||
/* FutureReservedWord */
|
||||
DEF(class, "class")
|
||||
DEF(const, "const")
|
||||
DEF(enum, "enum")
|
||||
DEF(export, "export")
|
||||
DEF(extends, "extends")
|
||||
DEF(import, "import")
|
||||
DEF(super, "super")
|
||||
/* FutureReservedWords when parsing strict mode code */
|
||||
DEF(implements, "implements")
|
||||
DEF(interface, "interface")
|
||||
DEF(let, "let")
|
||||
DEF(private, "private")
|
||||
DEF(protected, "protected")
|
||||
DEF(public, "public")
|
||||
DEF(static, "static")
|
||||
DEF(yield, "yield")
|
||||
DEF(await, "await")
|
||||
|
||||
/* empty string */
|
||||
DEF(empty_string, "")
|
||||
/* identifiers */
|
||||
DEF(length, "length")
|
||||
DEF(fileName, "fileName")
|
||||
DEF(lineNumber, "lineNumber")
|
||||
DEF(columnNumber, "columnNumber")
|
||||
DEF(message, "message")
|
||||
DEF(cause, "cause")
|
||||
DEF(errors, "errors")
|
||||
DEF(stack, "stack")
|
||||
DEF(name, "name")
|
||||
DEF(toString, "toString")
|
||||
DEF(toLocaleString, "toLocaleString")
|
||||
DEF(valueOf, "valueOf")
|
||||
DEF(eval, "eval")
|
||||
DEF(prototype, "prototype")
|
||||
DEF(constructor, "constructor")
|
||||
DEF(configurable, "configurable")
|
||||
DEF(writable, "writable")
|
||||
DEF(enumerable, "enumerable")
|
||||
DEF(value, "value")
|
||||
DEF(get, "get")
|
||||
DEF(set, "set")
|
||||
DEF(of, "of")
|
||||
DEF(__proto__, "__proto__")
|
||||
DEF(undefined, "undefined")
|
||||
DEF(number, "number")
|
||||
DEF(boolean, "boolean")
|
||||
DEF(string, "string")
|
||||
DEF(object, "object")
|
||||
DEF(symbol, "symbol")
|
||||
DEF(integer, "integer")
|
||||
DEF(unknown, "unknown")
|
||||
DEF(callee, "callee")
|
||||
DEF(caller, "caller")
|
||||
DEF(_eval_, "<eval>")
|
||||
DEF(_ret_, "<ret>")
|
||||
DEF(_var_, "<var>")
|
||||
DEF(_arg_var_, "<arg_var>")
|
||||
DEF(_with_, "<with>")
|
||||
DEF(lastIndex, "lastIndex")
|
||||
DEF(target, "target")
|
||||
DEF(index, "index")
|
||||
DEF(input, "input")
|
||||
DEF(defineProperties, "defineProperties")
|
||||
DEF(apply, "apply")
|
||||
DEF(join, "join")
|
||||
DEF(concat, "concat")
|
||||
DEF(split, "split")
|
||||
DEF(construct, "construct")
|
||||
DEF(getPrototypeOf, "getPrototypeOf")
|
||||
DEF(setPrototypeOf, "setPrototypeOf")
|
||||
DEF(isExtensible, "isExtensible")
|
||||
DEF(preventExtensions, "preventExtensions")
|
||||
DEF(has, "has")
|
||||
DEF(deleteProperty, "deleteProperty")
|
||||
DEF(defineProperty, "defineProperty")
|
||||
DEF(getOwnPropertyDescriptor, "getOwnPropertyDescriptor")
|
||||
DEF(ownKeys, "ownKeys")
|
||||
DEF(add, "add")
|
||||
DEF(done, "done")
|
||||
DEF(next, "next")
|
||||
DEF(values, "values")
|
||||
DEF(source, "source")
|
||||
DEF(flags, "flags")
|
||||
DEF(global, "global")
|
||||
DEF(unicode, "unicode")
|
||||
DEF(raw, "raw")
|
||||
DEF(new_target, "new.target")
|
||||
DEF(this_active_func, "this.active_func")
|
||||
DEF(home_object, "<home_object>")
|
||||
DEF(computed_field, "<computed_field>")
|
||||
DEF(static_computed_field, "<static_computed_field>") /* must come after computed_fields */
|
||||
DEF(class_fields_init, "<class_fields_init>")
|
||||
DEF(brand, "<brand>")
|
||||
DEF(hash_constructor, "#constructor")
|
||||
DEF(as, "as")
|
||||
DEF(from, "from")
|
||||
DEF(meta, "meta")
|
||||
DEF(_default_, "*default*")
|
||||
DEF(_star_, "*")
|
||||
DEF(Module, "Module")
|
||||
DEF(then, "then")
|
||||
DEF(resolve, "resolve")
|
||||
DEF(reject, "reject")
|
||||
DEF(promise, "promise")
|
||||
DEF(proxy, "proxy")
|
||||
DEF(revoke, "revoke")
|
||||
DEF(async, "async")
|
||||
DEF(exec, "exec")
|
||||
DEF(groups, "groups")
|
||||
DEF(indices, "indices")
|
||||
DEF(status, "status")
|
||||
DEF(reason, "reason")
|
||||
DEF(globalThis, "globalThis")
|
||||
DEF(bigint, "bigint")
|
||||
DEF(minus_zero, "-0")
|
||||
DEF(Infinity, "Infinity")
|
||||
DEF(minus_Infinity, "-Infinity")
|
||||
DEF(NaN, "NaN")
|
||||
DEF(hasIndices, "hasIndices")
|
||||
DEF(ignoreCase, "ignoreCase")
|
||||
DEF(multiline, "multiline")
|
||||
DEF(dotAll, "dotAll")
|
||||
DEF(sticky, "sticky")
|
||||
DEF(unicodeSets, "unicodeSets")
|
||||
/* the following 3 atoms are only used with CONFIG_ATOMICS */
|
||||
DEF(not_equal, "not-equal")
|
||||
DEF(timed_out, "timed-out")
|
||||
DEF(ok, "ok")
|
||||
/* */
|
||||
DEF(toJSON, "toJSON")
|
||||
/* class names */
|
||||
DEF(Object, "Object")
|
||||
DEF(Array, "Array")
|
||||
DEF(Error, "Error")
|
||||
DEF(Number, "Number")
|
||||
DEF(String, "String")
|
||||
DEF(Boolean, "Boolean")
|
||||
DEF(Symbol, "Symbol")
|
||||
DEF(Math, "Math")
|
||||
DEF(JSON, "JSON")
|
||||
DEF(Date, "Date")
|
||||
DEF(Function, "Function")
|
||||
DEF(GeneratorFunction, "GeneratorFunction")
|
||||
DEF(ForInIterator, "ForInIterator")
|
||||
DEF(RegExp, "RegExp")
|
||||
DEF(ArrayBuffer, "ArrayBuffer")
|
||||
DEF(SharedArrayBuffer, "SharedArrayBuffer")
|
||||
/* must keep same order as class IDs for typed arrays */
|
||||
DEF(Uint8ClampedArray, "Uint8ClampedArray")
|
||||
DEF(Int8Array, "Int8Array")
|
||||
DEF(Uint8Array, "Uint8Array")
|
||||
DEF(Int16Array, "Int16Array")
|
||||
DEF(Uint16Array, "Uint16Array")
|
||||
DEF(Int32Array, "Int32Array")
|
||||
DEF(Uint32Array, "Uint32Array")
|
||||
DEF(BigInt64Array, "BigInt64Array")
|
||||
DEF(BigUint64Array, "BigUint64Array")
|
||||
DEF(Float16Array, "Float16Array")
|
||||
DEF(Float32Array, "Float32Array")
|
||||
DEF(Float64Array, "Float64Array")
|
||||
DEF(DataView, "DataView")
|
||||
DEF(BigInt, "BigInt")
|
||||
DEF(WeakRef, "WeakRef")
|
||||
DEF(FinalizationRegistry, "FinalizationRegistry")
|
||||
DEF(Map, "Map")
|
||||
DEF(Set, "Set") /* Map + 1 */
|
||||
DEF(WeakMap, "WeakMap") /* Map + 2 */
|
||||
DEF(WeakSet, "WeakSet") /* Map + 3 */
|
||||
DEF(Map_Iterator, "Map Iterator")
|
||||
DEF(Set_Iterator, "Set Iterator")
|
||||
DEF(Array_Iterator, "Array Iterator")
|
||||
DEF(String_Iterator, "String Iterator")
|
||||
DEF(RegExp_String_Iterator, "RegExp String Iterator")
|
||||
DEF(Generator, "Generator")
|
||||
DEF(Proxy, "Proxy")
|
||||
DEF(Promise, "Promise")
|
||||
DEF(PromiseResolveFunction, "PromiseResolveFunction")
|
||||
DEF(PromiseRejectFunction, "PromiseRejectFunction")
|
||||
DEF(AsyncFunction, "AsyncFunction")
|
||||
DEF(AsyncFunctionResolve, "AsyncFunctionResolve")
|
||||
DEF(AsyncFunctionReject, "AsyncFunctionReject")
|
||||
DEF(AsyncGeneratorFunction, "AsyncGeneratorFunction")
|
||||
DEF(AsyncGenerator, "AsyncGenerator")
|
||||
DEF(EvalError, "EvalError")
|
||||
DEF(RangeError, "RangeError")
|
||||
DEF(ReferenceError, "ReferenceError")
|
||||
DEF(SyntaxError, "SyntaxError")
|
||||
DEF(TypeError, "TypeError")
|
||||
DEF(URIError, "URIError")
|
||||
DEF(InternalError, "InternalError")
|
||||
/* symbols */
|
||||
DEF(Symbol_toPrimitive, "Symbol.toPrimitive")
|
||||
DEF(Symbol_iterator, "Symbol.iterator")
|
||||
DEF(Symbol_match, "Symbol.match")
|
||||
DEF(Symbol_matchAll, "Symbol.matchAll")
|
||||
DEF(Symbol_replace, "Symbol.replace")
|
||||
DEF(Symbol_search, "Symbol.search")
|
||||
DEF(Symbol_split, "Symbol.split")
|
||||
DEF(Symbol_toStringTag, "Symbol.toStringTag")
|
||||
DEF(Symbol_isConcatSpreadable, "Symbol.isConcatSpreadable")
|
||||
DEF(Symbol_hasInstance, "Symbol.hasInstance")
|
||||
DEF(Symbol_species, "Symbol.species")
|
||||
DEF(Symbol_unscopables, "Symbol.unscopables")
|
||||
DEF(Symbol_asyncIterator, "Symbol.asyncIterator")
|
||||
|
||||
#endif /* DEF */
|
||||
1075
source/quickjs.h
1075
source/quickjs.h
File diff suppressed because it is too large
Load Diff
353
source/runtime.c
353
source/runtime.c
@@ -1,30 +1,5 @@
|
||||
/*
|
||||
* QuickJS Javascript Engine
|
||||
*
|
||||
* Copyright (c) 2017-2025 Fabrice Bellard
|
||||
* Copyright (c) 2017-2025 Charlie Gordon
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#define BLOB_IMPLEMENTATION
|
||||
#include "quickjs-internal.h"
|
||||
#include "pit_internal.h"
|
||||
#include <unistd.h>
|
||||
|
||||
// #define DUMP_BUDDY
|
||||
@@ -87,7 +62,7 @@ static inline JS_BOOL JS_IsInteger (JSValue v) {
|
||||
JSClassID js_class_id_alloc = JS_CLASS_INIT_COUNT;
|
||||
|
||||
/* === Public API wrappers (non-inline, for quickjs.h declarations) ===
|
||||
These delegate to the static inline versions in quickjs-internal.h. */
|
||||
These delegate to the static inline versions in pit_internal.h. */
|
||||
|
||||
JS_BOOL JS_IsStone(JSValue v) { return mist_is_stone(v); }
|
||||
JS_BOOL JS_IsArray(JSValue v) { return mist_is_array(v); }
|
||||
@@ -1475,6 +1450,7 @@ void gc_scan_object (JSContext *ctx, void *ptr, uint8_t *from_base, uint8_t *fro
|
||||
fn->u.cell.env_record = gc_copy_value (ctx, fn->u.cell.env_record, from_base, from_end, to_base, to_free, to_end);
|
||||
} else if (fn->kind == JS_FUNC_KIND_NATIVE) {
|
||||
fn->u.cell.outer_frame = gc_copy_value (ctx, fn->u.cell.outer_frame, from_base, from_end, to_base, to_free, to_end);
|
||||
fn->u.cell.env_record = gc_copy_value (ctx, fn->u.cell.env_record, from_base, from_end, to_base, to_free, to_end);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1935,13 +1911,15 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) {
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Fire GC hook if registered */
|
||||
if (ctx->trace_hook && (ctx->trace_type & JS_HOOK_GC))
|
||||
ctx->trace_hook(ctx, JS_HOOK_GC, NULL, ctx->trace_data);
|
||||
|
||||
/* Check memory limit — kill actor if heap exceeds cap */
|
||||
if (ctx->heap_memory_limit > 0 && ctx->current_block_size > ctx->heap_memory_limit) {
|
||||
#ifdef ACTOR_TRACE
|
||||
void *crt = ctx->user_opaque;
|
||||
if (crt)
|
||||
fprintf(stderr, "[ACTOR_TRACE] heap %zu > limit %zu, OOM\n",
|
||||
ctx->current_block_size, ctx->heap_memory_limit);
|
||||
fprintf(stderr, "[ACTOR_TRACE] heap %zu > limit %zu, OOM\n",
|
||||
ctx->current_block_size, ctx->heap_memory_limit);
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
@@ -2066,6 +2044,7 @@ JSContext *JS_NewContextRawWithHeapSize (JSRuntime *rt, size_t heap_size) {
|
||||
ctx->suspended_pc = 0;
|
||||
ctx->vm_call_depth = 0;
|
||||
ctx->heap_memory_limit = 0;
|
||||
ctx->actor_label = NULL;
|
||||
JS_AddGCRef(ctx, &ctx->suspended_frame_ref);
|
||||
ctx->suspended_frame_ref.val = JS_NULL;
|
||||
|
||||
@@ -2075,6 +2054,18 @@ JSContext *JS_NewContextRawWithHeapSize (JSRuntime *rt, size_t heap_size) {
|
||||
ctx->log_callback_js = JS_NULL;
|
||||
ctx->log_callback = NULL;
|
||||
|
||||
/* Register actor GCRef fields so the Cheney GC can relocate them. */
|
||||
JS_AddGCRef(ctx, &ctx->idx_buffer_ref);
|
||||
JS_AddGCRef(ctx, &ctx->on_exception_ref);
|
||||
JS_AddGCRef(ctx, &ctx->message_handle_ref);
|
||||
JS_AddGCRef(ctx, &ctx->unneeded_ref);
|
||||
JS_AddGCRef(ctx, &ctx->actor_sym_ref);
|
||||
ctx->idx_buffer_ref.val = JS_NULL;
|
||||
ctx->on_exception_ref.val = JS_NULL;
|
||||
ctx->message_handle_ref.val = JS_NULL;
|
||||
ctx->unneeded_ref.val = JS_NULL;
|
||||
ctx->actor_sym_ref.val = JS_NULL;
|
||||
|
||||
/* Initialize constant text pool (avoids overflow pages for common case) */
|
||||
{
|
||||
size_t ct_pool_size = 64 * 1024; /* 64KB initial CT pool */
|
||||
@@ -2151,11 +2142,7 @@ JSContext *JS_NewContextWithHeapSize (JSRuntime *rt, size_t heap_size) {
|
||||
return ctx;
|
||||
}
|
||||
|
||||
void *JS_GetContextOpaque (JSContext *ctx) { return ctx->user_opaque; }
|
||||
|
||||
void JS_SetContextOpaque (JSContext *ctx, void *opaque) {
|
||||
ctx->user_opaque = opaque;
|
||||
}
|
||||
|
||||
void JS_SetGCScanExternal(JSContext *ctx, JS_GCScanFn fn) {
|
||||
ctx->gc_scan_external = fn;
|
||||
@@ -2185,6 +2172,11 @@ void JS_FreeContext (JSContext *ctx) {
|
||||
|
||||
cell_rt_free_native_state(ctx);
|
||||
JS_DeleteGCRef(ctx, &ctx->suspended_frame_ref);
|
||||
JS_DeleteGCRef(ctx, &ctx->idx_buffer_ref);
|
||||
JS_DeleteGCRef(ctx, &ctx->on_exception_ref);
|
||||
JS_DeleteGCRef(ctx, &ctx->message_handle_ref);
|
||||
JS_DeleteGCRef(ctx, &ctx->unneeded_ref);
|
||||
JS_DeleteGCRef(ctx, &ctx->actor_sym_ref);
|
||||
|
||||
for (i = 0; i < ctx->class_count; i++) {
|
||||
}
|
||||
@@ -3285,14 +3277,28 @@ JS_RaiseDisrupt (JSContext *ctx, const char *fmt, ...) {
|
||||
va_start (ap, fmt);
|
||||
vsnprintf (buf, sizeof (buf), fmt, ap);
|
||||
va_end (ap);
|
||||
JS_Log (ctx, "error", "%s", buf);
|
||||
if (ctx->log_callback)
|
||||
JS_Log (ctx, "error", "%s", buf);
|
||||
ctx->current_exception = JS_TRUE;
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
|
||||
/* Log to "memory" channel + disrupt. Skips JS callback (can't allocate). */
|
||||
JSValue JS_RaiseOOM (JSContext *ctx) {
|
||||
fprintf (stderr, "out of memory\n");
|
||||
size_t used = (size_t)((uint8_t *)ctx->heap_free - (uint8_t *)ctx->heap_base);
|
||||
size_t block = ctx->current_block_size;
|
||||
size_t limit = ctx->heap_memory_limit;
|
||||
const char *label = ctx->actor_label;
|
||||
if (limit > 0) {
|
||||
fprintf(stderr, "out of memory: heap %zuKB / %zuKB block, limit %zuMB",
|
||||
used / 1024, block / 1024, limit / (1024 * 1024));
|
||||
} else {
|
||||
fprintf(stderr, "out of memory: heap %zuKB / %zuKB block, no limit",
|
||||
used / 1024, block / 1024);
|
||||
}
|
||||
if (label)
|
||||
fprintf(stderr, " [%s]", label);
|
||||
fprintf(stderr, "\n");
|
||||
ctx->current_exception = JS_TRUE;
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
@@ -8370,7 +8376,15 @@ static JSValue js_cell_array (JSContext *ctx, JSValue this_val, int argc, JSValu
|
||||
/* Map - GC-safe: root result throughout, use rooted refs for func and array */
|
||||
int arity = ((JSFunction *)JS_VALUE_GET_FUNCTION (arg1_ref.val))->length;
|
||||
|
||||
int reverse = argc > 2 && JS_ToBool (ctx, argv[2]);
|
||||
int reverse = 0;
|
||||
if (argc > 2 && !JS_IsNull (argv[2])) {
|
||||
if (!JS_IsBool (argv[2])) {
|
||||
JS_PopGCRef (ctx, &arg1_ref);
|
||||
JS_PopGCRef (ctx, &arg0_ref);
|
||||
return JS_RaiseDisrupt (ctx, "array: reverse must be a logical");
|
||||
}
|
||||
reverse = JS_VALUE_GET_BOOL (argv[2]);
|
||||
}
|
||||
JSValue exit_val = argc > 3 ? argv[3] : JS_NULL;
|
||||
|
||||
JSGCRef result_ref;
|
||||
@@ -8500,7 +8514,7 @@ static JSValue js_cell_array (JSContext *ctx, JSValue this_val, int argc, JSValu
|
||||
if (!JS_IsInteger (argv[1])) {
|
||||
JS_PopGCRef (ctx, &arg1_ref);
|
||||
JS_PopGCRef (ctx, &arg0_ref);
|
||||
return JS_NULL;
|
||||
return JS_RaiseDisrupt (ctx, "array slice: from must be an integer");
|
||||
}
|
||||
int from = JS_VALUE_GET_INT (argv[1]);
|
||||
int to;
|
||||
@@ -8508,7 +8522,7 @@ static JSValue js_cell_array (JSContext *ctx, JSValue this_val, int argc, JSValu
|
||||
if (!JS_IsNumber (argv[2]) || !JS_IsInteger (argv[2])) {
|
||||
JS_PopGCRef (ctx, &arg1_ref);
|
||||
JS_PopGCRef (ctx, &arg0_ref);
|
||||
return JS_NULL;
|
||||
return JS_RaiseDisrupt (ctx, "array slice: to must be an integer");
|
||||
}
|
||||
to = JS_VALUE_GET_INT (argv[2]);
|
||||
} else {
|
||||
@@ -8545,11 +8559,13 @@ static JSValue js_cell_array (JSContext *ctx, JSValue this_val, int argc, JSValu
|
||||
|
||||
JS_PopGCRef (ctx, &arg1_ref);
|
||||
JS_PopGCRef (ctx, &arg0_ref);
|
||||
return JS_NULL;
|
||||
return JS_RaiseDisrupt (ctx, "array: invalid argument combination");
|
||||
}
|
||||
|
||||
/* array(object) - keys */
|
||||
if (JS_IsRecord (arg)) {
|
||||
if (argc > 1 && JS_IsFunction (argv[1]))
|
||||
return JS_RaiseDisrupt (ctx, "array(record, fn) is not valid — use array(array(record), fn) to map over keys");
|
||||
/* Return object keys */
|
||||
return JS_GetOwnPropertyNames (ctx, arg);
|
||||
}
|
||||
@@ -8815,7 +8831,12 @@ static JSValue js_cell_array_reduce (JSContext *ctx, JSValue this_val, int argc,
|
||||
word_t len = arr->len;
|
||||
JSValue fn = argv[1];
|
||||
|
||||
int reverse = argc > 3 && JS_ToBool (ctx, argv[3]);
|
||||
int reverse = 0;
|
||||
if (argc > 3 && !JS_IsNull (argv[3])) {
|
||||
if (!JS_IsBool (argv[3]))
|
||||
return JS_RaiseDisrupt (ctx, "reduce: reverse must be a logical");
|
||||
reverse = JS_VALUE_GET_BOOL (argv[3]);
|
||||
}
|
||||
JSGCRef acc_ref;
|
||||
JSValue acc;
|
||||
|
||||
@@ -8891,7 +8912,12 @@ static JSValue js_cell_array_for (JSContext *ctx, JSValue this_val, int argc, JS
|
||||
word_t len = arr->len;
|
||||
if (len == 0) return JS_NULL;
|
||||
|
||||
int reverse = argc > 2 && JS_ToBool (ctx, argv[2]);
|
||||
int reverse = 0;
|
||||
if (argc > 2 && !JS_IsNull (argv[2])) {
|
||||
if (!JS_IsBool (argv[2]))
|
||||
return JS_RaiseDisrupt (ctx, "arrfor: reverse must be a logical");
|
||||
reverse = JS_VALUE_GET_BOOL (argv[2]);
|
||||
}
|
||||
JSValue exit_val = argc > 3 ? argv[3] : JS_NULL;
|
||||
|
||||
if (reverse) {
|
||||
@@ -8931,7 +8957,12 @@ static JSValue js_cell_array_find (JSContext *ctx, JSValue this_val, int argc, J
|
||||
JSArray *arr = JS_VALUE_GET_ARRAY (argv[0]);
|
||||
word_t len = arr->len;
|
||||
|
||||
int reverse = argc > 2 && JS_ToBool (ctx, argv[2]);
|
||||
int reverse = 0;
|
||||
if (argc > 2 && !JS_IsNull (argv[2])) {
|
||||
if (!JS_IsBool (argv[2]))
|
||||
return JS_RaiseDisrupt (ctx, "find: reverse must be a logical");
|
||||
reverse = JS_VALUE_GET_BOOL (argv[2]);
|
||||
}
|
||||
int32_t from;
|
||||
if (argc > 3 && !JS_IsNull (argv[3])) {
|
||||
if (JS_ToInt32 (ctx, &from, argv[3])) return JS_NULL;
|
||||
@@ -9409,7 +9440,7 @@ static JSValue js_cell_object (JSContext *ctx, JSValue this_val, int argc, JSVal
|
||||
/* Use text directly as key */
|
||||
JSValue prop_key = js_key_from_string (ctx, key);
|
||||
JSValue val;
|
||||
if (argc < 2 || JS_IsNull (func_ref.val)) {
|
||||
if (argc < 2) {
|
||||
val = JS_TRUE;
|
||||
} else if (is_func) {
|
||||
JSValue arg_key = key;
|
||||
@@ -9446,15 +9477,23 @@ static JSValue js_cell_fn_apply (JSContext *ctx, JSValue this_val, int argc, JSV
|
||||
if (argc < 1) return JS_NULL;
|
||||
if (!JS_IsFunction (argv[0])) return argv[0];
|
||||
|
||||
JSFunction *fn = JS_VALUE_GET_FUNCTION (argv[0]);
|
||||
|
||||
if (argc < 2)
|
||||
return JS_CallInternal (ctx, argv[0], JS_NULL, 0, NULL, 0);
|
||||
|
||||
if (!JS_IsArray (argv[1]))
|
||||
if (!JS_IsArray (argv[1])) {
|
||||
if (fn->length >= 0 && 1 > fn->length)
|
||||
return JS_RaiseDisrupt (ctx, "too many arguments for apply: expected %d, got 1", fn->length);
|
||||
return JS_CallInternal (ctx, argv[0], JS_NULL, 1, &argv[1], 0);
|
||||
}
|
||||
|
||||
JSArray *arr = JS_VALUE_GET_ARRAY (argv[1]);
|
||||
int len = arr->len;
|
||||
|
||||
if (fn->length >= 0 && len > fn->length)
|
||||
return JS_RaiseDisrupt (ctx, "too many arguments for apply: expected %d, got %d", fn->length, len);
|
||||
|
||||
if (len == 0)
|
||||
return JS_CallInternal (ctx, argv[0], JS_NULL, 0, NULL, 0);
|
||||
|
||||
@@ -11321,11 +11360,9 @@ static JSValue js_cell_is_blob (JSContext *ctx, JSValue this_val, int argc, JSVa
|
||||
static JSValue js_cell_is_data (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
|
||||
if (argc < 1) return JS_FALSE;
|
||||
JSValue val = argv[0];
|
||||
if (!mist_is_gc_object (val)) return JS_FALSE;
|
||||
if (JS_IsArray (val)) return JS_FALSE;
|
||||
/* data is text, number, logical, array, blob, or record — not function, null */
|
||||
if (JS_IsNull (val)) return JS_FALSE;
|
||||
if (JS_IsFunction (val)) return JS_FALSE;
|
||||
if (mist_is_blob (val)) return JS_FALSE;
|
||||
/* Check if it's a plain object (prototype is Object.prototype or null) */
|
||||
return JS_TRUE;
|
||||
}
|
||||
|
||||
@@ -11405,6 +11442,84 @@ static JSValue js_cell_is_letter (JSContext *ctx, JSValue this_val, int argc, JS
|
||||
return JS_NewBool (ctx, (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'));
|
||||
}
|
||||
|
||||
/* is_true(val) - check if value is exactly true */
|
||||
static JSValue js_cell_is_true (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
|
||||
if (argc < 1) return JS_FALSE;
|
||||
return JS_NewBool (ctx, argv[0] == JS_TRUE);
|
||||
}
|
||||
|
||||
/* is_false(val) - check if value is exactly false */
|
||||
static JSValue js_cell_is_false (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
|
||||
if (argc < 1) return JS_FALSE;
|
||||
return JS_NewBool (ctx, argv[0] == JS_FALSE);
|
||||
}
|
||||
|
||||
/* is_fit(val) - check if value is a safe integer (int32 or float with integer value <= 2^53) */
|
||||
static JSValue js_cell_is_fit (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
|
||||
if (argc < 1) return JS_FALSE;
|
||||
JSValue val = argv[0];
|
||||
if (JS_IsInt (val)) return JS_TRUE;
|
||||
if (JS_IsShortFloat (val)) {
|
||||
double d = JS_VALUE_GET_FLOAT64 (val);
|
||||
return JS_NewBool (ctx, isfinite (d) && trunc (d) == d && fabs (d) <= 9007199254740992.0);
|
||||
}
|
||||
return JS_FALSE;
|
||||
}
|
||||
|
||||
/* is_character(val) - check if value is a single character text */
|
||||
static JSValue js_cell_is_character (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
|
||||
if (argc < 1) return JS_FALSE;
|
||||
JSValue val = argv[0];
|
||||
if (!JS_IsText (val)) return JS_FALSE;
|
||||
return JS_NewBool (ctx, js_string_value_len (val) == 1);
|
||||
}
|
||||
|
||||
/* is_digit(val) - check if value is a single digit character */
|
||||
static JSValue js_cell_is_digit (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
|
||||
if (argc < 1) return JS_FALSE;
|
||||
JSValue val = argv[0];
|
||||
if (!JS_IsText (val)) return JS_FALSE;
|
||||
if (js_string_value_len (val) != 1) return JS_FALSE;
|
||||
uint32_t c = js_string_value_get (val, 0);
|
||||
return JS_NewBool (ctx, c >= '0' && c <= '9');
|
||||
}
|
||||
|
||||
/* is_lower(val) - check if value is a single lowercase letter */
|
||||
static JSValue js_cell_is_lower (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
|
||||
if (argc < 1) return JS_FALSE;
|
||||
JSValue val = argv[0];
|
||||
if (!JS_IsText (val)) return JS_FALSE;
|
||||
if (js_string_value_len (val) != 1) return JS_FALSE;
|
||||
uint32_t c = js_string_value_get (val, 0);
|
||||
return JS_NewBool (ctx, c >= 'a' && c <= 'z');
|
||||
}
|
||||
|
||||
/* is_upper(val) - check if value is a single uppercase letter */
|
||||
static JSValue js_cell_is_upper (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
|
||||
if (argc < 1) return JS_FALSE;
|
||||
JSValue val = argv[0];
|
||||
if (!JS_IsText (val)) return JS_FALSE;
|
||||
if (js_string_value_len (val) != 1) return JS_FALSE;
|
||||
uint32_t c = js_string_value_get (val, 0);
|
||||
return JS_NewBool (ctx, c >= 'A' && c <= 'Z');
|
||||
}
|
||||
|
||||
/* is_whitespace(val) - check if all characters are whitespace (non-empty) */
|
||||
static JSValue js_cell_is_whitespace (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
|
||||
if (argc < 1) return JS_FALSE;
|
||||
JSValue val = argv[0];
|
||||
if (!JS_IsText (val)) return JS_FALSE;
|
||||
int len = js_string_value_len (val);
|
||||
if (len == 0) return JS_FALSE;
|
||||
for (int i = 0; i < len; i++) {
|
||||
uint32_t c = js_string_value_get (val, i);
|
||||
if (!(c == ' ' || c == '\t' || c == '\n'
|
||||
|| c == '\r' || c == '\f' || c == '\v'))
|
||||
return JS_FALSE;
|
||||
}
|
||||
return JS_TRUE;
|
||||
}
|
||||
|
||||
/* is_proto(val, master) - check if val has master in prototype chain */
|
||||
static JSValue js_cell_is_proto (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
|
||||
if (argc < 2) return JS_FALSE;
|
||||
@@ -11462,6 +11577,47 @@ static JSValue js_cell_logical(JSContext *ctx, JSValue this_val, int argc, JSVal
|
||||
return JS_NULL;
|
||||
}
|
||||
|
||||
/* realloc wrapper for unicode_normalize */
|
||||
static void *normalize_realloc(void *opaque, void *ptr, size_t size) {
|
||||
(void)opaque;
|
||||
if (size == 0) {
|
||||
pjs_free(ptr);
|
||||
return NULL;
|
||||
}
|
||||
return pjs_realloc(ptr, size);
|
||||
}
|
||||
|
||||
/* normalize(text) — NFC normalization */
|
||||
static JSValue js_cell_normalize(JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
|
||||
if (argc < 1) return JS_NULL;
|
||||
JSValue val = argv[0];
|
||||
if (!JS_IsText(val)) return JS_NULL;
|
||||
int len = js_string_value_len(val);
|
||||
if (len == 0) return JS_NewString(ctx, "");
|
||||
uint32_t *src = pjs_malloc(len * sizeof(uint32_t));
|
||||
if (!src) return JS_EXCEPTION;
|
||||
for (int i = 0; i < len; i++)
|
||||
src[i] = js_string_value_get(val, i);
|
||||
uint32_t *dst = NULL;
|
||||
int dst_len = unicode_normalize(&dst, src, len, UNICODE_NFC, NULL,
|
||||
normalize_realloc);
|
||||
pjs_free(src);
|
||||
if (dst_len < 0) {
|
||||
pjs_free(dst);
|
||||
return JS_NULL;
|
||||
}
|
||||
JSText *str = js_alloc_string(ctx, dst_len);
|
||||
if (!str) {
|
||||
pjs_free(dst);
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
for (int i = 0; i < dst_len; i++)
|
||||
string_put(str, i, dst[i]);
|
||||
str->length = dst_len;
|
||||
pjs_free(dst);
|
||||
return pretext_end(ctx, str);
|
||||
}
|
||||
|
||||
/* starts_with(str, prefix) — search(str, prefix) == 0 */
|
||||
static JSValue js_cell_starts_with(JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
|
||||
if (argc < 2) return JS_NULL;
|
||||
@@ -11507,6 +11663,85 @@ static JSValue js_cell_some(JSContext *ctx, JSValue this_val, int argc, JSValue
|
||||
return JS_FALSE;
|
||||
}
|
||||
|
||||
/* C API: Type-check wrappers for sensory functions */
|
||||
JS_BOOL JS_IsDigit (JSContext *ctx, JSValue val) {
|
||||
JSValue r = js_cell_is_digit (ctx, JS_NULL, 1, &val);
|
||||
return JS_VALUE_GET_BOOL (r);
|
||||
}
|
||||
|
||||
JS_BOOL JS_IsLetter (JSContext *ctx, JSValue val) {
|
||||
JSValue r = js_cell_is_letter (ctx, JS_NULL, 1, &val);
|
||||
return JS_VALUE_GET_BOOL (r);
|
||||
}
|
||||
|
||||
JS_BOOL JS_IsLower (JSContext *ctx, JSValue val) {
|
||||
JSValue r = js_cell_is_lower (ctx, JS_NULL, 1, &val);
|
||||
return JS_VALUE_GET_BOOL (r);
|
||||
}
|
||||
|
||||
JS_BOOL JS_IsUpper (JSContext *ctx, JSValue val) {
|
||||
JSValue r = js_cell_is_upper (ctx, JS_NULL, 1, &val);
|
||||
return JS_VALUE_GET_BOOL (r);
|
||||
}
|
||||
|
||||
JS_BOOL JS_IsWhitespace (JSContext *ctx, JSValue val) {
|
||||
JSValue r = js_cell_is_whitespace (ctx, JS_NULL, 1, &val);
|
||||
return JS_VALUE_GET_BOOL (r);
|
||||
}
|
||||
|
||||
JS_BOOL JS_IsCharacter (JSContext *ctx, JSValue val) {
|
||||
JSValue r = js_cell_is_character (ctx, JS_NULL, 1, &val);
|
||||
return JS_VALUE_GET_BOOL (r);
|
||||
}
|
||||
|
||||
JS_BOOL JS_IsFit (JSContext *ctx, JSValue val) {
|
||||
JSValue r = js_cell_is_fit (ctx, JS_NULL, 1, &val);
|
||||
return JS_VALUE_GET_BOOL (r);
|
||||
}
|
||||
|
||||
JS_BOOL JS_IsData (JSValue val) {
|
||||
return !JS_IsNull (val) && !JS_IsFunction (val);
|
||||
}
|
||||
|
||||
JS_BOOL JS_IsActor (JSContext *ctx, JSValue val) {
|
||||
JSValue r = js_cell_is_actor (ctx, JS_NULL, 1, &val);
|
||||
return JS_VALUE_GET_BOOL (r);
|
||||
}
|
||||
|
||||
/* C API: logical(val) */
|
||||
JSValue JS_CellLogical (JSContext *ctx, JSValue val) {
|
||||
return js_cell_logical (ctx, JS_NULL, 1, &val);
|
||||
}
|
||||
|
||||
/* C API: every(arr, pred) */
|
||||
JSValue JS_CellEvery (JSContext *ctx, JSValue arr, JSValue pred) {
|
||||
JSValue argv[2] = { arr, pred };
|
||||
return js_cell_every (ctx, JS_NULL, 2, argv);
|
||||
}
|
||||
|
||||
/* C API: some(arr, pred) */
|
||||
JSValue JS_CellSome (JSContext *ctx, JSValue arr, JSValue pred) {
|
||||
JSValue argv[2] = { arr, pred };
|
||||
return js_cell_some (ctx, JS_NULL, 2, argv);
|
||||
}
|
||||
|
||||
/* C API: starts_with(text, prefix) */
|
||||
JSValue JS_CellStartsWith (JSContext *ctx, JSValue text, JSValue prefix) {
|
||||
JSValue argv[2] = { text, prefix };
|
||||
return js_cell_starts_with (ctx, JS_NULL, 2, argv);
|
||||
}
|
||||
|
||||
/* C API: ends_with(text, suffix) */
|
||||
JSValue JS_CellEndsWith (JSContext *ctx, JSValue text, JSValue suffix) {
|
||||
JSValue argv[2] = { text, suffix };
|
||||
return js_cell_ends_with (ctx, JS_NULL, 2, argv);
|
||||
}
|
||||
|
||||
/* C API: normalize(text) */
|
||||
JSValue JS_CellNormalize (JSContext *ctx, JSValue text) {
|
||||
return js_cell_normalize (ctx, JS_NULL, 1, &text);
|
||||
}
|
||||
|
||||
static void js_set_global_cfunc(JSContext *ctx, const char *name, JSCFunction *func, int length) {
|
||||
JSGCRef ref;
|
||||
JS_PushGCRef(ctx, &ref);
|
||||
@@ -11572,6 +11807,14 @@ static void JS_AddIntrinsicBaseObjects (JSContext *ctx) {
|
||||
js_set_global_cfunc(ctx, "is_text", js_cell_is_text, 1);
|
||||
js_set_global_cfunc(ctx, "is_proto", js_cell_is_proto, 2);
|
||||
js_set_global_cfunc(ctx, "is_letter", js_cell_is_letter, 1);
|
||||
js_set_global_cfunc(ctx, "is_true", js_cell_is_true, 1);
|
||||
js_set_global_cfunc(ctx, "is_false", js_cell_is_false, 1);
|
||||
js_set_global_cfunc(ctx, "is_fit", js_cell_is_fit, 1);
|
||||
js_set_global_cfunc(ctx, "is_character", js_cell_is_character, 1);
|
||||
js_set_global_cfunc(ctx, "is_digit", js_cell_is_digit, 1);
|
||||
js_set_global_cfunc(ctx, "is_lower", js_cell_is_lower, 1);
|
||||
js_set_global_cfunc(ctx, "is_upper", js_cell_is_upper, 1);
|
||||
js_set_global_cfunc(ctx, "is_whitespace", js_cell_is_whitespace, 1);
|
||||
|
||||
/* Utility functions */
|
||||
js_set_global_cfunc(ctx, "apply", js_cell_fn_apply, 2);
|
||||
@@ -11623,6 +11866,7 @@ static void JS_AddIntrinsicBaseObjects (JSContext *ctx) {
|
||||
js_set_global_cfunc(ctx, "ends_with", js_cell_ends_with, 2);
|
||||
js_set_global_cfunc(ctx, "every", js_cell_every, 2);
|
||||
js_set_global_cfunc(ctx, "some", js_cell_some, 2);
|
||||
js_set_global_cfunc(ctx, "normalize", js_cell_normalize, 1);
|
||||
|
||||
/* fn record with apply property */
|
||||
{
|
||||
@@ -11689,6 +11933,15 @@ void js_debug_sethook (JSContext *ctx, js_hook hook, int type, void *user) {
|
||||
ctx->trace_data = user;
|
||||
}
|
||||
|
||||
void js_debug_gethook (JSContext *ctx, js_hook *hook, int *type, void **user) {
|
||||
if (hook) *hook = ctx->trace_hook;
|
||||
if (type) *type = ctx->trace_type;
|
||||
if (user) *user = ctx->trace_data;
|
||||
}
|
||||
|
||||
size_t cell_ctx_heap_used (JSContext *ctx) {
|
||||
return (size_t)(ctx->heap_free - ctx->heap_base);
|
||||
}
|
||||
|
||||
/* Public API: get stack trace as JS array of {fn, file, line, col} objects.
|
||||
Does NOT clear reg_current_frame — caller is responsible if needed.
|
||||
|
||||
@@ -6,18 +6,20 @@
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <stdatomic.h>
|
||||
#include <poll.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
#include "stb_ds.h"
|
||||
#include "cell.h"
|
||||
#include "quickjs-internal.h"
|
||||
#include "cell_internal.h"
|
||||
#include "pit_internal.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <winsock2.h>
|
||||
#endif
|
||||
|
||||
typedef struct actor_node {
|
||||
cell_rt *actor;
|
||||
JSContext *actor;
|
||||
struct actor_node *next;
|
||||
} actor_node;
|
||||
|
||||
@@ -30,13 +32,13 @@ typedef enum {
|
||||
|
||||
typedef struct {
|
||||
uint64_t execute_at_ns;
|
||||
cell_rt *actor;
|
||||
JSContext *actor;
|
||||
uint32_t timer_id;
|
||||
timer_type type;
|
||||
uint32_t turn_gen; /* generation at registration time */
|
||||
} timer_node;
|
||||
|
||||
static timer_node *timer_heap = NULL;
|
||||
static timer_node *timer_heap = NULL;
|
||||
|
||||
// Priority queue indices
|
||||
#define PQ_READY 0
|
||||
@@ -92,7 +94,96 @@ static int has_any_work(actor_node *heads[]) {
|
||||
}
|
||||
|
||||
static pthread_mutex_t *actors_mutex;
|
||||
static struct { char *key; cell_rt *value; } *actors = NULL;
|
||||
static struct { char *key; JSContext *value; } *actors = NULL;
|
||||
|
||||
/* ============================================================
|
||||
I/O Watch Thread — poll()-based fd monitoring
|
||||
============================================================ */
|
||||
static io_watch *g_io_watches = NULL; /* stb_ds dynamic array */
|
||||
static pthread_mutex_t io_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_t io_thread;
|
||||
static int io_pipe[2] = {-1, -1}; /* self-pipe to wake poll() */
|
||||
|
||||
static void io_wake(void) {
|
||||
char c = 1;
|
||||
(void)write(io_pipe[1], &c, 1);
|
||||
}
|
||||
|
||||
static void *io_thread_func(void *arg) {
|
||||
(void)arg;
|
||||
while (1) {
|
||||
pthread_mutex_lock(&io_mutex);
|
||||
if (engine.shutting_down) {
|
||||
pthread_mutex_unlock(&io_mutex);
|
||||
return NULL;
|
||||
}
|
||||
int n = arrlen(g_io_watches);
|
||||
/* +1 for the wakeup pipe */
|
||||
struct pollfd *fds = malloc(sizeof(struct pollfd) * (n + 1));
|
||||
fds[0].fd = io_pipe[0];
|
||||
fds[0].events = POLLIN;
|
||||
for (int i = 0; i < n; i++) {
|
||||
fds[i + 1].fd = g_io_watches[i].fd;
|
||||
fds[i + 1].events = g_io_watches[i].events;
|
||||
}
|
||||
pthread_mutex_unlock(&io_mutex);
|
||||
|
||||
int ready = poll(fds, n + 1, 500); /* 500ms timeout for shutdown check */
|
||||
if (ready <= 0) {
|
||||
free(fds);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Drain wakeup pipe */
|
||||
if (fds[0].revents & POLLIN) {
|
||||
char buf[64];
|
||||
(void)read(io_pipe[0], buf, sizeof(buf));
|
||||
}
|
||||
|
||||
/* Fire callbacks for ready fds */
|
||||
pthread_mutex_lock(&io_mutex);
|
||||
for (int i = n - 1; i >= 0; i--) {
|
||||
if (i >= arrlen(g_io_watches)) continue;
|
||||
if (fds[i + 1].revents & g_io_watches[i].events) {
|
||||
io_watch w = g_io_watches[i];
|
||||
arrdel(g_io_watches, i); /* one-shot: remove before firing */
|
||||
pthread_mutex_unlock(&io_mutex);
|
||||
actor_clock(w.actor, w.callback);
|
||||
pthread_mutex_lock(&io_mutex);
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&io_mutex);
|
||||
free(fds);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void actor_watch(JSContext *actor, int fd, short events, JSValue fn) {
|
||||
io_watch w = { .fd = fd, .events = events, .actor = actor, .callback = fn };
|
||||
pthread_mutex_lock(&io_mutex);
|
||||
arrput(g_io_watches, w);
|
||||
pthread_mutex_unlock(&io_mutex);
|
||||
io_wake();
|
||||
}
|
||||
|
||||
void actor_watch_readable(JSContext *actor, int fd, JSValue fn) {
|
||||
actor_watch(actor, fd, POLLIN, fn);
|
||||
}
|
||||
|
||||
void actor_watch_writable(JSContext *actor, int fd, JSValue fn) {
|
||||
actor_watch(actor, fd, POLLOUT, fn);
|
||||
}
|
||||
|
||||
void actor_unwatch(JSContext *actor, int fd) {
|
||||
pthread_mutex_lock(&io_mutex);
|
||||
for (int i = arrlen(g_io_watches) - 1; i >= 0; i--) {
|
||||
if (g_io_watches[i].actor == actor && g_io_watches[i].fd == fd) {
|
||||
arrdel(g_io_watches, i);
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&io_mutex);
|
||||
io_wake();
|
||||
}
|
||||
|
||||
#define lockless_shdel(NAME, KEY) pthread_mutex_lock(NAME##_mutex); shdel(NAME, KEY); pthread_mutex_unlock(NAME##_mutex);
|
||||
#define lockless_shlen(NAME) ({ \
|
||||
@@ -109,7 +200,7 @@ static struct { char *key; cell_rt *value; } *actors = NULL;
|
||||
})
|
||||
#define lockless_shget(NAME, KEY) ({ \
|
||||
pthread_mutex_lock(NAME##_mutex); \
|
||||
cell_rt *_actor = shget(NAME, KEY); \
|
||||
JSContext *_actor = shget(NAME, KEY); \
|
||||
pthread_mutex_unlock(NAME##_mutex); \
|
||||
_actor; \
|
||||
})
|
||||
@@ -122,21 +213,21 @@ static struct { char *key; cell_rt *value; } *actors = NULL;
|
||||
})
|
||||
|
||||
// Forward declarations
|
||||
uint32_t actor_remove_cb(cell_rt *actor, uint32_t id, uint32_t interval);
|
||||
void actor_turn(cell_rt *actor);
|
||||
uint32_t actor_remove_cb(JSContext *actor, uint32_t id, uint32_t interval);
|
||||
void actor_turn(JSContext *actor);
|
||||
|
||||
void heap_push(uint64_t when, cell_rt *actor, uint32_t timer_id, timer_type type) {
|
||||
void heap_push(uint64_t when, JSContext *actor, uint32_t timer_id, timer_type type) {
|
||||
timer_node node = { .execute_at_ns = when, .actor = actor, .timer_id = timer_id, .type = type, .turn_gen = 0 };
|
||||
if (type == TIMER_PAUSE || type == TIMER_KILL)
|
||||
node.turn_gen = atomic_load_explicit(&actor->turn_gen, memory_order_relaxed);
|
||||
arrput(timer_heap, node);
|
||||
|
||||
|
||||
// Bubble up
|
||||
int i = arrlen(timer_heap) - 1;
|
||||
while (i > 0) {
|
||||
int parent = (i - 1) / 2;
|
||||
if (timer_heap[i].execute_at_ns >= timer_heap[parent].execute_at_ns) break;
|
||||
|
||||
|
||||
timer_node tmp = timer_heap[i];
|
||||
timer_heap[i] = timer_heap[parent];
|
||||
timer_heap[parent] = tmp;
|
||||
@@ -147,10 +238,10 @@ void heap_push(uint64_t when, cell_rt *actor, uint32_t timer_id, timer_type type
|
||||
// Helper: Heap Pop
|
||||
int heap_pop(timer_node *out) {
|
||||
if (arrlen(timer_heap) == 0) return 0;
|
||||
|
||||
|
||||
*out = timer_heap[0];
|
||||
timer_node last = arrpop(timer_heap);
|
||||
|
||||
|
||||
if (arrlen(timer_heap) > 0) {
|
||||
timer_heap[0] = last;
|
||||
// Bubble down
|
||||
@@ -160,14 +251,14 @@ int heap_pop(timer_node *out) {
|
||||
int left = 2 * i + 1;
|
||||
int right = 2 * i + 2;
|
||||
int smallest = i;
|
||||
|
||||
|
||||
if (left < n && timer_heap[left].execute_at_ns < timer_heap[smallest].execute_at_ns)
|
||||
smallest = left;
|
||||
if (right < n && timer_heap[right].execute_at_ns < timer_heap[smallest].execute_at_ns)
|
||||
smallest = right;
|
||||
|
||||
|
||||
if (smallest == i) break;
|
||||
|
||||
|
||||
timer_node tmp = timer_heap[i];
|
||||
timer_heap[i] = timer_heap[smallest];
|
||||
timer_heap[smallest] = tmp;
|
||||
@@ -180,7 +271,7 @@ int heap_pop(timer_node *out) {
|
||||
void *timer_thread_func(void *arg) {
|
||||
while (1) {
|
||||
pthread_mutex_lock(&engine.lock);
|
||||
|
||||
|
||||
if (engine.shutting_down) {
|
||||
pthread_mutex_unlock(&engine.lock);
|
||||
return NULL;
|
||||
@@ -203,10 +294,10 @@ void *timer_thread_func(void *arg) {
|
||||
uint32_t cur = atomic_load_explicit(&t.actor->turn_gen, memory_order_relaxed);
|
||||
if (cur == t.turn_gen) {
|
||||
if (t.type == TIMER_PAUSE) {
|
||||
JS_SetPauseFlag(t.actor->context, 1);
|
||||
JS_SetPauseFlag(t.actor, 1);
|
||||
} else {
|
||||
t.actor->disrupt = 1;
|
||||
JS_SetPauseFlag(t.actor->context, 2);
|
||||
JS_SetPauseFlag(t.actor, 2);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -216,26 +307,25 @@ void *timer_thread_func(void *arg) {
|
||||
JSValue cb = t.actor->timers[idx].value;
|
||||
hmdel(t.actor->timers, t.timer_id);
|
||||
actor_clock(t.actor, cb);
|
||||
JS_FreeValue(t.actor->context, cb);
|
||||
}
|
||||
pthread_mutex_unlock(t.actor->msg_mutex);
|
||||
}
|
||||
|
||||
continue;
|
||||
|
||||
continue;
|
||||
} else {
|
||||
// --- WAIT FOR DEADLINE ---
|
||||
struct timespec ts;
|
||||
uint64_t wait_ns = timer_heap[0].execute_at_ns - now;
|
||||
|
||||
|
||||
// Convert relative wait time to absolute CLOCK_REALTIME
|
||||
struct timespec now_real;
|
||||
clock_gettime(CLOCK_REALTIME, &now_real);
|
||||
|
||||
uint64_t deadline_real_ns = (uint64_t)now_real.tv_sec * 1000000000ULL +
|
||||
|
||||
uint64_t deadline_real_ns = (uint64_t)now_real.tv_sec * 1000000000ULL +
|
||||
(uint64_t)now_real.tv_nsec + wait_ns;
|
||||
ts.tv_sec = deadline_real_ns / 1000000000ULL;
|
||||
ts.tv_nsec = deadline_real_ns % 1000000000ULL;
|
||||
|
||||
|
||||
pthread_cond_timedwait(&engine.timer_cond, &engine.lock, &ts);
|
||||
}
|
||||
}
|
||||
@@ -244,7 +334,7 @@ void *timer_thread_func(void *arg) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void enqueue_actor_priority(cell_rt *actor) {
|
||||
void enqueue_actor_priority(JSContext *actor) {
|
||||
actor_node *n = malloc(sizeof(actor_node));
|
||||
n->actor = actor;
|
||||
n->next = NULL;
|
||||
@@ -301,13 +391,20 @@ void actor_initialize(void) {
|
||||
engine.mq_head[i] = NULL;
|
||||
engine.mq_tail[i] = NULL;
|
||||
}
|
||||
|
||||
|
||||
actors_mutex = malloc(sizeof(pthread_mutex_t));
|
||||
pthread_mutex_init(actors_mutex, NULL);
|
||||
|
||||
|
||||
// Start Timer Thread
|
||||
pthread_create(&engine.timer_thread, NULL, timer_thread_func, NULL);
|
||||
|
||||
// Start I/O Watch Thread
|
||||
if (pipe(io_pipe) == 0) {
|
||||
fcntl(io_pipe[0], F_SETFL, O_NONBLOCK);
|
||||
fcntl(io_pipe[1], F_SETFL, O_NONBLOCK);
|
||||
pthread_create(&io_thread, NULL, io_thread_func, NULL);
|
||||
}
|
||||
|
||||
// Start Workers
|
||||
#ifdef _WIN32
|
||||
SYSTEM_INFO sysinfo;
|
||||
@@ -323,7 +420,7 @@ void actor_initialize(void) {
|
||||
}
|
||||
}
|
||||
|
||||
void actor_free(cell_rt *actor)
|
||||
void actor_free(JSContext *actor)
|
||||
{
|
||||
if (actor->is_quiescent) {
|
||||
actor->is_quiescent = 0;
|
||||
@@ -360,46 +457,46 @@ void actor_free(cell_rt *actor)
|
||||
pthread_mutex_unlock(&engine.lock);
|
||||
}
|
||||
|
||||
/* Remove I/O watches for this actor */
|
||||
pthread_mutex_lock(&io_mutex);
|
||||
for (int i = arrlen(g_io_watches) - 1; i >= 0; i--) {
|
||||
if (g_io_watches[i].actor == actor)
|
||||
arrdel(g_io_watches, i);
|
||||
}
|
||||
pthread_mutex_unlock(&io_mutex);
|
||||
|
||||
// Do not go forward with actor destruction until the actor is completely free
|
||||
pthread_mutex_lock(actor->msg_mutex);
|
||||
pthread_mutex_lock(actor->mutex);
|
||||
|
||||
JSContext *js = actor->context;
|
||||
|
||||
JS_DeleteGCRef(js, &actor->idx_buffer_ref);
|
||||
JS_DeleteGCRef(js, &actor->on_exception_ref);
|
||||
JS_DeleteGCRef(js, &actor->message_handle_ref);
|
||||
JS_DeleteGCRef(js, &actor->unneeded_ref);
|
||||
JS_DeleteGCRef(js, &actor->actor_sym_ref);
|
||||
|
||||
for (int i = 0; i < hmlen(actor->timers); i++) {
|
||||
JS_FreeValue(js, actor->timers[i].value);
|
||||
}
|
||||
|
||||
|
||||
hmfree(actor->timers);
|
||||
|
||||
|
||||
/* Free all letters in the queue */
|
||||
for (int i = 0; i < arrlen(actor->letters); i++) {
|
||||
if (actor->letters[i].type == LETTER_BLOB) {
|
||||
blob_destroy(actor->letters[i].blob_data);
|
||||
} else if (actor->letters[i].type == LETTER_CALLBACK) {
|
||||
JS_FreeValue(js, actor->letters[i].callback);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
arrfree(actor->letters);
|
||||
|
||||
JS_FreeContext(js);
|
||||
|
||||
free(actor->id);
|
||||
|
||||
pthread_mutex_unlock(actor->mutex);
|
||||
pthread_mutex_destroy(actor->mutex);
|
||||
free(actor->mutex);
|
||||
pthread_mutex_unlock(actor->msg_mutex);
|
||||
pthread_mutex_destroy(actor->msg_mutex);
|
||||
free(actor->msg_mutex);
|
||||
|
||||
free(actor);
|
||||
|
||||
pthread_mutex_t *m = actor->mutex;
|
||||
pthread_mutex_t *mm = actor->msg_mutex;
|
||||
|
||||
JS_FreeContext(actor);
|
||||
|
||||
pthread_mutex_unlock(m);
|
||||
pthread_mutex_destroy(m);
|
||||
free(m);
|
||||
pthread_mutex_unlock(mm);
|
||||
pthread_mutex_destroy(mm);
|
||||
free(mm);
|
||||
|
||||
int actor_count = lockless_shlen(actors);
|
||||
if (actor_count == 0) {
|
||||
@@ -445,6 +542,16 @@ void exit_handler(void) {
|
||||
|
||||
pthread_join(engine.timer_thread, NULL);
|
||||
|
||||
/* Shut down I/O thread */
|
||||
if (io_pipe[1] >= 0) {
|
||||
io_wake();
|
||||
pthread_join(io_thread, NULL);
|
||||
close(io_pipe[0]);
|
||||
close(io_pipe[1]);
|
||||
io_pipe[0] = io_pipe[1] = -1;
|
||||
}
|
||||
arrfree(g_io_watches);
|
||||
|
||||
for (int i=0; i < engine.num_workers; i++) {
|
||||
pthread_join(engine.worker_threads[i], NULL);
|
||||
}
|
||||
@@ -467,7 +574,7 @@ int actor_exists(const char *id)
|
||||
return idx != -1;
|
||||
}
|
||||
|
||||
void set_actor_state(cell_rt *actor)
|
||||
void set_actor_state(JSContext *actor)
|
||||
{
|
||||
if (actor->disrupt) {
|
||||
#ifdef SCHEDULER_DEBUG
|
||||
@@ -548,7 +655,7 @@ void set_actor_state(cell_rt *actor)
|
||||
pthread_mutex_unlock(actor->msg_mutex);
|
||||
}
|
||||
|
||||
uint32_t actor_remove_cb(cell_rt *actor, uint32_t id, uint32_t interval)
|
||||
uint32_t actor_remove_cb(JSContext *actor, uint32_t id, uint32_t interval)
|
||||
{
|
||||
pthread_mutex_lock(actor->mutex);
|
||||
// Check if this timer is still valid (match actor->ar)
|
||||
@@ -556,22 +663,22 @@ uint32_t actor_remove_cb(cell_rt *actor, uint32_t id, uint32_t interval)
|
||||
pthread_mutex_unlock(actor->mutex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
actor->disrupt = 1;
|
||||
|
||||
|
||||
if (!JS_IsNull(actor->unneeded_ref.val)) {
|
||||
JSValue ret = JS_Call(actor->context, actor->unneeded_ref.val, JS_NULL, 0, NULL);
|
||||
uncaught_exception(actor->context, ret);
|
||||
JSValue ret = JS_Call(actor, actor->unneeded_ref.val, JS_NULL, 0, NULL);
|
||||
uncaught_exception(actor, ret);
|
||||
}
|
||||
|
||||
int should_free = (actor->state == ACTOR_IDLE);
|
||||
pthread_mutex_unlock(actor->mutex);
|
||||
|
||||
|
||||
if (should_free) actor_free(actor);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void actor_unneeded(cell_rt *actor, JSValue fn, double seconds)
|
||||
void actor_unneeded(JSContext *actor, JSValue fn, double seconds)
|
||||
{
|
||||
if (actor->disrupt) return;
|
||||
|
||||
@@ -582,14 +689,14 @@ void actor_unneeded(cell_rt *actor, JSValue fn, double seconds)
|
||||
|
||||
actor->unneeded_ref.val = fn;
|
||||
actor->ar_secs = seconds;
|
||||
|
||||
|
||||
END:
|
||||
if (actor->ar)
|
||||
actor->ar = 0;
|
||||
set_actor_state(actor);
|
||||
}
|
||||
|
||||
cell_rt *get_actor(char *id)
|
||||
JSContext *get_actor(char *id)
|
||||
{
|
||||
int idx = lockless_shgeti(actors, id);
|
||||
if (idx == -1) {
|
||||
@@ -622,17 +729,14 @@ void actor_loop()
|
||||
}
|
||||
}
|
||||
|
||||
cell_rt *create_actor(void *wota)
|
||||
extern JSRuntime *g_runtime;
|
||||
|
||||
JSContext *create_actor(void *wota)
|
||||
{
|
||||
cell_rt *actor = calloc(sizeof(*actor), 1);
|
||||
#ifdef HAVE_MIMALLOC
|
||||
actor->heap = mi_heap_new();
|
||||
#endif
|
||||
JSContext *actor = JS_NewContext(g_runtime);
|
||||
if (!actor) return NULL;
|
||||
|
||||
actor->init_wota = wota;
|
||||
/* GCRef fields are registered after JSContext creation in script_startup.
|
||||
For now, zero-init from calloc is sufficient (val = 0 = JS_MKVAL(JS_TAG_INT,0),
|
||||
which is not a pointer so GC-safe). The actual JS_NULL assignment and
|
||||
JS_AddGCRef happen in script_startup. */
|
||||
|
||||
arrsetcap(actor->letters, 5);
|
||||
|
||||
@@ -642,19 +746,22 @@ cell_rt *create_actor(void *wota)
|
||||
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
|
||||
pthread_mutex_init(actor->mutex, &attr);
|
||||
actor->msg_mutex = malloc(sizeof(pthread_mutex_t));
|
||||
pthread_mutex_init(actor->msg_mutex, &attr); // msg_mutex can be recursive too to be safe
|
||||
pthread_mutex_init(actor->msg_mutex, &attr);
|
||||
pthread_mutexattr_destroy(&attr);
|
||||
|
||||
|
||||
JS_SetGCScanExternal(actor, actor_gc_scan);
|
||||
JS_SetHeapMemoryLimit(actor, ACTOR_MEMORY_LIMIT);
|
||||
|
||||
/* Lock actor->mutex while initializing JS runtime. */
|
||||
pthread_mutex_lock(actor->mutex);
|
||||
script_startup(actor);
|
||||
set_actor_state(actor);
|
||||
pthread_mutex_unlock(actor->mutex);
|
||||
pthread_mutex_unlock(actor->mutex);
|
||||
|
||||
return actor;
|
||||
}
|
||||
|
||||
const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar)
|
||||
const char *register_actor(const char *id, JSContext *actor, int mainthread, double ar)
|
||||
{
|
||||
actor->main_thread_only = mainthread;
|
||||
actor->id = strdup(id);
|
||||
@@ -676,29 +783,29 @@ const char *register_actor(const char *id, cell_rt *actor, int mainthread, doubl
|
||||
|
||||
const char *send_message(const char *id, void *msg)
|
||||
{
|
||||
cell_rt *target = get_actor(id);
|
||||
JSContext *target = get_actor(id);
|
||||
if (!target) {
|
||||
blob_destroy((blob *)msg);
|
||||
return "Could not get actor from id.";
|
||||
}
|
||||
|
||||
|
||||
letter l;
|
||||
l.type = LETTER_BLOB;
|
||||
l.blob_data = (blob *)msg;
|
||||
|
||||
|
||||
pthread_mutex_lock(target->msg_mutex);
|
||||
arrput(target->letters, l);
|
||||
pthread_mutex_unlock(target->msg_mutex);
|
||||
|
||||
if (target->ar)
|
||||
target->ar = 0;
|
||||
|
||||
|
||||
set_actor_state(target);
|
||||
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void actor_turn(cell_rt *actor)
|
||||
void actor_turn(JSContext *actor)
|
||||
{
|
||||
pthread_mutex_lock(actor->mutex);
|
||||
#ifdef ACTOR_TRACE
|
||||
@@ -710,16 +817,21 @@ void actor_turn(cell_rt *actor)
|
||||
actor->name ? actor->name : actor->id, prev_state);
|
||||
#endif
|
||||
|
||||
if (actor->trace_hook)
|
||||
actor->trace_hook(actor->name, CELL_HOOK_ENTER);
|
||||
if (!actor->actor_trace_hook) {
|
||||
cell_hook gh = cell_trace_gethook();
|
||||
if (gh) actor->actor_trace_hook = gh;
|
||||
}
|
||||
|
||||
if (actor->actor_trace_hook)
|
||||
actor->actor_trace_hook(actor, CELL_HOOK_ENTER);
|
||||
|
||||
JSValue result;
|
||||
|
||||
if (actor->vm_suspended) {
|
||||
/* RESUME path: continue suspended turn under kill timer only */
|
||||
g_crash_ctx = actor->context;
|
||||
g_crash_ctx = actor;
|
||||
atomic_fetch_add_explicit(&actor->turn_gen, 1, memory_order_relaxed);
|
||||
JS_SetPauseFlag(actor->context, 0);
|
||||
JS_SetPauseFlag(actor, 0);
|
||||
actor->turn_start_ns = cell_ns();
|
||||
|
||||
/* Register kill timer only for resume */
|
||||
@@ -729,7 +841,7 @@ void actor_turn(cell_rt *actor)
|
||||
pthread_cond_signal(&engine.timer_cond);
|
||||
pthread_mutex_unlock(&engine.lock);
|
||||
|
||||
result = JS_ResumeRegisterVM(actor->context);
|
||||
result = JS_ResumeRegisterVM(actor);
|
||||
actor->vm_suspended = 0;
|
||||
|
||||
if (JS_IsSuspended(result)) {
|
||||
@@ -738,7 +850,7 @@ void actor_turn(cell_rt *actor)
|
||||
goto ENDTURN;
|
||||
}
|
||||
if (JS_IsException(result)) {
|
||||
if (!uncaught_exception(actor->context, result))
|
||||
if (!uncaught_exception(actor, result))
|
||||
actor->disrupt = 1;
|
||||
}
|
||||
actor->slow_strikes++;
|
||||
@@ -773,7 +885,7 @@ void actor_turn(cell_rt *actor)
|
||||
pthread_mutex_unlock(actor->msg_mutex);
|
||||
|
||||
atomic_fetch_add_explicit(&actor->turn_gen, 1, memory_order_relaxed);
|
||||
JS_SetPauseFlag(actor->context, 0);
|
||||
JS_SetPauseFlag(actor, 0);
|
||||
actor->turn_start_ns = cell_ns();
|
||||
|
||||
/* Register both pause and kill timers */
|
||||
@@ -785,35 +897,31 @@ void actor_turn(cell_rt *actor)
|
||||
pthread_cond_signal(&engine.timer_cond);
|
||||
pthread_mutex_unlock(&engine.lock);
|
||||
|
||||
g_crash_ctx = actor->context;
|
||||
g_crash_ctx = actor;
|
||||
|
||||
if (l.type == LETTER_BLOB) {
|
||||
size_t size = blob_length(l.blob_data) / 8;
|
||||
JSValue arg = js_new_blob_stoned_copy(actor->context,
|
||||
JSValue arg = js_new_blob_stoned_copy(actor,
|
||||
(void *)blob_data(l.blob_data), size);
|
||||
blob_destroy(l.blob_data);
|
||||
result = JS_Call(actor->context, actor->message_handle_ref.val,
|
||||
result = JS_Call(actor, actor->message_handle_ref.val,
|
||||
JS_NULL, 1, &arg);
|
||||
if (JS_IsSuspended(result)) {
|
||||
actor->vm_suspended = 1;
|
||||
actor->state = ACTOR_SLOW;
|
||||
JS_FreeValue(actor->context, arg);
|
||||
goto ENDTURN_SLOW;
|
||||
}
|
||||
if (!uncaught_exception(actor->context, result))
|
||||
if (!uncaught_exception(actor, result))
|
||||
actor->disrupt = 1;
|
||||
JS_FreeValue(actor->context, arg);
|
||||
} else if (l.type == LETTER_CALLBACK) {
|
||||
result = JS_Call(actor->context, l.callback, JS_NULL, 0, NULL);
|
||||
result = JS_Call(actor, l.callback, JS_NULL, 0, NULL);
|
||||
if (JS_IsSuspended(result)) {
|
||||
actor->vm_suspended = 1;
|
||||
actor->state = ACTOR_SLOW;
|
||||
JS_FreeValue(actor->context, l.callback);
|
||||
goto ENDTURN_SLOW;
|
||||
}
|
||||
if (!uncaught_exception(actor->context, result))
|
||||
if (!uncaught_exception(actor, result))
|
||||
actor->disrupt = 1;
|
||||
JS_FreeValue(actor->context, l.callback);
|
||||
}
|
||||
|
||||
if (actor->disrupt) goto ENDTURN;
|
||||
@@ -825,8 +933,8 @@ ENDTURN:
|
||||
atomic_fetch_add_explicit(&actor->turn_gen, 1, memory_order_relaxed);
|
||||
actor->state = ACTOR_IDLE;
|
||||
|
||||
if (actor->trace_hook)
|
||||
actor->trace_hook(actor->name, CELL_HOOK_EXIT);
|
||||
if (actor->actor_trace_hook)
|
||||
actor->actor_trace_hook(actor, CELL_HOOK_EXIT);
|
||||
|
||||
if (actor->disrupt) {
|
||||
#ifdef SCHEDULER_DEBUG
|
||||
@@ -852,8 +960,8 @@ ENDTURN_SLOW:
|
||||
fprintf(stderr, "[ACTOR_TRACE] %s: suspended mid-turn -> SLOW\n",
|
||||
actor->name ? actor->name : actor->id);
|
||||
#endif
|
||||
if (actor->trace_hook)
|
||||
actor->trace_hook(actor->name, CELL_HOOK_EXIT);
|
||||
if (actor->actor_trace_hook)
|
||||
actor->actor_trace_hook(actor, CELL_HOOK_EXIT);
|
||||
enqueue_actor_priority(actor);
|
||||
pthread_mutex_unlock(actor->mutex);
|
||||
}
|
||||
@@ -864,51 +972,61 @@ void actor_gc_scan(JSContext *ctx,
|
||||
uint8_t *from_base, uint8_t *from_end,
|
||||
uint8_t *to_base, uint8_t **to_free, uint8_t *to_end)
|
||||
{
|
||||
cell_rt *actor = JS_GetContextOpaque(ctx);
|
||||
if (!actor) return;
|
||||
|
||||
/* Lock msg_mutex to synchronize with the timer thread, which reads
|
||||
timers and writes letters under the same lock. */
|
||||
pthread_mutex_lock(actor->msg_mutex);
|
||||
if (ctx->msg_mutex)
|
||||
pthread_mutex_lock(ctx->msg_mutex);
|
||||
|
||||
for (int i = 0; i < arrlen(actor->letters); i++) {
|
||||
if (actor->letters[i].type == LETTER_CALLBACK) {
|
||||
actor->letters[i].callback = gc_copy_value(ctx,
|
||||
actor->letters[i].callback,
|
||||
for (int i = 0; i < arrlen(ctx->letters); i++) {
|
||||
if (ctx->letters[i].type == LETTER_CALLBACK) {
|
||||
ctx->letters[i].callback = gc_copy_value(ctx,
|
||||
ctx->letters[i].callback,
|
||||
from_base, from_end, to_base, to_free, to_end);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < hmlen(actor->timers); i++) {
|
||||
actor->timers[i].value = gc_copy_value(ctx,
|
||||
actor->timers[i].value,
|
||||
for (int i = 0; i < hmlen(ctx->timers); i++) {
|
||||
ctx->timers[i].value = gc_copy_value(ctx,
|
||||
ctx->timers[i].value,
|
||||
from_base, from_end, to_base, to_free, to_end);
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(actor->msg_mutex);
|
||||
if (ctx->msg_mutex)
|
||||
pthread_mutex_unlock(ctx->msg_mutex);
|
||||
|
||||
/* Scan I/O watch callbacks belonging to this actor */
|
||||
pthread_mutex_lock(&io_mutex);
|
||||
for (int i = 0; i < arrlen(g_io_watches); i++) {
|
||||
if (g_io_watches[i].actor == ctx) {
|
||||
g_io_watches[i].callback = gc_copy_value(ctx,
|
||||
g_io_watches[i].callback,
|
||||
from_base, from_end, to_base, to_free, to_end);
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&io_mutex);
|
||||
}
|
||||
|
||||
void actor_clock(cell_rt *actor, JSValue fn)
|
||||
void actor_clock(JSContext *actor, JSValue fn)
|
||||
{
|
||||
letter l;
|
||||
l.type = LETTER_CALLBACK;
|
||||
l.callback = JS_DupValue(actor->context, fn);
|
||||
l.callback = fn;
|
||||
pthread_mutex_lock(actor->msg_mutex);
|
||||
arrput(actor->letters, l);
|
||||
pthread_mutex_unlock(actor->msg_mutex);
|
||||
set_actor_state(actor);
|
||||
}
|
||||
|
||||
uint32_t actor_delay(cell_rt *actor, JSValue fn, double seconds)
|
||||
uint32_t actor_delay(JSContext *actor, JSValue fn, double seconds)
|
||||
{
|
||||
pthread_mutex_lock(actor->msg_mutex);
|
||||
|
||||
pthread_mutex_lock(actor->msg_mutex);
|
||||
|
||||
static uint32_t global_timer_id = 1;
|
||||
uint32_t id = global_timer_id++;
|
||||
|
||||
JSValue cb = JS_DupValue(actor->context, fn);
|
||||
|
||||
JSValue cb = fn;
|
||||
hmput(actor->timers, id, cb);
|
||||
|
||||
|
||||
uint64_t now = cell_ns();
|
||||
uint64_t execute_at = now + (uint64_t)(seconds * 1e9);
|
||||
|
||||
@@ -923,7 +1041,7 @@ uint32_t actor_delay(cell_rt *actor, JSValue fn, double seconds)
|
||||
return id;
|
||||
}
|
||||
|
||||
JSValue actor_remove_timer(cell_rt *actor, uint32_t timer_id)
|
||||
JSValue actor_remove_timer(JSContext *actor, uint32_t timer_id)
|
||||
{
|
||||
JSValue cb = JS_NULL;
|
||||
pthread_mutex_lock(actor->msg_mutex);
|
||||
@@ -936,3 +1054,57 @@ JSValue actor_remove_timer(cell_rt *actor, uint32_t timer_id)
|
||||
// Note: We don't remove from heap, it will misfire safely
|
||||
return cb;
|
||||
}
|
||||
|
||||
int JS_ActorExists(const char *actor_id)
|
||||
{
|
||||
return actor_exists(actor_id);
|
||||
}
|
||||
|
||||
const char *JS_SendMessage(JSContext *ctx, WotaBuffer *wb)
|
||||
{
|
||||
if (!wb || !wb->data || wb->size == 0)
|
||||
return "Empty WOTA buffer";
|
||||
|
||||
/* Wrap the caller's payload in the engine protocol envelope:
|
||||
{type: "user", data: <payload>}
|
||||
The header takes ~6 words; pre-allocate enough for header + payload. */
|
||||
WotaBuffer envelope;
|
||||
wota_buffer_init(&envelope, wb->size + 8);
|
||||
wota_write_record(&envelope, 2);
|
||||
wota_write_text(&envelope, "type");
|
||||
wota_write_text(&envelope, "user");
|
||||
wota_write_text(&envelope, "data");
|
||||
|
||||
/* Append the caller's pre-encoded WOTA payload words directly. */
|
||||
size_t need = envelope.size + wb->size;
|
||||
if (need > envelope.capacity) {
|
||||
size_t new_cap = envelope.capacity ? envelope.capacity * 2 : 8;
|
||||
while (new_cap < need) new_cap *= 2;
|
||||
envelope.data = realloc(envelope.data, new_cap * sizeof(uint64_t));
|
||||
envelope.capacity = new_cap;
|
||||
}
|
||||
memcpy(envelope.data + envelope.size, wb->data,
|
||||
wb->size * sizeof(uint64_t));
|
||||
envelope.size += wb->size;
|
||||
|
||||
size_t byte_len = envelope.size * sizeof(uint64_t);
|
||||
blob *msg = blob_new(byte_len * 8);
|
||||
if (!msg) {
|
||||
wota_buffer_free(&envelope);
|
||||
return "Could not allocate blob";
|
||||
}
|
||||
|
||||
blob_write_bytes(msg, envelope.data, byte_len);
|
||||
blob_make_stone(msg);
|
||||
wota_buffer_free(&envelope);
|
||||
|
||||
const char *err = send_message(ctx->id, msg);
|
||||
if (!err) {
|
||||
/* Success — send_message took ownership of the blob.
|
||||
Free the WotaBuffer internals since we consumed them. */
|
||||
wota_buffer_free(wb);
|
||||
}
|
||||
/* On failure, send_message already destroyed the blob.
|
||||
Caller still owns wb and must free it. */
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
#include "stb_ds.h"
|
||||
#include "cell.h"
|
||||
#include "cell_internal.h"
|
||||
#include "pit_internal.h"
|
||||
|
||||
// --- Data Structures ---
|
||||
|
||||
// Simple linked list for the ready queue
|
||||
typedef struct actor_node {
|
||||
cell_rt *actor;
|
||||
JSContext *actor;
|
||||
struct actor_node *next;
|
||||
} actor_node;
|
||||
|
||||
// Timer node for the min-heap
|
||||
typedef struct {
|
||||
uint64_t execute_at_ns;
|
||||
cell_rt *actor;
|
||||
JSContext *actor;
|
||||
uint32_t timer_id;
|
||||
int is_native; // 1 for native remove timer, 0 for JS timer
|
||||
} timer_node;
|
||||
@@ -24,27 +24,27 @@ static actor_node *ready_head = NULL;
|
||||
static actor_node *ready_tail = NULL;
|
||||
static timer_node *timer_heap = NULL; // stb_ds array
|
||||
|
||||
static struct { char *key; cell_rt *value; } *actors = NULL; // stb_ds hashmap
|
||||
static struct { char *key; JSContext *value; } *actors = NULL; // stb_ds hashmap
|
||||
|
||||
static int shutting_down = 0;
|
||||
|
||||
// --- Forward Declarations ---
|
||||
|
||||
void actor_turn(cell_rt *actor);
|
||||
uint32_t actor_remove_cb(cell_rt *actor, uint32_t id, uint32_t interval);
|
||||
void actor_turn(JSContext *actor);
|
||||
uint32_t actor_remove_cb(JSContext *actor, uint32_t id, uint32_t interval);
|
||||
|
||||
// --- Heap Helpers ---
|
||||
|
||||
static void heap_push(uint64_t when, cell_rt *actor, uint32_t timer_id, int is_native) {
|
||||
static void heap_push(uint64_t when, JSContext *actor, uint32_t timer_id, int is_native) {
|
||||
timer_node node = { .execute_at_ns = when, .actor = actor, .timer_id = timer_id, .is_native = is_native };
|
||||
arrput(timer_heap, node);
|
||||
|
||||
|
||||
// Bubble up
|
||||
int i = arrlen(timer_heap) - 1;
|
||||
while (i > 0) {
|
||||
int parent = (i - 1) / 2;
|
||||
if (timer_heap[i].execute_at_ns >= timer_heap[parent].execute_at_ns) break;
|
||||
|
||||
|
||||
timer_node tmp = timer_heap[i];
|
||||
timer_heap[i] = timer_heap[parent];
|
||||
timer_heap[parent] = tmp;
|
||||
@@ -54,10 +54,10 @@ static void heap_push(uint64_t when, cell_rt *actor, uint32_t timer_id, int is_n
|
||||
|
||||
static int heap_pop(timer_node *out) {
|
||||
if (arrlen(timer_heap) == 0) return 0;
|
||||
|
||||
|
||||
*out = timer_heap[0];
|
||||
timer_node last = arrpop(timer_heap);
|
||||
|
||||
|
||||
if (arrlen(timer_heap) > 0) {
|
||||
timer_heap[0] = last;
|
||||
// Bubble down
|
||||
@@ -67,14 +67,14 @@ static int heap_pop(timer_node *out) {
|
||||
int left = 2 * i + 1;
|
||||
int right = 2 * i + 2;
|
||||
int smallest = i;
|
||||
|
||||
|
||||
if (left < n && timer_heap[left].execute_at_ns < timer_heap[smallest].execute_at_ns)
|
||||
smallest = left;
|
||||
if (right < n && timer_heap[right].execute_at_ns < timer_heap[smallest].execute_at_ns)
|
||||
smallest = right;
|
||||
|
||||
|
||||
if (smallest == i) break;
|
||||
|
||||
|
||||
timer_node tmp = timer_heap[i];
|
||||
timer_heap[i] = timer_heap[smallest];
|
||||
timer_heap[smallest] = tmp;
|
||||
@@ -90,10 +90,10 @@ void actor_initialize(void)
|
||||
{
|
||||
}
|
||||
|
||||
void actor_free(cell_rt *actor)
|
||||
void actor_free(JSContext *actor)
|
||||
{
|
||||
shdel(actors, actor->id);
|
||||
|
||||
|
||||
// Remove from ready queue if present (O(N))
|
||||
if (ready_head) {
|
||||
if (ready_head->actor == actor) {
|
||||
@@ -115,43 +115,31 @@ void actor_free(cell_rt *actor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JSContext *js = actor->context;
|
||||
|
||||
JS_DeleteGCRef(js, &actor->idx_buffer_ref);
|
||||
JS_DeleteGCRef(js, &actor->on_exception_ref);
|
||||
JS_DeleteGCRef(js, &actor->message_handle_ref);
|
||||
JS_DeleteGCRef(js, &actor->unneeded_ref);
|
||||
JS_DeleteGCRef(js, &actor->actor_sym_ref);
|
||||
|
||||
/* Free timer callbacks stored in actor */
|
||||
|
||||
for (int i = 0; i < hmlen(actor->timers); i++) {
|
||||
JS_FreeValue(js, actor->timers[i].value);
|
||||
}
|
||||
|
||||
|
||||
hmfree(actor->timers);
|
||||
|
||||
|
||||
/* Free all letters in the queue */
|
||||
for (int i = 0; i < arrlen(actor->letters); i++) {
|
||||
if (actor->letters[i].type == LETTER_BLOB) {
|
||||
blob_destroy(actor->letters[i].blob_data);
|
||||
} else if (actor->letters[i].type == LETTER_CALLBACK) {
|
||||
JS_FreeValue(js, actor->letters[i].callback);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
arrfree(actor->letters);
|
||||
|
||||
JS_FreeContext(js);
|
||||
|
||||
free(actor->id);
|
||||
|
||||
free(actor);
|
||||
|
||||
JS_FreeContext(actor);
|
||||
|
||||
int actor_count = shlen(actors);
|
||||
if (actor_count == 0) exit(0);
|
||||
}
|
||||
|
||||
void actor_unneeded(cell_rt *actor, JSValue fn, double seconds)
|
||||
void actor_unneeded(JSContext *actor, JSValue fn, double seconds)
|
||||
{
|
||||
if (actor->disrupt) return;
|
||||
|
||||
@@ -162,15 +150,10 @@ void actor_unneeded(cell_rt *actor, JSValue fn, double seconds)
|
||||
|
||||
actor->unneeded_ref.val = fn;
|
||||
actor->ar_secs = seconds;
|
||||
|
||||
|
||||
END:
|
||||
// If there was an existing unneeded timer, it will be handled/ignored in set_actor_state logic
|
||||
// or we can explicitly invalidate it if we tracked the ID.
|
||||
// For now, set_actor_state will schedule a new one if idle.
|
||||
if (actor->ar) {
|
||||
// In single threaded, we can't easily remove from heap O(N),
|
||||
// but we can just let it fire and check ID match.
|
||||
actor->ar = 0;
|
||||
actor->ar = 0;
|
||||
}
|
||||
set_actor_state(actor);
|
||||
}
|
||||
@@ -195,15 +178,15 @@ int actor_exists(const char *id)
|
||||
return idx != -1;
|
||||
}
|
||||
|
||||
void set_actor_state(cell_rt *actor)
|
||||
void set_actor_state(JSContext *actor)
|
||||
{
|
||||
if (actor->disrupt) {
|
||||
actor_free(actor);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// No mutex needed in single threaded
|
||||
|
||||
|
||||
switch(actor->state) {
|
||||
case ACTOR_RUNNING:
|
||||
case ACTOR_READY:
|
||||
@@ -212,59 +195,59 @@ void set_actor_state(cell_rt *actor)
|
||||
actor->ar = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case ACTOR_IDLE:
|
||||
if (arrlen(actor->letters)) {
|
||||
actor->state = ACTOR_READY;
|
||||
|
||||
|
||||
// Add to ready queue
|
||||
actor_node *n = malloc(sizeof(actor_node));
|
||||
n->actor = actor;
|
||||
n->next = NULL;
|
||||
|
||||
|
||||
if (ready_tail) {
|
||||
ready_tail->next = n;
|
||||
} else {
|
||||
ready_head = n;
|
||||
}
|
||||
ready_tail = n;
|
||||
|
||||
|
||||
} else if (!arrlen(actor->letters) && !hmlen(actor->timers)) {
|
||||
// Schedule remove timer
|
||||
static uint32_t global_timer_id = 1;
|
||||
uint32_t id = global_timer_id++;
|
||||
actor->ar = id;
|
||||
|
||||
|
||||
uint64_t now = cell_ns();
|
||||
uint64_t execute_at = now + (uint64_t)(actor->ar_secs * 1e9);
|
||||
|
||||
|
||||
heap_push(execute_at, actor, id, 1); // 1 = native remove
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t actor_remove_cb(cell_rt *actor, uint32_t id, uint32_t interval)
|
||||
uint32_t actor_remove_cb(JSContext *actor, uint32_t id, uint32_t interval)
|
||||
{
|
||||
// Check if this timer is still valid (match actor->ar)
|
||||
if (actor->ar != id && id != 0) {
|
||||
if (actor->ar != id && id != 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
actor->disrupt = 1;
|
||||
|
||||
|
||||
if (!JS_IsNull(actor->unneeded_ref.val)) {
|
||||
JSValue ret = JS_Call(actor->context, actor->unneeded_ref.val, JS_NULL, 0, NULL);
|
||||
uncaught_exception(actor->context, ret);
|
||||
JSValue ret = JS_Call(actor, actor->unneeded_ref.val, JS_NULL, 0, NULL);
|
||||
uncaught_exception(actor, ret);
|
||||
}
|
||||
|
||||
int should_free = (actor->state == ACTOR_IDLE);
|
||||
|
||||
|
||||
if (should_free) actor_free(actor);
|
||||
return 0;
|
||||
}
|
||||
|
||||
cell_rt *get_actor(char *id)
|
||||
JSContext *get_actor(char *id)
|
||||
{
|
||||
int idx = shgeti(actors, id);
|
||||
if (idx == -1) {
|
||||
@@ -282,19 +265,19 @@ void actor_loop()
|
||||
actor_node *node = ready_head;
|
||||
ready_head = node->next;
|
||||
if (!ready_head) ready_tail = NULL;
|
||||
|
||||
|
||||
actor_turn(node->actor);
|
||||
free(node);
|
||||
continue; // Loop again to check for more work or timers
|
||||
}
|
||||
|
||||
|
||||
// 2. Check Timers
|
||||
uint64_t now = cell_ns();
|
||||
if (arrlen(timer_heap) > 0) {
|
||||
if (timer_heap[0].execute_at_ns <= now) {
|
||||
timer_node t;
|
||||
heap_pop(&t);
|
||||
|
||||
|
||||
if (t.is_native) {
|
||||
actor_remove_cb(t.actor, t.timer_id, 0);
|
||||
} else {
|
||||
@@ -304,7 +287,6 @@ void actor_loop()
|
||||
JSValue cb = t.actor->timers[idx].value;
|
||||
hmdel(t.actor->timers, t.timer_id);
|
||||
actor_clock(t.actor, cb);
|
||||
JS_FreeValue(t.actor->context, cb);
|
||||
}
|
||||
}
|
||||
continue; // Loop again
|
||||
@@ -320,30 +302,32 @@ void actor_loop()
|
||||
}
|
||||
}
|
||||
|
||||
cell_rt *create_actor(void *wota)
|
||||
extern JSRuntime *g_runtime;
|
||||
|
||||
JSContext *create_actor(void *wota)
|
||||
{
|
||||
cell_rt *actor = calloc(sizeof(*actor), 1);
|
||||
JSContext *actor = JS_NewContext(g_runtime);
|
||||
if (!actor) return NULL;
|
||||
|
||||
actor->init_wota = wota;
|
||||
/* GCRef fields are registered after JSContext creation in script_startup. */
|
||||
|
||||
arrsetcap(actor->letters, 5);
|
||||
|
||||
// No mutexes needed
|
||||
actor->mutex = NULL;
|
||||
actor->msg_mutex = NULL;
|
||||
|
||||
JS_SetGCScanExternal(actor, actor_gc_scan);
|
||||
JS_SetHeapMemoryLimit(actor, ACTOR_MEMORY_LIMIT);
|
||||
|
||||
script_startup(actor);
|
||||
set_actor_state(actor);
|
||||
|
||||
return actor;
|
||||
}
|
||||
|
||||
const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar)
|
||||
const char *register_actor(const char *id, JSContext *actor, int mainthread, double ar)
|
||||
{
|
||||
actor->main_thread_only = mainthread;
|
||||
actor->id = strdup(id);
|
||||
actor->ar_secs = ar;
|
||||
|
||||
|
||||
if (shgeti(actors, id) != -1) {
|
||||
free(actor->id);
|
||||
return "Actor with given ID already exists.";
|
||||
@@ -355,82 +339,80 @@ const char *register_actor(const char *id, cell_rt *actor, int mainthread, doubl
|
||||
|
||||
const char *send_message(const char *id, void *msg)
|
||||
{
|
||||
cell_rt *target = get_actor(id);
|
||||
JSContext *target = get_actor(id);
|
||||
if (!target) {
|
||||
blob_destroy((blob *)msg);
|
||||
return "Could not get actor from id.";
|
||||
}
|
||||
|
||||
|
||||
letter l;
|
||||
l.type = LETTER_BLOB;
|
||||
l.blob_data = (blob *)msg;
|
||||
|
||||
|
||||
arrput(target->letters, l);
|
||||
|
||||
if (target->ar) {
|
||||
// Invalidate unneeded timer
|
||||
target->ar = 0;
|
||||
}
|
||||
|
||||
|
||||
set_actor_state(target);
|
||||
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void actor_turn(cell_rt *actor)
|
||||
void actor_turn(JSContext *actor)
|
||||
{
|
||||
|
||||
actor->state = ACTOR_RUNNING;
|
||||
actor->state = ACTOR_RUNNING;
|
||||
JSValue result;
|
||||
|
||||
|
||||
TAKETURN:
|
||||
|
||||
if (!arrlen(actor->letters)) {
|
||||
goto ENDTURN;
|
||||
}
|
||||
letter l = actor->letters[0];
|
||||
arrdel(actor->letters, 0);
|
||||
|
||||
if (l.type == LETTER_BLOB) {
|
||||
arrdel(actor->letters, 0);
|
||||
|
||||
if (l.type == LETTER_BLOB) {
|
||||
// Create a JS blob from the C blob
|
||||
size_t size = blob_length(l.blob_data) / 8; // Convert bits to bytes
|
||||
JSValue arg = js_new_blob_stoned_copy(actor->context, (void *)blob_data(l.blob_data), size);
|
||||
JSValue arg = js_new_blob_stoned_copy(actor, (void *)blob_data(l.blob_data), size);
|
||||
blob_destroy(l.blob_data);
|
||||
result = JS_Call(actor->context, actor->message_handle_ref.val, JS_NULL, 1, &arg);
|
||||
uncaught_exception(actor->context, result);
|
||||
JS_FreeValue(actor->context, arg);
|
||||
result = JS_Call(actor, actor->message_handle_ref.val, JS_NULL, 1, &arg);
|
||||
uncaught_exception(actor, result);
|
||||
} else if (l.type == LETTER_CALLBACK) {
|
||||
result = JS_Call(actor->context, l.callback, JS_NULL, 0, NULL);
|
||||
uncaught_exception(actor->context, result);
|
||||
JS_FreeValue(actor->context, l.callback);
|
||||
result = JS_Call(actor, l.callback, JS_NULL, 0, NULL);
|
||||
uncaught_exception(actor, result);
|
||||
}
|
||||
|
||||
|
||||
if (actor->disrupt) goto ENDTURN;
|
||||
|
||||
|
||||
ENDTURN:
|
||||
actor->state = ACTOR_IDLE;
|
||||
|
||||
actor->state = ACTOR_IDLE;
|
||||
|
||||
|
||||
set_actor_state(actor);
|
||||
}
|
||||
|
||||
void actor_clock(cell_rt *actor, JSValue fn)
|
||||
void actor_clock(JSContext *actor, JSValue fn)
|
||||
{
|
||||
letter l;
|
||||
l.type = LETTER_CALLBACK;
|
||||
l.callback = JS_DupValue(actor->context, fn);
|
||||
l.callback = fn;
|
||||
arrput(actor->letters, l);
|
||||
set_actor_state(actor);
|
||||
}
|
||||
|
||||
uint32_t actor_delay(cell_rt *actor, JSValue fn, double seconds)
|
||||
uint32_t actor_delay(JSContext *actor, JSValue fn, double seconds)
|
||||
{
|
||||
static uint32_t global_timer_id = 1;
|
||||
uint32_t id = global_timer_id++;
|
||||
|
||||
JSValue cb = JS_DupValue(actor->context, fn);
|
||||
|
||||
JSValue cb = fn;
|
||||
hmput(actor->timers, id, cb);
|
||||
|
||||
|
||||
uint64_t now = cell_ns();
|
||||
uint64_t execute_at = now + (uint64_t)(seconds * 1e9);
|
||||
|
||||
@@ -439,7 +421,7 @@ uint32_t actor_delay(cell_rt *actor, JSValue fn, double seconds)
|
||||
return id;
|
||||
}
|
||||
|
||||
JSValue actor_remove_timer(cell_rt *actor, uint32_t timer_id)
|
||||
JSValue actor_remove_timer(JSContext *actor, uint32_t timer_id)
|
||||
{
|
||||
JSValue cb = JS_NULL;
|
||||
int id = hmgeti(actor->timers, timer_id);
|
||||
|
||||
1252
source/suite.c
1252
source/suite.c
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@ var show_ir = false
|
||||
var show_check = false
|
||||
var show_types = false
|
||||
var show_diagnose = false
|
||||
var no_inline = false
|
||||
var filename = null
|
||||
var i = 0
|
||||
var di = 0
|
||||
@@ -33,6 +34,8 @@ for (i = 0; i < length(args); i++) {
|
||||
show_types = true
|
||||
} else if (args[i] == '--diagnose') {
|
||||
show_diagnose = true
|
||||
} else if (args[i] == '--no-inline') {
|
||||
no_inline = true
|
||||
} else if (args[i] == '--help' || args[i] == '-h') {
|
||||
log.console("Usage: cell streamline [--stats] [--ir] [--check] [--types] [--diagnose] <file>")
|
||||
$stop()
|
||||
@@ -58,6 +61,11 @@ var compiled = null
|
||||
if (show_diagnose) {
|
||||
compiled = shop.mcode_file(filename)
|
||||
compiled._warn = true
|
||||
if (no_inline) compiled._no_inline = true
|
||||
optimized = use('streamline')(compiled)
|
||||
} else if (no_inline) {
|
||||
compiled = shop.mcode_file(filename)
|
||||
compiled._no_inline = true
|
||||
optimized = use('streamline')(compiled)
|
||||
} else {
|
||||
optimized = shop.compile_file(filename)
|
||||
@@ -363,12 +371,12 @@ if (show_diagnose) {
|
||||
di = 0
|
||||
while (di < length(optimized._diagnostics)) {
|
||||
diag = optimized._diagnostics[di]
|
||||
print(`${diag.file}:${text(diag.line)}:${text(diag.col)}: ${diag.severity}: ${diag.message}`)
|
||||
log.compile(`${diag.file}:${text(diag.line)}:${text(diag.col)}: ${diag.severity}: ${diag.message}`)
|
||||
di = di + 1
|
||||
}
|
||||
print(`\n${text(length(optimized._diagnostics))} diagnostic(s)`)
|
||||
log.compile(`\n${text(length(optimized._diagnostics))} diagnostic(s)`)
|
||||
} else {
|
||||
print("No diagnostics.")
|
||||
log.compile("No diagnostics.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
637
streamline.cm
637
streamline.cm
@@ -46,13 +46,17 @@ var streamline = function(ir, log) {
|
||||
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
|
||||
is_array: true, is_func: true, is_record: true, is_stone: true,
|
||||
is_blob: true, is_data: true,
|
||||
is_true: true, is_false: true, is_fit: true,
|
||||
is_char: true, is_digit: true, is_letter: true,
|
||||
is_lower: true, is_upper: true, is_ws: true, is_actor: true
|
||||
}
|
||||
var type_check_map = {
|
||||
is_int: T_INT, is_text: T_TEXT, is_num: T_NUM,
|
||||
is_bool: T_BOOL, is_null: T_NULL,
|
||||
is_array: T_ARRAY, is_func: T_FUNCTION,
|
||||
is_record: T_RECORD
|
||||
is_record: T_RECORD, is_blob: T_BLOB
|
||||
}
|
||||
|
||||
// simplify_algebra dispatch tables
|
||||
@@ -65,12 +69,19 @@ var streamline = function(ir, log) {
|
||||
var no_clear_ops = {
|
||||
int: true, access: true, true: true, false: true, move: true, null: true,
|
||||
jump: true, jump_true: true, jump_false: true, jump_not_null: true,
|
||||
wary_true: true, wary_false: true, jump_null: true, jump_empty: true,
|
||||
return: true, disrupt: true,
|
||||
store_field: true, store_index: true, store_dynamic: true,
|
||||
push: true, setarg: true, invoke: true, tail_invoke: true,
|
||||
stone_text: true
|
||||
}
|
||||
|
||||
var is_cond_jump = function(op) {
|
||||
return op == "jump_true" || op == "jump_false" || op == "jump_not_null"
|
||||
|| op == "wary_true" || op == "wary_false"
|
||||
|| op == "jump_null" || op == "jump_empty"
|
||||
}
|
||||
|
||||
// --- Logging support ---
|
||||
|
||||
var ir_stats = null
|
||||
@@ -130,6 +141,9 @@ var streamline = function(ir, log) {
|
||||
return T_UNKNOWN
|
||||
}
|
||||
|
||||
var slot_is = null
|
||||
var write_rules = null
|
||||
|
||||
// track_types reuses write_rules table; move handled specially
|
||||
// Ops safe to narrow from T_NUM to T_INT when both operands are T_INT.
|
||||
// Excludes divide (int/int can produce float) and pow (int**neg produces float).
|
||||
@@ -181,7 +195,7 @@ var streamline = function(ir, log) {
|
||||
return null
|
||||
}
|
||||
|
||||
var slot_is = function(slot_types, slot, typ) {
|
||||
slot_is = function(slot_types, slot, typ) {
|
||||
var known = slot_types[slot]
|
||||
if (known == null) {
|
||||
return false
|
||||
@@ -260,11 +274,15 @@ var streamline = function(ir, log) {
|
||||
shr: [2, T_INT, 3, T_INT], ushr: [2, T_INT, 3, T_INT],
|
||||
bitnot: [2, T_INT],
|
||||
concat: [2, T_TEXT, 3, T_TEXT],
|
||||
not: [2, T_BOOL], and: [2, T_BOOL, 3, T_BOOL], or: [2, T_BOOL, 3, T_BOOL],
|
||||
and: [2, T_BOOL, 3, T_BOOL], or: [2, T_BOOL, 3, T_BOOL],
|
||||
store_index: [1, T_ARRAY, 2, T_INT], store_field: [1, T_RECORD],
|
||||
push: [1, T_ARRAY],
|
||||
load_index: [2, T_ARRAY, 3, T_INT], load_field: [2, T_RECORD],
|
||||
pop: [2, T_ARRAY]
|
||||
pop: [2, T_ARRAY],
|
||||
is_text: [2, T_UNKNOWN], is_int: [2, T_UNKNOWN], is_num: [2, T_UNKNOWN],
|
||||
is_bool: [2, T_UNKNOWN], is_null: [2, T_UNKNOWN],
|
||||
is_array: [2, T_UNKNOWN], is_func: [2, T_UNKNOWN],
|
||||
is_record: [2, T_UNKNOWN], is_blob: [2, T_UNKNOWN]
|
||||
}
|
||||
|
||||
var infer_param_types = function(func) {
|
||||
@@ -349,7 +367,7 @@ var streamline = function(ir, log) {
|
||||
// across label join points.
|
||||
// Uses data-driven dispatch: each rule is [dest_pos, type].
|
||||
// =========================================================
|
||||
var write_rules = {
|
||||
write_rules = {
|
||||
int: [1, T_INT], true: [1, T_BOOL], false: [1, T_BOOL],
|
||||
null: [1, T_NULL], access: [1, null],
|
||||
array: [1, T_ARRAY], record: [1, T_RECORD],
|
||||
@@ -374,7 +392,12 @@ var streamline = function(ir, log) {
|
||||
is_int: [1, T_BOOL], is_text: [1, T_BOOL], is_num: [1, T_BOOL],
|
||||
is_bool: [1, T_BOOL], is_null: [1, T_BOOL], is_identical: [1, T_BOOL],
|
||||
is_array: [1, T_BOOL], is_func: [1, T_BOOL],
|
||||
is_record: [1, T_BOOL], is_stone: [1, T_BOOL]
|
||||
is_record: [1, T_BOOL], is_stone: [1, T_BOOL],
|
||||
is_blob: [1, T_BOOL], is_data: [1, T_BOOL],
|
||||
is_true: [1, T_BOOL], is_false: [1, T_BOOL], is_fit: [1, T_BOOL],
|
||||
is_char: [1, T_BOOL], is_digit: [1, T_BOOL], is_letter: [1, T_BOOL],
|
||||
is_lower: [1, T_BOOL], is_upper: [1, T_BOOL], is_ws: [1, T_BOOL],
|
||||
is_actor: [1, T_BOOL]
|
||||
}
|
||||
|
||||
// Known intrinsic return types for invoke result inference.
|
||||
@@ -382,7 +405,12 @@ var streamline = function(ir, log) {
|
||||
abs: T_NUM, floor: T_NUM, ceiling: T_NUM,
|
||||
round: T_NUM, trunc: T_NUM, fraction: T_NUM,
|
||||
integer: T_NUM, whole: T_NUM, sign: T_NUM,
|
||||
max: T_NUM, min: T_NUM, remainder: T_NUM, modulo: T_NUM
|
||||
max: T_NUM, min: T_NUM, remainder: T_NUM, modulo: T_NUM,
|
||||
is_integer: T_BOOL, is_text: T_BOOL, is_number: T_BOOL,
|
||||
is_null: T_BOOL, is_array: T_BOOL, is_function: T_BOOL,
|
||||
is_object: T_BOOL, is_logical: T_BOOL, is_stone: T_BOOL,
|
||||
is_blob: T_BOOL, starts_with: T_BOOL, ends_with: T_BOOL,
|
||||
some: T_BOOL, every: T_BOOL
|
||||
}
|
||||
|
||||
var narrow_arith_type = function(write_types, param_types, instr, typ) {
|
||||
@@ -682,7 +710,49 @@ var streamline = function(ir, log) {
|
||||
if (is_array(next)) {
|
||||
next_op = next[0]
|
||||
|
||||
if (next_op == "jump_false" && next[1] == dest) {
|
||||
// is_null + jump fusion: replace with jump_null / jump_not_null
|
||||
if (op == "is_null" && (next_op == "jump_true" || next_op == "wary_true") && next[1] == dest) {
|
||||
jlen = length(next)
|
||||
nc = nc + 1
|
||||
instructions[i] = "_nop_tc_" + text(nc)
|
||||
instructions[i + 1] = ["jump_null", src, next[2], next[jlen - 2], next[jlen - 1]]
|
||||
if (events != null) {
|
||||
events[] = {
|
||||
event: "rewrite",
|
||||
pass: "eliminate_type_checks",
|
||||
rule: "is_null_jump_fusion",
|
||||
at: i,
|
||||
before: [instr, next],
|
||||
after: [instructions[i], instructions[i + 1]],
|
||||
why: {slot: src, fused_to: "jump_null"}
|
||||
}
|
||||
}
|
||||
slot_types[dest] = T_BOOL
|
||||
i = i + 2
|
||||
continue
|
||||
}
|
||||
if (op == "is_null" && (next_op == "jump_false" || next_op == "wary_false") && next[1] == dest) {
|
||||
jlen = length(next)
|
||||
nc = nc + 1
|
||||
instructions[i] = "_nop_tc_" + text(nc)
|
||||
instructions[i + 1] = ["jump_not_null", src, next[2], next[jlen - 2], next[jlen - 1]]
|
||||
if (events != null) {
|
||||
events[] = {
|
||||
event: "rewrite",
|
||||
pass: "eliminate_type_checks",
|
||||
rule: "is_null_jump_fusion",
|
||||
at: i,
|
||||
before: [instr, next],
|
||||
after: [instructions[i], instructions[i + 1]],
|
||||
why: {slot: src, fused_to: "jump_not_null"}
|
||||
}
|
||||
}
|
||||
slot_types[dest] = T_BOOL
|
||||
i = i + 2
|
||||
continue
|
||||
}
|
||||
|
||||
if ((next_op == "jump_false" || next_op == "wary_false") && next[1] == dest) {
|
||||
target_label = next[2]
|
||||
if (slot_is(slot_types, src, checked_type)) {
|
||||
nc = nc + 1
|
||||
@@ -758,7 +828,7 @@ var streamline = function(ir, log) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (next_op == "jump_true" && next[1] == dest) {
|
||||
if ((next_op == "jump_true" || next_op == "wary_true") && next[1] == dest) {
|
||||
target_label = next[2]
|
||||
if (slot_is(slot_types, src, checked_type)) {
|
||||
nc = nc + 1
|
||||
@@ -909,6 +979,32 @@ var streamline = function(ir, log) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Wary-to-certain: if slot type is T_BOOL, downgrade wary to certain jump
|
||||
if (op == "wary_true" && slot_is(slot_types, instr[1], T_BOOL)) {
|
||||
instr[0] = "jump_true"
|
||||
if (events != null) {
|
||||
events[] = {
|
||||
event: "rewrite",
|
||||
pass: "eliminate_type_checks",
|
||||
rule: "wary_to_certain",
|
||||
at: i, before: "wary_true", after: "jump_true",
|
||||
why: {slot: instr[1], known_type: T_BOOL}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (op == "wary_false" && slot_is(slot_types, instr[1], T_BOOL)) {
|
||||
instr[0] = "jump_false"
|
||||
if (events != null) {
|
||||
events[] = {
|
||||
event: "rewrite",
|
||||
pass: "eliminate_type_checks",
|
||||
rule: "wary_to_certain",
|
||||
at: i, before: "wary_false", after: "jump_false",
|
||||
why: {slot: instr[1], known_type: T_BOOL}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
track_types(slot_types, instr)
|
||||
i = i + 1
|
||||
}
|
||||
@@ -1066,11 +1162,12 @@ var streamline = function(ir, log) {
|
||||
next_op = next[0]
|
||||
nlen = length(next)
|
||||
|
||||
// not d, x; jump_false d, label → jump_true x, label
|
||||
// not d, x; jump_false d, label → wary_true x, label
|
||||
// (removing `not` removes coercion, so result must be wary)
|
||||
if (next_op == "jump_false" && next[1] == instr[1]) {
|
||||
nc = nc + 1
|
||||
instructions[i] = "_nop_bl_" + text(nc)
|
||||
instructions[i + 1] = ["jump_true", instr[2], next[2], next[nlen - 2], next[nlen - 1]]
|
||||
instructions[i + 1] = ["wary_true", instr[2], next[2], next[nlen - 2], next[nlen - 1]]
|
||||
if (events != null) {
|
||||
events[] = {
|
||||
event: "rewrite", pass: "simplify_booleans",
|
||||
@@ -1083,11 +1180,11 @@ var streamline = function(ir, log) {
|
||||
continue
|
||||
}
|
||||
|
||||
// not d, x; jump_true d, label → jump_false x, label
|
||||
// not d, x; jump_true d, label → wary_false x, label
|
||||
if (next_op == "jump_true" && next[1] == instr[1]) {
|
||||
nc = nc + 1
|
||||
instructions[i] = "_nop_bl_" + text(nc)
|
||||
instructions[i + 1] = ["jump_false", instr[2], next[2], next[nlen - 2], next[nlen - 1]]
|
||||
instructions[i + 1] = ["wary_false", instr[2], next[2], next[nlen - 2], next[nlen - 1]]
|
||||
if (events != null) {
|
||||
events[] = {
|
||||
event: "rewrite", pass: "simplify_booleans",
|
||||
@@ -1100,6 +1197,40 @@ var streamline = function(ir, log) {
|
||||
continue
|
||||
}
|
||||
|
||||
// not d, x; wary_false d, label → wary_true x, label
|
||||
if (next_op == "wary_false" && next[1] == instr[1]) {
|
||||
nc = nc + 1
|
||||
instructions[i] = "_nop_bl_" + text(nc)
|
||||
instructions[i + 1] = ["wary_true", instr[2], next[2], next[nlen - 2], next[nlen - 1]]
|
||||
if (events != null) {
|
||||
events[] = {
|
||||
event: "rewrite", pass: "simplify_booleans",
|
||||
rule: "not_wary_false_fusion", at: i,
|
||||
before: [instr, next],
|
||||
after: [instructions[i], instructions[i + 1]]
|
||||
}
|
||||
}
|
||||
i = i + 2
|
||||
continue
|
||||
}
|
||||
|
||||
// not d, x; wary_true d, label → wary_false x, label
|
||||
if (next_op == "wary_true" && next[1] == instr[1]) {
|
||||
nc = nc + 1
|
||||
instructions[i] = "_nop_bl_" + text(nc)
|
||||
instructions[i + 1] = ["wary_false", instr[2], next[2], next[nlen - 2], next[nlen - 1]]
|
||||
if (events != null) {
|
||||
events[] = {
|
||||
event: "rewrite", pass: "simplify_booleans",
|
||||
rule: "not_wary_true_fusion", at: i,
|
||||
before: [instr, next],
|
||||
after: [instructions[i], instructions[i + 1]]
|
||||
}
|
||||
}
|
||||
i = i + 2
|
||||
continue
|
||||
}
|
||||
|
||||
// not d1, x; not d2, d1 → move d2, x
|
||||
if (next_op == "not" && next[2] == instr[1]) {
|
||||
nc = nc + 1
|
||||
@@ -1123,6 +1254,13 @@ var streamline = function(ir, log) {
|
||||
return null
|
||||
}
|
||||
|
||||
var slot_idx_special = null
|
||||
var get_slot_refs = null
|
||||
var slot_def_special = null
|
||||
var slot_use_special = null
|
||||
var get_slot_defs = null
|
||||
var get_slot_uses = null
|
||||
|
||||
// =========================================================
|
||||
// Pass: eliminate_moves — copy propagation + self-move nop
|
||||
// Tracks move chains within basic blocks, substitutes read
|
||||
@@ -1187,7 +1325,7 @@ var streamline = function(ir, log) {
|
||||
}
|
||||
|
||||
// Control flow with a read at position 1: substitute then clear
|
||||
if (op == "return" || op == "jump_true" || op == "jump_false" || op == "jump_not_null") {
|
||||
if (op == "return" || is_cond_jump(op)) {
|
||||
actual = copies[text(instr[1])]
|
||||
if (actual != null) {
|
||||
instr[1] = actual
|
||||
@@ -1369,7 +1507,7 @@ var streamline = function(ir, log) {
|
||||
op = instr[0]
|
||||
if (op == "jump") {
|
||||
target = instr[1]
|
||||
} else if (op == "jump_true" || op == "jump_false" || op == "jump_not_null") {
|
||||
} else if (is_cond_jump(op)) {
|
||||
target = instr[2]
|
||||
}
|
||||
if (target != null && is_text(target)) {
|
||||
@@ -1576,7 +1714,7 @@ var streamline = function(ir, log) {
|
||||
if (is_number(tgt)) stack[] = tgt
|
||||
continue
|
||||
}
|
||||
if (op == "jump_true" || op == "jump_false" || op == "jump_not_null") {
|
||||
if (is_cond_jump(op)) {
|
||||
tgt = label_map[instr[2]]
|
||||
if (is_number(tgt)) stack[] = tgt
|
||||
stack[] = idx + 1
|
||||
@@ -1670,7 +1808,7 @@ var streamline = function(ir, log) {
|
||||
// =========================================================
|
||||
|
||||
// Which instruction positions hold slot references (special cases)
|
||||
var slot_idx_special = {
|
||||
slot_idx_special = {
|
||||
get: [1], put: [1],
|
||||
access: [1], int: [1], function: [1], regexp: [1],
|
||||
true: [1], false: [1], null: [1],
|
||||
@@ -1681,11 +1819,12 @@ var streamline = function(ir, log) {
|
||||
frame: [1, 2], goframe: [1, 2],
|
||||
jump: [], disrupt: [],
|
||||
jump_true: [1], jump_false: [1], jump_not_null: [1],
|
||||
wary_true: [1], wary_false: [1], jump_null: [1], jump_empty: [1],
|
||||
return: [1],
|
||||
stone_text: [1]
|
||||
}
|
||||
|
||||
var get_slot_refs = function(instr) {
|
||||
get_slot_refs = function(instr) {
|
||||
var special = slot_idx_special[instr[0]]
|
||||
var result = null
|
||||
var j = 0
|
||||
@@ -1702,38 +1841,41 @@ var streamline = function(ir, log) {
|
||||
}
|
||||
|
||||
// DEF/USE classification: which instruction positions are definitions vs uses
|
||||
var slot_def_special = {
|
||||
slot_def_special = {
|
||||
get: [1], put: [], access: [1], int: [1], function: [1], regexp: [1],
|
||||
true: [1], false: [1], null: [1], record: [1], array: [1],
|
||||
invoke: [2], tail_invoke: [2], goinvoke: [],
|
||||
move: [1], load_field: [1], load_index: [1], load_dynamic: [1],
|
||||
pop: [1], frame: [1], goframe: [1],
|
||||
setarg: [], store_field: [], store_index: [], store_dynamic: [],
|
||||
delete: [1],
|
||||
push: [], set_var: [], stone_text: [],
|
||||
jump: [], jump_true: [], jump_false: [], jump_not_null: [],
|
||||
wary_true: [], wary_false: [], jump_null: [], jump_empty: [],
|
||||
return: [], disrupt: []
|
||||
}
|
||||
|
||||
var slot_use_special = {
|
||||
slot_use_special = {
|
||||
get: [], put: [1], access: [], int: [], function: [], regexp: [],
|
||||
true: [], false: [], null: [], record: [], array: [],
|
||||
invoke: [1], tail_invoke: [1], goinvoke: [1],
|
||||
move: [2], load_field: [2], load_index: [2, 3], load_dynamic: [2, 3],
|
||||
pop: [2], frame: [2], goframe: [2],
|
||||
setarg: [1, 3], store_field: [1, 3], store_index: [1, 2, 3],
|
||||
store_dynamic: [1, 2, 3],
|
||||
store_dynamic: [1, 2, 3], delete: [2],
|
||||
push: [1, 2], set_var: [1], stone_text: [1],
|
||||
jump: [], jump_true: [1], jump_false: [1], jump_not_null: [1],
|
||||
wary_true: [1], wary_false: [1], jump_null: [1], jump_empty: [1],
|
||||
return: [1], disrupt: []
|
||||
}
|
||||
|
||||
var get_slot_defs = function(instr) {
|
||||
get_slot_defs = function(instr) {
|
||||
var special = slot_def_special[instr[0]]
|
||||
if (special != null) return special
|
||||
return [1]
|
||||
}
|
||||
|
||||
var get_slot_uses = function(instr) {
|
||||
get_slot_uses = function(instr) {
|
||||
var special = slot_use_special[instr[0]]
|
||||
var result = null
|
||||
var j = 0
|
||||
@@ -1861,7 +2003,7 @@ var streamline = function(ir, log) {
|
||||
target = null
|
||||
if (op == "jump") {
|
||||
target = instr[1]
|
||||
} else if (op == "jump_true" || op == "jump_false" || op == "jump_not_null") {
|
||||
} else if (is_cond_jump(op)) {
|
||||
target = instr[2]
|
||||
}
|
||||
if (target == null || !is_text(target)) {
|
||||
@@ -2582,6 +2724,390 @@ var streamline = function(ir, log) {
|
||||
return null
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Sensory function IR synthesis for callback inlining
|
||||
// =========================================================
|
||||
var sensory_opcodes = {
|
||||
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",
|
||||
is_blob: "is_blob", is_data: "is_data",
|
||||
is_true: "is_true", is_false: "is_false", is_fit: "is_fit",
|
||||
is_character: "is_char", is_digit: "is_digit", is_letter: "is_letter",
|
||||
is_lower: "is_lower", is_upper: "is_upper", is_whitespace: "is_ws",
|
||||
is_actor: "is_actor", length: "length"
|
||||
}
|
||||
|
||||
var make_sensory_ir = function(name) {
|
||||
var opcode = sensory_opcodes[name]
|
||||
if (opcode == null) return null
|
||||
return {
|
||||
name: name, nr_args: 1, nr_close_slots: 0, nr_slots: 3,
|
||||
instructions: [[opcode, 2, 1, 0, 0], ["return", 2, 0, 0]]
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Inline eligibility check
|
||||
// =========================================================
|
||||
var prefer_inline_set = {
|
||||
filter: true, every: true, some: true, arrfor: true,
|
||||
reduce: true, array: true
|
||||
}
|
||||
|
||||
// Structural eligibility: closures, get/put, disruption, nested functions
|
||||
var can_inline_structural = function(callee_func) {
|
||||
var instrs = null
|
||||
var i = 0
|
||||
var instr = null
|
||||
if (callee_func.nr_close_slots > 0) return false
|
||||
instrs = callee_func.instructions
|
||||
if (instrs == null) return false
|
||||
i = 0
|
||||
while (i < length(instrs)) {
|
||||
instr = instrs[i]
|
||||
if (is_array(instr)) {
|
||||
if (instr[0] == "get" || instr[0] == "put") {
|
||||
return false
|
||||
}
|
||||
// Reject if function creates child functions (closures may capture
|
||||
// from the inlined frame, breaking get/put slot references)
|
||||
if (instr[0] == "function") {
|
||||
return false
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
if (callee_func.disruption_pc != null && callee_func.disruption_pc > 0) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Size eligibility: instruction count check
|
||||
var can_inline_size = function(callee_func, is_prefer) {
|
||||
var instrs = callee_func.instructions
|
||||
var count = 0
|
||||
var i = 0
|
||||
var limit = 0
|
||||
if (instrs == null) return false
|
||||
i = 0
|
||||
while (i < length(instrs)) {
|
||||
if (is_array(instrs[i])) count = count + 1
|
||||
i = i + 1
|
||||
}
|
||||
limit = is_prefer ? 200 : 40
|
||||
return count <= limit
|
||||
}
|
||||
|
||||
var can_inline = function(callee_func, is_prefer) {
|
||||
if (!can_inline_structural(callee_func)) return false
|
||||
return can_inline_size(callee_func, is_prefer)
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Pass: inline_calls — inline same-module + sensory functions
|
||||
// =========================================================
|
||||
var inline_counter = 0
|
||||
|
||||
var inline_calls = function(func, ir, log) {
|
||||
var instructions = func.instructions
|
||||
var num_instr = 0
|
||||
var i = 0
|
||||
var j = 0
|
||||
var k = 0
|
||||
var instr = null
|
||||
var op = null
|
||||
var changed = false
|
||||
var inline_count = 0
|
||||
var max_inlines = 20
|
||||
var slot_to_func_idx = {}
|
||||
var slot_to_intrinsic = {}
|
||||
var callee_slot = 0
|
||||
var frame_slot = 0
|
||||
var argc = 0
|
||||
var result_slot = 0
|
||||
var call_start = 0
|
||||
var call_end = 0
|
||||
var arg_slots = null
|
||||
var callee_func = null
|
||||
var is_prefer = false
|
||||
var base = 0
|
||||
var remap = null
|
||||
var cinstr = null
|
||||
var cop = null
|
||||
var new_instr = null
|
||||
var refs = null
|
||||
var label_prefix = null
|
||||
var cont_label = null
|
||||
var spliced = null
|
||||
var before = null
|
||||
var after = null
|
||||
var inlined_body = null
|
||||
var fi = null
|
||||
var intrinsic_name = null
|
||||
var is_single_use = false
|
||||
var ref_count = 0
|
||||
var ri = 0
|
||||
|
||||
if (instructions == null) return false
|
||||
num_instr = length(instructions)
|
||||
if (num_instr == 0) return false
|
||||
|
||||
// Build resolution maps
|
||||
i = 0
|
||||
while (i < num_instr) {
|
||||
instr = instructions[i]
|
||||
if (is_array(instr)) {
|
||||
op = instr[0]
|
||||
if (op == "function") {
|
||||
slot_to_func_idx[text(instr[1])] = instr[2]
|
||||
} else if (op == "access" && is_object(instr[2]) && instr[2].make == "intrinsic") {
|
||||
slot_to_intrinsic[text(instr[1])] = instr[2].name
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
// Scan for frame/setarg/invoke sequences and inline
|
||||
i = 0
|
||||
while (i < length(instructions)) {
|
||||
instr = instructions[i]
|
||||
if (!is_array(instr) || instr[0] != "frame") {
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
if (inline_count >= max_inlines) {
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
|
||||
frame_slot = instr[1]
|
||||
callee_slot = instr[2]
|
||||
argc = instr[3]
|
||||
call_start = i
|
||||
|
||||
// Collect setarg and find invoke
|
||||
arg_slots = array(argc + 1, -1)
|
||||
j = i + 1
|
||||
call_end = -1
|
||||
while (j < length(instructions)) {
|
||||
instr = instructions[j]
|
||||
if (!is_array(instr)) {
|
||||
j = j + 1
|
||||
continue
|
||||
}
|
||||
op = instr[0]
|
||||
if (op == "setarg" && instr[1] == frame_slot) {
|
||||
arg_slots[instr[2]] = instr[3]
|
||||
} else if ((op == "invoke" || op == "tail_invoke") && instr[1] == frame_slot) {
|
||||
result_slot = instr[2]
|
||||
call_end = j
|
||||
j = j + 1
|
||||
break
|
||||
} else if (op == "frame" || op == "goframe") {
|
||||
// Another frame before invoke — abort this pattern
|
||||
break
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
|
||||
if (call_end < 0) {
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
|
||||
// Resolve callee
|
||||
callee_func = null
|
||||
is_prefer = false
|
||||
|
||||
fi = slot_to_func_idx[text(callee_slot)]
|
||||
if (fi != null && ir.functions != null && fi >= 0 && fi < length(ir.functions)) {
|
||||
callee_func = ir.functions[fi]
|
||||
}
|
||||
|
||||
if (callee_func == null) {
|
||||
intrinsic_name = slot_to_intrinsic[text(callee_slot)]
|
||||
if (intrinsic_name != null) {
|
||||
if (sensory_opcodes[intrinsic_name] != null) {
|
||||
callee_func = make_sensory_ir(intrinsic_name)
|
||||
}
|
||||
if (callee_func != null) {
|
||||
is_prefer = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (callee_func == null) {
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if callee is a single-use function literal — skip size limit
|
||||
is_single_use = false
|
||||
if (fi != null) {
|
||||
ref_count = 0
|
||||
ri = 0
|
||||
while (ri < length(instructions)) {
|
||||
if (is_array(instructions[ri])) {
|
||||
// Count frame instructions that use this slot as callee (position 2)
|
||||
if (instructions[ri][0] == "frame" && instructions[ri][2] == callee_slot) {
|
||||
ref_count = ref_count + 1
|
||||
}
|
||||
// Also count setarg where slot is passed as value (position 3)
|
||||
if (instructions[ri][0] == "setarg" && instructions[ri][3] == callee_slot) {
|
||||
ref_count = ref_count + 1
|
||||
}
|
||||
}
|
||||
ri = ri + 1
|
||||
}
|
||||
if (ref_count <= 1) is_single_use = true
|
||||
}
|
||||
|
||||
// Check eligibility
|
||||
if (!can_inline_structural(callee_func)) {
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
if (!is_single_use && !can_inline_size(callee_func, is_prefer)) {
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
|
||||
// Slot remapping
|
||||
base = func.nr_slots
|
||||
func.nr_slots = func.nr_slots + callee_func.nr_slots
|
||||
remap = array(callee_func.nr_slots, -1)
|
||||
|
||||
// Slot 0 (this) → arg_slots[0] if provided, else allocate a null slot
|
||||
if (length(arg_slots) > 0 && arg_slots[0] >= 0) {
|
||||
remap[0] = arg_slots[0]
|
||||
} else {
|
||||
remap[0] = base
|
||||
}
|
||||
|
||||
// Params 1..nr_args → corresponding arg_slots
|
||||
j = 1
|
||||
while (j <= callee_func.nr_args) {
|
||||
if (j < length(arg_slots) && arg_slots[j] >= 0) {
|
||||
remap[j] = arg_slots[j]
|
||||
} else {
|
||||
remap[j] = base + j
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
|
||||
// Temporaries → fresh slots
|
||||
j = callee_func.nr_args + 1
|
||||
while (j < callee_func.nr_slots) {
|
||||
remap[j] = base + j
|
||||
j = j + 1
|
||||
}
|
||||
|
||||
// Generate unique label prefix
|
||||
inline_counter = inline_counter + 1
|
||||
label_prefix = "_inl" + text(inline_counter) + "_"
|
||||
cont_label = label_prefix + "cont"
|
||||
|
||||
// Build inlined body with remapping
|
||||
// Unmapped params (e.g. caller passes 1 arg to a 2-param function)
|
||||
// must be explicitly nulled. compress_slots may merge the fresh
|
||||
// slot (base+j) with a previously-live slot that holds a non-null
|
||||
// value, so the default-param jump_not_null preamble would skip
|
||||
// the default assignment and use a stale value instead.
|
||||
inlined_body = []
|
||||
j = 0
|
||||
while (j <= callee_func.nr_args) {
|
||||
if (!(j < length(arg_slots) && arg_slots[j] >= 0)) {
|
||||
inlined_body[] = ["null", remap[j], 0, 0]
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
k = 0
|
||||
while (k < length(callee_func.instructions)) {
|
||||
cinstr = callee_func.instructions[k]
|
||||
|
||||
// Labels (strings that aren't nop markers)
|
||||
if (is_text(cinstr)) {
|
||||
if (starts_with(cinstr, "_nop_")) {
|
||||
inlined_body[] = cinstr
|
||||
} else {
|
||||
inlined_body[] = label_prefix + cinstr
|
||||
}
|
||||
k = k + 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (!is_array(cinstr)) {
|
||||
inlined_body[] = cinstr
|
||||
k = k + 1
|
||||
continue
|
||||
}
|
||||
|
||||
cop = cinstr[0]
|
||||
|
||||
// Handle return → move + jump to continuation
|
||||
if (cop == "return") {
|
||||
new_instr = ["move", result_slot, remap[cinstr[1]], cinstr[2], cinstr[3]]
|
||||
inlined_body[] = new_instr
|
||||
inlined_body[] = ["jump", cont_label, cinstr[2], cinstr[3]]
|
||||
k = k + 1
|
||||
continue
|
||||
}
|
||||
|
||||
// Clone and remap the instruction
|
||||
new_instr = array(cinstr)
|
||||
refs = get_slot_refs(cinstr)
|
||||
j = 0
|
||||
while (j < length(refs)) {
|
||||
if (new_instr[refs[j]] >= 0 && new_instr[refs[j]] < length(remap)) {
|
||||
new_instr[refs[j]] = remap[new_instr[refs[j]]]
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
|
||||
// Remap labels in jump instructions
|
||||
if (cop == "jump" && is_text(cinstr[1]) && !starts_with(cinstr[1], "_nop_")) {
|
||||
new_instr[1] = label_prefix + cinstr[1]
|
||||
} else if (is_cond_jump(cop)
|
||||
&& is_text(cinstr[2]) && !starts_with(cinstr[2], "_nop_")) {
|
||||
new_instr[2] = label_prefix + cinstr[2]
|
||||
}
|
||||
|
||||
// Skip function instructions (don't inline nested function definitions)
|
||||
if (cop == "function") {
|
||||
// Keep the instruction but don't remap func_id — it still refers to ir.functions
|
||||
// Only remap slot position 1 (the destination slot)
|
||||
new_instr = array(cinstr)
|
||||
if (cinstr[1] >= 0 && cinstr[1] < length(remap)) {
|
||||
new_instr[1] = remap[cinstr[1]]
|
||||
}
|
||||
}
|
||||
|
||||
inlined_body[] = new_instr
|
||||
k = k + 1
|
||||
}
|
||||
|
||||
// Add continuation label
|
||||
inlined_body[] = cont_label
|
||||
|
||||
// Splice: replace instructions[call_start..call_end] with inlined_body
|
||||
before = array(instructions, 0, call_start)
|
||||
after = array(instructions, call_end + 1, length(instructions))
|
||||
spliced = array(before, inlined_body)
|
||||
instructions = array(spliced, after)
|
||||
func.instructions = instructions
|
||||
|
||||
changed = true
|
||||
inline_count = inline_count + 1
|
||||
|
||||
// Continue scanning from after the inlined body
|
||||
i = call_start + length(inlined_body)
|
||||
}
|
||||
|
||||
return changed
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Compose all passes
|
||||
// =========================================================
|
||||
@@ -2684,13 +3210,12 @@ var streamline = function(ir, log) {
|
||||
ir._diagnostics = []
|
||||
}
|
||||
|
||||
// Process main function
|
||||
// Phase 1: Optimize all functions (bottom-up, existing behavior)
|
||||
if (ir.main != null) {
|
||||
optimize_function(ir.main, log)
|
||||
insert_stone_text(ir.main, log)
|
||||
}
|
||||
|
||||
// Process all sub-functions (resolve closure types from parent first)
|
||||
var fi = 0
|
||||
if (ir.functions != null) {
|
||||
fi = 0
|
||||
@@ -2702,7 +3227,63 @@ var streamline = function(ir, log) {
|
||||
}
|
||||
}
|
||||
|
||||
// Compress slots across all functions (must run after per-function passes)
|
||||
// Phase 2: Inline pass
|
||||
if (ir._no_inline) {
|
||||
return ir
|
||||
}
|
||||
var changed_main = false
|
||||
var changed_fns = null
|
||||
if (ir.main != null) {
|
||||
changed_main = inline_calls(ir.main, ir, log)
|
||||
}
|
||||
if (ir.functions != null) {
|
||||
changed_fns = array(length(ir.functions), false)
|
||||
fi = 0
|
||||
while (fi < length(ir.functions)) {
|
||||
changed_fns[fi] = inline_calls(ir.functions[fi], ir, log)
|
||||
fi = fi + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Re-optimize inlined functions
|
||||
if (changed_main) {
|
||||
optimize_function(ir.main, log)
|
||||
insert_stone_text(ir.main, log)
|
||||
}
|
||||
if (ir.functions != null) {
|
||||
fi = 0
|
||||
while (fi < length(ir.functions)) {
|
||||
if (changed_fns != null && changed_fns[fi]) {
|
||||
optimize_function(ir.functions[fi], log)
|
||||
insert_stone_text(ir.functions[fi], log)
|
||||
}
|
||||
fi = fi + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4: Cascade — second inline round (callbacks inside inlined bodies)
|
||||
if (changed_main) {
|
||||
changed_main = inline_calls(ir.main, ir, log)
|
||||
if (changed_main) {
|
||||
optimize_function(ir.main, log)
|
||||
insert_stone_text(ir.main, log)
|
||||
}
|
||||
}
|
||||
if (ir.functions != null) {
|
||||
fi = 0
|
||||
while (fi < length(ir.functions)) {
|
||||
if (changed_fns != null && changed_fns[fi]) {
|
||||
changed_fns[fi] = inline_calls(ir.functions[fi], ir, log)
|
||||
if (changed_fns[fi]) {
|
||||
optimize_function(ir.functions[fi], log)
|
||||
insert_stone_text(ir.functions[fi], log)
|
||||
}
|
||||
}
|
||||
fi = fi + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 5: Compress slots across all functions (must run after per-function passes)
|
||||
compress_slots(ir)
|
||||
|
||||
// Expose DEF/USE functions via log if requested
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// Test backward type propagation
|
||||
// Functions that use typed ops on parameters should have
|
||||
// parameter types inferred and type checks eliminated
|
||||
|
||||
var sum_ints = function(a, b, c) {
|
||||
return a + b + c
|
||||
}
|
||||
|
||||
var count_down = function(n) {
|
||||
var i = n
|
||||
var total = 0
|
||||
while (i > 0) {
|
||||
total = total + i
|
||||
i = i - 1
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
var concat_all = function(a, b, c) {
|
||||
return a + b + c
|
||||
}
|
||||
|
||||
if (sum_ints(1, 2, 3) != 6) { log.error("FAIL sum_ints") }
|
||||
if (count_down(5) != 15) { log.error("FAIL count_down") }
|
||||
if (concat_all("a", "b", "c") != "abc") { log.error("FAIL concat_all") }
|
||||
|
||||
log.test("backward type tests passed")
|
||||
@@ -1,12 +0,0 @@
|
||||
// Test array guard emission and optimization
|
||||
var a = [1, 2, 3]
|
||||
push(a, 4)
|
||||
var v = a[]
|
||||
|
||||
var f = function(arr) {
|
||||
push(arr, 99)
|
||||
var x = arr[]
|
||||
return x
|
||||
}
|
||||
|
||||
f(a)
|
||||
@@ -1,63 +0,0 @@
|
||||
// Test all inlined intrinsics
|
||||
var arr = [1, 2, 3]
|
||||
var rec = {a: 1}
|
||||
var fn = function() { return 1 }
|
||||
var txt = "hello"
|
||||
var num = 42
|
||||
var boo = true
|
||||
|
||||
// is_array
|
||||
if (!is_array(arr)) { log.error("FAIL is_array(arr)") }
|
||||
if (is_array(rec)) { log.error("FAIL is_array(rec)") }
|
||||
if (is_array(42)) { log.error("FAIL is_array(42)") }
|
||||
|
||||
// is_object
|
||||
if (!is_object(rec)) { log.error("FAIL is_object(rec)") }
|
||||
if (is_object(arr)) { log.error("FAIL is_object(arr)") }
|
||||
if (is_object(42)) { log.error("FAIL is_object(42)") }
|
||||
|
||||
// is_function
|
||||
if (!is_function(fn)) { log.error("FAIL is_function(fn)") }
|
||||
if (is_function(rec)) { log.error("FAIL is_function(rec)") }
|
||||
|
||||
// is_stone
|
||||
var frozen = stone([1, 2])
|
||||
if (!is_stone(frozen)) { log.error("FAIL is_stone(frozen)") }
|
||||
if (is_stone(arr)) { log.error("FAIL is_stone(arr)") }
|
||||
if (!is_stone(42)) { log.error("FAIL is_stone(42)") }
|
||||
if (!is_stone("hi")) { log.error("FAIL is_stone(str)") }
|
||||
|
||||
// length
|
||||
if (length(arr) != 3) { log.error("FAIL length(arr)") }
|
||||
if (length(txt) != 5) { log.error("FAIL length(txt)") }
|
||||
if (length([]) != 0) { log.error("FAIL length([])") }
|
||||
|
||||
// is_integer (already existed but now inlined)
|
||||
if (!is_integer(42)) { log.error("FAIL is_integer(42)") }
|
||||
if (is_integer(3.14)) { log.error("FAIL is_integer(3.14)") }
|
||||
|
||||
// is_text
|
||||
if (!is_text("hi")) { log.error("FAIL is_text(hi)") }
|
||||
if (is_text(42)) { log.error("FAIL is_text(42)") }
|
||||
|
||||
// is_number
|
||||
if (!is_number(42)) { log.error("FAIL is_number(42)") }
|
||||
if (!is_number(3.14)) { log.error("FAIL is_number(3.14)") }
|
||||
if (is_number("hi")) { log.error("FAIL is_number(hi)") }
|
||||
|
||||
// is_logical
|
||||
if (!is_logical(true)) { log.error("FAIL is_logical(true)") }
|
||||
if (is_logical(42)) { log.error("FAIL is_logical(42)") }
|
||||
|
||||
// is_null
|
||||
if (!is_null(null)) { log.error("FAIL is_null(null)") }
|
||||
if (is_null(42)) { log.error("FAIL is_null(42)") }
|
||||
|
||||
// push (inlined)
|
||||
var a = [1]
|
||||
push(a, 2)
|
||||
push(a, 3)
|
||||
if (length(a) != 3) { log.error("FAIL push length") }
|
||||
if (a[2] != 3) { log.error("FAIL push value") }
|
||||
|
||||
log.test("all intrinsic tests passed")
|
||||
@@ -1,42 +0,0 @@
|
||||
var fd = use("fd")
|
||||
var json = use("json")
|
||||
var tokenize = use("tokenize")
|
||||
var parse = use("parse")
|
||||
var fold = use("fold")
|
||||
|
||||
var src = text(fd.slurp("internal/bootstrap.cm"))
|
||||
var tok_result = tokenize(src, "bootstrap.cm")
|
||||
var ast = parse(tok_result.tokens, src, "bootstrap.cm", tokenize)
|
||||
var i = 0
|
||||
var folded = null
|
||||
var ast_json = null
|
||||
var f = null
|
||||
var bytecode = null
|
||||
var has_errors = ast.errors != null && length(ast.errors) > 0
|
||||
if (has_errors) {
|
||||
log.error("PARSE ERRORS:")
|
||||
while (i < length(ast.errors)) {
|
||||
log.error(text(ast.errors[i].line) + ":" + text(ast.errors[i].column) + " " + ast.errors[i].message)
|
||||
i = i + 1
|
||||
}
|
||||
} else {
|
||||
log.test("Parse OK")
|
||||
log.test(" statements: " + text(length(ast.statements)))
|
||||
if (ast.functions != null) {
|
||||
log.test(" functions: " + text(length(ast.functions)))
|
||||
} else {
|
||||
log.test(" functions: null")
|
||||
}
|
||||
folded = fold(ast)
|
||||
ast_json = json.encode(folded)
|
||||
f = fd.open("/tmp/bootstrap_ast.json", "w")
|
||||
fd.write(f, ast_json)
|
||||
fd.close(f)
|
||||
log.test("Wrote AST to /tmp/bootstrap_ast.json")
|
||||
bytecode = mach_compile_ast("bootstrap", ast_json)
|
||||
log.test("Bytecode size: " + text(length(bytecode)))
|
||||
f = fd.open("/tmp/bootstrap_test.mach", "w")
|
||||
fd.write(f, bytecode)
|
||||
fd.close(f)
|
||||
log.test("Wrote bytecode to /tmp/bootstrap_test.mach")
|
||||
}
|
||||
251
tests/cellfs_test.ce
Normal file
251
tests/cellfs_test.ce
Normal file
@@ -0,0 +1,251 @@
|
||||
// Test: cellfs mounting, sync access, and async requestors
|
||||
//
|
||||
// Known limitation:
|
||||
// - is_directory() uses raw path instead of res.path for fs mounts,
|
||||
// so @-prefixed paths fail (e.g. '@mount/dir'). Works via stat().
|
||||
// - cellfs.slurp() has no http code path; use cellfs.get() for http mounts.
|
||||
|
||||
var cellfs = use('cellfs')
|
||||
var fd = use('fd')
|
||||
|
||||
var pkg_dir = '.cell/packages/gitea.pockle.world/john/prosperon'
|
||||
|
||||
// Mount the prosperon package directory as 'prosperon'
|
||||
cellfs.mount(pkg_dir, 'prosperon')
|
||||
|
||||
// --- exists ---
|
||||
var found = cellfs.exists('@prosperon/color.cm')
|
||||
if (!found) {
|
||||
log.error("exists('@prosperon/color.cm') returned false")
|
||||
disrupt
|
||||
}
|
||||
log.console("exists: ok")
|
||||
|
||||
// exists returns false for missing files
|
||||
var missing = cellfs.exists('@prosperon/no_such_file.cm')
|
||||
if (missing) {
|
||||
log.error("exists returned true for missing file")
|
||||
disrupt
|
||||
}
|
||||
log.console("exists (missing): ok")
|
||||
|
||||
// --- slurp ---
|
||||
var data = cellfs.slurp('@prosperon/color.cm')
|
||||
if (!is_blob(data)) {
|
||||
log.error("slurp did not return a blob")
|
||||
disrupt
|
||||
}
|
||||
if (length(data) == 0) {
|
||||
log.error("slurp returned empty blob")
|
||||
disrupt
|
||||
}
|
||||
log.console(`slurp: ok (${length(data)} bits)`)
|
||||
|
||||
// --- enumerate ---
|
||||
var files = cellfs.enumerate('@prosperon', false)
|
||||
if (!is_array(files)) {
|
||||
log.error("enumerate did not return an array")
|
||||
disrupt
|
||||
}
|
||||
if (length(files) == 0) {
|
||||
log.error("enumerate returned empty array")
|
||||
disrupt
|
||||
}
|
||||
// color.cm should be in the listing
|
||||
var found_color = false
|
||||
arrfor(files, function(f) {
|
||||
if (f == 'color.cm') {
|
||||
found_color = true
|
||||
return true
|
||||
}
|
||||
}, false, true)
|
||||
if (!found_color) {
|
||||
log.error("enumerate did not include color.cm")
|
||||
disrupt
|
||||
}
|
||||
log.console(`enumerate: ok (${length(files)} entries, found color.cm)`)
|
||||
|
||||
// enumerate recursive
|
||||
var rfiles = cellfs.enumerate('@prosperon', true)
|
||||
if (length(rfiles) <= length(files)) {
|
||||
log.error("recursive enumerate should return more entries")
|
||||
disrupt
|
||||
}
|
||||
log.console(`enumerate recursive: ok (${length(rfiles)} entries)`)
|
||||
|
||||
// --- stat ---
|
||||
var st = cellfs.stat('@prosperon/color.cm')
|
||||
if (!is_object(st)) {
|
||||
log.error("stat did not return an object")
|
||||
disrupt
|
||||
}
|
||||
if (st.filesize == null || st.filesize == 0) {
|
||||
log.error("stat filesize missing or zero")
|
||||
disrupt
|
||||
}
|
||||
log.console(`stat: ok (size=${st.filesize}, mtime=${st.modtime})`)
|
||||
|
||||
// stat on a directory
|
||||
var dir_st = cellfs.stat('@prosperon/docs')
|
||||
if (!dir_st.isDirectory) {
|
||||
log.error("stat('@prosperon/docs').isDirectory returned false")
|
||||
disrupt
|
||||
}
|
||||
log.console("stat (directory): ok")
|
||||
|
||||
// --- searchpath ---
|
||||
var sp = cellfs.searchpath()
|
||||
if (!is_array(sp)) {
|
||||
log.error("searchpath did not return an array")
|
||||
disrupt
|
||||
}
|
||||
if (length(sp) == 0) {
|
||||
log.error("searchpath returned empty array")
|
||||
disrupt
|
||||
}
|
||||
log.console(`searchpath: ok (${length(sp)} mounts)`)
|
||||
|
||||
// --- resolve ---
|
||||
var res = cellfs.resolve('@prosperon/color.cm')
|
||||
if (!is_object(res)) {
|
||||
log.error("resolve did not return an object")
|
||||
disrupt
|
||||
}
|
||||
if (res.mount.name != 'prosperon') {
|
||||
log.error("resolve returned wrong mount name")
|
||||
disrupt
|
||||
}
|
||||
if (res.path != 'color.cm') {
|
||||
log.error("resolve returned wrong path")
|
||||
disrupt
|
||||
}
|
||||
log.console("resolve: ok")
|
||||
|
||||
// --- realdir ---
|
||||
var rd = cellfs.realdir('@prosperon/color.cm')
|
||||
if (!is_text(rd)) {
|
||||
log.error("realdir did not return text")
|
||||
disrupt
|
||||
}
|
||||
if (!ends_with(rd, 'color.cm')) {
|
||||
log.error("realdir does not end with color.cm")
|
||||
disrupt
|
||||
}
|
||||
log.console(`realdir: ok (${rd})`)
|
||||
|
||||
// --- unmount and re-mount ---
|
||||
cellfs.unmount('prosperon')
|
||||
var after_unmount = cellfs.searchpath()
|
||||
var unmount_ok = true
|
||||
arrfor(after_unmount, function(m) {
|
||||
if (m.name == 'prosperon') {
|
||||
unmount_ok = false
|
||||
return true
|
||||
}
|
||||
}, false, true)
|
||||
if (!unmount_ok) {
|
||||
log.error("unmount failed, mount still present")
|
||||
disrupt
|
||||
}
|
||||
log.console("unmount: ok")
|
||||
|
||||
// re-mount for further tests
|
||||
cellfs.mount(pkg_dir, 'prosperon')
|
||||
|
||||
// --- match (wildstar) ---
|
||||
var m1 = cellfs.match('color.cm', '*.cm')
|
||||
if (!m1) {
|
||||
log.error("match('color.cm', '*.cm') returned false")
|
||||
disrupt
|
||||
}
|
||||
var m2 = cellfs.match('color.cm', '*.ce')
|
||||
if (m2) {
|
||||
log.error("match('color.cm', '*.ce') returned true")
|
||||
disrupt
|
||||
}
|
||||
log.console("match: ok")
|
||||
|
||||
// --- globfs ---
|
||||
var cm_files = cellfs.globfs(['*.cm'], '@prosperon')
|
||||
if (!is_array(cm_files)) {
|
||||
log.error("globfs did not return an array")
|
||||
disrupt
|
||||
}
|
||||
if (length(cm_files) == 0) {
|
||||
log.error("globfs returned empty array")
|
||||
disrupt
|
||||
}
|
||||
// all results should end in .cm
|
||||
var all_cm = true
|
||||
arrfor(cm_files, function(f) {
|
||||
if (!ends_with(f, '.cm')) {
|
||||
all_cm = false
|
||||
return true
|
||||
}
|
||||
}, false, true)
|
||||
if (!all_cm) {
|
||||
log.error("globfs returned non-.cm files")
|
||||
disrupt
|
||||
}
|
||||
log.console(`globfs: ok (${length(cm_files)} .cm files)`)
|
||||
|
||||
log.console("--- sync tests passed ---")
|
||||
|
||||
// --- Requestor tests ---
|
||||
|
||||
// get requestor for a local fs mount
|
||||
var get_color = cellfs.get('@prosperon/color.cm')
|
||||
|
||||
get_color(function(result, reason) {
|
||||
if (reason != null) {
|
||||
log.error(`get color.cm failed: ${reason}`)
|
||||
disrupt
|
||||
}
|
||||
if (!is_blob(result)) {
|
||||
log.error("get did not return a blob")
|
||||
disrupt
|
||||
}
|
||||
if (length(result) == 0) {
|
||||
log.error("get returned empty blob")
|
||||
disrupt
|
||||
}
|
||||
log.console(`get (fs): ok (${length(result)} bits)`)
|
||||
|
||||
// parallel requestor test - fetch multiple files at once
|
||||
var get_core = cellfs.get('@prosperon/core.cm')
|
||||
var get_ease = cellfs.get('@prosperon/ease.cm')
|
||||
|
||||
parallel([get_color, get_core, get_ease])(function(results, reason) {
|
||||
if (reason != null) {
|
||||
log.error(`parallel get failed: ${reason}`)
|
||||
disrupt
|
||||
}
|
||||
if (length(results) != 3) {
|
||||
log.error(`parallel expected 3 results, got ${length(results)}`)
|
||||
disrupt
|
||||
}
|
||||
log.console(`parallel get: ok (${length(results)} files fetched)`)
|
||||
|
||||
// HTTP mount test — network may not be available in test env
|
||||
cellfs.mount('http://example.com', 'web')
|
||||
var web_res = cellfs.resolve('@web/')
|
||||
if (web_res.mount.type != 'http') {
|
||||
log.error("http mount type is not 'http'")
|
||||
disrupt
|
||||
}
|
||||
log.console("http mount: ok (type=http)")
|
||||
|
||||
var get_web = cellfs.get('@web/')
|
||||
get_web(function(body, reason) {
|
||||
if (reason != null) {
|
||||
log.console(`get (http): skipped (${reason})`)
|
||||
} else {
|
||||
log.console(`get (http): ok (${length(body)} bits)`)
|
||||
}
|
||||
|
||||
log.console("--- requestor tests passed ---")
|
||||
log.console("all cellfs tests passed")
|
||||
$stop()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,11 @@
|
||||
var blob = use('blob')
|
||||
var time = use('time')
|
||||
var random = use('random').random_fit
|
||||
|
||||
return {
|
||||
test_guid: function() {
|
||||
var st = time.number()
|
||||
var guid = blob(256, $random_fit)
|
||||
var guid = blob(256, random)
|
||||
stone(guid)
|
||||
var btime = time.number()-st
|
||||
st = time.number()
|
||||
|
||||
@@ -10,5 +10,7 @@ $portal(e => {
|
||||
$receiver(e => {
|
||||
log.console(`Got message: ${e}`)
|
||||
send(e, {greet: "Hello back!"})
|
||||
$delay(_ => $stop(), 0.2)
|
||||
})
|
||||
|
||||
// stop after portal is confirmed working
|
||||
var _t = $delay(_ => $stop(), 0.5)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Test pronto functions
|
||||
// Tests for fallback, parallel, race, sequence, time_limit, requestorize, objectify
|
||||
// Tests for fallback, parallel, race, sequence
|
||||
|
||||
var test_count = 0
|
||||
|
||||
@@ -89,49 +89,17 @@ return {
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
test_time_limit: function() {
|
||||
log.console("Testing time_limit...")
|
||||
var slow_req = make_requestor("slow_req", 0.5, true) // takes 0.5s
|
||||
var timed_req = time_limit(slow_req, 0.2) // 0.2s limit
|
||||
test_immediate_requestors: function() {
|
||||
log.console("Testing immediate requestors...")
|
||||
var req1 = make_requestor("imm1", 0, true)
|
||||
var req2 = make_requestor("imm2", 0, true)
|
||||
|
||||
timed_req(function(result, reason) {
|
||||
sequence([req1, req2])(function(result, reason) {
|
||||
if (result != null) {
|
||||
log.console(`Time limit succeeded: ${result}`)
|
||||
log.console(`Immediate sequence result: ${result}`)
|
||||
} else {
|
||||
log.console(`Time limit failed: ${reason}`)
|
||||
log.console(`Immediate sequence failed: ${reason}`)
|
||||
}
|
||||
}, 100)
|
||||
},
|
||||
|
||||
test_requestorize: function() {
|
||||
log.console("Testing requestorize...")
|
||||
var add_one = function(x) { return x + 1 }
|
||||
var req = requestorize(add_one)
|
||||
|
||||
req(function(result, reason) {
|
||||
if (result != null) {
|
||||
log.console(`Requestorize result: ${result}`)
|
||||
} else {
|
||||
log.console(`Requestorize failed: ${reason}`)
|
||||
}
|
||||
}, 42)
|
||||
},
|
||||
|
||||
test_objectify: function() {
|
||||
log.console("Testing objectify...")
|
||||
var req_a = make_requestor("obj_req_a", 0.1, true)
|
||||
var req_b = make_requestor("obj_req_b", 0.1, true)
|
||||
var req_c = make_requestor("obj_req_c", 0.1, true)
|
||||
|
||||
var parallel_obj = objectify(parallel)
|
||||
var req = parallel_obj({a: req_a, b: req_b, c: req_c})
|
||||
|
||||
req(function(result, reason) {
|
||||
if (result != null) {
|
||||
log.console(`Objectify result: ${result}`)
|
||||
} else {
|
||||
log.console(`Objectify failed: ${reason}`)
|
||||
}
|
||||
}, 1000)
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
@@ -456,11 +456,12 @@ return {
|
||||
},
|
||||
|
||||
test_mutual_recursion: function() {
|
||||
var isOdd = null
|
||||
var isEven = function(n) {
|
||||
if (n == 0) return true
|
||||
return isOdd(n - 1)
|
||||
}
|
||||
var isOdd = function(n) {
|
||||
isOdd = function(n) {
|
||||
if (n == 0) return false
|
||||
return isEven(n - 1)
|
||||
}
|
||||
|
||||
107
todo/jit.md
107
todo/jit.md
@@ -1,107 +0,0 @@
|
||||
Yep — what you’re describing is *exactly* how fast JS engines make “normal-looking arrays” fast: **an array carries an internal “elements kind”**, e.g. “all int32”, “all doubles”, “boxed values”, and it *transitions* to a more general representation the moment you store something that doesn’t fit. V8 literally documents this “elements kinds” + transitions model (packed smi → packed double → packed elements, plus “holey” variants). ([V8][1])
|
||||
|
||||
### 1) “Numbers-only until polluted” arrays: do it — but keep it brutally simple
|
||||
|
||||
For CellScript, you can get most of the win with just **three** internal kinds:
|
||||
|
||||
* **PACKED_I32** (or “fit int” if you want)
|
||||
* **PACKED_F64**
|
||||
* **PACKED_VALUE** (boxed JSValue)
|
||||
|
||||
Rules:
|
||||
|
||||
* On write, if kind is I32 and value is i32 → store unboxed.
|
||||
* If value is non-i32 number → upgrade whole backing store to F64.
|
||||
* If value is non-number → upgrade to VALUE.
|
||||
* Never downgrade (keep it one-way like V8 does). ([V8][1])
|
||||
|
||||
Extra credit that matters more than it sounds: **forbid sparse arrays** (or define them away). If an out-of-range write extends the array, *fill with null* so the storage remains dense. That keeps iteration tight and avoids “holey” variants (which are a real perf cliff in engines). ([V8][1])
|
||||
Your standard library already nudges toward dense arrays (constructors like `array(n)` fill with null).
|
||||
|
||||
### 2) Your “fast property op assuming data properties only” is the biggest bang
|
||||
|
||||
Since you’ve banned Proxy and all descriptor/accessor machinery , you can add VM ops that assume the world is sane:
|
||||
|
||||
* `GET_PROP_PLAIN`
|
||||
* `SET_PROP_PLAIN`
|
||||
|
||||
Then slap an **inline cache** (IC) on them: cache `(shape_id, slot_offset)` for a given property name/key. On hit → direct load/store by offset. On miss → slow path resolves, updates cache.
|
||||
|
||||
This is not hypothetical: QuickJS forks have pursued this; QuickJS-NG had discussion of polymorphic inline caches (PolyIC) with reported big wins in some forks. ([GitHub][2])
|
||||
|
||||
Even if you keep “objects are fully dynamic”, ICs still work great because most call sites are monomorphic in practice.
|
||||
|
||||
### 3) “What else should I remove to make JITing easier + faster?”
|
||||
|
||||
The best deletions are the ones that eliminate **invalidation** (stuff that forces “anything could change”):
|
||||
|
||||
1. **Prototype mutation** (you already forbid it; `meme` creates, “prototypes cannot be changed”).
|
||||
2. **Accessors / defineProperty / descriptors** (you already forbid it).
|
||||
3. **Proxy / Reflect** (already gone).
|
||||
4. **Property enumeration order guarantees** — and you already *don’t* guarantee key order for `array(object)`.
|
||||
That’s secretly huge: you can store properties in whatever layout is fastest (hash + compact slots) without “insertion order” bookkeeping.
|
||||
5. **Sparse arrays / hole semantics** (if you delete this, your array JIT story becomes *way* easier).
|
||||
|
||||
Stuff that’s *less* important than people think:
|
||||
|
||||
* Keeping `delete` as a keyword is fine *if* you implement it in a JIT-friendly way (next point).
|
||||
|
||||
### 4) You can keep `delete` without wrecking shapes: make it a “logical delete”
|
||||
|
||||
If you want “`obj[k] = null` deletes it”, you can implement deletion as:
|
||||
|
||||
* keep the slot/offset **stable**
|
||||
* store **null** and mark the property as “absent for enumeration / membership”
|
||||
|
||||
So the shape doesn’t thrash and cached offsets stay valid. `delete obj[k]` becomes the same thing.
|
||||
|
||||
That’s the trick: you keep the *semantics* of deletion, but avoid the worst-case performance behavior (shape churn) that makes JITs sad.
|
||||
|
||||
### 5) What assumptions do “meme + immutable prototypes” unlock?
|
||||
|
||||
Two big ones:
|
||||
|
||||
* **Prototype chain links never change**, so once you’ve specialized a load, you don’t need “prototype changed” invalidation machinery.
|
||||
* If your prototypes are usually **stone** (module exports from `use()` are stone) , then prototype *contents* don’t change either. That means caching “property X lives on prototype P at offset Y” is stable forever.
|
||||
|
||||
In a JIT or even in an interpreter with ICs, you can:
|
||||
|
||||
* guard receiver shape once per loop (or hoist it)
|
||||
* do direct loads either from receiver or a known prototype object
|
||||
|
||||
### 6) What do `stone` and `def` buy you, concretely?
|
||||
|
||||
**stone(value)** is a promise: “no more mutations, deep.”
|
||||
That unlocks:
|
||||
|
||||
* hoisting shape checks out of loops (because the receiver won’t change shape mid-loop)
|
||||
* for stone arrays: no push/pop → stable length + stable element kind
|
||||
* for stone objects: stable slot layout; you can treat them like read-only structs *when the key is known*
|
||||
|
||||
But: **stone doesn’t magically remove the need to identify which layout you’re looking at.** If the receiver is not a compile-time constant, you still need *some* guard (shape id or pointer class id). The win is you can often make that guard **once**, then blast through the loop.
|
||||
|
||||
**def** is about *bindings*, not object mutability:
|
||||
|
||||
* a `def` global / module binding can be constant-folded and inlined
|
||||
* a `def` that holds a `key()` capability makes `obj[that_key]` an excellent JIT target: the key identity is constant, so the lookup can be cached very aggressively.
|
||||
|
||||
### 7) LuaJIT comparison: what it’s doing, and where you could beat it
|
||||
|
||||
LuaJIT is fast largely because it’s a **tracing JIT**: it records a hot path, emits IR, and inserts **guards** that bail out if assumptions break. ([GitHub][3])
|
||||
Tables also have a split **array part + hash part** representation, which is why “array-ish” use is fast. ([Percona Community][4])
|
||||
|
||||
Could CellScript beat LuaJIT? Not as an interpreter. But with:
|
||||
|
||||
* unboxed dense arrays (like above),
|
||||
* plain-data-property ICs,
|
||||
* immutable prototypes,
|
||||
* plus either a trace JIT or a simple baseline JIT…
|
||||
|
||||
…you can absolutely be in “LuaJIT-ish” territory for the patterns you care about (actors + data + tight array loops). The big JS engines are still monsters in general-purpose optimization, but your *constraints* are real leverage if you cash them in at the VM/JIT level.
|
||||
|
||||
If you implement only two performance features this year: **(1) dense unboxed arrays with one-way kind transitions, and (2) inline-cached GET/SET for plain properties.** Everything else is garnish.
|
||||
|
||||
[1]: https://v8.dev/blog/elements-kinds?utm_source=chatgpt.com "Elements kinds in V8"
|
||||
[2]: https://github.com/quickjs-ng/quickjs/issues/116?utm_source=chatgpt.com "Optimization: Add support for Poly IC · Issue #116 · quickjs- ..."
|
||||
[3]: https://github.com/tarantool/tarantool/wiki/LuaJIT-SSA-IR?utm_source=chatgpt.com "LuaJIT SSA IR"
|
||||
[4]: https://percona.community/blog/2020/04/29/the-anatomy-of-luajit-tables-and-whats-special-about-them/?utm_source=chatgpt.com "The Anatomy of LuaJIT Tables and What's Special About Them"
|
||||
@@ -1,207 +0,0 @@
|
||||
Yep — here’s the concrete picture, with the “no-Proxy” trampoline approach, and what it can/can’t do.
|
||||
|
||||
## A concrete hot-reload example (with trampolines)
|
||||
|
||||
### `sprite.cell` v1
|
||||
|
||||
```js
|
||||
// sprite.cell
|
||||
var X = key('x')
|
||||
var Y = key('y')
|
||||
|
||||
def proto = {
|
||||
move: function(dx, dy) {
|
||||
this[X] += dx
|
||||
this[Y] += dy
|
||||
}
|
||||
}
|
||||
|
||||
var make = function(x, y) {
|
||||
var s = meme(proto)
|
||||
s[X] = x
|
||||
s[Y] = y
|
||||
return s
|
||||
}
|
||||
|
||||
return {
|
||||
proto: proto,
|
||||
make: make
|
||||
}
|
||||
```
|
||||
|
||||
### What the runtime stores on first load
|
||||
|
||||
Internally (not visible to Cell), you keep a per-module record:
|
||||
|
||||
```js
|
||||
module = {
|
||||
scope: { X, Y, proto, make }, // bindings created by var/def in module
|
||||
export_current: { proto, make }, // what the module returned
|
||||
export_handle: null // stable thing returned by use() in hot mode
|
||||
}
|
||||
```
|
||||
|
||||
Now, **in hot-reload mode**, `use('sprite')` returns `export_handle` instead of `export_current`. Since you don’t have Proxy/getters, the handle can only be “dynamic” for **functions** (because functions are called). So you generate trampolines for exported functions:
|
||||
|
||||
```js
|
||||
// export_handle is a plain object
|
||||
export_handle = stone({
|
||||
// stable reference (proto is identity-critical and will be patched in place)
|
||||
proto: module.scope.proto,
|
||||
|
||||
// trampoline (always calls the latest implementation)
|
||||
make: function(...args) {
|
||||
return module.scope.make.apply(null, args)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Note what this buys you:
|
||||
|
||||
* Anyone who cached `var sprite = use('sprite')` keeps the same `sprite` object forever.
|
||||
* Calling `sprite.make(...)` always goes through the trampoline and hits the *current* `module.scope.make`.
|
||||
|
||||
### Reload to `sprite.cell` v2
|
||||
|
||||
Say v2 changes `move` and `make`:
|
||||
|
||||
```js
|
||||
def proto = {
|
||||
move: function(dx, dy) {
|
||||
// new behavior
|
||||
this[X] = this[X] + dx * 2
|
||||
this[Y] = this[Y] + dy * 2
|
||||
}
|
||||
}
|
||||
|
||||
var make = function(x, y) { ... } // changed too
|
||||
return { proto, make }
|
||||
```
|
||||
|
||||
Runtime reload sequence (safe point: between actor turns):
|
||||
|
||||
1. Evaluate the new module to produce `new_scope` and `new_export`.
|
||||
2. Reconcile into the old module record:
|
||||
|
||||
* **`var` bindings:** rebind
|
||||
|
||||
* `old.scope.make = new.scope.make`
|
||||
* (and any other `var`s)
|
||||
|
||||
* **`def` bindings:** keep the binding identity, but if it’s an object you want hot-updatable, **patch in place**
|
||||
|
||||
* `old.scope.proto.move = new.scope.proto.move`
|
||||
* (and other fields on proto)
|
||||
|
||||
Now the magic happens:
|
||||
|
||||
* Existing instances `s` have prototype `old.scope.proto` (stable identity).
|
||||
* You patched `old.scope.proto.move` to point at the new function.
|
||||
* So `s.move(...)` immediately uses the new behavior.
|
||||
* And `sprite.make(...)` goes through the trampoline to `old.scope.make`, which you rebound to the new `make`.
|
||||
|
||||
That’s “real” hot reload without Proxy.
|
||||
|
||||
---
|
||||
|
||||
## “Module exports just a function” — yes, and it’s actually the easiest
|
||||
|
||||
If a module returns a function:
|
||||
|
||||
```js
|
||||
// returns a function directly
|
||||
return function(x) { ... }
|
||||
```
|
||||
|
||||
Hot-reload mode can return a **trampoline function**:
|
||||
|
||||
```js
|
||||
handle = stone(function(...args) {
|
||||
return module.scope.export_function.apply(this, args)
|
||||
})
|
||||
```
|
||||
|
||||
On reload, you rebind `module.scope.export_function` to the new function, and all cached references keep working.
|
||||
|
||||
---
|
||||
|
||||
## “Module exports just a string” — possible, but not hot-swappable by reference (without changing semantics)
|
||||
|
||||
If the export is a primitive (text/number/logical/null), there’s no call boundary to hang a trampoline on. If you do:
|
||||
|
||||
```js
|
||||
return "hello"
|
||||
```
|
||||
|
||||
Then anyone who did:
|
||||
|
||||
```js
|
||||
def msg = use('msg') // msg is a text value
|
||||
```
|
||||
|
||||
…is holding the text itself. You can’t “update” that value in place without either:
|
||||
|
||||
### Option 1: Accept the limitation (recommended)
|
||||
|
||||
* Hot reload still reloads the module.
|
||||
* But **previously returned primitive exports don’t change**; callers must call `use()` again to see the new value.
|
||||
|
||||
This keeps your semantics clean.
|
||||
|
||||
### Option 2: Dev-mode wrapping (changes semantics)
|
||||
|
||||
In hot-reload mode only, return a box/thunk instead:
|
||||
|
||||
* box: `{ get: function(){...} }`
|
||||
* thunk: `function(){ return current_text }`
|
||||
|
||||
But then code that expects a text breaks unless it’s written to handle the box/thunk. Usually not worth it unless you explicitly want “dev mode has different types”.
|
||||
|
||||
**Best convention:** if you want a reloadable “string export”, export a function:
|
||||
|
||||
```js
|
||||
var value = "hello"
|
||||
return { get: function() { return value } }
|
||||
```
|
||||
|
||||
Now `get()` is trampoline-able.
|
||||
|
||||
---
|
||||
|
||||
## About `var` vs `def` on reload
|
||||
|
||||
You’re very close, just phrase it precisely:
|
||||
|
||||
* **`var`**: binding is hot-rebindable
|
||||
On reload, `old.scope[name] = new.scope[name]`.
|
||||
|
||||
* **`def`**: binding identity is stable (const binding)
|
||||
On reload, you do **not** rebind the slot.
|
||||
|
||||
But: for `def` that points to **mutable objects that must preserve identity** (like prototypes), you *can still patch the object’s fields in place*:
|
||||
|
||||
* binding stays the same object
|
||||
* the object’s contents update
|
||||
|
||||
That’s not “setting new defs to old defs”; it’s “keeping old defs, optionally copying new content into them”.
|
||||
|
||||
If you want to avoid surprises, make one explicit rule:
|
||||
|
||||
* “def objects may be patched in place during hot reload; def primitives are never replaced.”
|
||||
|
||||
---
|
||||
|
||||
## One important consequence of “no Proxy / no getters”
|
||||
|
||||
Your trampoline trick only guarantees hot-reload for:
|
||||
|
||||
* exported **functions** (via trampolines)
|
||||
* exported **objects whose identity never changes** (like `proto`), because the handle can point at the stable old object
|
||||
|
||||
It **does not** guarantee hot-reload for exported scalars that you expect to change (because the handle can’t dynamically compute a property value).
|
||||
|
||||
That’s fine! It just becomes a convention: “export state through functions, export identity anchors as objects.”
|
||||
|
||||
---
|
||||
|
||||
If you keep those rules crisp in the doc, your hot reload story becomes genuinely robust *and* lightweight: most work is “rebind vars” + “patch proto tables” + “trampoline exported functions.” The rest is just conventions that make distributed actor code sane.
|
||||
3
util.cm
3
util.cm
@@ -1,5 +1,6 @@
|
||||
var shop = use('internal/shop')
|
||||
|
||||
return {
|
||||
file_reload: shop.file_reload
|
||||
file_reload: shop.file_reload,
|
||||
reload: shop.module_reload
|
||||
}
|
||||
3211
vm_suite.ce
3211
vm_suite.ce
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user