76 Commits

Author SHA1 Message Date
John Alanbrook
a1b41d5ecf rm push/pop 2026-02-26 08:13:18 -06:00
John Alanbrook
eb19b18594 slow messags 2026-02-26 00:56:43 -06:00
John Alanbrook
e203700d37 Merge branch 'fix_log_err' 2026-02-25 23:30:36 -06:00
John Alanbrook
c56444556d fix log in engine err 2026-02-25 23:30:32 -06:00
John Alanbrook
080e675d18 better update output 2026-02-25 23:29:37 -06:00
John Alanbrook
957b964d9d better disrupt message on fd 2026-02-25 20:38:34 -06:00
John Alanbrook
fa9d2609b1 fix fd bug 2026-02-25 20:25:36 -06:00
John Alanbrook
e38c2f07bf Merge branch 'async_fd' 2026-02-25 17:43:12 -06:00
John Alanbrook
ecc1777b24 async cellfs 2026-02-25 17:43:01 -06:00
John Alanbrook
1cfd5b8133 add hot reload to util 2026-02-25 17:28:11 -06:00
John Alanbrook
9c1141f408 Merge branch 'async_http' 2026-02-25 16:39:16 -06:00
John Alanbrook
696cca530b internal pronto 2026-02-25 16:39:12 -06:00
John Alanbrook
c92a4087a6 Merge branch 'async_fd' 2026-02-25 16:24:20 -06:00
John Alanbrook
01637c49b0 fix sendmessage 2026-02-25 16:24:08 -06:00
John Alanbrook
f9e660ebaa better log 2026-02-25 16:07:39 -06:00
John Alanbrook
4f8fada57d Merge branch 'audit_build' 2026-02-25 16:06:17 -06:00
John Alanbrook
adcaa92bea better logging for compiling 2026-02-25 16:05:37 -06:00
John Alanbrook
fc36707b39 Merge branch 'async_fd' 2026-02-25 16:03:55 -06:00
John Alanbrook
7cb8ce7945 async fd 2026-02-25 16:03:52 -06:00
John Alanbrook
bb7997a751 fix sendmessage 2026-02-25 15:26:20 -06:00
John Alanbrook
327b990442 Merge branch 'master' into async_http 2026-02-25 14:48:46 -06:00
John Alanbrook
51c0a0b306 tls and http 2026-02-25 14:48:37 -06:00
John Alanbrook
8ac82016dd api for sending wota messages direct 2026-02-25 12:45:02 -06:00
John Alanbrook
d0bf757d91 remove dupavlue and freevalue 2026-02-25 09:40:58 -06:00
John Alanbrook
c77f1f8639 move general http business to http and out of the probe cli 2026-02-24 21:06:43 -06:00
John Alanbrook
2b877e6b0c add http.cm and probe 2026-02-24 21:04:03 -06:00
John Alanbrook
3d4c0ec3d3 more comprehensive C suite 2026-02-24 17:51:36 -06:00
John Alanbrook
33d9013409 fix tests; add comprehensive tests for functions and fix bugs in the mach VM regarding them. 2026-02-24 17:41:18 -06:00
John Alanbrook
c2f57d1dae fix tests 2026-02-24 16:55:07 -06:00
John Alanbrook
7bd17c6476 Merge branch 'native_boot' 2026-02-23 19:07:07 -06:00
John Alanbrook
5ac1620b48 Merge branch 'fix_missing_stop' 2026-02-23 19:02:03 -06:00
John Alanbrook
124c9536b4 no more implicit returning in programs and mdouels 2026-02-23 19:01:58 -06:00
John Alanbrook
940807c37a parallel compiling; no more var hoisting; audit reports function hoisting 2026-02-23 18:57:47 -06:00
John Alanbrook
70f560550f rm tests 2026-02-23 18:16:07 -06:00
John Alanbrook
060a494f47 Merge branch 'fix_missing_stop' 2026-02-23 18:09:41 -06:00
John Alanbrook
6812d3edbc enet portal works 2026-02-23 18:09:35 -06:00
John Alanbrook
4da15d2a3e refactor 2026-02-23 18:08:13 -06:00
John Alanbrook
a34566a0c1 further refactor 2026-02-23 17:22:23 -06:00
John Alanbrook
9f7d861932 Merge branch 'audit_c_api' 2026-02-23 16:54:25 -06:00
John Alanbrook
c5536697ff merge cell 2026-02-23 16:54:19 -06:00
John Alanbrook
d066ab03cd correct caching 2026-02-23 13:37:11 -06:00
John Alanbrook
9c1cb43c7d fix repeated loads 2026-02-23 12:39:55 -06:00
John Alanbrook
99fb575c9c Merge branch 'native_boot' 2026-02-23 11:20:41 -06:00
John Alanbrook
193991c532 canonical paths 2026-02-23 11:18:08 -06:00
John Alanbrook
76552c6854 faster boot by refactor qbe_emit 2026-02-23 11:16:13 -06:00
John Alanbrook
f26b6e853d fix string indexing in native 2026-02-23 10:39:49 -06:00
John Alanbrook
94c28f0e17 more native without fallback 2026-02-23 10:20:18 -06:00
John Alanbrook
a18584afd3 Merge branch 'native_boot' 2026-02-23 09:18:31 -06:00
John Alanbrook
3f6cfad7ef fix gc scanning for env on native fns 2026-02-23 09:18:25 -06:00
John Alanbrook
b03edb0d90 Merge branch 'native_boot' 2026-02-22 20:48:19 -06:00
John Alanbrook
62440d3ed6 trace 2026-02-22 20:48:17 -06:00
John Alanbrook
4edc4b7cc5 native boot 2026-02-22 20:47:26 -06:00
John Alanbrook
012b507415 add -e flag 2026-02-22 11:24:27 -06:00
John Alanbrook
ee6398ada9 Merge branch 'master' into fuse_bug 2026-02-22 10:51:07 -06:00
John Alanbrook
173438e8bc add tests 2026-02-22 10:48:09 -06:00
John Alanbrook
7ac5ac63d2 Merge branch 'array_push_bug' 2026-02-22 10:35:00 -06:00
John Alanbrook
a05f180356 fix fuse bug 2026-02-22 10:34:55 -06:00
John Alanbrook
d88692cd30 fix inline issue 2026-02-22 10:31:15 -06:00
John Alanbrook
b0ac5de7e2 more comprehensive vm_suite 2026-02-22 10:08:40 -06:00
John Alanbrook
1d4fc11772 Merge remote-tracking branch 'origin/master' 2026-02-22 09:04:03 -06:00
John Alanbrook
7372b80e07 wary jumps 2026-02-21 20:58:24 -06:00
John Alanbrook
d27047dd82 Merge branch 'optimize_mcode' into fix_aot 2026-02-21 20:42:25 -06:00
John Alanbrook
8e96379377 wary booleans 2026-02-21 20:42:17 -06:00
John Alanbrook
8f415fea80 Merge branch 'optimize_mcode' 2026-02-21 19:45:14 -06:00
John Alanbrook
cec0b99207 correct apply check and add apply opcode 2026-02-21 19:43:05 -06:00
John Alanbrook
2d4645da9c Merge branch 'optimize_mcode' 2026-02-21 19:42:19 -06:00
John Alanbrook
017b63ba80 inline intrinsics 2026-02-21 19:23:53 -06:00
John Alanbrook
99fa86a09c asserts only for frame gets 2026-02-21 19:06:41 -06:00
John Alanbrook
6d6b53009f hash fingerprint for copmile chain 2026-02-21 17:25:14 -06:00
John Alanbrook
cc7fc6b667 fix inlining default params issue 2026-02-21 17:03:00 -06:00
John Alanbrook
bbeb757e40 faster 2026-02-21 16:23:44 -06:00
John Alanbrook
2ac446f7cf inline fns 2026-02-21 15:05:57 -06:00
John Alanbrook
517bd64275 Merge branch 'optimize_mcode' into fix_aot 2026-02-21 14:13:51 -06:00
John Alanbrook
7d0c96f328 inline 2026-02-21 13:44:34 -06:00
John Alanbrook
e7ed6bd8b2 Merge branch 'optimize_mcode' 2026-02-21 04:13:29 -06:00
John Alanbrook
700b640cf1 type propagation 2026-02-21 04:13:21 -06:00
126 changed files with 94732 additions and 50300 deletions

5
.gitignore vendored
View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
#include "quickjs.h"
#include "miniz.h"
#include "cell.h"
#include "miniz.h"
static JSClassID js_reader_class_id;
static JSClassID js_writer_class_id;
@@ -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);

View File

@@ -4,24 +4,44 @@
// 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]
}
@@ -32,37 +52,75 @@ 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 (function_hoist) {
tokenize_mod = use('tokenize')
parse_mod = use('parse')
arrfor(packages, function(p) {
var scripts = shop.get_package_scripts(p)
var pkg_dir = shop.get_package_dir(p)
if (length(scripts) == 0) return
arrfor(scripts, function(script) {
var src_path = pkg_dir + '/' + script
var src = null
var tok_result = null
var ast = null
var scan = function() {
if (!fd.is_file(src_path)) return
src = text(fd.slurp(src_path))
tok_result = tokenize_mod(src, script)
ast = parse_mod(tok_result.tokens, src, script, tokenize_mod)
if (ast._hoisted_fns != null && length(ast._hoisted_fns) > 0) {
log.console(p + '/' + script + ":")
hoist_files = hoist_files + 1
arrfor(ast._hoisted_fns, function(ref) {
var msg = " " + ref.name
if (ref.line != null) msg = msg + " (ref line " + text(ref.line)
if (ref.decl_line != null) msg = msg + ", declared line " + text(ref.decl_line)
if (ref.line != null) msg = msg + ")"
log.console(msg)
hoist_refs = hoist_refs + 1
})
}
} disruption {
// skip files that fail to parse
}
scan()
})
})
log.console("")
log.console("Summary: " + text(hoist_files) + " files with function hoisting, " + text(hoist_refs) + " total forward references")
return
}
arrfor(packages, function(p) {
var scripts = shop.get_package_scripts(p)
var result = null
var resolution = null
if (length(scripts) == 0) return
log.console("Auditing " + p + " (" + text(length(scripts)) + " scripts)...")
var result = shop.build_package_scripts(p)
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)
all_failures[] = p + ": " + e
})
// Check use() resolution
var resolution = shop.audit_use_resolution(p)
resolution = shop.audit_use_resolution(p)
arrfor(resolution.unresolved, function(u) {
push(all_unresolved, p + '/' + u.script + ": use('" + u.module + "') cannot be resolved")
all_unresolved[] = p + '/' + u.script + ": use('" + u.module + "') cannot be resolved"
})
})
@@ -83,11 +141,9 @@ if (length(all_unresolved) > 0) {
log.console("")
}
var summary = "Audit complete: " + text(total_ok) + "/" + text(total_scripts) + " scripts compiled"
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()

View File

@@ -28,7 +28,7 @@ function strip_mode_flags() {
} else if (a == '--compare') {
bench_mode = "compare"
} else {
push(filtered, a)
filtered[] = a
}
})
_args = filtered
@@ -197,7 +197,7 @@ function collect_benches(package_name, specific_bench) {
match_base = ends_with(match_name, '.cm') ? text(match_name, 0, -3) : match_name
if (bench_name != match_base) return
}
push(bench_files, f)
bench_files[] = f
}
})
return bench_files
@@ -355,7 +355,7 @@ function run_single_bench(bench_fn, bench_name) {
if (teardown_fn) teardown_fn(state)
ns_per_op = is_batch ? duration / batch_size : duration
push(timings_per_op, ns_per_op)
timings_per_op[] = ns_per_op
} else {
start = os.now()
if (is_batch) {
@@ -366,7 +366,7 @@ function run_single_bench(bench_fn, bench_name) {
duration = os.now() - start
ns_per_op = is_batch ? duration / batch_size : duration
push(timings_per_op, ns_per_op)
timings_per_op[] = ns_per_op
}
}
@@ -442,11 +442,11 @@ function load_bench_module(f, package_name, mode) {
function collect_bench_fns(bench_mod) {
var benches = []
if (is_function(bench_mod)) {
push(benches, {name: 'main', fn: bench_mod})
benches[] = {name: 'main', fn: bench_mod}
} else if (is_object(bench_mod)) {
arrfor(array(bench_mod), function(k) {
if (is_function(bench_mod[k]))
push(benches, {name: k, fn: bench_mod[k]})
benches[] = {name: k, fn: bench_mod[k]}
})
}
return benches
@@ -524,7 +524,7 @@ function run_benchmarks(package_name, specific_bench) {
result = run_single_bench(b.fn, b.name)
result.package = pkg_result.package
result.mode = bench_mode == "compare" ? "bytecode" : bench_mode
push(file_result.benchmarks, result)
file_result.benchmarks[] = result
pkg_result.total++
log.console(` ${result.name}`)
@@ -538,7 +538,7 @@ function run_benchmarks(package_name, specific_bench) {
nat_result = run_single_bench(native_benches[nat_b].fn, b.name)
nat_result.package = pkg_result.package
nat_result.mode = "native"
push(file_result.benchmarks, nat_result)
file_result.benchmarks[] = nat_result
pkg_result.total++
print_bench_result(nat_result, "native ")
@@ -570,7 +570,7 @@ function run_benchmarks(package_name, specific_bench) {
name: b.name,
error: "benchmark disrupted"
}
push(file_result.benchmarks, error_result)
file_result.benchmarks[] = error_result
pkg_result.total++
}
})
@@ -586,12 +586,12 @@ function run_benchmarks(package_name, specific_bench) {
name: "load_module",
error: "error loading module"
}
push(file_result.benchmarks, error_result)
file_result.benchmarks[] = error_result
pkg_result.total++
}
if (length(file_result.benchmarks) > 0) {
push(pkg_result.files, file_result)
pkg_result.files[] = file_result
}
})
@@ -604,15 +604,15 @@ var packages = null
if (all_pkgs) {
if (testlib.is_valid_package('.')) {
push(all_results, run_benchmarks(null, null))
all_results[] = run_benchmarks(null, null)
}
packages = shop.list_packages()
arrfor(packages, function(p) {
push(all_results, run_benchmarks(p, null))
all_results[] = run_benchmarks(p, null)
})
} else {
push(all_results, run_benchmarks(target_pkg, target_bench))
all_results[] = run_benchmarks(target_pkg, target_bench)
}
// Calculate totals
@@ -688,7 +688,7 @@ Total benchmarks: ${total_benches}
var pkg_benches = []
arrfor(pkg_res.files, function(f) {
arrfor(f.benchmarks, function(benchmark) {
push(pkg_benches, benchmark)
pkg_benches[] = benchmark
})
})

View File

@@ -1,194 +0,0 @@
// bench_native.ce — compare VM vs native execution speed
//
// Usage:
// cell --dev bench_native.ce <module.cm> [iterations]
//
// Compiles (if needed) and benchmarks a module via both VM and native dylib.
// Reports median/mean timing per benchmark + speedup ratio.
var os = use('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
}

View File

@@ -27,13 +27,13 @@ function send(mailbox, msg) {
function receive(mailbox) {
if (length(mailbox.queue) == 0) return null
mailbox.delivered++
return pop(mailbox.queue)
return mailbox.queue[]
}
function drain(mailbox) {
var count = 0
while (length(mailbox.queue) > 0) {
pop(mailbox.queue)
mailbox.queue[]
count++
}
return count

View File

@@ -13,13 +13,13 @@ function generate_records(n) {
var dept_vals = ["eng", "sales", "ops", "hr", "marketing"]
for (i = 0; i < n; i++) {
x = ((x * 1103515245 + 12345) & 0x7FFFFFFF) | 0
push(records, {
records[] = {
id: i + 1,
name: `user_${i}`,
score: (x % 1000) / 10,
status: status_vals[i % 4],
department: dept_vals[i % 5]
})
}
}
return records
}
@@ -30,7 +30,7 @@ function filter_records(records, field, value) {
var i = 0
for (i = 0; i < length(records); i++) {
if (records[i][field] == value) {
push(result, records[i])
result[] = records[i]
}
}
return result
@@ -45,7 +45,7 @@ function group_by(records, field) {
key = records[i][field]
if (!key) key = "unknown"
if (!groups[key]) groups[key] = []
push(groups[key], records[i])
groups[key][] = records[i]
}
return groups
}
@@ -70,13 +70,13 @@ function aggregate(groups) {
if (grp[j].score < mn) mn = grp[j].score
if (grp[j].score > mx) mx = grp[j].score
}
push(result, {
result[] = {
group: keys[i],
count: length(grp),
average: total / length(grp),
low: mn,
high: mx
})
}
}
return result
}

View File

@@ -57,7 +57,7 @@ function build_chain(n) {
var constraints = []
var i = 0
for (i = 0; i < n; i++) {
push(vars, make_variable(`v${i}`, 0))
vars[] = make_variable(`v${i}`, 0)
}
// Set first variable
@@ -69,8 +69,8 @@ function build_chain(n) {
self.variables[1].value = self.variables[0].value + 1
self.output = self.variables[1]
})
push(constraints, c)
push(vars[i].constraints, c)
constraints[] = c
vars[i].constraints[] = c
}
return {vars: vars, constraints: constraints}
@@ -83,8 +83,8 @@ function build_projection(n) {
var constraints = []
var i = 0
for (i = 0; i < n; i++) {
push(src, make_variable(`src${i}`, i * 10))
push(dst, make_variable(`dst${i}`, 0))
src[] = make_variable(`src${i}`, i * 10)
dst[] = make_variable(`dst${i}`, 0)
}
var scale_c = null
@@ -93,8 +93,8 @@ function build_projection(n) {
self.variables[1].value = self.variables[0].value * 2 + 1
self.output = self.variables[1]
})
push(constraints, scale_c)
push(dst[i].constraints, scale_c)
constraints[] = scale_c
dst[i].constraints[] = scale_c
}
return {src: src, dst: dst, constraints: constraints}

View File

@@ -12,7 +12,7 @@ function make_words(count) {
var words = []
var i = 0
for (i = 0; i < count; i++) {
push(words, base_words[i % length(base_words)])
words[] = base_words[i % length(base_words)]
}
return words
}
@@ -39,7 +39,7 @@ function top_n(freq, n) {
var pairs = []
var i = 0
for (i = 0; i < length(keys); i++) {
push(pairs, {word: keys[i], count: freq[keys[i]]})
pairs[] = {word: keys[i], count: freq[keys[i]]}
}
var sorted = sort(pairs, "count")
// Return last N (highest counts)
@@ -47,7 +47,7 @@ function top_n(freq, n) {
var start = length(sorted) - n
if (start < 0) start = 0
for (i = start; i < length(sorted); i++) {
push(result, sorted[i])
result[] = sorted[i]
}
return result
}
@@ -62,7 +62,7 @@ function group_by_length(words) {
w = words[i]
k = text(length(w))
if (!groups[k]) groups[k] = []
push(groups[k], w)
groups[k][] = w
}
return groups
}

View File

@@ -27,13 +27,13 @@ function make_array_data(size) {
var arr = []
var i = 0
for (i = 0; i < size; i++) {
push(arr, {
arr[] = {
id: i,
name: `item_${i}`,
active: i % 2 == 0,
score: i * 1.5,
tags: [`tag_${i % 5}`, `tag_${(i + 1) % 5}`]
})
}
}
return arr
}

View File

@@ -16,7 +16,7 @@ function make_obj_yx(x, y) {
function make_packed_array(n) {
var a = []
var i = 0
for (i = 0; i < n; i++) push(a, i)
for (i = 0; i < n; i++) a[] = i
return a
}
@@ -144,7 +144,7 @@ return {
var a = null
for (j = 0; j < n; j++) {
a = []
for (i = 0; i < 256; i++) push(a, i)
for (i = 0; i < 256; i++) a[] = i
x = (x + length(a)) | 0
}
return blackhole(sink, x)

View File

@@ -272,7 +272,7 @@ return {
for (i = 0; i < n; i++) {
push(a, i)
if (length(a) > 64) {
v = pop(a)
v = a[]
x = (x + v) | 0
}
}

View File

@@ -16,21 +16,21 @@ function tokenize(src) {
ch = chars[i]
if (ch == " " || ch == "\n" || ch == "\t") {
if (length(buf) > 0) {
push(tokens, buf)
tokens[] = buf
buf = ""
}
} else if (ch == "(" || ch == ")" || ch == "+" || ch == "-"
|| ch == "*" || ch == "=" || ch == ";" || ch == ",") {
if (length(buf) > 0) {
push(tokens, buf)
tokens[] = buf
buf = ""
}
push(tokens, ch)
tokens[] = ch
} else {
buf = buf + ch
}
}
if (length(buf) > 0) push(tokens, buf)
if (length(buf) > 0) tokens[] = buf
return tokens
}
@@ -49,21 +49,21 @@ function parse_tokens(tokens) {
i++ // skip =
i++
if (i < length(tokens)) node.value = tokens[i]
push(ast, node)
ast[] = node
} else if (tok == "return") {
node = {type: "return", value: null}
i++
if (i < length(tokens)) node.value = tokens[i]
push(ast, node)
ast[] = node
} else if (tok == "function") {
node = {type: "func", name: null, body: []}
i++
if (i < length(tokens)) node.name = tokens[i]
// Skip to matching )
while (i < length(tokens) && tokens[i] != ")") i++
push(ast, node)
ast[] = node
} else {
push(ast, {type: "expr", value: tok})
ast[] = {type: "expr", value: tok}
}
}
return ast
@@ -121,7 +121,7 @@ function simulate_build(n_modules, deps_per_module) {
// Generate all module sources
for (i = 0; i < n_modules; i++) {
src = generate_module(i, deps_per_module)
push(modules, src)
modules[] = src
}
// "Load" each module: tokenize → parse → evaluate
@@ -173,7 +173,7 @@ function topo_sort(n_modules, deps_per_module) {
for (j = 0; j < deps_per_module; j++) {
if (j < i) {
dep = "mod_" + text(j)
push(adj[dep], name)
adj[dep][] = name
in_degree[name] = in_degree[name] + 1
}
}
@@ -183,7 +183,7 @@ function topo_sort(n_modules, deps_per_module) {
var queue = []
var keys = array(in_degree)
for (i = 0; i < length(keys); i++) {
if (in_degree[keys[i]] == 0) push(queue, keys[i])
if (in_degree[keys[i]] == 0) queue[] = keys[i]
}
var order = []
@@ -193,12 +193,12 @@ function topo_sort(n_modules, deps_per_module) {
while (qi < length(queue)) {
current = queue[qi]
qi++
push(order, current)
order[] = current
neighbors = adj[current]
if (neighbors) {
for (i = 0; i < length(neighbors); i++) {
in_degree[neighbors[i]] = in_degree[neighbors[i]] - 1
if (in_degree[neighbors[i]] == 0) push(queue, neighbors[i])
if (in_degree[neighbors[i]] == 0) queue[] = neighbors[i]
}
}
}

View File

@@ -7,7 +7,7 @@ function make_random_array(n, seed) {
var i = 0
for (i = 0; i < n; i++) {
x = ((x * 1103515245 + 12345) & 0x7FFFFFFF) | 0
push(a, x % 10000)
a[] = x % 10000
}
return a
}
@@ -15,7 +15,7 @@ function make_random_array(n, seed) {
function make_descending(n) {
var a = []
var i = 0
for (i = n - 1; i >= 0; i--) push(a, i)
for (i = n - 1; i >= 0; i--) a[] = i
return a
}
@@ -58,19 +58,19 @@ function merge(a, b) {
var j = 0
while (i < length(a) && j < length(b)) {
if (a[i] <= b[j]) {
push(result, a[i])
result[] = a[i]
i++
} else {
push(result, b[j])
result[] = b[j]
j++
}
}
while (i < length(a)) {
push(result, a[i])
result[] = a[i]
i++
}
while (j < length(b)) {
push(result, b[j])
result[] = b[j]
j++
}
return result
@@ -97,7 +97,7 @@ function sort_records(n) {
var i = 0
for (i = 0; i < n; i++) {
x = ((x * 1103515245 + 12345) & 0x7FFFFFFF) | 0
push(records, {id: i, score: x % 10000, name: `item_${i}`})
records[] = {id: i, score: x % 10000, name: `item_${i}`}
}
return sort(records, "score")
}

View File

@@ -23,7 +23,7 @@ function build_index(txt) {
if (!index[w]) {
index[w] = []
}
push(index[w], i)
index[w][] = i
}
return index
}

View File

@@ -48,7 +48,7 @@ function tree_map(node, fn) {
function tree_flatten(node, result) {
if (!node) return null
tree_flatten(node.left, result)
push(result, node.val)
result[] = node.val
tree_flatten(node.right, result)
return null
}
@@ -126,7 +126,7 @@ return {
// Build a balanced BST of 1024 elements
var data = []
var i = 0
for (i = 0; i < 1024; i++) push(data, i)
for (i = 0; i < 1024; i++) data[] = i
var bst = build_balanced(data, 0, 1023)
var found = 0
for (i = 0; i < n; i++) {

View File

@@ -1,20 +0,0 @@
#!/bin/bash
# Run hyperfine with parameter lists
# This will create a cross-product of all libraries × all scenarios
hyperfine \
--warmup 3 \
--runs 20 \
-i \
--export-csv wota_vs_nota_vs_json.csv \
--export-json wota_vs_nota_vs_json.json \
--export-markdown wota_vs_nota_vs_json.md \
--parameter-list lib wota,nota,json \
--parameter-list scen empty,integers,floats,strings,objects,nested,large_array \
'cell benchmarks/wota_nota_json {lib} {scen}'
echo "Benchmark complete! Results saved to:"
echo " - wota_vs_nota_vs_json.csv"
echo " - wota_vs_nota_vs_json.json"
echo " - wota_vs_nota_vs_json.md"

View File

@@ -95,12 +95,12 @@ function benchArrayOps() {
var arr = [];
var j = 0
for (j = 0; j < iterations.medium; j++) {
push(arr, j);
arr[] = j;
}
});
var arr = [];
for (i = 0; i < 10000; i++) push(arr, i);
for (i = 0; i < 10000; i++) arr[] = i;
var accessTime = measureTime(function() {
var sum = 0;
@@ -188,7 +188,7 @@ function benchStringOps() {
});
for (i = 0; i < 1000; i++) {
push(strings, "string" + i);
strings[] = "string" + i;
}
var joinTime = measureTime(function() {
@@ -261,13 +261,13 @@ function benchClosures() {
var funcs = [];
var j = 0
for (j = 0; j < iterations.medium; j++) {
push(funcs, makeAdder(j));
funcs[] = makeAdder(j);
}
});
var adders = [];
for (i = 0; i < 1000; i++) {
push(adders, makeAdder(i));
adders[] = makeAdder(i);
}
var closureCallTime = measureTime(function() {

View File

@@ -15,7 +15,7 @@ var nll = null
var oll = null
for (i = 0; i < 10000; i++) {
accstr += i;
push(newarr, text(i))
newarr[] = text(i)
}
var jsonDecodeTimes = [];
var jsonEncodeTimes = [];
@@ -26,19 +26,19 @@ var notaSizes = [];
for (i = 0; i < 100; i++) {
start = os.now();
jll = json.decode(ll);
push(jsonDecodeTimes, (os.now() - start) * 1000);
jsonDecodeTimes[] = (os.now() - start) * 1000;
start = os.now();
jsonStr = JSON.stringify(jll);
push(jsonEncodeTimes, (os.now() - start) * 1000);
jsonEncodeTimes[] = (os.now() - start) * 1000;
start = os.now();
nll = nota.encode(jll);
push(notaEncodeTimes, (os.now() - start) * 1000);
notaEncodeTimes[] = (os.now() - start) * 1000;
start = os.now();
oll = nota.decode(nll);
push(notaDecodeTimes, (os.now() - start) * 1000);
notaDecodeTimes[] = (os.now() - start) * 1000;
}
function getStats(arr) {

File diff suppressed because it is too large Load Diff

View File

@@ -99,7 +99,7 @@ function runBenchmarkForLibrary(lib, bench) {
for (j = 0; j < length(bench.data); j++) {
e = lib.encode(bench.data[j]);
if (i == 0) {
push(encodedList, e);
encodedList[] = e;
totalSize += lib.getSize(e);
}
}

192
boot.ce Normal file
View File

@@ -0,0 +1,192 @@
// cell boot [--native] <program> - Pre-compile all module dependencies in parallel
//
// Discovers all transitive module dependencies for a program,
// checks which are not yet cached, and compiles uncached ones
// in parallel using worker actors composed via parallel() requestors.
//
// Also used as a child actor by engine.cm for auto-boot.
var shop = use('internal/shop')
var fd = use('fd')
var pkg_tools = use('package')
var build = use('build')
var is_native = false
var target_prog = null
var target_pkg = null
var i = 0
// Child actor mode: receive message from engine.cm
var _child_mode = false
var run_boot = null
$receiver(function(msg) {
_child_mode = true
is_native = msg.native || false
target_prog = msg.program
target_pkg = msg.package
run_boot()
})
// CLI mode: parse arguments
if (args && length(args) > 0) {
for (i = 0; i < length(args); i = i + 1) {
if (args[i] == '--native') {
is_native = true
} else if (args[i] == '--help' || args[i] == '-h') {
log.console("Usage: cell boot [--native] <program>")
log.console("")
log.console("Pre-compile all module dependencies for a program.")
log.console("Uncached modules are compiled in parallel.")
$stop()
} else if (!starts_with(args[i], '-')) {
target_prog = args[i]
}
}
if (!target_prog) {
log.error("boot: no program specified")
$stop()
}
}
// Discover all transitive module dependencies for a file
function discover_deps(file_path) {
return shop.trace_deps(file_path)
}
// Filter out already-cached modules
function filter_uncached(deps) {
var uncached = []
var j = 0
var s = null
j = 0
while (j < length(deps.scripts)) {
s = deps.scripts[j]
if (is_native) {
if (!shop.is_native_cached(s.path, s.package)) {
uncached[] = {type: 'native_script', path: s.path, package: s.package}
}
} else {
if (!shop.is_cached(s.path)) {
uncached[] = {type: 'script', path: s.path, package: s.package}
}
}
j = j + 1
}
// Expand C packages into individual files for parallel compilation
var target = build.detect_host_target()
var pkg = null
var c_files = null
var k = 0
j = 0
while (j < length(deps.c_packages)) {
pkg = deps.c_packages[j]
if (pkg != 'core') {
c_files = pkg_tools.get_c_files(pkg, target, true)
k = 0
while (k < length(c_files)) {
uncached[] = {type: 'c_file', package: pkg, file: c_files[k]}
k = k + 1
}
}
j = j + 1
}
return uncached
}
function item_name(item) {
if (item.path) return item.path
if (item.file) return item.package + '/' + item.file
return item.package
}
// Create a requestor that spawns a compile_worker actor for one item
function make_compile_requestor(item) {
var worker = null
var name = item_name(item)
return function(callback, value) {
log.console('boot: spawning worker for ' + name)
$start(function(event) {
if (event.type == 'greet') {
worker = event.actor
send(event.actor, {
type: item.type,
path: item.path,
package: item.package,
file: item.file
})
}
if (event.type == 'stop') {
callback(name)
}
if (event.type == 'disrupt') {
log.error('boot: worker failed for ' + name)
callback(null, {message: 'compile failed: ' + name})
}
}, 'compile_worker')
return function cancel(reason) {
if (worker) $stop(worker)
}
}
}
run_boot = function() {
var prog_path = null
var prog_info = null
var deps = null
var uncached = null
var requestors = null
var p = null
// Resolve the program path
if (target_prog) {
p = target_prog
if (ends_with(p, '.ce')) p = text(p, 0, -3)
prog_info = shop.resolve_program ? shop.resolve_program(p, target_pkg) : null
if (prog_info) {
prog_path = prog_info.path
if (!target_pkg && prog_info.pkg) target_pkg = prog_info.pkg
} else {
prog_path = p + '.ce'
if (!fd.is_file(prog_path)) {
prog_path = null
}
}
}
if (!prog_path || !fd.is_file(prog_path)) {
log.error('boot: could not find program: ' + text(target_prog || ''))
$stop()
return
}
// Discover all transitive deps
deps = discover_deps(prog_path)
uncached = filter_uncached(deps)
if (length(uncached) == 0) {
log.console('boot: all modules cached')
$stop()
return
}
// Compile uncached modules in parallel using worker actors
log.console('boot: ' + text(length(uncached)) + ' modules to compile')
requestors = array(uncached, make_compile_requestor)
parallel(requestors)(function(results, reason) {
if (reason) {
log.error('boot: ' + (reason.message || text(reason)))
} else {
log.console('boot: compiled ' + text(length(results)) + ' modules')
}
$stop()
}, null)
}
// CLI mode: start immediately
if (!_child_mode && target_prog) {
run_boot()
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2764
boot/qbe.cm.mcode Normal file

File diff suppressed because it is too large Load Diff

34991
boot/qbe_emit.cm.mcode Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

205
build.cm
View File

@@ -65,7 +65,7 @@ function replace_sigils(str, pkg_dir) {
function replace_sigils_array(flags, pkg_dir) {
var result = []
arrfor(flags, function(flag) {
push(result, replace_sigils(flag, pkg_dir))
result[] = replace_sigils(flag, pkg_dir)
})
return result
}
@@ -179,7 +179,7 @@ function bmfst_save(cmd_str, src_path, deps, obj_path) {
arrfor(deps, function(dep_path) {
var st = memo_stat(dep_path)
if (st)
push(entries, {p: dep_path, m: st.m, s: st.s})
entries[] = {p: dep_path, m: st.m, s: st.s}
})
var mf = {o: obj_path, d: entries}
var mf_path = bmfst_path(cmd_str, src_path)
@@ -191,16 +191,16 @@ function bmfst_save(cmd_str, src_path, deps, obj_path) {
function bmfst_dl_key(setup, link_info) {
var parts = [setup.cmd_str, setup.src_path]
push(parts, 'target:' + text(link_info.target))
push(parts, 'cc:' + text(link_info.cc))
parts[] = 'target:' + text(link_info.target)
parts[] = 'cc:' + text(link_info.cc)
arrfor(link_info.extra_objects, function(obj) {
if (obj != null) push(parts, 'extra:' + text(obj))
if (obj != null) parts[] = 'extra:' + text(obj)
})
arrfor(link_info.ldflags, function(flag) {
push(parts, 'ldflag:' + text(flag))
parts[] = 'ldflag:' + text(flag)
})
arrfor(link_info.target_ldflags, function(flag) {
push(parts, 'target_ldflag:' + text(flag))
parts[] = 'target_ldflag:' + text(flag)
})
return text(parts, '\n')
}
@@ -227,7 +227,7 @@ function bmfst_dl_save(setup, link_info, deps, dylib_path) {
arrfor(deps, function(dep_path) {
var st = memo_stat(dep_path)
if (st)
push(entries, {p: dep_path, m: st.m, s: st.s})
entries[] = {p: dep_path, m: st.m, s: st.s}
})
var mf = {dylib: dylib_path, d: entries}
var mf_path = cache_path(bmfst_dl_key(setup, link_info), SALT_BMFST_DL)
@@ -259,7 +259,7 @@ function get_c_deps(cc, flags, src_path) {
var dep_file = '/tmp/cell_deps_' + content_hash(src_path) + '.d'
var dep_cmd = [cc, '-MM', '-MG', '-MF', '"' + dep_file + '"']
dep_cmd = array(dep_cmd, flags)
push(dep_cmd, '"' + src_path + '"')
dep_cmd[] = '"' + src_path + '"'
var ret = os.system(text(dep_cmd, ' ') + ' 2>/dev/null')
if (ret != 0) return [src_path]
if (!fd.is_file(dep_file)) return [src_path]
@@ -274,9 +274,9 @@ function hash_all_deps(cmd_str, deps) {
arrfor(deps, function(dep_path) {
var content = memo_read(dep_path)
if (content != null)
push(parts, dep_path + '\n' + content)
parts[] = dep_path + '\n' + content
else
push(parts, dep_path + '\n<missing>')
parts[] = dep_path + '\n<missing>'
})
return text(parts, '\n')
}
@@ -310,16 +310,16 @@ function compile_setup(pkg, file, target, opts) {
common_flags = array(common_flags, ['-Os', '-DNDEBUG'])
}
push(common_flags, '-DCELL_USE_NAME=' + sym_name)
push(common_flags, '-I"' + pkg_dir + '"')
common_flags[] = '-DCELL_USE_NAME=' + sym_name
common_flags[] = '-I"' + pkg_dir + '"'
if (fd.is_dir(pkg_dir + '/include')) {
push(common_flags, '-I"' + pkg_dir + '/include"')
common_flags[] = '-I"' + pkg_dir + '/include"'
}
if (pkg != 'core') {
core_dir = shop.get_package_dir('core')
push(common_flags, '-I"' + core_dir + '/source"')
common_flags[] = '-I"' + core_dir + '/source"'
}
arrfor(cflags, function(flag) {
@@ -331,16 +331,16 @@ function compile_setup(pkg, file, target, opts) {
f = '-I"' + pkg_dir + '/' + ipath + '"'
}
}
push(common_flags, f)
common_flags[] = f
})
arrfor(target_cflags, function(flag) {
push(common_flags, flag)
common_flags[] = flag
})
var cmd_parts = [cc, '-c', '-fPIC']
cmd_parts = array(cmd_parts, common_flags)
push(cmd_parts, '"' + src_path + '"')
cmd_parts[] = '"' + src_path + '"'
return {
cmd_str: text(cmd_parts, ' '),
@@ -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)
@@ -513,7 +513,7 @@ Build.build_package = function(pkg, target, exclude_main, buildtype) {
arrfor(c_files, function(file) {
var obj = Build.compile_file(pkg, file, _target, {buildtype: _buildtype, cflags: cached_cflags})
push(objects, obj)
objects[] = obj
})
return objects
@@ -527,16 +527,16 @@ Build.build_package = function(pkg, target, exclude_main, buildtype) {
// link_opts: {extra_objects, ldflags, target_ldflags, target, cc}
function compute_dylib_content(full_content, link_opts) {
var parts = [full_content]
push(parts, 'target:' + text(link_opts.target))
push(parts, 'cc:' + text(link_opts.cc))
parts[] = 'target:' + text(link_opts.target)
parts[] = 'cc:' + text(link_opts.cc)
arrfor(link_opts.extra_objects, function(obj) {
if (obj != null) push(parts, 'extra:' + text(obj))
if (obj != null) parts[] = 'extra:' + text(obj)
})
arrfor(link_opts.ldflags, function(flag) {
push(parts, 'ldflag:' + text(flag))
parts[] = 'ldflag:' + text(flag)
})
arrfor(link_opts.target_ldflags, function(flag) {
push(parts, 'target_ldflag:' + text(flag))
parts[] = 'target_ldflag:' + text(flag)
})
return text(parts, '\n')
}
@@ -570,7 +570,7 @@ Build.build_module_dylib = function(pkg, file, target, opts) {
f = '-L"' + setup.pkg_dir + '/' + lpath + '"'
}
}
push(resolved_ldflags, f)
resolved_ldflags[] = f
})
var build_dir = get_build_dir()
@@ -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
@@ -681,25 +683,31 @@ Build.build_module_dylib = function(pkg, file, target, opts) {
'-Wl,-rpath,' + local_dir
])
} else if (tc.system == 'windows') {
push(cmd_parts, '-Wl,--allow-shlib-undefined')
cmd_parts[] = '-Wl,--allow-shlib-undefined'
}
push(cmd_parts, '-L"' + local_dir + '"')
push(cmd_parts, '"' + text(obj) + '"')
cmd_parts[] = '-L"' + local_dir + '"'
cmd_parts[] = '"' + text(obj) + '"'
arrfor(_extra, function(extra_obj) {
if (extra_obj != null) push(cmd_parts, '"' + text(extra_obj) + '"')
if (extra_obj != null) cmd_parts[] = '"' + text(extra_obj) + '"'
})
cmd_parts = array(cmd_parts, resolved_ldflags)
cmd_parts = array(cmd_parts, target_ldflags)
push(cmd_parts, '-o')
push(cmd_parts, '"' + dylib_path + '"')
cmd_parts[] = '-o'
cmd_parts[] = '"' + dylib_path + '"'
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) {
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) 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)
@@ -734,7 +767,7 @@ Build.build_dynamic = function(pkg, target, buildtype, opts) {
if (pkg != 'core') {
arrfor(sources, function(src_file) {
var obj = Build.compile_file(pkg, src_file, _target, {buildtype: _buildtype, cflags: cached_cflags, verbose: _opts.verbose, force: _opts.force})
if (obj != null) push(support_objects, obj)
if (obj != null) support_objects[] = obj
})
}
@@ -742,10 +775,16 @@ Build.build_dynamic = function(pkg, target, buildtype, opts) {
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})
results[] = {file: file, symbol: sym_name, dylib: dylib}
} else {
failed = failed + 1
}
done = done + 1
})
if (total > 0)
log.build(` Building C modules (${text(done)} ok${failed > 0 ? `, ${text(failed)} failed` : ''})`)
// Write manifest so runtime can find dylibs without the build module
var mpath = manifest_path(pkg)
fd.slurpwrite(mpath, stone(blob(json.encode(results))))
@@ -775,7 +814,7 @@ Build.build_static = function(packages, target, output, buildtype) {
var objects = Build.build_package(pkg, _target, !is_core, _buildtype)
arrfor(objects, function(obj) {
push(all_objects, obj)
all_objects[] = obj
})
// Collect LDFLAGS (with sigil replacement)
@@ -795,7 +834,7 @@ Build.build_static = function(packages, target, output, buildtype) {
f = '-L"' + pkg_dir + '/' + lpath + '"'
}
}
push(all_ldflags, f)
all_ldflags[] = f
})
}
})
@@ -817,18 +856,18 @@ Build.build_static = function(packages, target, output, buildtype) {
var cmd_parts = [cc]
arrfor(all_objects, function(obj) {
push(cmd_parts, '"' + obj + '"')
cmd_parts[] = '"' + obj + '"'
})
arrfor(all_ldflags, function(flag) {
push(cmd_parts, flag)
cmd_parts[] = flag
})
arrfor(target_ldflags, function(flag) {
push(cmd_parts, flag)
cmd_parts[] = flag
})
push(cmd_parts, '-o', '"' + out_path + '"')
cmd_parts[] = '-o', '"' + out_path + '"'
var cmd_str = text(cmd_parts, ' ')
@@ -882,7 +921,7 @@ function qbe_insert_dead_labels(il_text) {
line = lines[i]
trimmed = trim(line)
if (need_label && !starts_with(trimmed, '@') && !starts_with(trimmed, '}') && length(trimmed) > 0) {
push(result, "@_dead_" + text(dead_id))
result[] = "@_dead_" + text(dead_id)
dead_id = dead_id + 1
need_label = false
}
@@ -892,7 +931,7 @@ function qbe_insert_dead_labels(il_text) {
if (starts_with(trimmed, 'ret ') || starts_with(trimmed, 'jmp ')) {
need_label = true
}
push(result, line)
result[] = line
i = i + 1
}
return text(result, "\n")
@@ -920,9 +959,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 +1018,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 +1040,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 +1098,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
}
@@ -1067,16 +1122,16 @@ Build.compile_cm_to_mach = function(src_path) {
// output: path to write the generated .c file
Build.generate_module_table = function(modules, output) {
var lines = []
push(lines, '/* Generated module table — do not edit */')
push(lines, '#include <stddef.h>')
push(lines, '#include <string.h>')
push(lines, '')
push(lines, 'struct cell_embedded_entry {')
push(lines, ' const char *name;')
push(lines, ' const unsigned char *data;')
push(lines, ' size_t size;')
push(lines, '};')
push(lines, '')
lines[] = '/* Generated module table — do not edit */'
lines[] = '#include <stddef.h>'
lines[] = '#include <string.h>'
lines[] = ''
lines[] = 'struct cell_embedded_entry {'
lines[] = ' const char *name;'
lines[] = ' const unsigned char *data;'
lines[] = ' size_t size;'
lines[] = '};'
lines[] = ''
var entries = []
arrfor(modules, function(mod) {
@@ -1085,27 +1140,27 @@ Build.generate_module_table = function(modules, output) {
var bytes = array(mach)
var hex = []
arrfor(bytes, function(b) {
push(hex, '0x' + text(b, 'h2'))
hex[] = '0x' + text(b, 'h2')
})
push(lines, 'static const unsigned char mod_' + safe + '_data[] = {')
push(lines, ' ' + text(hex, ', '))
push(lines, '};')
push(lines, '')
push(entries, safe)
lines[] = 'static const unsigned char mod_' + safe + '_data[] = {'
lines[] = ' ' + text(hex, ', ')
lines[] = '};'
lines[] = ''
entries[] = safe
log.console('Embedded: ' + mod.name + ' (' + text(length(bytes)) + ' bytes)')
})
// Lookup function
push(lines, 'const struct cell_embedded_entry *cell_embedded_module_lookup(const char *name) {')
lines[] = 'const struct cell_embedded_entry *cell_embedded_module_lookup(const char *name) {'
arrfor(modules, function(mod, i) {
var safe = entries[i]
push(lines, ' if (strcmp(name, "' + mod.name + '") == 0) {')
push(lines, ' static const struct cell_embedded_entry e = {"' + mod.name + '", mod_' + safe + '_data, sizeof(mod_' + safe + '_data)};')
push(lines, ' return &e;')
push(lines, ' }')
lines[] = ' if (strcmp(name, "' + mod.name + '") == 0) {'
lines[] = ' static const struct cell_embedded_entry e = {"' + mod.name + '", mod_' + safe + '_data, sizeof(mod_' + safe + '_data)};'
lines[] = ' return &e;'
lines[] = ' }'
})
push(lines, ' return (void *)0;')
push(lines, '}')
lines[] = ' return (void *)0;'
lines[] = '}'
var c_text = text(lines, '\n')
fd.slurpwrite(output, stone(blob(c_text)))
@@ -1133,14 +1188,14 @@ Build.build_all_dynamic = function(target, buildtype, opts) {
// Build core first
if (find(packages, function(p) { return p == 'core' }) != null) {
core_mods = Build.build_dynamic('core', _target, _buildtype, _opts)
push(results, {package: 'core', modules: core_mods})
results[] = {package: 'core', modules: core_mods}
}
// Build other packages
arrfor(packages, function(pkg) {
if (pkg == 'core') return
var pkg_mods = Build.build_dynamic(pkg, _target, _buildtype, _opts)
push(results, {package: pkg, modules: pkg_mods})
results[] = {package: pkg, modules: pkg_mods}
})
// Print build report

154
cellfs.cm
View File

@@ -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)
}
})
}
}
mounts[] = mount_info
return
}
st = fd.stat(source)
if (st.isDirectory) {
mount_info.type = 'fs'
} else if (st.isFile) {
@@ -163,7 +187,7 @@ function mount(source, name) {
log.error("Unsupported mount source type: " + source); disrupt
}
push(mounts, mount_info)
mounts[] = mount_info
}
function unmount(name_or_source) {
@@ -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() {
@@ -317,7 +356,7 @@ function enumerate(_path, recurse) {
arrfor(list, function(item) {
var item_rel = rel_prefix ? rel_prefix + "/" + item : item
var child_st = null
push(results, item_rel)
results[] = item_rel
if (recurse) {
child_st = fd.stat(fd.join_paths(curr_full, item))
@@ -357,7 +396,7 @@ function enumerate(_path, recurse) {
if (!seen[rel]) {
seen[rel] = true
push(results, rel)
results[] = rel
}
}
})
@@ -416,7 +455,7 @@ function globfs(globs, _dir) {
}
} else {
if (!check_neg(item_rel) && check_pos(item_rel)) {
push(results, item_rel)
results[] = item_rel
}
}
})
@@ -440,7 +479,7 @@ function globfs(globs, _dir) {
if (length(rel) == 0) return
if (!check_neg(rel) && check_pos(rel)) {
push(results, rel)
results[] = rel
}
}
})
@@ -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

18
cfg.ce
View File

@@ -168,7 +168,7 @@ var run = function() {
if (is_array(instr)) {
if (block_start_pcs[text(pc)]) {
if (current_block != null) {
push(blocks, current_block)
blocks[] = current_block
}
current_block = {
id: length(blocks),
@@ -184,7 +184,7 @@ var run = function() {
}
if (current_block != null) {
push(current_block.instrs, {pc: pc, instr: instr})
current_block.instrs[] = {pc: pc, instr: instr}
current_block.end_pc = pc
n = length(instr)
line_num = instr[n - 2]
@@ -200,7 +200,7 @@ var run = function() {
ii = ii + 1
}
if (current_block != null) {
push(blocks, current_block)
blocks[] = current_block
}
// Build block index
@@ -235,19 +235,19 @@ var run = function() {
if (target_bi <= bi) {
edge_type = "loop back-edge"
}
push(blk.edges, {target: target_bi, kind: edge_type})
blk.edges[] = {target: target_bi, kind: edge_type}
}
if (is_conditional_jump(last_op)) {
if (bi + 1 < length(blocks)) {
push(blk.edges, {target: bi + 1, kind: "fallthrough"})
blk.edges[] = {target: bi + 1, kind: "fallthrough"}
}
}
} else if (is_terminator(last_op)) {
push(blk.edges, {target: -1, kind: "EXIT (" + last_op + ")"})
blk.edges[] = {target: -1, kind: "EXIT (" + last_op + ")"}
} else {
if (bi + 1 < length(blocks)) {
push(blk.edges, {target: bi + 1, kind: "fallthrough"})
blk.edges[] = {target: bi + 1, kind: "fallthrough"}
}
}
}
@@ -308,7 +308,7 @@ var run = function() {
parts = []
j = 1
while (j < n - 2) {
push(parts, fmt_val(instr[j]))
parts[] = fmt_val(instr[j])
j = j + 1
}
operands = text(parts, ", ")
@@ -381,7 +381,7 @@ var run = function() {
parts = []
j = 1
while (j < n - 2) {
push(parts, fmt_val(instr[j]))
parts[] = fmt_val(instr[j])
j = j + 1
}
operands = text(parts, ", ")

View File

@@ -93,13 +93,13 @@ if (is_shop_scope) {
packages_to_clean = shop.list_packages()
} else {
// Single package
push(packages_to_clean, scope)
packages_to_clean[] = scope
if (deep) {
_gather = function() {
deps = pkg.gather_dependencies(scope)
arrfor(deps, function(dep) {
push(packages_to_clean, dep)
packages_to_clean[] = dep
})
} disruption {
// Skip if can't read dependencies
@@ -116,11 +116,11 @@ var packages_dir = replace(shop.get_package_dir(''), /\/$/, '') // Get base pack
if (clean_build) {
// Nuke entire build cache (content-addressed, per-package clean impractical)
if (fd.is_dir(build_dir)) {
push(dirs_to_delete, build_dir)
dirs_to_delete[] = build_dir
}
// Clean orphaned lib/ directory if it exists (legacy)
if (fd.is_dir(lib_dir)) {
push(dirs_to_delete, lib_dir)
dirs_to_delete[] = lib_dir
}
}
@@ -128,7 +128,7 @@ if (clean_fetch) {
if (is_shop_scope) {
// Clean entire packages directory (dangerous!)
if (fd.is_dir(packages_dir)) {
push(dirs_to_delete, packages_dir)
dirs_to_delete[] = packages_dir
}
} else {
// Clean specific package directories
@@ -137,7 +137,7 @@ if (clean_fetch) {
var pkg_dir = shop.get_package_dir(p)
if (fd.is_dir(pkg_dir) || fd.is_link(pkg_dir)) {
push(dirs_to_delete, pkg_dir)
dirs_to_delete[] = pkg_dir
}
})
}

40
compile_worker.ce Normal file
View File

@@ -0,0 +1,40 @@
// compile_worker - Worker actor that compiles a single module and replies
//
// Receives a message with:
// {type: 'script', path, package} — bytecode compile
// {type: 'native_script', path, package} — native compile
// {type: 'c_package', package} — C package build
// {type: 'c_file', package, file} — single C module build
//
// Replies with {ok: true/false, path} and stops.
var shop = use('internal/shop')
var build = use('build')
$receiver(function(msg) {
var name = msg.path || (msg.file ? msg.package + '/' + msg.file : msg.package)
var _work = function() {
if (msg.type == 'script') {
log.console('compile_worker: compiling ' + name)
shop.precompile(msg.path, msg.package)
} else if (msg.type == 'native_script') {
log.console('compile_worker: native compiling ' + name)
build.compile_native(msg.path, null, null, msg.package)
} else if (msg.type == 'c_package') {
log.console('compile_worker: building package ' + name)
build.build_dynamic(msg.package, null, null, null)
} else if (msg.type == 'c_file') {
log.console('compile_worker: building ' + name)
build.compile_c_module(msg.package, msg.file)
}
log.console('compile_worker: done ' + name)
send(msg, {ok: true, path: name})
} disruption {
log.error('compile_worker: failed ' + name)
send(msg, {ok: false, error: 'compile failed'})
}
_work()
$stop()
})
var _t = $delay($stop, 120)

View File

@@ -1,4 +1,5 @@
#include "cell.h"
#include "pit_internal.h"
JSC_CCALL(os_mem_limit, JS_SetMemoryLimit(JS_GetRuntime(js), js2number(js,argv[0])))
JSC_CCALL(os_max_stacksize, JS_SetMaxStackSize(js, js2number(js,argv[0])))

24
diff.ce
View File

@@ -55,7 +55,7 @@ function collect_tests(specific_test) {
match_base = ends_with(match_name, '.cm') ? text(match_name, 0, -3) : match_name
if (test_name != match_base) continue
}
push(test_files, f)
test_files[] = f
}
}
return test_files
@@ -100,7 +100,7 @@ function diff_test_file(file_path) {
src = text(fd.slurp(src_path))
ast = analyze(src, src_path)
} disruption {
push(results.errors, `failed to parse ${file_path}`)
results.errors[] = `failed to parse ${file_path}`
return results
}
_read()
@@ -124,14 +124,14 @@ function diff_test_file(file_path) {
// Compare module-level behavior
if (opt_error != noopt_error) {
push(results.errors, `module load mismatch: opt=${opt_error != null ? opt_error : "ok"} noopt=${noopt_error != null ? noopt_error : "ok"}`)
results.errors[] = `module load mismatch: opt=${opt_error != null ? opt_error : "ok"} noopt=${noopt_error != null ? noopt_error : "ok"}`
results.failed = results.failed + 1
return results
}
if (opt_error != null) {
// Both disrupted during load — that's consistent
results.passed = results.passed + 1
push(results.tests, {name: "<module>", status: "passed"})
results.tests[] = {name: "<module>", status: "passed"}
return results
}
@@ -161,15 +161,15 @@ function diff_test_file(file_path) {
_run_one_noopt()
if (opt_err != noopt_err) {
push(results.tests, {name: k, status: "failed"})
push(results.errors, `${k}: disruption mismatch opt=${opt_err != null ? opt_err : "ok"} noopt=${noopt_err != null ? noopt_err : "ok"}`)
results.tests[] = {name: k, status: "failed"}
results.errors[] = `${k}: disruption mismatch opt=${opt_err != null ? opt_err : "ok"} noopt=${noopt_err != null ? noopt_err : "ok"}`
results.failed = results.failed + 1
} else if (!values_equal(opt_result, noopt_result)) {
push(results.tests, {name: k, status: "failed"})
push(results.errors, `${k}: result mismatch opt=${describe(opt_result)} noopt=${describe(noopt_result)}`)
results.tests[] = {name: k, status: "failed"}
results.errors[] = `${k}: result mismatch opt=${describe(opt_result)} noopt=${describe(noopt_result)}`
results.failed = results.failed + 1
} else {
push(results.tests, {name: k, status: "passed"})
results.tests[] = {name: k, status: "passed"}
results.passed = results.passed + 1
}
}
@@ -178,11 +178,11 @@ function diff_test_file(file_path) {
} else {
// Compare direct return values
if (!values_equal(mod_opt, mod_noopt)) {
push(results.tests, {name: "<return>", status: "failed"})
push(results.errors, `return value mismatch: opt=${describe(mod_opt)} noopt=${describe(mod_noopt)}`)
results.tests[] = {name: "<return>", status: "failed"}
results.errors[] = `return value mismatch: opt=${describe(mod_opt)} noopt=${describe(mod_noopt)}`
results.failed = results.failed + 1
} else {
push(results.tests, {name: "<return>", status: "passed"})
results.tests[] = {name: "<return>", status: "passed"}
results.passed = results.passed + 1
}
}

View File

@@ -93,7 +93,7 @@ var run = function() {
var operands = null
var line_str = null
while (j < n - 2) {
push(parts, fmt_val(instr[j]))
parts[] = fmt_val(instr[j])
j = j + 1
}
operands = text(parts, ", ")

View File

@@ -206,7 +206,7 @@ var run = function() {
parts = []
j = 1
while (j < n - 2) {
push(parts, fmt_val(instr[j]))
parts[] = fmt_val(instr[j])
j = j + 1
}
operands = text(parts, ", ")

233
docs/library/probe.md Normal file
View File

@@ -0,0 +1,233 @@
---
title: "probe"
description: "Runtime observability for actors"
weight: 90
type: "docs"
---
Runtime observability for actors. Register named probe functions on any actor and query them over HTTP while the program runs.
```javascript
var probe = use('probe')
```
The probe server starts automatically on the first `register()` call, listening on `127.0.0.1:9000`.
## Registering Probes
### probe.register(target, probes)
Register a group of probe functions under a target name. Each probe is a function that receives an `args` record and returns a value.
```javascript
var probe = use('probe')
var world = {
entities: [
{id: 1, name: "player", x: 10, y: 20, hp: 100},
{id: 2, name: "goblin", x: 55, y: 30, hp: 40}
],
tick: 0
}
probe.register("game", {
state: function(args) {
return world
},
entities: function(args) {
return world.entities
},
entity: function(args) {
return find(world.entities, function(e) {
return e.id == args.id
})
}
})
probe.register("render", {
info: function(args) {
return {fps: 60, draw_calls: 128, batches: 12}
}
})
```
A target is just a namespace — group related probes under the same target name. Register as many targets as you like; the server starts once and serves them all.
## HTTP Endpoints
All responses are JSON with an `ok` field.
### GET /discover
Lists all registered targets and their probe names. Designed for tooling — an LLM or dashboard can call this first to learn what's available, then query specific probes.
```
$ curl http://127.0.0.1:9000/discover
```
```json
{
"ok": true,
"targets": {
"game": ["state", "entities", "entity"],
"render": ["info"]
}
}
```
### POST /probe
Call a single probe function by target and name. Optionally pass arguments.
```
$ curl -X POST -H "Content-Type: application/json" \
-d '{"target":"game","name":"state"}' \
http://127.0.0.1:9000/probe
```
```json
{
"ok": true,
"result": {
"entities": [
{"id": 1, "name": "player", "x": 10, "y": 20, "hp": 100},
{"id": 2, "name": "goblin", "x": 55, "y": 30, "hp": 40}
],
"tick": 4821
}
}
```
With arguments:
```
$ curl -X POST -H "Content-Type: application/json" \
-d '{"target":"game","name":"entity","args":{"id":1}}' \
http://127.0.0.1:9000/probe
```
```json
{
"ok": true,
"result": {"id": 1, "name": "player", "x": 10, "y": 20, "hp": 100}
}
```
### POST /snapshot
Call multiple probes in one request. Returns all results keyed by `target/name`.
```
$ curl -X POST -H "Content-Type: application/json" \
-d '{"probes":[{"target":"game","name":"state"},{"target":"render","name":"info"}]}' \
http://127.0.0.1:9000/snapshot
```
```json
{
"ok": true,
"results": {
"game/state": {
"entities": [
{"id": 1, "name": "player", "x": 10, "y": 20, "hp": 100},
{"id": 2, "name": "goblin", "x": 55, "y": 30, "hp": 40}
],
"tick": 4821
},
"render/info": {
"fps": 60,
"draw_calls": 128,
"batches": 12
}
}
}
```
### Errors
Unknown paths return 404:
```json
{"ok": false, "error": "not found"}
```
Unknown targets or probe names:
```json
{"ok": false, "error": "unknown probe: game/nonexistent"}
```
If a probe function disrupts:
```json
{"ok": false, "error": "probe failed"}
```
## Example
A game actor with a simulation loop and probe observability:
```javascript
// game.ce
var probe = use('probe')
var state = {
entities: [
{id: 1, name: "player", x: 0, y: 0, hp: 100},
{id: 2, name: "enemy", x: 50, y: 50, hp: 60}
],
frame: 0,
paused: false
}
probe.register("game", {
state: function(args) {
return state
},
entities: function(args) {
return state.entities
},
entity: function(args) {
return find(state.entities, function(e) {
return e.id == args.id
})
}
})
// game loop
def tick = function(_) {
if (!state.paused) {
state.frame = state.frame + 1
// ... update entities, physics, AI ...
}
$delay(tick, 0.016)
}
$delay(tick, 0.016)
```
While the game runs, query it from a terminal:
```
$ curl -s http://127.0.0.1:9000/discover | jq .targets
{
"game": ["state", "entities", "entity"]
}
$ curl -s -X POST -d '{"target":"game","name":"state"}' \
-H "Content-Type: application/json" \
http://127.0.0.1:9000/probe | jq .result.frame
7834
$ curl -s -X POST -d '{"target":"game","name":"entity","args":{"id":1}}' \
-H "Content-Type: application/json" \
http://127.0.0.1:9000/probe | jq .result
{
"id": 1,
"name": "player",
"x": 142,
"y": 87,
"hp": 100
}
```
Probes run inside the actor's normal turn, so the values are always consistent — never a half-updated frame.

View File

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

View File

@@ -241,7 +241,7 @@ source/
**`cell_runtime.c`** is the single file that defines the native code contract. It should:
1. Include `quickjs-internal.h` for access to value representation and heap types
1. Include `pit_internal.h` for access to value representation and heap types
2. Export all `cell_rt_*` functions with C linkage (no `static`)
3. Keep each function thin — delegate to existing `JS_*` functions where possible
4. Handle GC safety: after any allocation (frame, string, array), callers' frames may have moved

2
fd.cm
View File

@@ -83,7 +83,7 @@ fd.globfs = function(globs, dir) {
}
} else {
if (!check_neg(item_rel) && check_pos(item_rel)) {
push(results, item_rel)
results[] = item_rel
}
}
});

29
fold.cm
View File

@@ -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
@@ -710,26 +709,26 @@ var fold = function(ast) {
if (sv != null && sv.nr_uses == 0) {
if (is_pure(stmt.right)) stmt.dead = true
if (stmt.right != null && stmt.right.kind == "(" && stmt.right.expression != null && stmt.right.expression.name == "use") {
push(ast._diagnostics, {
ast._diagnostics[] = {
severity: "warning",
line: stmt.left.from_row + 1,
col: stmt.left.from_column + 1,
message: `unused import '${name}'`
})
}
} else if (stmt.kind == "def") {
push(ast._diagnostics, {
ast._diagnostics[] = {
severity: "warning",
line: stmt.left.from_row + 1,
col: stmt.left.from_column + 1,
message: `unused constant '${name}'`
})
}
} else {
push(ast._diagnostics, {
ast._diagnostics[] = {
severity: "warning",
line: stmt.left.from_row + 1,
col: stmt.left.from_column + 1,
message: `unused variable '${name}'`
})
}
}
}
}
@@ -743,15 +742,15 @@ var fold = function(ast) {
sv = scope_var(fn_nr, stmt.name)
if (sv != null && sv.nr_uses == 0) {
stmt.dead = true
push(ast._diagnostics, {
ast._diagnostics[] = {
severity: "warning",
line: stmt.from_row + 1,
col: stmt.from_column + 1,
message: `unused function '${stmt.name}'`
})
}
}
if (stmt.dead != true) push(out, stmt)
}
if (stmt.dead != true) out[] = stmt
i = i + 1
}
return out
@@ -1040,7 +1039,7 @@ var fold = function(ast) {
i = 0
while (i < length(ast.intrinsics)) {
if (used_intrinsics[ast.intrinsics[i]] == true) {
push(new_intrinsics, ast.intrinsics[i])
new_intrinsics[] = ast.intrinsics[i]
}
i = i + 1
}
@@ -1072,16 +1071,16 @@ var fold = function(ast) {
fn_sv = scope_var(0, fn.name)
if (fn_sv != null && fn_sv.nr_uses == 0) {
fn.dead = true
push(ast._diagnostics, {
ast._diagnostics[] = {
severity: "warning",
line: fn.from_row + 1,
col: fn.from_column + 1,
message: `unused function '${fn.name}'`
})
}
}
}
if (fn.dead != true) {
push(live_fns, fn)
live_fns[] = fn
}
fi = fi + 1
}

12
fuzz.ce
View File

@@ -89,7 +89,7 @@ function run_fuzz(seed_val) {
var _parse = function() {
ast = analyze(src, name + ".cm")
} disruption {
push(errors, "parse error")
errors[] = "parse error"
}
_parse()
if (length(errors) > 0) return {seed: seed_val, errors: errors, src: src}
@@ -112,7 +112,7 @@ function run_fuzz(seed_val) {
// Check module-level behavior
if (opt_err != noopt_err) {
push(errors, `module load: opt=${opt_err != null ? opt_err : "ok"} noopt=${noopt_err != null ? noopt_err : "ok"}`)
errors[] = `module load: opt=${opt_err != null ? opt_err : "ok"} noopt=${noopt_err != null ? noopt_err : "ok"}`
return {seed: seed_val, errors: errors, src: src}
}
if (opt_err != null) {
@@ -137,10 +137,10 @@ function run_fuzz(seed_val) {
_run()
if (is_text(ret)) {
push(errors, `self-check ${key}: ${ret}`)
errors[] = `self-check ${key}: ${ret}`
}
if (run_err != null) {
push(errors, `self-check ${key}: unexpected disruption`)
errors[] = `self-check ${key}: unexpected disruption`
}
}
k = k + 1
@@ -174,9 +174,9 @@ function run_fuzz(seed_val) {
_run_noopt()
if (opt_fn_err != noopt_fn_err) {
push(errors, `diff ${key2}: opt=${opt_fn_err != null ? opt_fn_err : "ok"} noopt=${noopt_fn_err != null ? noopt_fn_err : "ok"}`)
errors[] = `diff ${key2}: opt=${opt_fn_err != null ? opt_fn_err : "ok"} noopt=${noopt_fn_err != null ? noopt_fn_err : "ok"}`
} else if (!values_equal(opt_result, noopt_result)) {
push(errors, `diff ${key2}: opt=${describe(opt_result)} noopt=${describe(noopt_result)}`)
errors[] = `diff ${key2}: opt=${describe(opt_result)} noopt=${describe(noopt_result)}`
}
}
k2 = k2 + 1

View File

@@ -241,7 +241,7 @@ function gen_array_test() {
var v = 0
while (i < n) {
v = rand_int(-100, 100)
push(vals, v)
vals[] = v
sum = sum + v
i = i + 1
}
@@ -249,7 +249,7 @@ function gen_array_test() {
var val_strs = []
i = 0
while (i < n) {
push(val_strs, text(vals[i]))
val_strs[] = text(vals[i])
i = i + 1
}

View File

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

View File

@@ -98,7 +98,7 @@ function gather_graph(locator, visited) {
arrfor(array(deps), function(alias) {
var dep_locator = deps[alias]
add_node(dep_locator)
push(edges, { from: locator, to: dep_locator, alias: alias })
edges[] = { from: locator, to: dep_locator, alias: alias }
gather_graph(dep_locator, visited)
})
}
@@ -117,7 +117,7 @@ if (show_world) {
packages = shop.list_packages()
arrfor(packages, function(p) {
if (p != 'core') {
push(roots, p)
roots[] = p
}
})
} else {
@@ -128,7 +128,7 @@ if (show_world) {
target_locator = shop.resolve_locator(target_locator)
push(roots, target_locator)
roots[] = target_locator
}
arrfor(roots, function(root) {
@@ -164,7 +164,7 @@ if (format == 'tree') {
children = []
arrfor(edges, function(e) {
if (e.from == locator) {
push(children, e)
children[] = e
}
})
@@ -180,7 +180,7 @@ if (format == 'tree') {
children = []
arrfor(edges, function(e) {
if (e.from == roots[i]) {
push(children, e)
children[] = e
}
})
@@ -230,7 +230,7 @@ if (format == 'tree') {
}
arrfor(array(nodes), function(id) {
push(output.nodes, nodes[id])
output.nodes[] = nodes[id]
})
output.edges = edges

783
http.cm Normal file
View File

@@ -0,0 +1,783 @@
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 (text version, for async responses)
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
}
// decode chunked transfer encoding (blob version, preserves binary data)
function decode_chunked_blob(buf, body_start_bytes) {
var result = Blob()
var pos = body_start_bytes
var total_bytes = length(buf) / 8
var header_end = null
var header_blob = null
var header_text = null
var crlf_pos = null
var chunk_size = null
var chunk_data = null
while (pos < total_bytes) {
header_end = pos + 20
if (header_end > total_bytes) header_end = total_bytes
header_blob = buf.read_blob(pos * 8, header_end * 8)
stone(header_blob)
header_text = text(header_blob)
crlf_pos = search(header_text, CRLF)
if (crlf_pos == null) break
chunk_size = number(text(header_text, 0, crlf_pos), 16)
if (chunk_size == null || chunk_size == 0) break
pos = pos + crlf_pos + 2
chunk_data = buf.read_blob(pos * 8, (pos + chunk_size) * 8)
stone(chunk_data)
result.write_blob(chunk_data)
pos = pos + chunk_size + 2
}
stone(result)
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
var status_line = null
var status_code = null
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)
status_line = text(header_text, 0, search(header_text, CRLF) || length(header_text))
status_code = number(text(status_line, 9, 12))
if (status_code == null || status_code < 200 || status_code >= 300) {
log.error("fetch: " + status_line)
disrupt
}
if (search(lower(header_text), "transfer-encoding: chunked") != null) {
body = decode_chunked_blob(buf, hdr_end + 4)
return body
}
// 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
}

View File

@@ -109,7 +109,7 @@ function trace_imports(file_path, depth) {
all_packages[imp_pkg] = true
push(all_imports, {
all_imports[] = {
from: file_path,
from_pkg: file_pkg,
module_path: mod_path,
@@ -117,7 +117,7 @@ function trace_imports(file_path, depth) {
package: imp_pkg,
type: imp_type,
depth: depth
})
}
// Recurse into resolved scripts
if (resolved && (ends_with(resolved, '.cm') || ends_with(resolved, '.ce'))) {

View File

@@ -1,6 +1,6 @@
// Minimal bootstrap — seeds the content-addressed cache
// Only runs on cold start (C runtime couldn't find engine in cache)
// Hidden vars: os, core_path, shop_path
// Hidden vars: os, core_path, shop_path, native_mode (optional)
var load_internal = os.load_internal
function use_embed(name) {
return load_internal("js_core_" + name + "_use")
@@ -89,8 +89,179 @@ function compile_and_cache(name, source_path) {
}
}
// Seed the cache with everything engine needs
var seed_files = [
// --- 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"},
@@ -98,11 +269,10 @@ var seed_files = [
{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
_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")

472
internal/enet.c Normal file
View File

@@ -0,0 +1,472 @@
#include "cell.h"
#define ENET_IMPLEMENTATION
#include "enet.h"
#include <stdio.h>
#include <string.h>
#include <math.h>
static JSClassID enet_host_id;
static JSClassID enet_peer_class_id;
static void js_enet_host_finalizer(JSRuntime *rt, JSValue val)
{
ENetHost *host = JS_GetOpaque(val, enet_host_id);
if (host) enet_host_destroy(host);
}
static JSClassDef enet_host_def = {
"ENetHost",
.finalizer = js_enet_host_finalizer,
};
static JSClassDef enet_peer_def = {
"ENetPeer",
};
/* Helper: create a JS peer wrapper for an ENetPeer pointer.
Fresh wrapper each time — no caching in peer->data. */
static JSValue peer_wrap(JSContext *ctx, ENetPeer *peer)
{
JSValue obj = JS_NewObjectClass(ctx, enet_peer_class_id);
if (JS_IsException(obj)) return obj;
JS_SetOpaque(obj, peer);
return obj;
}
/* ── Host functions ─────────────────────────────────────────── */
static JSValue js_enet_create_host(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetHost *host;
ENetAddress address;
ENetAddress *send = &address;
size_t peer_count = 1000;
size_t channel_limit = 0;
enet_uint32 incoming_bandwidth = 0;
enet_uint32 outgoing_bandwidth = 0;
JSValue obj;
if (argc < 1 || !JS_IsRecord(argv[0])) {
host = enet_host_create(NULL, peer_count, channel_limit, incoming_bandwidth, outgoing_bandwidth);
if (!host) return JS_RaiseDisrupt(ctx, "Failed to create ENet client host");
goto wrap;
}
JSValue config_obj = argv[0];
JSValue addr_val = JS_GetPropertyStr(ctx, config_obj, "address");
const char *addr_str = JS_IsText(addr_val) ? JS_ToCString(ctx, addr_val) : NULL;
if (!addr_str)
send = NULL;
else {
JSValue port_val = JS_GetPropertyStr(ctx, config_obj, "port");
int32_t port32 = 0;
JS_ToInt32(ctx, &port32, port_val);
if (strcmp(addr_str, "any") == 0)
address.host = ENET_HOST_ANY;
else if (strcmp(addr_str, "broadcast") == 0)
enet_address_set_host_ip(&address, "255.255.255.255");
else {
int err = enet_address_set_host_ip(&address, addr_str);
if (err != 0) {
JS_FreeCString(ctx, addr_str);
return JS_RaiseDisrupt(ctx, "Failed to set host IP. Error: %d", err);
}
}
address.port = (enet_uint16)port32;
JS_FreeCString(ctx, addr_str);
}
JSValue chan_val = JS_GetPropertyStr(ctx, config_obj, "channels");
JS_ToUint32(ctx, &channel_limit, chan_val);
JSValue in_bw_val = JS_GetPropertyStr(ctx, config_obj, "incoming_bandwidth");
JS_ToUint32(ctx, &incoming_bandwidth, in_bw_val);
JSValue out_bw_val = JS_GetPropertyStr(ctx, config_obj, "outgoing_bandwidth");
JS_ToUint32(ctx, &outgoing_bandwidth, 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");
wrap:
obj = JS_NewObjectClass(ctx, enet_host_id);
if (JS_IsException(obj)) {
enet_host_destroy(host);
return obj;
}
JS_SetOpaque(obj, host);
return obj;
}
/* service(host, callback [, timeout]) */
static JSValue js_enet_service(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 2) return JS_RaiseDisrupt(ctx, "service: expected (host, callback)");
ENetHost *host = JS_GetOpaque(argv[0], enet_host_id);
if (!host) return JS_RaiseDisrupt(ctx, "service: invalid host");
if (!JS_IsFunction(argv[1]))
return JS_RaiseDisrupt(ctx, "service: expected callback function");
enet_uint32 timeout_ms = 0;
if (argc >= 3 && !JS_IsNull(argv[2])) {
double secs = 0;
JS_ToFloat64(ctx, &secs, argv[2]);
if (secs > 0) timeout_ms = (enet_uint32)(secs * 1000.0);
}
JS_FRAME(ctx);
JS_ROOT(event_obj, JS_NULL);
ENetEvent event;
while (enet_host_service(host, &event, timeout_ms) > 0) {
event_obj.val = JS_NewObject(ctx);
JSValue peer_val = peer_wrap(ctx, event.peer);
JS_SetPropertyStr(ctx, event_obj.val, "peer", peer_val);
switch (event.type) {
case ENET_EVENT_TYPE_CONNECT:
JS_SetPropertyStr(ctx, event_obj.val, "type", JS_NewString(ctx, "connect"));
break;
case ENET_EVENT_TYPE_RECEIVE:
JS_SetPropertyStr(ctx, event_obj.val, "type", JS_NewString(ctx, "receive"));
JS_SetPropertyStr(ctx, event_obj.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.val, "data", data_val);
}
enet_packet_destroy(event.packet);
break;
case ENET_EVENT_TYPE_DISCONNECT:
JS_SetPropertyStr(ctx, event_obj.val, "type", JS_NewString(ctx, "disconnect"));
break;
case ENET_EVENT_TYPE_DISCONNECT_TIMEOUT:
JS_SetPropertyStr(ctx, event_obj.val, "type", JS_NewString(ctx, "disconnect_timeout"));
break;
case ENET_EVENT_TYPE_NONE:
JS_SetPropertyStr(ctx, event_obj.val, "type", JS_NewString(ctx, "none"));
break;
}
JS_Call(ctx, argv[1], JS_NULL, 1, &event_obj.val);
}
JS_RETURN_NULL();
}
/* connect(host, address, port) → peer */
static JSValue js_enet_connect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 3) return JS_RaiseDisrupt(ctx, "connect: expected (host, address, port)");
ENetHost *host = JS_GetOpaque(argv[0], enet_host_id);
if (!host) return JS_RaiseDisrupt(ctx, "connect: invalid host");
const char *hostname = JS_ToCString(ctx, argv[1]);
if (!hostname) return JS_EXCEPTION;
int port;
JS_ToInt32(ctx, &port, argv[2]);
ENetAddress address;
enet_address_set_host(&address, hostname);
JS_FreeCString(ctx, hostname);
address.port = port;
ENetPeer *peer = enet_host_connect(host, &address, 2, 0);
if (!peer) return JS_RaiseDisrupt(ctx, "No available peers for connection");
return peer_wrap(ctx, peer);
}
/* flush(host) */
static JSValue js_enet_flush(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 1) return JS_RaiseDisrupt(ctx, "flush: expected (host)");
ENetHost *host = JS_GetOpaque(argv[0], enet_host_id);
if (!host) return JS_RaiseDisrupt(ctx, "flush: invalid host");
enet_host_flush(host);
return JS_NULL;
}
/* broadcast(host, data) */
static JSValue js_enet_broadcast(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 2) return JS_RaiseDisrupt(ctx, "broadcast: expected (host, data)");
ENetHost *host = JS_GetOpaque(argv[0], enet_host_id);
if (!host) return JS_RaiseDisrupt(ctx, "broadcast: invalid host");
const char *data_str = NULL;
size_t data_len = 0;
uint8_t *buf = NULL;
if (JS_IsText(argv[1])) {
data_str = JS_ToCStringLen(ctx, &data_len, argv[1]);
if (!data_str) return JS_EXCEPTION;
} else if (js_is_blob(ctx, argv[1])) {
buf = js_get_blob_data(ctx, &data_len, argv[1]);
if (!buf) return JS_EXCEPTION;
} else {
return JS_RaiseDisrupt(ctx, "broadcast: data must be string or blob");
}
ENetPacket *packet = enet_packet_create(data_str ? (const void *)data_str : (const void *)buf, data_len, ENET_PACKET_FLAG_RELIABLE);
if (data_str) JS_FreeCString(ctx, data_str);
if (!packet) return JS_RaiseDisrupt(ctx, "Failed to create ENet packet");
enet_host_broadcast(host, 0, packet);
return JS_NULL;
}
/* host_port(host) → number */
static JSValue js_enet_host_port(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 1) return JS_RaiseDisrupt(ctx, "host_port: expected (host)");
ENetHost *host = JS_GetOpaque(argv[0], enet_host_id);
if (!host) return JS_RaiseDisrupt(ctx, "host_port: invalid host");
return JS_NewInt32(ctx, host->address.port);
}
/* host_address(host) → string */
static JSValue js_enet_host_address(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 1) return JS_RaiseDisrupt(ctx, "host_address: expected (host)");
ENetHost *host = JS_GetOpaque(argv[0], enet_host_id);
if (!host) return JS_RaiseDisrupt(ctx, "host_address: invalid host");
char ip_str[128];
if (enet_address_get_host_ip(&host->address, ip_str, sizeof(ip_str)) != 0)
return JS_NULL;
return JS_NewString(ctx, ip_str);
}
/* ── Peer functions ─────────────────────────────────────────── */
/* send(peer, data) */
static JSValue js_enet_send(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 2) return JS_RaiseDisrupt(ctx, "send: expected (peer, data)");
ENetPeer *peer = JS_GetOpaque(argv[0], enet_peer_class_id);
if (!peer) return JS_RaiseDisrupt(ctx, "send: invalid peer");
const char *data_str = NULL;
size_t data_len = 0;
uint8_t *buf = NULL;
if (JS_IsText(argv[1])) {
data_str = JS_ToCStringLen(ctx, &data_len, argv[1]);
if (!data_str) return JS_EXCEPTION;
} else if (js_is_blob(ctx, argv[1])) {
buf = js_get_blob_data(ctx, &data_len, argv[1]);
if (!buf) return JS_EXCEPTION;
} else {
return JS_RaiseDisrupt(ctx, "send: data must be string or blob");
}
ENetPacket *packet = enet_packet_create(data_str ? (const void *)data_str : (const void *)buf, data_len, ENET_PACKET_FLAG_RELIABLE);
if (data_str) JS_FreeCString(ctx, data_str);
if (!packet) return JS_RaiseDisrupt(ctx, "Failed to create ENet packet");
if (enet_peer_send(peer, 0, packet) < 0) return JS_RaiseDisrupt(ctx, "Unable to send packet");
return JS_NULL;
}
/* disconnect(peer) */
static JSValue js_enet_disconnect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 1) return JS_RaiseDisrupt(ctx, "disconnect: expected (peer)");
ENetPeer *peer = JS_GetOpaque(argv[0], enet_peer_class_id);
if (!peer) return JS_RaiseDisrupt(ctx, "disconnect: invalid peer");
enet_peer_disconnect(peer, 0);
return JS_NULL;
}
/* disconnect_now(peer) */
static JSValue js_enet_disconnect_now(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 1) return JS_RaiseDisrupt(ctx, "disconnect_now: expected (peer)");
ENetPeer *peer = JS_GetOpaque(argv[0], enet_peer_class_id);
if (!peer) return JS_RaiseDisrupt(ctx, "disconnect_now: invalid peer");
enet_peer_disconnect_now(peer, 0);
return JS_NULL;
}
/* disconnect_later(peer) */
static JSValue js_enet_disconnect_later(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 1) return JS_RaiseDisrupt(ctx, "disconnect_later: expected (peer)");
ENetPeer *peer = JS_GetOpaque(argv[0], enet_peer_class_id);
if (!peer) return JS_RaiseDisrupt(ctx, "disconnect_later: invalid peer");
enet_peer_disconnect_later(peer, 0);
return JS_NULL;
}
/* reset(peer) */
static JSValue js_enet_reset(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 1) return JS_RaiseDisrupt(ctx, "reset: expected (peer)");
ENetPeer *peer = JS_GetOpaque(argv[0], enet_peer_class_id);
if (!peer) return JS_RaiseDisrupt(ctx, "reset: invalid peer");
enet_peer_reset(peer);
return JS_NULL;
}
/* ping(peer) */
static JSValue js_enet_ping(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 1) return JS_RaiseDisrupt(ctx, "ping: expected (peer)");
ENetPeer *peer = JS_GetOpaque(argv[0], enet_peer_class_id);
if (!peer) return JS_RaiseDisrupt(ctx, "ping: invalid peer");
enet_peer_ping(peer);
return JS_NULL;
}
/* throttle_configure(peer, interval, acceleration, deceleration) */
static JSValue js_enet_throttle_configure(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 4) return JS_RaiseDisrupt(ctx, "throttle_configure: expected (peer, interval, accel, decel)");
ENetPeer *peer = JS_GetOpaque(argv[0], enet_peer_class_id);
if (!peer) return JS_RaiseDisrupt(ctx, "throttle_configure: invalid peer");
int interval, acceleration, deceleration;
if (JS_ToInt32(ctx, &interval, argv[1]) || JS_ToInt32(ctx, &acceleration, argv[2]) || JS_ToInt32(ctx, &deceleration, argv[3]))
return JS_RaiseDisrupt(ctx, "throttle_configure: expected integer arguments");
enet_peer_throttle_configure(peer, interval, acceleration, deceleration);
return JS_NULL;
}
/* peer_timeout(peer, limit, min, max) */
static JSValue js_enet_peer_timeout(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 4) return JS_RaiseDisrupt(ctx, "peer_timeout: expected (peer, limit, min, max)");
ENetPeer *peer = JS_GetOpaque(argv[0], enet_peer_class_id);
if (!peer) return JS_RaiseDisrupt(ctx, "peer_timeout: invalid peer");
int timeout_limit, timeout_min, timeout_max;
if (JS_ToInt32(ctx, &timeout_limit, argv[1]) || JS_ToInt32(ctx, &timeout_min, argv[2]) || JS_ToInt32(ctx, &timeout_max, argv[3]))
return JS_RaiseDisrupt(ctx, "peer_timeout: expected integer arguments");
enet_peer_timeout(peer, timeout_limit, timeout_min, timeout_max);
return JS_NULL;
}
/* ── Peer property getters ──────────────────────────────────── */
#define PEER_GETTER(name, field, convert) \
static JSValue js_enet_##name(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { \
if (argc < 1) return JS_RaiseDisrupt(ctx, #name ": expected (peer)"); \
ENetPeer *peer = JS_GetOpaque(argv[0], enet_peer_class_id); \
if (!peer) return JS_RaiseDisrupt(ctx, #name ": invalid peer"); \
return convert(ctx, peer->field); \
}
static inline JSValue _int32(JSContext *ctx, int v) { return JS_NewInt32(ctx, v); }
static inline JSValue _uint32(JSContext *ctx, unsigned int v) { return JS_NewUint32(ctx, v); }
PEER_GETTER(peer_rtt, roundTripTime, _int32)
PEER_GETTER(peer_rtt_variance, roundTripTimeVariance, _int32)
PEER_GETTER(peer_last_send_time, lastSendTime, _int32)
PEER_GETTER(peer_last_receive_time, lastReceiveTime, _int32)
PEER_GETTER(peer_mtu, mtu, _int32)
PEER_GETTER(peer_outgoing_data_total, outgoingDataTotal, _int32)
PEER_GETTER(peer_incoming_data_total, incomingDataTotal, _int32)
PEER_GETTER(peer_packet_loss, packetLoss, _int32)
PEER_GETTER(peer_state, state, _int32)
PEER_GETTER(peer_reliable_data_in_transit, reliableDataInTransit, _int32)
static JSValue js_enet_peer_incoming_bandwidth(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 1) return JS_RaiseDisrupt(ctx, "peer_incoming_bandwidth: expected (peer)");
ENetPeer *peer = JS_GetOpaque(argv[0], enet_peer_class_id);
if (!peer) return JS_RaiseDisrupt(ctx, "peer_incoming_bandwidth: invalid peer");
if (peer->incomingBandwidth == 0) return JS_NewFloat64(ctx, INFINITY);
return JS_NewInt32(ctx, peer->incomingBandwidth);
}
static JSValue js_enet_peer_outgoing_bandwidth(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 1) return JS_RaiseDisrupt(ctx, "peer_outgoing_bandwidth: expected (peer)");
ENetPeer *peer = JS_GetOpaque(argv[0], enet_peer_class_id);
if (!peer) return JS_RaiseDisrupt(ctx, "peer_outgoing_bandwidth: invalid peer");
if (peer->outgoingBandwidth == 0) return JS_NewFloat64(ctx, INFINITY);
return JS_NewInt32(ctx, peer->outgoingBandwidth);
}
static JSValue js_enet_peer_port(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 1) return JS_RaiseDisrupt(ctx, "peer_port: expected (peer)");
ENetPeer *peer = JS_GetOpaque(argv[0], enet_peer_class_id);
if (!peer) return JS_RaiseDisrupt(ctx, "peer_port: invalid peer");
return JS_NewUint32(ctx, peer->address.port);
}
static JSValue js_enet_peer_address(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 1) return JS_RaiseDisrupt(ctx, "peer_address: expected (peer)");
ENetPeer *peer = JS_GetOpaque(argv[0], enet_peer_class_id);
if (!peer) return JS_RaiseDisrupt(ctx, "peer_address: invalid peer");
char ip_str[128];
if (enet_address_get_host_ip(&peer->address, ip_str, sizeof(ip_str)) != 0)
return JS_NULL;
return JS_NewString(ctx, ip_str);
}
static JSValue js_enet_resolve_hostname(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
const char *hostname = JS_ToCString(ctx, argv[0]);
JS_FreeCString(ctx, hostname);
return JS_NULL;
}
/* ── Module export ──────────────────────────────────────────── */
static const JSCFunctionListEntry js_enet_funcs[] = {
/* host */
JS_CFUNC_DEF("create_host", 1, js_enet_create_host),
JS_CFUNC_DEF("service", 2, js_enet_service),
JS_CFUNC_DEF("connect", 3, js_enet_connect),
JS_CFUNC_DEF("flush", 1, js_enet_flush),
JS_CFUNC_DEF("broadcast", 2, js_enet_broadcast),
JS_CFUNC_DEF("host_port", 1, js_enet_host_port),
JS_CFUNC_DEF("host_address", 1, js_enet_host_address),
/* peer */
JS_CFUNC_DEF("send", 2, js_enet_send),
JS_CFUNC_DEF("disconnect", 1, js_enet_disconnect),
JS_CFUNC_DEF("disconnect_now", 1, js_enet_disconnect_now),
JS_CFUNC_DEF("disconnect_later", 1, js_enet_disconnect_later),
JS_CFUNC_DEF("reset", 1, js_enet_reset),
JS_CFUNC_DEF("ping", 1, js_enet_ping),
JS_CFUNC_DEF("throttle_configure", 4, js_enet_throttle_configure),
JS_CFUNC_DEF("peer_timeout", 4, js_enet_peer_timeout),
JS_CFUNC_DEF("peer_address", 1, js_enet_peer_address),
JS_CFUNC_DEF("peer_port", 1, js_enet_peer_port),
JS_CFUNC_DEF("peer_rtt", 1, js_enet_peer_rtt),
JS_CFUNC_DEF("peer_rtt_variance", 1, js_enet_peer_rtt_variance),
JS_CFUNC_DEF("peer_incoming_bandwidth", 1, js_enet_peer_incoming_bandwidth),
JS_CFUNC_DEF("peer_outgoing_bandwidth", 1, js_enet_peer_outgoing_bandwidth),
JS_CFUNC_DEF("peer_last_send_time", 1, js_enet_peer_last_send_time),
JS_CFUNC_DEF("peer_last_receive_time", 1, js_enet_peer_last_receive_time),
JS_CFUNC_DEF("peer_mtu", 1, js_enet_peer_mtu),
JS_CFUNC_DEF("peer_outgoing_data_total", 1, js_enet_peer_outgoing_data_total),
JS_CFUNC_DEF("peer_incoming_data_total", 1, js_enet_peer_incoming_data_total),
JS_CFUNC_DEF("peer_packet_loss", 1, js_enet_peer_packet_loss),
JS_CFUNC_DEF("peer_state", 1, js_enet_peer_state),
JS_CFUNC_DEF("peer_reliable_data_in_transit", 1, js_enet_peer_reliable_data_in_transit),
JS_CFUNC_DEF("resolve_hostname", 1, js_enet_resolve_hostname),
};
JSValue js_core_internal_enet_use(JSContext *ctx)
{
enet_initialize();
JS_FRAME(ctx);
JS_NewClassID(&enet_host_id);
JS_NewClass(ctx, enet_host_id, &enet_host_def);
JS_NewClassID(&enet_peer_class_id);
JS_NewClass(ctx, enet_peer_class_id, &enet_peer_def);
JS_ROOT(export_obj, JS_NewObject(ctx));
JS_SetPropertyFunctionList(ctx, export_obj.val, js_enet_funcs, countof(js_enet_funcs));
JS_RETURN(export_obj.val);
}

File diff suppressed because it is too large Load Diff

View File

@@ -117,10 +117,10 @@ JSC_CCALL(fd_read,
JSC_SCALL(fd_slurp,
struct stat st;
if (stat(str, &st) != 0)
return JS_RaiseDisrupt(js, "stat failed: %s", strerror(errno));
return JS_RaiseDisrupt(js, "stat failed for %s: %s", str, strerror(errno));
if (!S_ISREG(st.st_mode))
return JS_RaiseDisrupt(js, "path is not a regular file");
return JS_RaiseDisrupt(js, "path %s is not a regular file", str);
size_t size = st.st_size;
if (size == 0)
@@ -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) {

View File

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

View File

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

View File

@@ -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);
atomic_fetch_add_explicit(&js->turn_gen, 1, memory_order_relaxed);
JS_SetPauseFlag(js, 0);
crt->turn_start_ns = cell_ns();
}
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) {

View File

@@ -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 = {}
@@ -249,16 +246,19 @@ Shop.file_info = function(file) {
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
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)
@@ -964,8 +1021,29 @@ function ensure_package_dylibs(pkg) {
var build_mod = use_cache['core/build']
var target = null
var c_files = null
var _all_ok = true
var _ri = 0
if (build_mod) {
// Fast path: if manifest exists and all dylibs are present, skip build_dynamic
results = read_dylib_manifest(_pkg)
if (results != null) {
_all_ok = true
_ri = 0
while (_ri < length(results)) {
if (results[_ri].dylib && !fd.is_file(results[_ri].dylib)) {
_all_ok = false
break
}
_ri = _ri + 1
}
if (_all_ok) {
log.shop('manifest ok for ' + _pkg + ' (' + text(length(results)) + ' modules)')
} else {
results = null
}
}
if (results == null) {
target = detect_host_target()
if (!target) return null
@@ -977,6 +1055,7 @@ function ensure_package_dylibs(pkg) {
log.shop('ensuring C modules for ' + _pkg)
results = build_mod.build_dynamic(_pkg, target, 'release', {})
}
} else {
// No build module at runtime — read manifest from cell build
results = read_dylib_manifest(_pkg)
@@ -1292,6 +1371,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 +1383,27 @@ function execute_module(info)
pkg = file_info.package
env.use = make_use_fn(pkg, true)
env = stone(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 +1415,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 +1424,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
}
@@ -1398,18 +1499,18 @@ Shop.use = function use(path, _pkg_ctx) {
if (use_cache[info.cache_key])
return use_cache[info.cache_key]
push(use_stack, _use_entry)
use_stack[] = _use_entry
var _use_result = null
var _use_ok = false
var _load = function() {
_use_result = execute_module(info)
_use_ok = true
} disruption {
pop(use_stack)
use_stack[]
disrupt
}
_load()
pop(use_stack)
use_stack[]
use_cache[info.cache_key] = _use_result
return _use_result
}
@@ -1543,12 +1644,16 @@ function download_zip(pkg, commit_hash) {
return _download()
}
// Get zip from cache, returns null if not cached
// Get zip from cache, returns null if not cached or empty
function get_cached_zip(pkg, commit_hash) {
var cache_path = get_cache_path(pkg, commit_hash)
if (fd.is_file(cache_path))
return fd.slurp(cache_path)
var data = null
if (fd.is_file(cache_path)) {
data = fd.slurp(cache_path)
stone(data)
if (length(data) > 0) return data
fd.remove(cache_path)
}
return null
}
@@ -1804,6 +1909,7 @@ Shop.sync_with_deps = function(pkg, opts) {
if (visited[current]) continue
visited[current] = true
log.build(' Fetching ' + current + '...')
Shop.sync(current, opts)
_read_deps = function() {
@@ -1817,7 +1923,7 @@ Shop.sync_with_deps = function(pkg, opts) {
arrfor(array(deps), function(alias) {
dep_locator = deps[alias]
if (!visited[dep_locator])
push(queue, dep_locator)
queue[] = dep_locator
})
}
}
@@ -1903,19 +2009,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 +2038,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)
@@ -1943,7 +2062,7 @@ function get_package_scripts(package)
for (i = 0; i < length(files); i++) {
file = files[i]
if (ends_with(file, '.cm') || ends_with(file, '.ce')) {
push(scripts, file)
scripts[] = file
}
}
@@ -1965,7 +2084,7 @@ function extract_use_calls(source) {
if (end == null) end = search(text(source, start), '"')
if (end != null) {
arg = text(source, start, start + end)
push(uses, arg)
uses[] = arg
}
idx = search(text(source, idx + 4), "use(")
if (idx != null) idx = idx + (source.length - (source.length - idx))
@@ -1988,12 +2107,18 @@ Shop.build_package_scripts = function(package)
resolve_mod_fn(pkg_dir + '/' + script, package)
ok = ok + 1
} disruption {
push(errors, script)
log.console(" compile error: " + package + '/' + script)
errors[] = script
log.build(" compile error: " + package + '/' + script)
}
_try()
})
if (length(errors) > 0) {
log.build(' Compiling scripts (' + text(ok) + ' ok, ' + text(length(errors)) + ' errors)')
} else if (ok > 0) {
log.build(' Compiling scripts (' + text(ok) + ' ok)')
}
return {ok: ok, errors: errors, total: length(scripts)}
}
@@ -2035,14 +2160,14 @@ Shop.audit_use_resolution = function(package) {
end = search(rest, quote)
if (end == null) continue
arg = text(rest, 0, end)
if (length(arg) > 0) push(uses, arg)
if (length(arg) > 0) uses[] = arg
rest = text(rest, end + 1)
}
arrfor(uses, function(mod) {
var _resolve = function() {
info = resolve_module_info(mod, package)
if (!info) push(unresolved, {script: script, module: mod})
if (!info) unresolved[] = {script: script, module: mod}
} disruption {}
_resolve()
})
@@ -2070,6 +2195,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 +2262,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 +2282,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 +2335,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()
@@ -2218,7 +2418,7 @@ Shop.audit_packages = function() {
if (package == 'core') return
if (fd.is_dir(package)) return
if (fetch_remote_hash(package)) return
push(bad, package)
bad[] = package
})
return bad
@@ -2259,7 +2459,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 +2468,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)
}

View File

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

View File

@@ -208,11 +208,11 @@ Link.sync_all = function(shop) {
// Validate target exists
var link_target = resolve_link_target(target)
if (!fd.is_dir(link_target)) {
push(errors, canonical + ': target ' + link_target + ' does not exist')
errors[] = canonical + ': target ' + link_target + ' does not exist'
return
}
if (!fd.is_file(link_target + '/cell.toml')) {
push(errors, canonical + ': target ' + link_target + ' is not a valid package')
errors[] = canonical + ': target ' + link_target + ' is not a valid package'
return
}
@@ -246,7 +246,7 @@ Link.sync_all = function(shop) {
count = count + 1
} disruption {
push(errors, canonical + ': sync failed')
errors[] = canonical + ': sync failed'
}
_sync()
})

14
list.ce
View File

@@ -89,16 +89,16 @@ var run = function() {
// Add status indicators
status = []
if (link_target) {
push(status, "linked -> " + link_target)
status[] = "linked -> " + link_target
}
if (lock_entry && lock_entry.commit) {
push(status, "@" + text(lock_entry.commit, 0, 8))
status[] = "@" + text(lock_entry.commit, 0, 8)
}
if (lock_entry && lock_entry.type == 'local') {
push(status, "local")
status[] = "local"
}
if (!lock_entry) {
push(status, "not installed")
status[] = "not installed"
}
if (length(status) > 0) {
@@ -136,11 +136,11 @@ if (mode == 'local') {
var link_target = links[p]
if (link_target) {
push(linked_pkgs, p)
linked_pkgs[] = p
} else if (lock_entry && lock_entry.type == 'local') {
push(local_pkgs, p)
local_pkgs[] = p
} else {
push(remote_pkgs, p)
remote_pkgs[] = p
}
})

496
log.ce
View File

@@ -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,244 @@ 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)
channel = args[1]
config = load_config()
if (!config || !config.sink) {
log.console("No sinks configured")
return
}
st = fd.stat(sink.path)
if (st.size == last_size) {
$delay(poll, 1)
// 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
}
})
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
}
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
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)
}
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
}
parse_fn()
if (!poll_entry) continue
os.print(format_entry(poll_entry) + "\n")
}
$delay(poll, 1)
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
}
poll()
function do_enable() {
var channel = null
var config = null
var sink = null
var i = 0
var already = false
var new_exclude = []
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)) {
new_exclude = []
arrfor(sink.exclude, function(ex) {
if (ex != channel) 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)
}
function do_disable() {
var channel = null
var config = null
var sink = null
var i = 0
var new_channels = []
var already_excluded = false
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 = []
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) new_channels[] = ch
})
sink.channels = new_channels
}
}
save_config(config)
log.console("Disabled channel: " + channel)
}
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 })
}
} else if (is_array(sink.channels)) {
arrfor(sink.channels, function(ch) { active[ch] = true })
}
} else {
active.console = true
active.error = true
}
log.console("Channels:")
arrfor(known_channels, function(ch) {
var status = active[ch] ? "enabled" : "disabled"
log.console(" " + ch + ": " + status)
})
}
// Main dispatch
@@ -335,16 +481,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()
}

View File

@@ -85,7 +85,7 @@ var dump_function = function(func, name) {
parts = []
j = 1
while (j < n - 2) {
push(parts, fmt_val(instr[j]))
parts[] = fmt_val(instr[j])
j = j + 1
}
operands = text(parts, ", ")

642
mcode.cm

File diff suppressed because it is too large Load Diff

View File

@@ -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'
]
@@ -193,5 +196,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')

View File

@@ -1,588 +0,0 @@
#include "cell.h"
#define ENET_IMPLEMENTATION
#include "enet.h"
#include <stdio.h>
#include <string.h>
#include <math.h>
static JSClassID enet_host_id;
static JSClassID enet_peer_class_id;
static void js_enet_host_finalizer(JSRuntime *rt, JSValue val)
{
ENetHost *host = JS_GetOpaque(val, enet_host_id);
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);
}
// Initialize the ENet library. Must be called before using any ENet functionality.
static JSValue js_enet_initialize(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (enet_initialize() != 0) return JS_RaiseDisrupt(ctx, "Error initializing ENet");
return JS_NULL;
}
// Deinitialize the ENet library, cleaning up all resources. Call this when you no longer
// need any ENet functionality.
static JSValue js_enet_deinitialize(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
enet_deinitialize();
return JS_NULL;
}
// 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.
static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetHost *host;
ENetAddress address;
ENetAddress *send = &address;
size_t peer_count = 1000;
size_t channel_limit = 0;
enet_uint32 incoming_bandwidth = 0;
enet_uint32 outgoing_bandwidth = 0;
JSValue obj;
if (argc < 1 || !JS_IsRecord(argv[0])) {
host = enet_host_create(NULL, peer_count, channel_limit, incoming_bandwidth, outgoing_bandwidth);
if (!host) return JS_RaiseDisrupt(ctx, "Failed to create ENet client host");
goto wrap;
}
JSValue config_obj = argv[0];
JSValue addr_val = JS_GetPropertyStr(ctx, config_obj, "address");
const char *addr_str = JS_IsText(addr_val) ? JS_ToCString(ctx, addr_val) : NULL;
JS_FreeValue(ctx, addr_val);
if (!addr_str)
send = NULL;
else {
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;
else if (strcmp(addr_str, "broadcast") == 0)
enet_address_set_host_ip(&address, "255.255.255.255");
else {
int err = enet_address_set_host_ip(&address, addr_str);
if (err != 0) {
JS_FreeCString(ctx, addr_str);
return JS_RaiseDisrupt(ctx, "Failed to set host IP from '%s'. Error: %d", addr_str, err);
}
}
address.port = (enet_uint16)port32;
JS_FreeCString(ctx, addr_str);
}
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");
wrap:
obj = JS_NewObjectClass(ctx, enet_host_id);
if (JS_IsException(obj)) {
enet_host_destroy(host);
return obj;
}
JS_SetOpaque(obj, host);
return obj;
}
// Helper function to get a JSValue for an ENetPeer.
static JSValue peer_get_value(JSContext *ctx, ENetPeer *peer)
{
if (!peer->data) {
peer->data = malloc(sizeof(JSValue));
*(JSValue*)peer->data = JS_NewObjectClass(ctx, enet_peer_class_id);
JS_SetOpaque(*(JSValue*)peer->data, peer);
}
return JS_DupValue(ctx, *(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
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");
double secs;
JS_ToFloat64(ctx, &secs, argv[1]);
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));
switch (event.type) {
case ENET_EVENT_TYPE_CONNECT:
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "connect"));
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
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);
}
enet_packet_destroy(event.packet);
break;
case ENET_EVENT_TYPE_DISCONNECT:
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "disconnect"));
break;
case ENET_EVENT_TYPE_DISCONNECT_TIMEOUT:
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "disconnect_timeout"));
break;
case ENET_EVENT_TYPE_NONE:
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "none"));
break;
}
// TODO: raise exception?
JS_FreeValue(ctx, event_obj);
}
return JS_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.
static JSValue js_enet_host_connect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
if (!host) return JS_EXCEPTION;
if (argc < 2) return JS_RaiseDisrupt(ctx, "Expected 2 arguments: hostname, port");
const char *hostname = JS_ToCString(ctx, argv[0]);
if (!hostname) return JS_EXCEPTION;
int port;
JS_ToInt32(ctx, &port, argv[1]);
ENetAddress address;
enet_address_set_host(&address, hostname);
JS_FreeCString(ctx, hostname);
address.port = port;
ENetPeer *peer = enet_host_connect(host, &address, 2, 0);
if (!peer) return JS_RaiseDisrupt(ctx, "No available peers for initiating an ENet connection");
return peer_get_value(ctx, peer);
}
// Flush all pending outgoing packets for this host immediately.
//
// :return: None
static JSValue js_enet_host_flush(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
if (!host) return JS_EXCEPTION;
enet_host_flush(host);
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
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");
const char *data_str = NULL;
size_t data_len = 0;
uint8_t *buf = NULL;
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])) {
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");
}
ENetPacket *packet = enet_packet_create(data_str ? (const void*)data_str : (const void*)buf, data_len, ENET_PACKET_FLAG_RELIABLE);
if (data_str) JS_FreeCString(ctx, data_str);
if (!packet) return JS_RaiseDisrupt(ctx, "Failed to create ENet packet");
enet_host_broadcast(host, 0, packet);
return JS_NULL;
}
static JSValue js_enet_host_get_port(JSContext *js, JSValueConst self, int argc, JSValueConst *argv)
{
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)
{
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);
if (!peer) return JS_EXCEPTION;
enet_peer_disconnect(peer, 0);
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");
const char *data_str = NULL;
size_t data_len = 0;
uint8_t *buf = NULL;
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])) {
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");
}
ENetPacket *packet = enet_packet_create(data_str ? (const void*)data_str : (const void*)buf, data_len, ENET_PACKET_FLAG_RELIABLE);
if (data_str) JS_FreeCString(ctx, data_str);
if (!packet) return JS_RaiseDisrupt(ctx, "Failed to create ENet packet");
if (enet_peer_send(peer, 0, packet) < 0) return JS_RaiseDisrupt(ctx, "Unable to send packet");
return JS_NULL;
}
// Immediately terminate the connection to this peer, discarding any pending data.
//
// :return: None
static JSValue js_enet_peer_disconnect_now(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_EXCEPTION;
enet_peer_disconnect_now(peer, 0);
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);
if (!peer) return JS_EXCEPTION;
enet_peer_disconnect_later(peer, 0);
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);
if (!peer) return JS_EXCEPTION;
enet_peer_reset(peer);
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);
if (!peer) return JS_EXCEPTION;
enet_peer_ping(peer);
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);
if (!peer) return JS_EXCEPTION;
int interval, acceleration, deceleration;
if (argc < 3 || JS_ToInt32(ctx, &interval, argv[0]) || JS_ToInt32(ctx, &acceleration, argv[1]) || JS_ToInt32(ctx, &deceleration, argv[2]))
return JS_RaiseDisrupt(ctx, "Expected 3 int arguments: interval, acceleration, deceleration");
enet_peer_throttle_configure(peer, interval, acceleration, deceleration);
return JS_NULL;
}
static JSValue js_enet_peer_timeout(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_EXCEPTION;
int timeout_limit, timeout_min, timeout_max;
if (argc < 3 || JS_ToInt32(ctx, &timeout_limit, argv[0]) || JS_ToInt32(ctx, &timeout_min, argv[1]) || JS_ToInt32(ctx, &timeout_max, argv[2]))
return JS_RaiseDisrupt(ctx, "Expected 3 integer arguments: timeout_limit, timeout_min, timeout_max");
enet_peer_timeout(peer, timeout_limit, timeout_min, timeout_max);
return JS_NULL;
}
// Class definitions
static JSClassDef enet_host = {
"ENetHost",
.finalizer = js_enet_host_finalizer,
};
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)
{
// TODO: implement
const char *hostname = JS_ToCString(js, argv[0]);
JS_FreeCString(js, hostname);
return JS_NULL;
}
static const JSCFunctionListEntry js_enet_funcs[] = {
JS_CFUNC_DEF("initialize", 0, js_enet_initialize),
JS_CFUNC_DEF("deinitialize", 0, js_enet_deinitialize),
JS_CFUNC_DEF("create_host", 1, js_enet_host_create),
JS_CFUNC_DEF("resolve_hostname", 1, js_enet_resolve_hostname),
};
static const JSCFunctionListEntry js_enet_host_funcs[] = {
JS_CFUNC_DEF("service", 2, js_enet_host_service),
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),
};
static JSValue js_enet_peer_get_rtt(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
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)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_EXCEPTION;
if (peer->incomingBandwidth == 0) return JS_NewFloat64(ctx, INFINITY);
return JS_NewInt32(ctx, peer->incomingBandwidth);
}
static JSValue js_enet_peer_get_outgoing_bandwidth(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 (peer->outgoingBandwidth == 0) return JS_NewFloat64(ctx, INFINITY);
return JS_NewInt32(ctx, peer->outgoingBandwidth);
}
static JSValue js_enet_peer_get_last_send_time(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
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)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_EXCEPTION;
return JS_NewInt32(ctx, peer->lastReceiveTime);
}
static JSValue js_enet_peer_get_mtu(JSContext *ctx, JSValueConst this_val)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_NewFloat64(ctx, INFINITY);
return JS_NewInt32(ctx, peer->mtu);
}
static JSValue js_enet_peer_get_outgoing_data_total(JSContext *ctx, JSValueConst this_val)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_NewFloat64(ctx, INFINITY);
return JS_NewInt32(ctx, peer->outgoingDataTotal);
}
static JSValue js_enet_peer_get_incoming_data_total(JSContext *ctx, JSValueConst this_val)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_NewFloat64(ctx, INFINITY);
return JS_NewInt32(ctx, peer->incomingDataTotal);
}
static JSValue js_enet_peer_get_rtt_variance(JSContext *ctx, JSValueConst this_val)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_NewFloat64(ctx, INFINITY);
return JS_NewInt32(ctx, peer->roundTripTimeVariance);
}
static JSValue js_enet_peer_get_packet_loss(JSContext *ctx, JSValueConst this_val)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_NewFloat64(ctx, INFINITY);
return JS_NewInt32(ctx, peer->packetLoss);
}
static JSValue js_enet_peer_get_state(JSContext *ctx, JSValueConst this_val)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_NewInt32(ctx, -1);
return JS_NewInt32(ctx, peer->state);
}
static JSValue js_enet_peer_get_reliable_data_in_transit(JSContext *ctx, JSValueConst this_val)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_NewFloat64(ctx, INFINITY);
return JS_NewInt32(ctx, peer->reliableDataInTransit);
}
static JSValue js_enet_peer_get_port(JSContext *js, JSValueConst self)
{
ENetPeer *peer = JS_GetOpaque(self, enet_peer_class_id);
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);
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);
}
static const JSCFunctionListEntry js_enet_peer_funcs[] = {
JS_CFUNC_DEF("send", 1, js_enet_peer_send),
JS_CFUNC_DEF("disconnect", 0, js_enet_peer_disconnect),
JS_CFUNC_DEF("disconnect_now", 0, js_enet_peer_disconnect_now),
JS_CFUNC_DEF("disconnect_later", 0, js_enet_peer_disconnect_later),
JS_CFUNC_DEF("reset", 0, js_enet_peer_reset),
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),
};
JSValue js_core_enet_use(JSContext *ctx)
{
JS_FRAME(ctx);
JS_NewClassID(&enet_host_id);
JS_NewClass(ctx, enet_host_id, &enet_host);
JS_ROOT(host_proto, JS_NewObject(ctx));
JS_SetPropertyFunctionList(ctx, host_proto.val, js_enet_host_funcs, countof(js_enet_host_funcs));
JS_SetClassProto(ctx, enet_host_id, host_proto.val);
JS_NewClassID(&enet_peer_class_id);
JS_NewClass(ctx, enet_peer_class_id, &enet_peer_class);
JS_ROOT(peer_proto, JS_NewObject(ctx));
JS_SetPropertyFunctionList(ctx, peer_proto.val, js_enet_peer_funcs, countof(js_enet_peer_funcs));
JS_SetClassProto(ctx, enet_peer_class_id, peer_proto.val);
JS_ROOT(export_obj, JS_NewObject(ctx));
JS_SetPropertyFunctionList(ctx, export_obj.val, js_enet_funcs, countof(js_enet_funcs));
JS_RETURN(export_obj.val);
}

View File

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

View File

@@ -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);
@@ -570,6 +569,103 @@ JSC_CCALL(socket_close,
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;
)
static const JSCFunctionListEntry js_socket_funcs[] = {
MIST_FUNC_DEF(socket, getaddrinfo, 3),
MIST_FUNC_DEF(socket, socket, 3),
@@ -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
View 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

View File

@@ -88,9 +88,9 @@ var packages = ['core']
var deps = pkg_tools.gather_dependencies(target_package)
for (i = 0; i < length(deps); i++) {
push(packages, deps[i])
packages[] = deps[i]
}
push(packages, target_package)
packages[] = target_package
// Remove duplicates
var unique_packages = []
@@ -98,7 +98,7 @@ var seen = {}
for (i = 0; i < length(packages); i++) {
if (!seen[packages[i]]) {
seen[packages[i]] = true
push(unique_packages, packages[i])
unique_packages[] = packages[i]
}
}
packages = unique_packages

View File

@@ -198,7 +198,7 @@ package.find_packages = function(dir) {
var list = fd.readdir(dir)
if (!list) return found
if (fd.is_file(dir + '/cell.toml'))
push(found, dir)
found[] = dir
arrfor(list, function(item) {
if (item == '.' || item == '..' || item == '.cell' || item == '.git') return
var full = dir + '/' + item
@@ -207,7 +207,7 @@ package.find_packages = function(dir) {
if (st && st.isDirectory) {
sub = package.find_packages(full)
arrfor(sub, function(p) {
push(found, p)
found[] = p
})
}
})
@@ -227,14 +227,14 @@ package.list_modules = function(name) {
var stem = null
for (i = 0; i < length(files); i++) {
if (ends_with(files[i], '.cm')) {
push(modules, text(files[i], 0, -3))
modules[] = text(files[i], 0, -3)
}
}
var c_files = package.get_c_files(name, null, true)
for (i = 0; i < length(c_files); i++) {
stem = ends_with(c_files[i], '.cpp') ? text(c_files[i], 0, -4) : text(c_files[i], 0, -2)
if (find(modules, function(m) { return m == stem }) == null)
push(modules, stem)
modules[] = stem
}
return modules
}
@@ -245,7 +245,7 @@ package.list_programs = function(name) {
var i = 0
for (i = 0; i < length(files); i++) {
if (ends_with(files[i], '.ce')) {
push(programs, text(files[i], 0, -3))
programs[] = text(files[i], 0, -3)
}
}
return programs
@@ -360,7 +360,7 @@ package.get_c_files = function(name, target, exclude_main) {
basename = fd.basename(selected)
if (basename == 'main.c' || starts_with(basename, 'main_')) return
}
push(result, selected)
result[] = selected
}
})

156
parse.cm
View File

@@ -90,12 +90,12 @@ var parse = function(tokens, src, filename, tokenizer) {
var parse_error = function(token, msg) {
if (error_count >= 5) return null
error_count = error_count + 1
push(errors, {
errors[] = {
message: msg,
line: token.from_row + 1,
column: token.from_column + 1,
offset: token.at
})
}
}
var _keywords = {
@@ -230,8 +230,8 @@ var parse = function(tokens, src, filename, tokenizer) {
if (tv[tvi] == "\\" && tvi + 1 < tvlen) {
esc_ch = tv[tvi + 1]
esc_val = template_escape_map[esc_ch]
if (esc_val != null) { push(fmt_parts, esc_val) }
else { push(fmt_parts, esc_ch) }
if (esc_val != null) { fmt_parts[] = esc_val }
else { fmt_parts[] = esc_ch }
tvi = tvi + 2
} else if (tv[tvi] == "$" && tvi + 1 < tvlen && tv[tvi + 1] == "{") {
tvi = tvi + 2
@@ -239,27 +239,27 @@ var parse = function(tokens, src, filename, tokenizer) {
expr_parts = []
while (tvi < tvlen && depth > 0) {
tc = tv[tvi]
if (tc == "{") { depth = depth + 1; push(expr_parts, tc); tvi = tvi + 1 }
if (tc == "{") { depth = depth + 1; expr_parts[] = tc; tvi = tvi + 1 }
else if (tc == "}") {
depth = depth - 1
if (depth > 0) { push(expr_parts, tc) }
if (depth > 0) { expr_parts[] = tc }
tvi = tvi + 1
}
else if (tc == "'" || tc == "\"" || tc == "`") {
tq = tc
push(expr_parts, tc)
expr_parts[] = tc
tvi = tvi + 1
while (tvi < tvlen && tv[tvi] != tq) {
if (tv[tvi] == "\\" && tvi + 1 < tvlen) {
push(expr_parts, tv[tvi])
expr_parts[] = tv[tvi]
tvi = tvi + 1
}
push(expr_parts, tv[tvi])
expr_parts[] = tv[tvi]
tvi = tvi + 1
}
if (tvi < tvlen) { push(expr_parts, tv[tvi]); tvi = tvi + 1 }
if (tvi < tvlen) { expr_parts[] = tv[tvi]; tvi = tvi + 1 }
} else {
push(expr_parts, tc)
expr_parts[] = tc
tvi = tvi + 1
}
}
@@ -274,14 +274,14 @@ var parse = function(tokens, src, filename, tokenizer) {
} else {
sub_expr = sub_stmt
}
push(tpl_list, sub_expr)
tpl_list[] = sub_expr
}
push(fmt_parts, "{")
push(fmt_parts, text(idx))
push(fmt_parts, "}")
fmt_parts[] = "{"
fmt_parts[] = text(idx)
fmt_parts[] = "}"
idx = idx + 1
} else {
push(fmt_parts, tv[tvi])
fmt_parts[] = tv[tvi]
tvi = tvi + 1
}
}
@@ -332,7 +332,7 @@ var parse = function(tokens, src, filename, tokenizer) {
advance()
while (tok.kind != "]" && tok.kind != "eof") {
elem = parse_assign_expr()
if (elem != null) push(list, elem)
if (elem != null) list[] = elem
if (tok.kind == ",") advance()
else break
}
@@ -395,7 +395,7 @@ var parse = function(tokens, src, filename, tokenizer) {
advance()
param.expression = parse_assign_expr()
}
push(params, param)
params[] = param
} else {
parse_error(tok, "expected parameter name")
break
@@ -436,7 +436,7 @@ var parse = function(tokens, src, filename, tokenizer) {
} else {
parse_error(tok, "expected ':' after property name")
}
push(list, pair)
list[] = pair
if (tok.kind == ",") advance()
else if (tok.kind == "{") {
if (right && right.kind == "(") {
@@ -473,17 +473,17 @@ var parse = function(tokens, src, filename, tokenizer) {
flags_parts = []
while (rpos < _src_len && src[rpos] != "/") {
if (src[rpos] == "\\" && rpos + 1 < _src_len) {
push(pattern_parts, src[rpos])
push(pattern_parts, src[rpos + 1])
pattern_parts[] = src[rpos]
pattern_parts[] = src[rpos + 1]
rpos = rpos + 2
} else {
push(pattern_parts, src[rpos])
pattern_parts[] = src[rpos]
rpos = rpos + 1
}
}
if (rpos < _src_len) rpos = rpos + 1
while (rpos < _src_len && is_letter(src[rpos])) {
push(flags_parts, src[rpos])
flags_parts[] = src[rpos]
rpos = rpos + 1
}
node.pattern = text(pattern_parts)
@@ -557,7 +557,7 @@ var parse = function(tokens, src, filename, tokenizer) {
new_node.list = args_list
while (tok.kind != ")" && tok.kind != "eof") {
arg = parse_assign_expr()
if (arg != null) push(args_list, arg)
if (arg != null) args_list[] = arg
if (tok.kind == ",") advance()
else break
}
@@ -830,7 +830,7 @@ var parse = function(tokens, src, filename, tokenizer) {
before = cursor
stmt = parse_statement()
if (stmt != null) {
push(stmts, stmt)
stmts[] = stmt
} else if (cursor == before) {
sync_to_statement()
}
@@ -872,14 +872,14 @@ var parse = function(tokens, src, filename, tokenizer) {
param.name = tok.value
pname = tok.value
if (find(prev_names, pname) != null) parse_error(tok, "duplicate parameter name '" + pname + "'")
push(prev_names, pname)
prev_names[] = pname
advance()
ast_node_end(param)
if (tok.kind == "=" || tok.kind == "|") {
advance()
param.expression = parse_assign_expr()
}
push(params, param)
params[] = param
} else {
parse_error(tok, "expected parameter name")
break
@@ -959,7 +959,7 @@ var parse = function(tokens, src, filename, tokenizer) {
param.name = tok.value
advance()
ast_node_end(param)
push(params, param)
params[] = param
} else if (tok.kind == "(") {
advance()
prev_names = []
@@ -969,14 +969,14 @@ var parse = function(tokens, src, filename, tokenizer) {
param.name = tok.value
pname = tok.value
if (find(prev_names, pname) != null) parse_error(tok, "duplicate parameter name '" + pname + "'")
push(prev_names, pname)
prev_names[] = pname
advance()
ast_node_end(param)
if (tok.kind == "=" || tok.kind == "|") {
advance()
param.expression = parse_assign_expr()
}
push(params, param)
params[] = param
} else {
parse_error(tok, "expected parameter name")
break
@@ -1010,7 +1010,7 @@ var parse = function(tokens, src, filename, tokenizer) {
expr = parse_assign_expr()
ret.expression = expr
ast_node_end(ret)
push(stmts, ret)
stmts[] = ret
node.statements = stmts
}
@@ -1110,7 +1110,7 @@ var parse = function(tokens, src, filename, tokenizer) {
parse_error(start, "'var' declarations must be initialized; use 'var " + var_name + " = null' if no value is needed")
}
ast_node_end(node)
push(decls, node)
decls[] = node
decl_count = decl_count + 1
if (tok.kind == ",") advance()
else break
@@ -1142,7 +1142,7 @@ var parse = function(tokens, src, filename, tokenizer) {
_control_depth = _control_depth + 1
_expecting_body = true
body = parse_statement()
if (body != null) push(then_stmts, body)
if (body != null) then_stmts[] = body
else_ifs = []
node.list = else_ifs
if (tok.kind == "else") {
@@ -1151,7 +1151,7 @@ var parse = function(tokens, src, filename, tokenizer) {
_control_depth = saved_cd
_control_type = saved_ct
elif = parse_statement()
if (elif != null) push(else_ifs, elif)
if (elif != null) else_ifs[] = elif
ast_node_end(node)
return node
} else {
@@ -1159,7 +1159,7 @@ var parse = function(tokens, src, filename, tokenizer) {
node.else = else_stmts
_expecting_body = true
body = parse_statement()
if (body != null) push(else_stmts, body)
if (body != null) else_stmts[] = body
}
}
_control_depth = saved_cd
@@ -1185,7 +1185,7 @@ var parse = function(tokens, src, filename, tokenizer) {
_control_depth = _control_depth + 1
_expecting_body = true
body = parse_statement()
if (body != null) push(stmts, body)
if (body != null) stmts[] = body
_control_depth = saved_cd
_control_type = saved_ct
ast_node_end(node)
@@ -1203,7 +1203,7 @@ var parse = function(tokens, src, filename, tokenizer) {
_control_depth = _control_depth + 1
_expecting_body = true
body = parse_statement()
if (body != null) push(stmts, body)
if (body != null) stmts[] = body
_control_depth = saved_cd
_control_type = saved_ct
if (tok.kind == "while") advance()
@@ -1256,7 +1256,7 @@ var parse = function(tokens, src, filename, tokenizer) {
_control_depth = _control_depth + 1
_expecting_body = true
body = parse_statement()
if (body != null) push(stmts, body)
if (body != null) stmts[] = body
_control_depth = saved_cd
_control_type = saved_ct
ast_node_end(node)
@@ -1402,9 +1402,9 @@ var parse = function(tokens, src, filename, tokenizer) {
stmt = parse_statement()
if (stmt != null) {
if (stmt.kind == "function") {
push(functions, stmt)
functions[] = stmt
} else {
push(statements, stmt)
statements[] = stmt
}
} else if (cursor == before) {
sync_to_statement()
@@ -1420,12 +1420,13 @@ 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}
if (node.from_row != null) err.line = node.from_row + 1
if (node.from_column != null) err.column = node.from_column + 1
push(sem_errors, err)
sem_errors[] = err
}
var make_scope = function(parent, fn_nr, opts) {
@@ -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
scope.vars[] = entry
}
var sem_lookup_var = function(scope, name) {
@@ -1499,7 +1503,7 @@ var parse = function(tokens, src, filename, tokenizer) {
}
var sem_add_intrinsic = function(name) {
if (find(intrinsics, name) == null) push(intrinsics, name)
if (find(intrinsics, name) == null) intrinsics[] = name
}
var functino_names = {
@@ -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])
@@ -1846,7 +1828,7 @@ var parse = function(tokens, src, filename, tokenizer) {
}
}
sr = sem_build_scope_record(fn_scope)
push(scopes_array, sr.rec)
scopes_array[] = sr.rec
expr.nr_slots = sr.nr_slots
expr.nr_close_slots = sr.nr_close
return null
@@ -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) {
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])
@@ -2116,7 +2110,7 @@ var parse = function(tokens, src, filename, tokenizer) {
}
}
sr = sem_build_scope_record(fn_scope)
push(scopes_array, sr.rec)
scopes_array[] = sr.rec
stmt.nr_slots = sr.nr_slots
stmt.nr_close_slots = sr.nr_close
return null
@@ -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
}
@@ -2154,13 +2153,14 @@ var parse = function(tokens, src, filename, tokenizer) {
new_scopes = [sr.rec]
i = 0
while (i < length(scopes_array)) {
push(new_scopes, scopes_array[i])
new_scopes[] = scopes_array[i]
i = i + 1
}
scopes_array = new_scopes
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
}
@@ -2183,7 +2183,7 @@ var parse = function(tokens, src, filename, tokenizer) {
if (ast.errors != null) {
_mi = 0
while (_mi < length(errors)) {
push(ast.errors, errors[_mi])
ast.errors[] = errors[_mi]
_mi = _mi + 1
}
} else {

338
plan.md
View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -149,7 +149,7 @@ if (!is_array(args) || length(args) < 1) {
}
for (; i < length(args) - 1; i++) {
push(sources, args[i])
sources[] = args[i]
}
archive = args[length(args) - 1]

View File

@@ -78,7 +78,7 @@ var run = function() {
arrfor(all_packages, function(p) {
if (p == 'core') return
if (!needed[p] && find(packages_to_remove, p) == null) {
push(packages_to_remove, p)
packages_to_remove[] = p
}
})
}

View File

@@ -165,11 +165,11 @@ for (i = 0; i < length(sorted); i++) {
// Format output
status_parts = []
if (is_linked) push(status_parts, "linked")
if (is_local) push(status_parts, "local")
if (!is_in_lock) push(status_parts, "not in lock")
if (!is_fetched) push(status_parts, "not fetched")
if (has_c_files) push(status_parts, "has C modules")
if (is_linked) status_parts[] = "linked"
if (is_local) status_parts[] = "local"
if (!is_in_lock) status_parts[] = "not in lock"
if (!is_fetched) status_parts[] = "not fetched"
if (has_c_files) status_parts[] = "has C modules"
commit_str = ""
if (lock_entry && lock_entry.commit) {

View File

@@ -21,7 +21,7 @@ var packages = shop.list_packages()
arrfor(packages, function(package_name) {
// Check if package name matches
if (search(package_name, query) != null) {
push(found_packages, package_name)
found_packages[] = package_name
}
// Search modules and actors within the package
@@ -29,14 +29,14 @@ arrfor(packages, function(package_name) {
var modules = pkg.list_modules(package_name)
arrfor(modules, function(mod) {
if (search(mod, query) != null) {
push(found_modules, package_name + ':' + mod)
found_modules[] = package_name + ':' + mod
}
})
var actors = pkg.list_programs(package_name)
arrfor(actors, function(actor) {
if (search(actor, query) != null) {
push(found_actors, package_name + ':' + actor)
found_actors[] = package_name + ':' + actor
}
})
} disruption {

View File

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

View File

@@ -179,7 +179,7 @@ var run = function() {
first_def[slot_num] = pc
first_def_op[slot_num] = op
}
push(events, {kind: "DEF", slot: operand_val, pc: pc, instr: instr})
events[] = {kind: "DEF", slot: operand_val, pc: pc, instr: instr}
}
di = di + 1
}
@@ -191,7 +191,7 @@ var run = function() {
slot_num = text(operand_val)
if (!uses[slot_num]) uses[slot_num] = 0
uses[slot_num] = uses[slot_num] + 1
push(events, {kind: "USE", slot: operand_val, pc: pc, instr: instr})
events[] = {kind: "USE", slot: operand_val, pc: pc, instr: instr}
}
ui = ui + 1
}
@@ -219,7 +219,7 @@ var run = function() {
parts = []
j = 1
while (j < n - 2) {
push(parts, fmt_val(evt.instr[j]))
parts[] = fmt_val(evt.instr[j])
j = j + 1
}
operands = text(parts, ", ")

View File

@@ -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,101 @@ 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)
/* Engine-env log proxy: routes log("channel", [msg]) through JS_Log.
Before set_log is called, JS_Log falls back to stderr.
After set_log, JS_Log forwards to the engine's JS log function.
This exists so mcode-generated type-check error paths (which always
access 'log' as an intrinsic) work inside engine.cm itself. */
static JSValue js_engine_log(JSContext *js, JSValue self,
int argc, JSValue *argv) {
if (argc < 2) return JS_NULL;
const char *channel = JS_ToCString(js, argv[0]);
if (!channel) return JS_NULL;
JSValue msg_val = JS_GetPropertyNumber(js, argv[1], 0);
const char *msg = JS_ToCString(js, msg_val);
if (msg) {
JS_Log(js, channel, "%s", msg);
JS_FreeCString(js, msg);
}
JS_FreeCString(js, channel);
return JS_NULL;
}
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 +378,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 +407,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);
}
@@ -381,18 +434,20 @@ void script_startup(cell_rt *prt)
}
tmp = shop_path ? JS_NewString(js, shop_path) : JS_NULL;
JS_SetPropertyStr(js, env_ref.val, "shop_path", tmp);
tmp = JS_NewCFunction(js, js_engine_log, "log", 2);
JS_SetPropertyStr(js, env_ref.val, "log", tmp);
// Stone the environment
JSValue hidden_env = JS_Stone(js, env_ref.val);
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 +510,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 +520,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 +548,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 +595,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 +628,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 +690,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,13 +716,31 @@ int cell_init(int argc, char **argv)
return 1;
}
// Retry engine from cache (new-style bootstrap seeds it)
// 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
@@ -672,10 +754,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 +765,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 {
@@ -697,13 +783,20 @@ int cell_init(int argc, char **argv)
}
JS_SetPropertyStr(ctx, env_ref.val, "args", args_ref.val);
JS_DeleteGCRef(ctx, &args_ref);
tmp = JS_NewCFunction(ctx, js_engine_log, "log", 2);
JS_SetPropertyStr(ctx, env_ref.val, "log", tmp);
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 +819,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 +862,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 +889,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);

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -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 (1000ULL * 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);

View File

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

View File

@@ -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);
/* 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);
}
}
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);
/* 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;
}
}
/* 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) {
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 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;
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);
}
}
/* ============================================================
Boolean conversion wrapper
============================================================ */
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);
}
}
int qbe_to_bool(JSContext *ctx, JSValue v) {
return JS_ToBool(ctx, v);
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)
============================================================ */
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));
/* 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_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();
}
/* 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;
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;
}
}
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;

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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,14 +1911,17 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) {
}
#endif
/* Check memory limit — kill actor if heap exceeds cap */
/* 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.
Use JS_Log "memory" channel which always fprintf's (safe during GC). */
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);
#endif
JS_Log(ctx, "memory", "%s: heap %zuKB exceeds limit %zuMB, killing",
ctx->name ? ctx->name : ctx->id,
ctx->current_block_size / 1024,
ctx->heap_memory_limit / (1024 * 1024));
return -1;
}
@@ -2066,6 +2045,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 +2055,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 +2143,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 +2173,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 +3278,27 @@ JS_RaiseDisrupt (JSContext *ctx, const char *fmt, ...) {
va_start (ap, fmt);
vsnprintf (buf, sizeof (buf), fmt, ap);
va_end (ap);
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). */
/* Log to "memory" channel + disrupt. Skips JS callback (can't allocate).
Uses fprintf directly — safe even during OOM. */
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 *name = ctx->name ? ctx->name : ctx->id;
const char *label = ctx->actor_label;
fprintf(stderr, "[memory] %s: out of memory — heap %zuKB / %zuKB block",
name ? name : "?", used / 1024, block / 1024);
if (limit > 0)
fprintf(stderr, ", limit %zuMB", limit / (1024 * 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.
@@ -11756,7 +12009,7 @@ void JS_CrashPrintStack(JSContext *ctx) {
if (!ctx) return;
if (JS_IsNull(ctx->reg_current_frame)) return;
static const char hdr[] = "\n--- JS Stack at crash ---\n";
static const char hdr[] = "\n--- JS Stack ---\n";
write(STDERR_FILENO, hdr, sizeof(hdr) - 1);
JSFrameRegister *frame = (JSFrameRegister *)JS_VALUE_GET_PTR(ctx->reg_current_frame);

View File

@@ -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,7 +32,7 @@ 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 */
@@ -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,10 +213,10 @@ 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);
@@ -202,11 +293,17 @@ void *timer_thread_func(void *arg) {
/* Only fire if turn_gen still matches (stale timers are ignored) */
uint32_t cur = atomic_load_explicit(&t.actor->turn_gen, memory_order_relaxed);
if (cur == t.turn_gen) {
/* Can't call JS_Log from timer thread — use fprintf */
const char *name = t.actor->name ? t.actor->name : t.actor->id;
if (t.type == TIMER_PAUSE) {
JS_SetPauseFlag(t.actor->context, 1);
fprintf(stderr, "[slow] %s: pausing (turn exceeded %llums)\n",
name, (unsigned long long)(ACTOR_FAST_TIMER_NS / 1000000));
JS_SetPauseFlag(t.actor, 1);
} else {
fprintf(stderr, "[slow] %s: kill timer fired (turn exceeded %llus)\n",
name, (unsigned long long)(ACTOR_SLOW_TIMER_NS / 1000000000));
t.actor->disrupt = 1;
JS_SetPauseFlag(t.actor->context, 2);
JS_SetPauseFlag(t.actor, 2);
}
}
} else {
@@ -216,7 +313,6 @@ 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);
}
@@ -244,7 +340,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;
@@ -308,6 +404,13 @@ void actor_initialize(void) {
// 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 +426,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,20 +463,19 @@ 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);
@@ -383,23 +485,24 @@ void actor_free(cell_rt *actor)
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);
pthread_mutex_t *m = actor->mutex;
pthread_mutex_t *mm = actor->msg_mutex;
free(actor);
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 +548,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,20 +580,14 @@ 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
fprintf(stderr, "set_actor_state: %s disrupted, freeing\n", actor->name ? actor->name : actor->id);
#endif
actor_free(actor);
return;
}
pthread_mutex_lock(actor->msg_mutex);
#ifdef SCHEDULER_DEBUG
fprintf(stderr, "set_actor_state: %s state=%d letters=%ld\n", actor->name ? actor->name : actor->id, actor->state, (long)arrlen(actor->letters));
#endif
switch(actor->state) {
case ACTOR_RUNNING:
case ACTOR_READY:
@@ -494,9 +601,6 @@ void set_actor_state(cell_rt *actor)
actor->is_quiescent = 0;
atomic_fetch_sub(&engine.quiescent_count, 1);
}
#ifdef SCHEDULER_DEBUG
fprintf(stderr, "set_actor_state: %s IDLE->READY, enqueueing (main=%d)\n", actor->name ? actor->name : actor->id, actor->main_thread_only);
#endif
actor->state = ACTOR_READY;
actor->ar = 0;
enqueue_actor_priority(actor);
@@ -548,7 +652,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)
@@ -560,8 +664,8 @@ uint32_t actor_remove_cb(cell_rt *actor, uint32_t id, uint32_t interval)
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);
@@ -571,7 +675,7 @@ uint32_t actor_remove_cb(cell_rt *actor, uint32_t id, uint32_t interval)
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;
@@ -589,7 +693,7 @@ void actor_unneeded(cell_rt *actor, JSValue fn, double seconds)
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 +726,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,9 +743,12 @@ 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);
@@ -654,7 +758,7 @@ cell_rt *create_actor(void *wota)
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,7 +780,7 @@ 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.";
@@ -698,7 +802,7 @@ const char *send_message(const char *id, void *msg)
return NULL;
}
void actor_turn(cell_rt *actor)
void actor_turn(JSContext *actor)
{
pthread_mutex_lock(actor->mutex);
#ifdef ACTOR_TRACE
@@ -710,16 +814,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,29 +838,27 @@ 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)) {
/* Still suspended after kill timer — shouldn't happen, kill it */
fprintf(stderr, "[slow] %s: still suspended after resume, killing\n",
actor->name ? actor->name : actor->id);
actor->disrupt = 1;
goto ENDTURN;
}
if (JS_IsException(result)) {
if (!uncaught_exception(actor->context, result))
if (!uncaught_exception(actor, result))
actor->disrupt = 1;
}
actor->slow_strikes++;
#ifdef ACTOR_TRACE
fprintf(stderr, "[ACTOR_TRACE] %s: slow strike %d/%d\n",
JS_Log(actor, "slow", "%s: slow strike %d/%d",
actor->name ? actor->name : actor->id,
actor->slow_strikes, ACTOR_SLOW_STRIKES_MAX);
#endif
if (actor->slow_strikes >= ACTOR_SLOW_STRIKES_MAX) {
#ifdef ACTOR_TRACE
fprintf(stderr, "[ACTOR_TRACE] %s: %d slow strikes, killing\n",
JS_Log(actor, "slow", "%s: killed after %d consecutive slow turns",
actor->name ? actor->name : actor->id, actor->slow_strikes);
#endif
actor->disrupt = 1;
}
goto ENDTURN;
@@ -764,16 +871,12 @@ void actor_turn(cell_rt *actor)
pthread_mutex_unlock(actor->msg_mutex);
goto ENDTURN;
}
#ifdef SCHEDULER_DEBUG
fprintf(stderr, "actor_turn: %s has %d letters, type=%d\n",
actor->name ? actor->name : actor->id, pending, actor->letters[0].type);
#endif
letter l = actor->letters[0];
arrdel(actor->letters, 0);
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 +888,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,35 +924,28 @@ 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
fprintf(stderr, "actor_turn ENDTURN: %s disrupted, freeing\n",
actor->name ? actor->name : actor->id);
#endif
pthread_mutex_unlock(actor->mutex);
actor_free(actor);
return;
}
#ifdef SCHEDULER_DEBUG
fprintf(stderr, "actor_turn ENDTURN: %s has %ld letters, calling set_actor_state\n",
actor->name ? actor->name : actor->id, (long)arrlen(actor->letters));
#endif
set_actor_state(actor);
pthread_mutex_unlock(actor->mutex);
return;
ENDTURN_SLOW:
g_crash_ctx = NULL;
#ifdef ACTOR_TRACE
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);
/* VM suspended mid-turn — can't call JS_Log, use fprintf.
Print stack trace while frames are still intact. */
fprintf(stderr, "[slow] %s: suspended mid-turn, entering slow queue (strike %d/%d)\n",
actor->name ? actor->name : actor->id,
actor->slow_strikes + 1, ACTOR_SLOW_STRIKES_MAX);
if (actor->actor_trace_hook)
actor->actor_trace_hook(actor, CELL_HOOK_EXIT);
enqueue_actor_priority(actor);
pthread_mutex_unlock(actor->mutex);
}
@@ -864,49 +956,59 @@ 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);
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();
@@ -923,7 +1025,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 +1038,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;
}

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