Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b7cde9400 | ||
|
|
a1ee7dd458 | ||
|
|
9dbe699033 | ||
|
|
f809cb05f0 | ||
|
|
788ea98651 | ||
|
|
433ce8a86e | ||
|
|
cd6e357b6e | ||
|
|
f4f56ed470 | ||
|
|
ff61ab1f50 | ||
|
|
46c345d34e | ||
|
|
dc440587ff | ||
|
|
8f92870141 | ||
|
|
7fc4a205f6 | ||
|
|
23b201bdd7 | ||
|
|
913ec9afb1 | ||
|
|
56de0ce803 | ||
|
|
96bbb9e4c8 | ||
|
|
ebd624b772 |
25
Makefile
25
Makefile
@@ -5,15 +5,11 @@
|
||||
# or manually build with meson once.
|
||||
#
|
||||
# The cell shop is at ~/.cell and core scripts are installed to ~/.cell/core
|
||||
#
|
||||
# See BUILDING.md for details on the bootstrap process and .mach files.
|
||||
|
||||
CELL_SHOP = $(HOME)/.cell
|
||||
CELL_CORE_PACKAGE = $(CELL_SHOP)/packages/core
|
||||
|
||||
# .cm sources that compile to .mcode bytecode
|
||||
MACH_SOURCES = tokenize.cm parse.cm fold.cm mcode.cm \
|
||||
internal/bootstrap.cm internal/engine.cm
|
||||
doit: bootstrap
|
||||
|
||||
maker: install
|
||||
|
||||
@@ -22,7 +18,7 @@ makecell:
|
||||
cp cell /opt/homebrew/bin/
|
||||
|
||||
# Install core: symlink this directory to ~/.cell/core
|
||||
install: bootstrap .mach.stamp $(CELL_SHOP)
|
||||
install: cell $(CELL_SHOP)
|
||||
@echo "Linking cell core to $(CELL_CORE_PACKAGE)"
|
||||
rm -rf $(CELL_CORE_PACKAGE)
|
||||
ln -s $(PWD) $(CELL_CORE_PACKAGE)
|
||||
@@ -46,16 +42,6 @@ libcell_runtime.dylib: $(CELL_SHOP)/build/dynamic
|
||||
cell_main: source/main.c libcell_runtime.dylib
|
||||
cc -o cell_main source/main.c -L. -lcell_runtime -Wl,-rpath,@loader_path -Wl,-rpath,/opt/homebrew/lib
|
||||
|
||||
# Regenerate .mach bytecode when any .cm source changes
|
||||
.mach.stamp: $(MACH_SOURCES)
|
||||
./cell --dev regen
|
||||
@touch $@
|
||||
|
||||
# Force-regenerate all .mach bytecode files
|
||||
regen:
|
||||
./cell --core . regen
|
||||
@touch .mach.stamp
|
||||
|
||||
# Create the cell shop directories
|
||||
$(CELL_SHOP):
|
||||
mkdir -p $(CELL_SHOP)
|
||||
@@ -78,13 +64,12 @@ bootstrap:
|
||||
meson compile -C build_bootstrap
|
||||
cp build_bootstrap/cell .
|
||||
cp build_bootstrap/libcell_runtime.dylib .
|
||||
@echo "Bootstrap complete. Cell shop initialized at $(CELL_SHOP)"
|
||||
@echo "Now run 'make' to rebuild with cell itself."
|
||||
@echo "Bootstrap complete. Run cell like ./cell --dev to use a local shop at .cell."
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
rm -rf $(CELL_SHOP)/build build_bootstrap
|
||||
rm -f cell cell_main libcell_runtime.dylib .mach.stamp
|
||||
rm -f cell cell_main libcell_runtime.dylib
|
||||
|
||||
# Ensure dynamic build directory exists
|
||||
$(CELL_SHOP)/build/dynamic: $(CELL_SHOP)
|
||||
@@ -95,4 +80,4 @@ meson:
|
||||
meson setup build_dbg -Dbuildtype=debugoptimized
|
||||
meson install -C build_dbg
|
||||
|
||||
.PHONY: cell static bootstrap clean meson install regen
|
||||
.PHONY: cell static bootstrap clean meson install
|
||||
|
||||
@@ -64,13 +64,16 @@ var format_ns = function(ns) {
|
||||
|
||||
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)) {
|
||||
var keys = array(mod)
|
||||
var i = 0
|
||||
keys = array(mod)
|
||||
i = 0
|
||||
while (i < length(keys)) {
|
||||
var k = keys[i]
|
||||
k = keys[i]
|
||||
if (is_function(mod[k])) {
|
||||
push(benches, {name: k, fn: mod[k]})
|
||||
}
|
||||
@@ -128,10 +131,11 @@ if (length(vm_benches) == 0) {
|
||||
var native_mod = null
|
||||
var native_benches = []
|
||||
var has_native = fd.is_file(dylib_path)
|
||||
var lib = null
|
||||
|
||||
if (has_native) {
|
||||
print('loading native module: ' + dylib_path)
|
||||
var lib = os.dylib_open(dylib_path)
|
||||
lib = os.dylib_open(dylib_path)
|
||||
native_mod = os.dylib_symbol(lib, symbol)
|
||||
native_benches = collect_benches(native_mod)
|
||||
} else {
|
||||
@@ -146,27 +150,34 @@ print('samples: ' + text(iterations) + ' (warmup: ' + text(WARMUP) + ')')
|
||||
print('')
|
||||
|
||||
var pad = function(s, n) {
|
||||
while (length(s) < n) s = s + ' '
|
||||
return s
|
||||
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)) {
|
||||
var b = vm_benches[i]
|
||||
var vm_result = run_bench(b.fn, 'vm')
|
||||
b = vm_benches[i]
|
||||
vm_result = run_bench(b.fn, 'vm')
|
||||
|
||||
print(pad(b.name, 20) + ' VM: ' + pad(format_ns(vm_result.median), 12) + ' (median) ' + format_ns(vm_result.mean) + ' (mean)')
|
||||
|
||||
// find matching native bench
|
||||
var j = 0
|
||||
var found = false
|
||||
j = 0
|
||||
found = false
|
||||
while (j < length(native_benches)) {
|
||||
if (native_benches[j].name == b.name) {
|
||||
var nat_result = run_bench(native_benches[j].fn, 'native')
|
||||
nat_result = run_bench(native_benches[j].fn, 'native')
|
||||
print(pad('', 20) + ' NT: ' + pad(format_ns(nat_result.median), 12) + ' (median) ' + format_ns(nat_result.mean) + ' (mean)')
|
||||
|
||||
if (nat_result.median > 0) {
|
||||
var speedup = vm_result.median / nat_result.median
|
||||
speedup = vm_result.median / nat_result.median
|
||||
print(pad('', 20) + ' speedup: ' + text(round(speedup * 100) / 100) + 'x')
|
||||
}
|
||||
found = true
|
||||
|
||||
13898
boot/bootstrap.cm.mcode
13898
boot/bootstrap.cm.mcode
File diff suppressed because it is too large
Load Diff
39881
boot/engine.cm.mcode
39881
boot/engine.cm.mcode
File diff suppressed because it is too large
Load Diff
2244
boot/fd.cm.mcode
2244
boot/fd.cm.mcode
File diff suppressed because it is too large
Load Diff
39395
boot/fold.cm.mcode
39395
boot/fold.cm.mcode
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
14083
boot/link.cm.mcode
14083
boot/link.cm.mcode
File diff suppressed because it is too large
Load Diff
45017
boot/mcode.cm.mcode
45017
boot/mcode.cm.mcode
File diff suppressed because it is too large
Load Diff
14693
boot/package.cm.mcode
14693
boot/package.cm.mcode
File diff suppressed because it is too large
Load Diff
52683
boot/parse.cm.mcode
52683
boot/parse.cm.mcode
File diff suppressed because it is too large
Load Diff
8189
boot/pronto.cm.mcode
8189
boot/pronto.cm.mcode
File diff suppressed because it is too large
Load Diff
11500
boot/qbe.cm.mcode
11500
boot/qbe.cm.mcode
File diff suppressed because it is too large
Load Diff
77345
boot/qbe_emit.cm.mcode
77345
boot/qbe_emit.cm.mcode
File diff suppressed because it is too large
Load Diff
@@ -1,153 +0,0 @@
|
||||
// seed_bootstrap.cm — minimal bootstrap for regenerating boot files
|
||||
// Loads only the compiler pipeline, runs a script directly (no engine/actors)
|
||||
// Usage: ./cell --dev --seed regen
|
||||
//
|
||||
// Hidden env: os, core_path, shop_path, args, json
|
||||
|
||||
var load_internal = os.load_internal
|
||||
var fd = load_internal("js_core_internal_fd_use")
|
||||
|
||||
var use_cache = {}
|
||||
use_cache['fd'] = fd
|
||||
use_cache['os'] = os
|
||||
use_cache['json'] = json
|
||||
|
||||
function use_basic(path) {
|
||||
if (use_cache[path])
|
||||
return use_cache[path]
|
||||
var result = load_internal("js_core_" + replace(path, '/', '_') + "_use")
|
||||
if (result) {
|
||||
use_cache[path] = result
|
||||
return result
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Load a module from boot .mcode — no caching, just eval
|
||||
function boot_load(name) {
|
||||
var mcode_path = core_path + '/boot/' + name + ".cm.mcode"
|
||||
var mcode_json = null
|
||||
if (!fd.is_file(mcode_path)) {
|
||||
print("seed: missing boot mcode: " + mcode_path + "\n")
|
||||
disrupt
|
||||
}
|
||||
mcode_json = text(fd.slurp(mcode_path))
|
||||
return mach_eval_mcode(name, mcode_json, {use: use_basic})
|
||||
}
|
||||
|
||||
var tokenize_mod = boot_load("tokenize")
|
||||
var parse_mod = boot_load("parse")
|
||||
var fold_mod = boot_load("fold")
|
||||
var mcode_mod = boot_load("mcode")
|
||||
var streamline_mod = boot_load("streamline")
|
||||
|
||||
use_cache['tokenize'] = tokenize_mod
|
||||
use_cache['parse'] = parse_mod
|
||||
use_cache['fold'] = fold_mod
|
||||
use_cache['mcode'] = mcode_mod
|
||||
use_cache['streamline'] = streamline_mod
|
||||
|
||||
function analyze(src, filename) {
|
||||
var tok_result = tokenize_mod(src, filename)
|
||||
var ast = parse_mod(tok_result.tokens, src, filename, tokenize_mod)
|
||||
var _i = 0
|
||||
var e = null
|
||||
var has_errors = ast.errors != null && length(ast.errors) > 0
|
||||
if (has_errors) {
|
||||
while (_i < length(ast.errors)) {
|
||||
e = ast.errors[_i]
|
||||
if (e.line != null) {
|
||||
print(`${filename}:${text(e.line)}:${text(e.column)}: error: ${e.message}`)
|
||||
} else {
|
||||
print(`${filename}: error: ${e.message}`)
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
disrupt
|
||||
}
|
||||
return fold_mod(ast)
|
||||
}
|
||||
|
||||
function run_ast(name, ast, env) {
|
||||
var compiled = mcode_mod(ast)
|
||||
var optimized = streamline_mod(compiled)
|
||||
var mcode_json = json.encode(optimized)
|
||||
return mach_eval_mcode(name, mcode_json, env)
|
||||
}
|
||||
|
||||
function use_fn(path) {
|
||||
var result = null
|
||||
var file_path = null
|
||||
var script = null
|
||||
var ast = null
|
||||
var mcode_path = null
|
||||
var mcode_json = null
|
||||
if (use_cache[path])
|
||||
return use_cache[path]
|
||||
|
||||
// Try C embed
|
||||
result = load_internal("js_core_" + replace(path, '/', '_') + "_use")
|
||||
if (result) {
|
||||
use_cache[path] = result
|
||||
return result
|
||||
}
|
||||
|
||||
// Try boot mcode
|
||||
mcode_path = core_path + '/boot/' + path + '.cm.mcode'
|
||||
if (fd.is_file(mcode_path)) {
|
||||
mcode_json = text(fd.slurp(mcode_path))
|
||||
result = mach_eval_mcode(path, mcode_json, {use: use_fn})
|
||||
use_cache[path] = result
|
||||
return result
|
||||
}
|
||||
|
||||
// Try .cm source (CWD then core)
|
||||
file_path = path + '.cm'
|
||||
if (!fd.is_file(file_path))
|
||||
file_path = core_path + '/' + path + '.cm'
|
||||
if (fd.is_file(file_path)) {
|
||||
script = text(fd.slurp(file_path))
|
||||
ast = analyze(script, file_path)
|
||||
result = run_ast(path, ast, {use: use_fn})
|
||||
use_cache[path] = result
|
||||
return result
|
||||
}
|
||||
|
||||
print("seed: module not found: " + path + "\n")
|
||||
disrupt
|
||||
}
|
||||
|
||||
// Run the program from args
|
||||
var program = args[0]
|
||||
var user_args = []
|
||||
var _j = 1
|
||||
var prog_path = null
|
||||
var script = null
|
||||
var ast = null
|
||||
|
||||
if (!program) {
|
||||
print("seed: no program specified\n")
|
||||
disrupt
|
||||
}
|
||||
|
||||
while (_j < length(args)) {
|
||||
push(user_args, args[_j])
|
||||
_j = _j + 1
|
||||
}
|
||||
|
||||
prog_path = program + '.ce'
|
||||
if (!fd.is_file(prog_path))
|
||||
prog_path = core_path + '/' + program + '.ce'
|
||||
if (!fd.is_file(prog_path)) {
|
||||
prog_path = program + '.cm'
|
||||
if (!fd.is_file(prog_path))
|
||||
prog_path = core_path + '/' + program + '.cm'
|
||||
}
|
||||
if (!fd.is_file(prog_path)) {
|
||||
print("seed: program not found: " + program + "\n")
|
||||
disrupt
|
||||
}
|
||||
|
||||
script = text(fd.slurp(prog_path))
|
||||
ast = analyze(script, prog_path)
|
||||
run_ast(program, ast, {use: use_fn, args: user_args})
|
||||
File diff suppressed because it is too large
Load Diff
17230
boot/streamline.cm.mcode
17230
boot/streamline.cm.mcode
File diff suppressed because it is too large
Load Diff
14564
boot/time.cm.mcode
14564
boot/time.cm.mcode
File diff suppressed because it is too large
Load Diff
20456
boot/tokenize.cm.mcode
20456
boot/tokenize.cm.mcode
File diff suppressed because it is too large
Load Diff
12640
boot/toml.cm.mcode
12640
boot/toml.cm.mcode
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
24524
boot/verify_ir.cm.mcode
24524
boot/verify_ir.cm.mcode
File diff suppressed because it is too large
Load Diff
4
build.cm
4
build.cm
@@ -288,7 +288,7 @@ Build.build_module_dylib = function(pkg, file, target, buildtype) {
|
||||
}
|
||||
|
||||
// Install to deterministic lib/<pkg>/<stem>.dylib
|
||||
var file_stem = fd.stem(file)
|
||||
var file_stem = file
|
||||
var install_dir = shop.get_lib_dir() + '/' + shop.lib_name_for_package(pkg)
|
||||
var stem_dir = fd.dirname(file_stem)
|
||||
if (stem_dir && stem_dir != '.') {
|
||||
@@ -539,7 +539,7 @@ Build.compile_native = function(src_path, target, buildtype, pkg) {
|
||||
|
||||
// Install to deterministic lib/<pkg>/<stem>.dylib
|
||||
if (pkg) {
|
||||
native_stem = fd.stem(fd.basename(src_path))
|
||||
native_stem = fd.basename(src_path)
|
||||
native_install_dir = shop.get_lib_dir() + '/' + shop.lib_name_for_package(pkg)
|
||||
ensure_dir(native_install_dir)
|
||||
native_install_path = native_install_dir + '/' + native_stem + dylib_ext
|
||||
|
||||
2
clean.ce
2
clean.ce
@@ -89,6 +89,7 @@ var dirs_to_delete = []
|
||||
|
||||
// Gather packages to clean
|
||||
var packages_to_clean = []
|
||||
var _gather = null
|
||||
|
||||
if (is_shop_scope) {
|
||||
packages_to_clean = shop.list_packages()
|
||||
@@ -99,7 +100,6 @@ if (is_shop_scope) {
|
||||
// Single package
|
||||
push(packages_to_clean, scope)
|
||||
|
||||
var _gather = null
|
||||
if (deep) {
|
||||
_gather = function() {
|
||||
deps = pkg.gather_dependencies(scope)
|
||||
|
||||
92
compare_aot.ce
Normal file
92
compare_aot.ce
Normal file
@@ -0,0 +1,92 @@
|
||||
// compare_aot.ce — compile a .cm module via both paths and compare results
|
||||
//
|
||||
// Usage:
|
||||
// cell --dev compare_aot.ce <module.cm>
|
||||
|
||||
var build = use('build')
|
||||
var fd_mod = use('fd')
|
||||
var os = use('os')
|
||||
var json = use('json')
|
||||
|
||||
var show = function(v) {
|
||||
return json.encode(v)
|
||||
}
|
||||
|
||||
if (length(args) < 1) {
|
||||
print('usage: cell --dev compare_aot.ce <module.cm>')
|
||||
return
|
||||
}
|
||||
|
||||
var file = args[0]
|
||||
if (!fd_mod.is_file(file)) {
|
||||
if (!ends_with(file, '.cm') && fd_mod.is_file(file + '.cm'))
|
||||
file = file + '.cm'
|
||||
else {
|
||||
print('file not found: ' + file)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var abs = fd_mod.realpath(file)
|
||||
|
||||
// Shared compilation front-end
|
||||
var tokenize = use('tokenize')
|
||||
var parse_mod = use('parse')
|
||||
var fold = use('fold')
|
||||
var mcode_mod = use('mcode')
|
||||
var streamline_mod = use('streamline')
|
||||
|
||||
var src = text(fd_mod.slurp(abs))
|
||||
var tok = tokenize(src, abs)
|
||||
var ast = parse_mod(tok.tokens, src, abs, tokenize)
|
||||
var folded = fold(ast)
|
||||
var compiled = mcode_mod(folded)
|
||||
var optimized = streamline_mod(compiled)
|
||||
|
||||
// --- Interpreted (mach VM) ---
|
||||
print('--- interpreted ---')
|
||||
var mcode_json = json.encode(optimized)
|
||||
var mach_blob = mach_compile_mcode_bin(abs, mcode_json)
|
||||
var result_interp = mach_load(mach_blob, stone({}))
|
||||
print('result: ' + show(result_interp))
|
||||
|
||||
// --- Native (AOT via QBE) ---
|
||||
print('\n--- native ---')
|
||||
var dylib_path = build.compile_native(abs, null, null, null)
|
||||
print('dylib: ' + dylib_path)
|
||||
|
||||
var handle = os.dylib_open(dylib_path)
|
||||
if (!handle) {
|
||||
print('failed to open dylib')
|
||||
return
|
||||
}
|
||||
|
||||
// Build env with runtime functions. Must include starts_with etc. because
|
||||
// the GC can lose global object properties after compaction.
|
||||
var env = stone({
|
||||
logical: logical,
|
||||
some: some,
|
||||
every: every,
|
||||
starts_with: starts_with,
|
||||
ends_with: ends_with,
|
||||
log: log,
|
||||
fallback: fallback,
|
||||
parallel: parallel,
|
||||
race: race,
|
||||
sequence: sequence
|
||||
})
|
||||
|
||||
var result_native = os.native_module_load(handle, env)
|
||||
print('result: ' + show(result_native))
|
||||
|
||||
// --- Comparison ---
|
||||
print('\n--- comparison ---')
|
||||
var s_interp = show(result_interp)
|
||||
var s_native = show(result_native)
|
||||
if (s_interp == s_native) {
|
||||
print('MATCH')
|
||||
} else {
|
||||
print('MISMATCH')
|
||||
print(' interp: ' + s_interp)
|
||||
print(' native: ' + s_native)
|
||||
}
|
||||
82
compile.ce
82
compile.ce
@@ -1,85 +1,27 @@
|
||||
// compile.ce — compile a .cm module to native .dylib via QBE
|
||||
// compile.ce — compile a .cm or .ce file to native .dylib via QBE
|
||||
//
|
||||
// Usage:
|
||||
// cell --dev compile.ce <file.cm>
|
||||
// cell compile <file.cm|file.ce>
|
||||
//
|
||||
// Produces <file>.dylib in the current directory.
|
||||
// Installs the dylib to .cell/lib/<pkg>/<stem>.dylib
|
||||
|
||||
var shop = use('internal/shop')
|
||||
var build = use('build')
|
||||
var fd = use('fd')
|
||||
var os = use('os')
|
||||
|
||||
if (length(args) < 1) {
|
||||
print('usage: cell --dev compile.ce <file.cm>')
|
||||
print('usage: cell compile <file.cm|file.ce>')
|
||||
return
|
||||
}
|
||||
|
||||
var file = args[0]
|
||||
var base = file
|
||||
if (ends_with(base, '.cm')) {
|
||||
base = text(base, 0, length(base) - 3)
|
||||
}
|
||||
|
||||
var safe = replace(replace(base, '/', '_'), '-', '_')
|
||||
var symbol = 'js_' + safe + '_use'
|
||||
var tmp = '/tmp/qbe_' + safe
|
||||
var ssa_path = tmp + '.ssa'
|
||||
var s_path = tmp + '.s'
|
||||
var o_path = tmp + '.o'
|
||||
var rt_o_path = '/tmp/qbe_rt.o'
|
||||
var dylib_path = file + '.dylib'
|
||||
var cwd = fd.getcwd()
|
||||
var rc = 0
|
||||
|
||||
// Step 1: emit QBE IL
|
||||
print('emit qbe...')
|
||||
rc = os.system('cd ' + cwd + ' && ./cell --dev qbe.ce ' + file + ' > ' + ssa_path)
|
||||
if (rc != 0) {
|
||||
print('failed to emit qbe il')
|
||||
if (!fd.is_file(file)) {
|
||||
print('file not found: ' + file)
|
||||
return
|
||||
}
|
||||
|
||||
// Step 2: append wrapper function — called as symbol(ctx) by os.dylib_symbol.
|
||||
// Delegates to cell_rt_module_entry which heap-allocates a frame
|
||||
// (so closures survive) and calls cell_main.
|
||||
var wrapper_cmd = `printf '\nexport function l $` + symbol + `(l %%ctx) {\n@entry\n %%result =l call $cell_rt_module_entry(l %%ctx)\n ret %%result\n}\n' >> ` + ssa_path
|
||||
rc = os.system(wrapper_cmd)
|
||||
if (rc != 0) {
|
||||
print('wrapper append failed')
|
||||
return
|
||||
}
|
||||
var abs = fd.realpath(file)
|
||||
var file_info = shop.file_info(abs)
|
||||
var pkg = file_info.package
|
||||
|
||||
// Step 3: compile QBE IL to assembly
|
||||
print('qbe compile...')
|
||||
rc = os.system('qbe -o ' + s_path + ' ' + ssa_path)
|
||||
if (rc != 0) {
|
||||
print('qbe compilation failed')
|
||||
return
|
||||
}
|
||||
|
||||
// Step 4: assemble
|
||||
print('assemble...')
|
||||
rc = os.system('cc -c ' + s_path + ' -o ' + o_path)
|
||||
if (rc != 0) {
|
||||
print('assembly failed')
|
||||
return
|
||||
}
|
||||
|
||||
// Step 5: compile runtime stubs (cached — skip if already built)
|
||||
if (!fd.is_file(rt_o_path)) {
|
||||
print('compile runtime stubs...')
|
||||
rc = os.system('cc -c ' + cwd + '/qbe_rt.c -o ' + rt_o_path + ' -fPIC')
|
||||
if (rc != 0) {
|
||||
print('runtime stubs compilation failed')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6: link dylib
|
||||
print('link...')
|
||||
rc = os.system('cc -shared -fPIC -undefined dynamic_lookup ' + o_path + ' ' + rt_o_path + ' -o ' + cwd + '/' + dylib_path)
|
||||
if (rc != 0) {
|
||||
print('linking failed')
|
||||
return
|
||||
}
|
||||
|
||||
print('built: ' + dylib_path)
|
||||
build.compile_native(abs, null, null, pkg)
|
||||
|
||||
4
diff.ce
4
diff.ce
@@ -129,11 +129,11 @@ function diff_test_file(file_path) {
|
||||
|
||||
// Build env for module loading
|
||||
var make_env = function() {
|
||||
return {
|
||||
return stone({
|
||||
use: function(path) {
|
||||
return shop.use(path, use_pkg)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Read and parse
|
||||
|
||||
93
docs/shop.md
93
docs/shop.md
@@ -9,18 +9,36 @@ The shop is the module resolution and loading engine behind `use()`. It handles
|
||||
|
||||
## Startup Pipeline
|
||||
|
||||
When `pit` runs a program, three layers bootstrap in sequence:
|
||||
When `pit` runs a program, startup takes one of two paths:
|
||||
|
||||
### Fast path (warm cache)
|
||||
|
||||
```
|
||||
bootstrap.cm → engine.cm → shop.cm → user program
|
||||
C runtime → engine.cm (from cache) → shop.cm → user program
|
||||
```
|
||||
|
||||
**bootstrap.cm** loads the compiler toolchain (tokenize, parse, fold, mcode, streamline) from pre-compiled bytecode. It defines `analyze()` (source to AST) and `compile_to_blob()` (AST to binary blob). It then loads engine.cm.
|
||||
The C runtime hashes the source of `internal/engine.cm` with BLAKE2 and looks up the hash in the content-addressed cache (`~/.pit/build/<hash>`). On a cache hit, engine.cm loads directly — no bootstrap involved.
|
||||
|
||||
**engine.cm** creates the actor runtime (`$_`), defines `use_core()` for loading core modules, and populates the environment that shop receives. It then loads shop.cm via `use_core('internal/shop')`.
|
||||
### Cold path (first run or cache cleared)
|
||||
|
||||
```
|
||||
C runtime → bootstrap.cm → (seeds cache) → engine.cm (from cache) → shop.cm → user program
|
||||
```
|
||||
|
||||
On a cache miss, the C runtime loads `boot/bootstrap.cm.mcode` (a pre-compiled seed). Bootstrap compiles engine.cm and the pipeline modules (tokenize, parse, fold, mcode, streamline) from source and caches the results. The C runtime then retries the engine cache lookup, which now succeeds.
|
||||
|
||||
### Engine
|
||||
|
||||
**engine.cm** is self-sufficient. It loads its own compilation pipeline from the content-addressed cache, with fallback to the pre-compiled seeds in `boot/`. It defines `analyze()` (source to AST), `compile_to_blob()` (AST to binary blob), and `use_core()` for loading core modules. It creates the actor runtime and loads shop.cm via `use_core('internal/shop')`.
|
||||
|
||||
### Shop
|
||||
|
||||
**shop.cm** receives its dependencies through the module environment — `analyze`, `run_ast_fn`, `use_cache`, `shop_path`, `runtime_env`, `content_hash`, `cache_path`, and others. It defines `Shop.use()`, which is the function behind every `use()` call in user code.
|
||||
|
||||
### Cache invalidation
|
||||
|
||||
All caching is content-addressed by BLAKE2 hash of the source. When any source file changes, its hash changes and the old cache entry is simply never looked up again. No manual invalidation is needed. To force a full rebuild, delete `~/.pit/build/`.
|
||||
|
||||
## Module Resolution
|
||||
|
||||
When `use('path')` is called from a package context, the shop resolves the module through a multi-layer search. Both the `.cm` script file and C symbol are resolved independently, and the one with the narrowest scope wins.
|
||||
@@ -63,16 +81,17 @@ When loading a module, the shop checks (in order):
|
||||
|
||||
1. **In-memory cache** — `use_cache[key]`, checked first on every `use()` call
|
||||
2. **Installed dylib** — per-file `.dylib` in `~/.pit/lib/<pkg>/<stem>.dylib`
|
||||
3. **Internal symbols** — statically linked into the `pit` binary (fat builds)
|
||||
4. **Installed mach** — pre-compiled bytecode in `~/.pit/lib/<pkg>/<stem>.mach`
|
||||
5. **Cached bytecode** — content-addressed in `~/.pit/build/<hash>` (no extension)
|
||||
6. **Cached .mcode IR** — JSON IR in `~/.pit/build/<hash>.mcode`
|
||||
7. **Adjacent .mach/.mcode** — files alongside the source (e.g., `sprite.mach`)
|
||||
8. **Source compilation** — full pipeline: analyze, mcode, streamline, serialize
|
||||
3. **Installed mach** — pre-compiled bytecode in `~/.pit/lib/<pkg>/<stem>.mach`
|
||||
4. **Cached bytecode** — content-addressed in `~/.pit/build/<hash>` (no extension)
|
||||
5. **Cached .mcode IR** — JSON IR in `~/.pit/build/<hash>.mcode`
|
||||
6. **Internal symbols** — statically linked into the `pit` binary (fat builds)
|
||||
7. **Source compilation** — full pipeline: analyze, mcode, streamline, serialize
|
||||
|
||||
When both a `.dylib` and `.mach` exist for the same module in `lib/`, the dylib is selected. Dylib resolution also wins over internal symbols, so a dylib in `lib/` can hot-patch a fat binary. Delete the dylib to fall back to mach or static.
|
||||
|
||||
Results from steps 6-8 are cached back to the content-addressed store for future loads.
|
||||
Results from steps 5-7 are cached back to the content-addressed store for future loads.
|
||||
|
||||
Each loading method (except the in-memory cache) can be individually enabled or disabled via `shop.toml` policy flags — see [Shop Configuration](#shop-configuration) below.
|
||||
|
||||
### Content-Addressed Store
|
||||
|
||||
@@ -89,7 +108,7 @@ This scheme provides automatic cache invalidation: when source changes, its hash
|
||||
|
||||
### Core Module Caching
|
||||
|
||||
Core modules loaded via `use_core()` in engine.cm follow the same pattern. On first startup after a fresh install, core modules are compiled from `.cm.mcode` JSON IR and cached as `.mach` blobs. Subsequent startups load from cache, skipping the JSON parse and compile steps entirely.
|
||||
Core modules loaded via `use_core()` in engine.cm follow the same content-addressed pattern. On first use, a module is compiled from source and cached by the BLAKE2 hash of its source content. Subsequent loads with unchanged source hit the cache directly.
|
||||
|
||||
User scripts (`.ce` files) are also cached. The first run compiles and caches; subsequent runs with unchanged source load from cache.
|
||||
|
||||
@@ -124,6 +143,48 @@ When a module is loaded, the shop builds an `env` object that becomes the module
|
||||
|
||||
The set of injected capabilities is controlled by `script_inject_for()`, which can be tuned per package or file.
|
||||
|
||||
## Shop Configuration
|
||||
|
||||
The shop reads an optional `shop.toml` file from the shop root (`~/.pit/shop.toml`). This file controls which loading methods are permitted through policy flags.
|
||||
|
||||
### Policy Flags
|
||||
|
||||
All flags default to `true`. Set a flag to `false` to disable that loading method.
|
||||
|
||||
```toml
|
||||
[policy]
|
||||
allow_dylib = true # per-file .dylib loading (requires dlopen)
|
||||
allow_static = true # statically linked C symbols (fat builds)
|
||||
allow_mach = true # pre-compiled .mach bytecode (lib/ and build cache)
|
||||
allow_compile = true # on-the-fly source compilation
|
||||
```
|
||||
|
||||
### Example Configurations
|
||||
|
||||
**Production lockdown** — only use pre-compiled artifacts, never compile from source:
|
||||
|
||||
```toml
|
||||
[policy]
|
||||
allow_compile = false
|
||||
```
|
||||
|
||||
**Pure-script mode** — bytecode only, no native code:
|
||||
|
||||
```toml
|
||||
[policy]
|
||||
allow_dylib = false
|
||||
allow_static = false
|
||||
```
|
||||
|
||||
**No dlopen platforms** — static linking and bytecode only:
|
||||
|
||||
```toml
|
||||
[policy]
|
||||
allow_dylib = false
|
||||
```
|
||||
|
||||
If `shop.toml` is missing or has no `[policy]` section, all methods are enabled (default behavior).
|
||||
|
||||
## Shop Directory Layout
|
||||
|
||||
```
|
||||
@@ -145,16 +206,18 @@ The set of injected capabilities is controlled by `script_inject_for()`, which c
|
||||
│ └── <hash>.mcode # cached JSON IR
|
||||
├── cache/ # downloaded package zip archives
|
||||
├── lock.toml # installed package versions and commit hashes
|
||||
└── link.toml # local development link overrides
|
||||
├── link.toml # local development link overrides
|
||||
└── shop.toml # optional shop configuration and policy flags
|
||||
```
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `internal/bootstrap.cm` | Loads compiler, defines `analyze()` and `compile_to_blob()` |
|
||||
| `internal/engine.cm` | Actor runtime, `use_core()`, environment setup |
|
||||
| `internal/bootstrap.cm` | Minimal cache seeder (cold start only) |
|
||||
| `internal/engine.cm` | Self-sufficient entry point: compilation pipeline, actor runtime, `use_core()` |
|
||||
| `internal/shop.cm` | Module resolution, compilation, caching, C extension loading |
|
||||
| `internal/os.c` | OS intrinsics: dylib ops, internal symbol lookup, embedded modules |
|
||||
| `package.cm` | Package directory detection, alias resolution, file listing |
|
||||
| `link.cm` | Development link management (link.toml read/write) |
|
||||
| `boot/*.cm.mcode` | Pre-compiled pipeline seeds (tokenize, parse, fold, mcode, bootstrap) |
|
||||
|
||||
@@ -82,3 +82,14 @@ Named property instructions (`LOAD_FIELD`, `STORE_FIELD`, `DELETE`) use the iABC
|
||||
2. `LOAD_DYNAMIC` / `STORE_DYNAMIC` / `DELETEINDEX` — use the register-based variant
|
||||
|
||||
This is transparent to the mcode compiler and streamline optimizer.
|
||||
|
||||
## Arithmetic Dispatch
|
||||
|
||||
Arithmetic ops (ADD, SUB, MUL, DIV, MOD, POW) are executed inline without calling the polymorphic `reg_vm_binop()` helper. Since mcode's type guard dispatch guarantees both operands are numbers:
|
||||
|
||||
1. **Int-int fast path**: `JS_VALUE_IS_BOTH_INT` → native integer arithmetic with int32 overflow check. Overflow promotes to float64.
|
||||
2. **Float fallback**: `JS_ToFloat64` → native floating-point operation. Non-finite results produce null.
|
||||
|
||||
DIV and MOD check for zero divisor (→ null). POW uses `pow()` with non-finite handling for finite inputs.
|
||||
|
||||
Comparison ops (EQ through GE) and bitwise ops still use `reg_vm_binop()` for their slow paths, as they handle a wider range of type combinations (string comparisons, null equality, etc.).
|
||||
|
||||
@@ -26,7 +26,7 @@ Every heap-allocated object begins with a 64-bit header word (`objhdr_t`):
|
||||
|
||||
### Flags (bits 3-7)
|
||||
|
||||
- **Bit 3 (S)** — Stone flag. If set, the object is immutable and excluded from GC.
|
||||
- **Bit 3 (S)** — Stone flag. If set, the object is immutable. Stone text in the constant table (ct) is not copied by GC since it lives outside the heap; stone objects on the GC heap are copied normally.
|
||||
- **Bit 4 (P)** — Properties flag.
|
||||
- **Bit 5 (A)** — Array flag.
|
||||
- **Bit 7 (R)** — Reserved.
|
||||
@@ -69,7 +69,9 @@ struct JSText {
|
||||
};
|
||||
```
|
||||
|
||||
Text is stored as UTF-32, with two 32-bit codepoints packed per 64-bit word. When a text object is stoned, the length field is repurposed to cache the hash value (computed via `fash64`), since stoned text is immutable and the hash never changes.
|
||||
Text is stored as UTF-32, with two 32-bit codepoints packed per 64-bit word.
|
||||
|
||||
A mutable text (pretext) uses capacity for the allocated slot count and length for the current codepoint count. When a pretext is stoned, the capacity field is set to the actual length (codepoint count), and the length field is zeroed for use as a lazy hash cache (computed via `fash64` on first use as a key). Since stoned text is immutable, the hash never changes. Stoning is done in-place — no new allocation is needed.
|
||||
|
||||
## Record
|
||||
|
||||
@@ -111,7 +113,7 @@ struct JSFrame {
|
||||
objhdr_t header; // type=6, capacity=slot count
|
||||
JSValue function; // owning function
|
||||
JSValue caller; // parent frame
|
||||
uint32_t return_pc; // return address
|
||||
JSValue address; // return address
|
||||
JSValue slots[]; // [this][args][captured][locals][temps]
|
||||
};
|
||||
```
|
||||
@@ -138,4 +140,4 @@ All objects are aligned to 8 bytes. The total size in bytes for each type:
|
||||
| Record | `8 + 8 + 8 + (capacity + 1) * 16` |
|
||||
| Function | `sizeof(JSFunction)` (fixed) |
|
||||
| Code | `sizeof(JSFunctionBytecode)` (fixed) |
|
||||
| Frame | `8 + 8 + 8 + 4 + capacity * 8` |
|
||||
| Frame | `8 + 8 + 8 + 8 + capacity * 8` |
|
||||
|
||||
@@ -104,7 +104,8 @@ pit --emit-qbe script.ce > output.ssa
|
||||
| `streamline.cm` | Mcode IR optimizer |
|
||||
| `qbe_emit.cm` | Mcode IR → QBE IL emitter |
|
||||
| `qbe.cm` | QBE IL operation templates |
|
||||
| `internal/bootstrap.cm` | Pipeline orchestrator |
|
||||
| `internal/bootstrap.cm` | Cache seeder (cold start only) |
|
||||
| `internal/engine.cm` | Self-sufficient pipeline loader and orchestrator |
|
||||
|
||||
## Debug Tools
|
||||
|
||||
|
||||
@@ -45,11 +45,10 @@ Backward inference rules:
|
||||
|
||||
| Operator class | Operand type inferred |
|
||||
|---|---|
|
||||
| `subtract`, `multiply`, `divide`, `modulo`, `pow`, `negate` | T_NUM |
|
||||
| `eq_int`, `ne_int`, `lt_int`, `gt_int`, `le_int`, `ge_int`, bitwise ops | T_INT |
|
||||
| `eq_float`, `ne_float`, `lt_float`, `gt_float`, `le_float`, `ge_float` | T_FLOAT |
|
||||
| `concat`, text comparisons | T_TEXT |
|
||||
| `eq_bool`, `ne_bool`, `not`, `and`, `or` | T_BOOL |
|
||||
| `add`, `subtract`, `multiply`, `divide`, `modulo`, `pow`, `negate` | T_NUM |
|
||||
| bitwise ops (`bitand`, `bitor`, `bitxor`, `shl`, `shr`, `ushr`, `bitnot`) | T_INT |
|
||||
| `concat` | T_TEXT |
|
||||
| `not`, `and`, `or` | T_BOOL |
|
||||
| `store_index` (object operand) | T_ARRAY |
|
||||
| `store_index` (index operand) | T_INT |
|
||||
| `store_field` (object operand) | T_RECORD |
|
||||
@@ -59,9 +58,11 @@ Backward inference rules:
|
||||
| `load_field` (object operand) | T_RECORD |
|
||||
| `pop` (array operand) | T_ARRAY |
|
||||
|
||||
Note: `add` is excluded from backward inference because it is polymorphic — it handles both numeric addition and text concatenation. Only operators that are unambiguously numeric can infer T_NUM.
|
||||
Typed comparison operators (`eq_int`, `lt_float`, `lt_text`, etc.) and typed boolean comparisons (`eq_bool`, `ne_bool`) are excluded from backward inference. These ops always appear inside guard dispatch patterns (`is_type` + `jump_false` + typed_op), where mutually exclusive branches use the same slot with different types. Including them would merge conflicting types (e.g., T_INT from `lt_int` + T_FLOAT from `lt_float` + T_TEXT from `lt_text`) into T_UNKNOWN, losing all type information. Only unconditionally executed ops contribute to backward inference.
|
||||
|
||||
When a slot appears with conflicting type inferences, the result is `unknown`. INT + FLOAT conflicts produce `num`.
|
||||
Note: `add` infers T_NUM even though it is polymorphic (numeric addition or text concatenation). When `add` appears in the IR, both operands have already passed a `is_num` guard, so they are guaranteed to be numeric. The text concatenation path uses `concat` instead.
|
||||
|
||||
When a slot appears with conflicting type inferences, the merge widens: INT + FLOAT → NUM, INT + NUM → NUM, FLOAT + NUM → NUM. Incompatible types (e.g., NUM + TEXT) produce `unknown`.
|
||||
|
||||
**Nop prefix:** none (analysis only, does not modify instructions)
|
||||
|
||||
@@ -88,8 +89,9 @@ Write type mapping:
|
||||
| `length` | T_INT |
|
||||
| bitwise ops | T_INT |
|
||||
| `concat` | T_TEXT |
|
||||
| `negate` | T_NUM |
|
||||
| `add`, `subtract`, `multiply`, `divide`, `modulo`, `pow` | T_NUM |
|
||||
| bool ops, comparisons, `in` | T_BOOL |
|
||||
| generic arithmetic (`add`, `subtract`, `negate`, etc.) | T_UNKNOWN |
|
||||
| `move`, `load_field`, `load_index`, `load_dynamic`, `pop`, `get` | T_UNKNOWN |
|
||||
| `invoke`, `tail_invoke` | T_UNKNOWN |
|
||||
|
||||
@@ -100,8 +102,9 @@ Common patterns this enables:
|
||||
- **Length variables** (`var len = length(arr)`): written by `length` (T_INT) only → invariant T_INT
|
||||
- **Boolean flags** (`var found = false; ... found = true`): written by `false` and `true` → invariant T_BOOL
|
||||
- **Locally-created containers** (`var arr = []`): written by `array` only → invariant T_ARRAY
|
||||
- **Numeric accumulators** (`var sum = 0; sum = sum - x`): written by `access 0` (T_INT) and `subtract` (T_NUM) → merges to T_NUM
|
||||
|
||||
Note: Loop counters (`var i = 0; i = i + 1`) are NOT invariant because `add` produces T_UNKNOWN. However, if `i` is a function parameter used in arithmetic, backward inference from `subtract`/`multiply`/etc. will infer T_NUM for it, which persists across labels.
|
||||
Note: Loop counters using `+` (`var i = 0; i = i + 1`) may not achieve write-type invariance because the `+` operator emits a guard dispatch with both `concat` (T_TEXT) and `add` (T_NUM) paths writing to the same temp slot, producing T_UNKNOWN. However, when one operand is a known number literal, `mcode.cm` emits a numeric-only path (see "Known-Number Add Shortcut" below), avoiding the text dispatch. Other arithmetic ops (`-`, `*`, `/`, `%`, `**`) always emit a single numeric write path and work cleanly with write-type analysis.
|
||||
|
||||
**Nop prefix:** none (analysis only, does not modify instructions)
|
||||
|
||||
@@ -109,9 +112,11 @@ Note: Loop counters (`var i = 0; i = i + 1`) are NOT invariant because `add` pro
|
||||
|
||||
Forward pass that tracks the known type of each slot. When a type check (`is_int`, `is_text`, `is_num`, etc.) is followed by a conditional jump, and the slot's type is already known, the check and jump can be eliminated or converted to an unconditional jump.
|
||||
|
||||
Three cases:
|
||||
Five cases:
|
||||
|
||||
- **Known match** (e.g., `is_int` on a slot known to be `int`): both the check and the conditional jump are eliminated (nop'd).
|
||||
- **Subsumption match** (e.g., `is_num` on a slot known to be `int` or `float`): since `int` and `float` are subtypes of `num`, both the check and jump are eliminated.
|
||||
- **Subsumption partial** (e.g., `is_int` on a slot known to be `num`): the `num` type could be `int` or `float`, so the check must remain. On fallthrough, the slot narrows to the checked subtype (`int`). This is NOT a mismatch — `num` values can pass an `is_int` check.
|
||||
- **Known mismatch** (e.g., `is_text` on a slot known to be `int`): the check is nop'd and the conditional jump is rewritten to an unconditional `jump`.
|
||||
- **Unknown**: the check remains, but on fallthrough, the slot's type is narrowed to the checked type (enabling downstream eliminations).
|
||||
|
||||
@@ -212,12 +217,44 @@ These inlined opcodes have corresponding Mach VM implementations in `mach.c`.
|
||||
|
||||
Arithmetic operations use generic opcodes: `add`, `subtract`, `multiply`, `divide`, `modulo`, `pow`, `negate`. There are no type-dispatched variants (e.g., no `add_int`/`add_float`).
|
||||
|
||||
The Mach VM dispatches at runtime with an int-first fast path via `reg_vm_binop()`: it checks `JS_VALUE_IS_BOTH_INT` first for fast integer arithmetic, then falls back to float conversion, text concatenation (for `add` only), or type error.
|
||||
The Mach VM handles arithmetic inline with a two-tier fast path. Since mcode's type guard dispatch guarantees both operands are numbers by the time arithmetic executes, the VM does not need polymorphic dispatch:
|
||||
|
||||
1. **Int-int fast path**: `JS_VALUE_IS_BOTH_INT` → native integer arithmetic with overflow check. If the result fits int32, returns int32; otherwise promotes to float64.
|
||||
2. **Float fallback**: `JS_ToFloat64` both operands → native floating-point arithmetic. Non-finite results (infinity, NaN) produce null.
|
||||
|
||||
Division and modulo additionally check for zero divisor (→ null). Power uses `pow()` with non-finite handling.
|
||||
|
||||
The legacy `reg_vm_binop()` function remains available for comparison operators and any non-mcode bytecode paths, but arithmetic ops no longer call it.
|
||||
|
||||
Bitwise operations (`shl`, `shr`, `ushr`, `bitand`, `bitor`, `bitxor`, `bitnot`) remain integer-only and disrupt if operands are not integers.
|
||||
|
||||
The QBE/native backend maps generic arithmetic to helper calls (`qbe.add`, `qbe.sub`, etc.). The vision for the native path is that with sufficient type inference, the backend can unbox proven-numeric values to raw registers, operate directly, and only rebox at boundaries (returns, calls, stores).
|
||||
|
||||
## Known-Number Add Shortcut
|
||||
|
||||
The `+` operator is the only arithmetic op that is polymorphic at the mcode level — `emit_add_decomposed` in `mcode.cm` emits a guard dispatch that checks for text (→ `concat`) before numeric (→ `add`). This dual dispatch means the temp slot is written by both `concat` (T_TEXT) and `add` (T_NUM), producing T_UNKNOWN in write-type analysis.
|
||||
|
||||
When either operand is a known number literal (e.g., `i + 1`, `x + 0.5`), `emit_add_decomposed` skips the text dispatch entirely and emits `emit_numeric_binop("add")` — a single `is_num` guard + `add` with no `concat` path. This is safe because text concatenation requires both operands to be text; a known number can never participate in concat.
|
||||
|
||||
This optimization eliminates 6-8 instructions from the add block (two `is_text` checks, two conditional jumps, `concat`, `jump`) and produces a clean single-type write path that works with write-type analysis.
|
||||
|
||||
Other arithmetic ops (`subtract`, `multiply`, etc.) always use `emit_numeric_binop` and never have this problem.
|
||||
|
||||
## Target Slot Propagation
|
||||
|
||||
For simple local variable assignments (`i = expr`), the mcode compiler passes the variable's register slot as a `target` to the expression compiler. Binary operations that use `emit_numeric_binop` (subtract, multiply, divide, modulo, pow) can write directly to the target slot instead of allocating a temp and emitting a `move`:
|
||||
|
||||
```
|
||||
// Before: i = i - 1
|
||||
subtract 7, 2, 6 // temp = i - 1
|
||||
move 2, 7 // i = temp
|
||||
|
||||
// After: i = i - 1
|
||||
subtract 2, 2, 6 // i = i - 1 (direct)
|
||||
```
|
||||
|
||||
The `+` operator is excluded from target slot propagation when it would use the full text+num dispatch (i.e., when neither operand is a known number), because writing both `concat` and `add` to the variable's slot would pollute its write type. When the known-number shortcut applies, `+` uses `emit_numeric_binop` and would be safe for target propagation, but this is not currently implemented — the exclusion is by operator kind, not by dispatch path.
|
||||
|
||||
## Debugging Tools
|
||||
|
||||
Three dump tools inspect the IR at different stages:
|
||||
@@ -295,6 +332,18 @@ The current purity set is conservative (only `is_*`). It could be expanded by:
|
||||
- **User function purity**: Analyze user-defined function bodies during pre_scan. A function is pure if its body contains only pure expressions and calls to known-pure functions. This requires fixpoint iteration for mutual recursion.
|
||||
- **Callback-aware purity**: Intrinsics like `filter`, `find`, `reduce`, `some`, `every` are pure if their callback argument is pure.
|
||||
|
||||
### Move Type Resolution in Write-Type Analysis
|
||||
|
||||
Currently, `move` instructions produce T_UNKNOWN in write-type analysis. This prevents type propagation through moves — e.g., a slot written by `access 0` (T_INT) and `move` from an `add` result (T_NUM) merges to T_UNKNOWN instead of T_NUM.
|
||||
|
||||
A two-pass approach would fix this: first compute write types for all non-move instructions, then resolve moves by looking up the source slot's computed type. If the source has a known type, merge it into the destination; if unknown, skip the move (don't poison the destination with T_UNKNOWN).
|
||||
|
||||
This was implemented and tested but causes a bootstrap failure during self-hosting convergence. The root cause is not yet understood — the optimizer modifies its own bytecode, and the move resolution changes the type landscape enough to produce different code on each pass, preventing convergence. Further investigation is needed; the fix is correct in isolation but interacts badly with the self-hosting fixed-point iteration.
|
||||
|
||||
### Target Slot Propagation for Add with Known Numbers
|
||||
|
||||
When the known-number add shortcut applies (one operand is a literal number), the generated code uses `emit_numeric_binop` which has a single write path. Target slot propagation should be safe in this case, but is currently blocked by the blanket `kind != "+"` exclusion. Refining the exclusion to check whether the shortcut will apply (by testing `is_known_number` on either operand) would enable direct writes for patterns like `i = i + 1`.
|
||||
|
||||
### Forward Type Narrowing from Typed Operations
|
||||
|
||||
With unified arithmetic (generic `add`/`subtract`/`multiply`/`divide`/`modulo`/`negate` instead of typed variants), this approach is no longer applicable. Typed comparisons (`eq_int`, `lt_float`, etc.) still exist and their operands have known types, but these are already handled by backward inference.
|
||||
|
||||
22
dump_ir.ce
Normal file
22
dump_ir.ce
Normal file
@@ -0,0 +1,22 @@
|
||||
var tokenize = use('tokenize')
|
||||
var parse_mod = use('parse')
|
||||
var fold = use('fold')
|
||||
var mcode_mod = use('mcode')
|
||||
var streamline_mod = use('streamline')
|
||||
var json = use('json')
|
||||
var fd = use('fd')
|
||||
|
||||
var file = args[0]
|
||||
var src = text(fd.slurp(file))
|
||||
var tok = tokenize(src, file)
|
||||
var ast = parse_mod(tok.tokens, src, file, tokenize)
|
||||
var folded = fold(ast)
|
||||
var compiled = mcode_mod(folded)
|
||||
var optimized = streamline_mod(compiled)
|
||||
|
||||
var instrs = optimized.main.instructions
|
||||
var i = 0
|
||||
while (i < length(instrs)) {
|
||||
print(text(i) + ': ' + json.encode(instrs[i]))
|
||||
i = i + 1
|
||||
}
|
||||
112
fix_pipeline.md
112
fix_pipeline.md
@@ -1,112 +0,0 @@
|
||||
# Fix Compilation Pipeline Bootstrap
|
||||
|
||||
## Problem
|
||||
|
||||
After merging `fix_gc` into `pitweb`, the compilation pipeline `.cm` source files
|
||||
(tokenize.cm, parse.cm, fold.cm, mcode.cm, streamline.cm) cannot bootstrap themselves.
|
||||
|
||||
The old pitweb pipeline mcode compiles the merged `.cm` source without errors, but the
|
||||
resulting new pipeline mcode is **semantically broken** — it can't even compile
|
||||
`var x = 42; print(x)`.
|
||||
|
||||
Both branches worked independently. The merge introduced no syntax errors, but the old
|
||||
pitweb compiler produces incorrect bytecode from the merged pipeline source. This is a
|
||||
classic bootstrapping problem: the new pipeline needs a compatible compiler to build
|
||||
itself, but the only available compiler (old pitweb) miscompiles it.
|
||||
|
||||
## Current State
|
||||
|
||||
- `boot/tokenize.cm.mcode` through `boot/streamline.cm.mcode` contain the **old pitweb**
|
||||
pipeline mcode (pre-merge). These pass 641/641 vm_suite tests.
|
||||
- All other boot mcode files (engine, bootstrap, seed_bootstrap, plus core modules like
|
||||
fd, time, toml, etc.) are compiled from the merged source and work correctly.
|
||||
- The merged pipeline `.cm` source has changes from fix_gc that are **not active** — the
|
||||
runtime uses the old pitweb pipeline mcode.
|
||||
|
||||
**The old pitweb pipeline is NOT fully working.** While it passes the test suite, it
|
||||
miscompiles nested function declarations. This breaks:
|
||||
|
||||
- `toml.encode()` — the encoder uses nested `function` declarations inside `encode_toml`
|
||||
- `Shop.save_lock()` — calls `toml.encode()`, so any lock.toml mutation fails
|
||||
- Any other `.cm` module that uses nested named function declarations
|
||||
|
||||
This means the **ID-based package symbol naming** (Phase 2 in the plan) is blocked: it
|
||||
needs `save_lock()` to persist package IDs to lock.toml.
|
||||
|
||||
The shop.cm changes for ID-based naming are already written and correct — they just need
|
||||
a working pipeline underneath. Once the pipeline is fixed, the ID system will work.
|
||||
|
||||
## What Changed in the Pipeline
|
||||
|
||||
The fix_gc merge brought these changes to the pipeline `.cm` files:
|
||||
|
||||
- **mcode.cm**: Type-guarded arithmetic (`emit_add_decomposed` now generates `is_text`/`is_num`
|
||||
checks instead of letting the VM dispatch), `emit_numeric_binop` for subtract/multiply/etc.,
|
||||
`sensory_ops` lookup table, array/record literal count args (`["array", dest, count]`
|
||||
instead of `["array", dest, 0]`)
|
||||
- **fold.cm**: Lookup tables (`binary_ops`, `unary_ops`, `assign_ops`, etc.) replacing
|
||||
if-chains, combined `"array"` and `"text literal"` handling
|
||||
- **tokenize.cm**: ~500 lines of changes
|
||||
- **streamline.cm**: ~700 lines of changes
|
||||
- **parse.cm**: ~40 lines of changes (minor)
|
||||
|
||||
## Regen Flags
|
||||
|
||||
`regen.ce` now has two modes:
|
||||
|
||||
```
|
||||
./cell --dev --seed regen # default: skip pipeline files
|
||||
./cell --dev --seed regen --all # include pipeline files (tokenize/parse/fold/mcode/streamline)
|
||||
```
|
||||
|
||||
The default mode is safe — it regenerates everything except the 5 pipeline files,
|
||||
preserving the working old pitweb pipeline mcode.
|
||||
|
||||
## How to Fix
|
||||
|
||||
The goal is to get the merged pipeline `.cm` source to produce working mcode when
|
||||
compiled by the current (old pitweb) pipeline. The process:
|
||||
|
||||
1. Start from the current repo state (old pitweb pipeline mcode in boot/)
|
||||
2. Edit one or more pipeline `.cm` files to fix the issue
|
||||
3. Regen with `--all` to recompile everything including pipeline:
|
||||
```
|
||||
./cell --dev --seed regen --all
|
||||
```
|
||||
4. Test the new pipeline with a simple sanity check:
|
||||
```
|
||||
rm -rf .cell/build/*
|
||||
echo 'var x = 42; print(x)' > /tmp/test.ce
|
||||
./cell --dev --seed /tmp/test
|
||||
```
|
||||
5. If that works, run the full test suite:
|
||||
```
|
||||
rm -rf .cell/build/*
|
||||
./cell --dev vm_suite
|
||||
```
|
||||
6. If tests pass, regen again (the new pipeline compiles itself):
|
||||
```
|
||||
./cell --dev --seed regen --all
|
||||
```
|
||||
7. Repeat steps 4-6 until **idempotent** — two consecutive `regen --all` runs produce
|
||||
identical boot mcode and all tests pass.
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
- The old pitweb pipeline mcode is always available via:
|
||||
```
|
||||
git checkout HEAD^1 -- boot/tokenize.cm.mcode boot/parse.cm.mcode \
|
||||
boot/fold.cm.mcode boot/mcode.cm.mcode boot/streamline.cm.mcode
|
||||
```
|
||||
- Use `--seed` mode for testing compilation — it bypasses the engine entirely and
|
||||
loads the pipeline directly from boot mcode.
|
||||
- The failure mode is silent: the old compiler compiles the new source without errors
|
||||
but produces wrong bytecode.
|
||||
- Known broken patterns with the old pitweb pipeline:
|
||||
- `var x = 42; print(x)` fails when compiled by the regenned pipeline mcode
|
||||
- Nested named function declarations (`function foo() {}` inside another function)
|
||||
produce "not a function" errors — this breaks `toml.encode()`
|
||||
- Test with: `echo 'var toml = use("toml"); print(toml.encode({a: 1}))' > /tmp/t.ce && ./cell --dev /tmp/t.ce`
|
||||
- The most likely culprits are the mcode.cm changes (type-guarded arithmetic, array/record
|
||||
count args) since these change the bytecode format. The fold.cm changes (lookup tables)
|
||||
are more likely safe refactors.
|
||||
4
fuzz.ce
4
fuzz.ce
@@ -134,7 +134,7 @@ function run_fuzz(seed_val) {
|
||||
|
||||
// Run optimized
|
||||
var _opt = function() {
|
||||
mod_opt = run_ast_fn(name, ast, {use: function(p) { return use(p) }})
|
||||
mod_opt = run_ast_fn(name, ast, stone({use: function(p) { return use(p) }}))
|
||||
} disruption {
|
||||
opt_err = "disrupted"
|
||||
}
|
||||
@@ -142,7 +142,7 @@ function run_fuzz(seed_val) {
|
||||
|
||||
// Run unoptimized
|
||||
var _noopt = function() {
|
||||
mod_noopt = run_ast_noopt_fn(name + "_noopt", ast, {use: function(p) { return use(p) }})
|
||||
mod_noopt = run_ast_noopt_fn(name + "_noopt", ast, stone({use: function(p) { return use(p) }}))
|
||||
} disruption {
|
||||
noopt_err = "disrupted"
|
||||
}
|
||||
|
||||
7
graph.ce
7
graph.ce
@@ -143,6 +143,9 @@ arrfor(roots, function(root) {
|
||||
})
|
||||
|
||||
// Output based on format
|
||||
var children = null
|
||||
var j = 0
|
||||
var output = null
|
||||
if (format == 'tree') {
|
||||
function print_tree(locator, prefix, is_last, visited) {
|
||||
var node = null
|
||||
@@ -178,8 +181,6 @@ if (format == 'tree') {
|
||||
}
|
||||
}
|
||||
|
||||
var children = null
|
||||
var j = 0
|
||||
for (i = 0; i < length(roots); i++) {
|
||||
log.console(roots[i])
|
||||
|
||||
@@ -230,7 +231,7 @@ if (format == 'tree') {
|
||||
log.console("}")
|
||||
|
||||
} else if (format == 'json') {
|
||||
var output = {
|
||||
output = {
|
||||
nodes: [],
|
||||
edges: []
|
||||
}
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
// Hidden vars come from env:
|
||||
// CLI mode (cell_init): os, args, core_path, shop_path
|
||||
// Actor spawn (script_startup): os, json, actorsym, init, core_path, shop_path
|
||||
// args[0] = script name, args[1..] = user args
|
||||
// Minimal bootstrap — seeds the content-addressed cache
|
||||
// Only runs on cold start (C runtime couldn't find engine in cache)
|
||||
// Hidden vars: os, core_path, shop_path
|
||||
var load_internal = os.load_internal
|
||||
function use_embed(name) {
|
||||
return load_internal("js_core_" + name + "_use")
|
||||
}
|
||||
|
||||
var fd = use_embed('internal_fd')
|
||||
var json = use_embed('json')
|
||||
var json_mod = use_embed('json')
|
||||
var crypto = use_embed('crypto')
|
||||
|
||||
var use_cache = {}
|
||||
use_cache['fd'] = fd
|
||||
use_cache['os'] = os
|
||||
use_cache['json'] = json
|
||||
use_cache['crypto'] = crypto
|
||||
|
||||
function content_hash(content) {
|
||||
return text(crypto.blake2(content), 'h')
|
||||
}
|
||||
@@ -29,248 +22,84 @@ function cache_path(hash) {
|
||||
function ensure_build_dir() {
|
||||
if (!shop_path) return null
|
||||
var dir = shop_path + '/build'
|
||||
if (!fd.is_dir(dir)) {
|
||||
fd.mkdir(dir)
|
||||
}
|
||||
if (!fd.is_dir(dir)) fd.mkdir(dir)
|
||||
return dir
|
||||
}
|
||||
|
||||
// Bootstrap: load tokenize.cm, parse.cm, fold.cm from pre-compiled mach bytecode
|
||||
function use_basic(path) {
|
||||
if (use_cache[path])
|
||||
return use_cache[path]
|
||||
var result = use_embed(replace(path, '/', '_'))
|
||||
use_cache[path] = result
|
||||
return result
|
||||
}
|
||||
|
||||
// Load a module from cached .mach or .mcode bytecode
|
||||
function boot_load(name, env) {
|
||||
var mcode_path = core_path + '/boot/' + name + ".cm.mcode"
|
||||
// Load seed pipeline from boot/ (tokenize, parse, mcode only)
|
||||
function boot_load(name) {
|
||||
var mcode_path = core_path + '/boot/' + name + '.cm.mcode'
|
||||
var mcode_blob = null
|
||||
var hash = null
|
||||
var cached = null
|
||||
var mcode_json = null
|
||||
var mach_blob = null
|
||||
if (fd.is_file(mcode_path)) {
|
||||
mcode_blob = fd.slurp(mcode_path)
|
||||
hash = content_hash(mcode_blob)
|
||||
cached = cache_path(hash)
|
||||
if (cached && fd.is_file(cached)) {
|
||||
return mach_load(fd.slurp(cached), env)
|
||||
}
|
||||
mcode_json = text(mcode_blob)
|
||||
mach_blob = mach_compile_mcode_bin(name, mcode_json)
|
||||
if (cached) {
|
||||
ensure_build_dir()
|
||||
fd.slurpwrite(cached, mach_blob)
|
||||
}
|
||||
return mach_load(mach_blob, env)
|
||||
if (!fd.is_file(mcode_path)) {
|
||||
print("error: missing seed: " + name + "\n")
|
||||
disrupt
|
||||
}
|
||||
print("error: missing bootstrap bytecode: " + name + "\n")
|
||||
disrupt
|
||||
mcode_blob = fd.slurp(mcode_path)
|
||||
mach_blob = mach_compile_mcode_bin(name, text(mcode_blob))
|
||||
return mach_load(mach_blob, stone({use: use_embed}))
|
||||
}
|
||||
|
||||
var boot_env = {use: use_basic}
|
||||
var tokenize_mod = boot_load("tokenize", boot_env)
|
||||
var parse_mod = boot_load("parse", boot_env)
|
||||
var fold_mod = boot_load("fold", boot_env)
|
||||
use_cache['tokenize'] = tokenize_mod
|
||||
use_cache['parse'] = parse_mod
|
||||
use_cache['fold'] = fold_mod
|
||||
var tokenize_mod = boot_load("tokenize")
|
||||
var parse_mod = boot_load("parse")
|
||||
var fold_mod = boot_load("fold")
|
||||
var mcode_mod = boot_load("mcode")
|
||||
|
||||
// Always load mcode compiler module
|
||||
var mcode_mod = boot_load("mcode", boot_env)
|
||||
use_cache['mcode'] = mcode_mod
|
||||
var streamline_mod = null
|
||||
|
||||
// Warn if any .cm source is newer than its compiled bytecode
|
||||
function check_mach_stale() {
|
||||
var sources = [
|
||||
{src: "tokenize.cm", mcode: "boot/tokenize.cm.mcode"},
|
||||
{src: "parse.cm", mcode: "boot/parse.cm.mcode"},
|
||||
{src: "fold.cm", mcode: "boot/fold.cm.mcode"},
|
||||
{src: "mcode.cm", mcode: "boot/mcode.cm.mcode"},
|
||||
{src: "streamline.cm", mcode: "boot/streamline.cm.mcode"},
|
||||
{src: "qbe.cm", mcode: "boot/qbe.cm.mcode"},
|
||||
{src: "qbe_emit.cm", mcode: "boot/qbe_emit.cm.mcode"},
|
||||
{src: "verify_ir.cm", mcode: "boot/verify_ir.cm.mcode"},
|
||||
{src: "internal/bootstrap.cm", mcode: "boot/bootstrap.cm.mcode"},
|
||||
{src: "internal/engine.cm", mcode: "boot/engine.cm.mcode"}
|
||||
]
|
||||
var stale = []
|
||||
var _i = 0
|
||||
var cm_path = null
|
||||
var mcode_path = null
|
||||
var cm_stat = null
|
||||
var compiled_stat = null
|
||||
var entry = null
|
||||
while (_i < length(sources)) {
|
||||
entry = sources[_i]
|
||||
cm_path = core_path + '/' + entry.src
|
||||
mcode_path = core_path + '/' + entry.mcode
|
||||
if (fd.is_file(mcode_path) && fd.is_file(cm_path)) {
|
||||
compiled_stat = fd.stat(mcode_path)
|
||||
cm_stat = fd.stat(cm_path)
|
||||
if (cm_stat.mtime > compiled_stat.mtime) {
|
||||
push(stale, entry.src)
|
||||
}
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
if (length(stale) > 0) {
|
||||
print("warning: bytecode is stale for: " + text(stale, ", ") + "\n")
|
||||
print("run 'make regen' to update\n")
|
||||
}
|
||||
}
|
||||
check_mach_stale()
|
||||
|
||||
// analyze: tokenize + parse, check for errors
|
||||
function analyze(src, filename) {
|
||||
var tok_result = tokenize_mod(src, filename)
|
||||
var ast = parse_mod(tok_result.tokens, src, filename, tokenize_mod)
|
||||
var _i = 0
|
||||
var prev_line = -1
|
||||
var prev_msg = null
|
||||
var e = null
|
||||
var msg = null
|
||||
var line = null
|
||||
var col = null
|
||||
var has_errors = ast.errors != null && length(ast.errors) > 0
|
||||
if (has_errors) {
|
||||
while (_i < length(ast.errors)) {
|
||||
e = ast.errors[_i]
|
||||
msg = e.message
|
||||
line = e.line
|
||||
col = e.column
|
||||
if (msg != prev_msg || line != prev_line) {
|
||||
if (line != null && col != null) {
|
||||
print(`${filename}:${text(line)}:${text(col)}: error: ${msg}`)
|
||||
} else {
|
||||
print(`${filename}: error: ${msg}`)
|
||||
}
|
||||
}
|
||||
prev_line = line
|
||||
prev_msg = msg
|
||||
if (e.line != null && e.column != null)
|
||||
print(`${filename}:${text(e.line)}:${text(e.column)}: error: ${msg}`)
|
||||
else
|
||||
print(`${filename}: error: ${msg}`)
|
||||
_i = _i + 1
|
||||
}
|
||||
disrupt
|
||||
}
|
||||
ast = fold_mod(ast)
|
||||
return ast
|
||||
return fold_mod(ast)
|
||||
}
|
||||
|
||||
// Load optimization pipeline modules (needs analyze to be defined)
|
||||
streamline_mod = boot_load("streamline", boot_env)
|
||||
use_cache['streamline'] = streamline_mod
|
||||
|
||||
// Lazy-loaded verify_ir module (loaded on first use)
|
||||
var _verify_ir_mod = null
|
||||
|
||||
// Run AST through mcode pipeline → register VM
|
||||
function run_ast(name, ast, env) {
|
||||
var compiled = mcode_mod(ast)
|
||||
if (os._verify_ir) {
|
||||
if (_verify_ir_mod == null) {
|
||||
_verify_ir_mod = boot_load('verify_ir', boot_env)
|
||||
}
|
||||
compiled._verify = true
|
||||
compiled._verify_mod = _verify_ir_mod
|
||||
}
|
||||
var optimized = streamline_mod(compiled)
|
||||
// Clean up verify properties before JSON encoding
|
||||
if (optimized._verify) {
|
||||
delete optimized._verify
|
||||
delete optimized._verify_mod
|
||||
}
|
||||
var mcode_json = json.encode(optimized)
|
||||
var mach_blob = mach_compile_mcode_bin(name, mcode_json)
|
||||
return mach_load(mach_blob, env)
|
||||
}
|
||||
|
||||
// Run AST through mcode pipeline WITHOUT optimization → register VM
|
||||
function run_ast_noopt(name, ast, env) {
|
||||
var compiled = mcode_mod(ast)
|
||||
var mcode_json = json.encode(compiled)
|
||||
var mach_blob = mach_compile_mcode_bin(name, mcode_json)
|
||||
return mach_load(mach_blob, env)
|
||||
}
|
||||
|
||||
// Compile AST to blob without loading (for caching)
|
||||
function compile_to_blob(name, ast) {
|
||||
var compiled = mcode_mod(ast)
|
||||
var optimized = streamline_mod(compiled)
|
||||
return mach_compile_mcode_bin(name, json.encode(optimized))
|
||||
}
|
||||
|
||||
// Helper to load engine.cm and run it with given env
|
||||
function load_engine(env) {
|
||||
var mcode_path = core_path + '/boot/engine.cm.mcode'
|
||||
var mcode_blob = null
|
||||
var hash = null
|
||||
var cached = null
|
||||
function compile_and_cache(name, source_path) {
|
||||
var source_blob = fd.slurp(source_path)
|
||||
var hash = content_hash(source_blob)
|
||||
var cached = cache_path(hash)
|
||||
var ast = null
|
||||
var compiled = null
|
||||
var mcode_json = null
|
||||
var mach_blob = null
|
||||
var engine_src = null
|
||||
var engine_ast = null
|
||||
if (fd.is_file(mcode_path)) {
|
||||
mcode_blob = fd.slurp(mcode_path)
|
||||
hash = content_hash(mcode_blob)
|
||||
cached = cache_path(hash)
|
||||
if (cached && fd.is_file(cached)) {
|
||||
return mach_load(fd.slurp(cached), env)
|
||||
}
|
||||
mcode_json = text(mcode_blob)
|
||||
mach_blob = mach_compile_mcode_bin('engine', mcode_json)
|
||||
if (cached) {
|
||||
ensure_build_dir()
|
||||
fd.slurpwrite(cached, mach_blob)
|
||||
}
|
||||
return mach_load(mach_blob, env)
|
||||
if (cached && fd.is_file(cached)) return
|
||||
ast = analyze(text(source_blob), source_path)
|
||||
compiled = mcode_mod(ast)
|
||||
mcode_json = json_mod.encode(compiled)
|
||||
mach_blob = mach_compile_mcode_bin(name, mcode_json)
|
||||
if (cached) {
|
||||
ensure_build_dir()
|
||||
fd.slurpwrite(cached, mach_blob)
|
||||
}
|
||||
// Fallback: compile from source
|
||||
var engine_cm = core_path + '/internal/engine.cm'
|
||||
engine_src = text(fd.slurp(engine_cm))
|
||||
engine_ast = analyze(engine_src, engine_cm)
|
||||
return run_ast('engine', engine_ast, env)
|
||||
}
|
||||
|
||||
// Detect mode and route
|
||||
// CLI mode has 'args'; actor spawn mode has 'init'
|
||||
var program = null
|
||||
var user_args = []
|
||||
var _j = 0
|
||||
|
||||
if (args != null) {
|
||||
// CLI mode — always run as actor program (.ce)
|
||||
program = args[0]
|
||||
if (!program) {
|
||||
print("error: no program specified\n")
|
||||
disrupt
|
||||
}
|
||||
_j = 1
|
||||
while (_j < length(args)) {
|
||||
push(user_args, args[_j])
|
||||
_j = _j + 1
|
||||
}
|
||||
|
||||
load_engine({
|
||||
os: os, actorsym: actorsym,
|
||||
init: {program: program, arg: user_args},
|
||||
core_path: core_path, shop_path: shop_path, json: json,
|
||||
analyze: analyze, run_ast_fn: run_ast, run_ast_noopt_fn: run_ast_noopt,
|
||||
use_cache: use_cache,
|
||||
content_hash: content_hash, cache_path: cache_path,
|
||||
ensure_build_dir: ensure_build_dir, compile_to_blob_fn: compile_to_blob
|
||||
})
|
||||
} else {
|
||||
// Actor spawn mode — load engine.cm with full actor env
|
||||
load_engine({
|
||||
os: os, actorsym: actorsym, init: init,
|
||||
core_path: core_path, shop_path: shop_path, json: json,
|
||||
analyze: analyze, run_ast_fn: run_ast, run_ast_noopt_fn: run_ast_noopt,
|
||||
use_cache: use_cache,
|
||||
content_hash: content_hash, cache_path: cache_path,
|
||||
ensure_build_dir: ensure_build_dir, compile_to_blob_fn: compile_to_blob
|
||||
})
|
||||
// Seed the cache with everything engine needs
|
||||
var seed_files = [
|
||||
{name: "tokenize", path: "tokenize.cm"},
|
||||
{name: "parse", path: "parse.cm"},
|
||||
{name: "fold", path: "fold.cm"},
|
||||
{name: "mcode", path: "mcode.cm"},
|
||||
{name: "streamline", path: "streamline.cm"},
|
||||
{name: "engine", path: "internal/engine.cm"}
|
||||
]
|
||||
var _i = 0
|
||||
var entry = null
|
||||
while (_i < length(seed_files)) {
|
||||
entry = seed_files[_i]
|
||||
compile_and_cache(entry.name, core_path + '/' + entry.path)
|
||||
_i = _i + 1
|
||||
}
|
||||
print("bootstrap: cache seeded\n")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Hidden vars (os, actorsym, init, core_path, shop_path, analyze, run_ast_fn, run_ast_noopt_fn, json, use_cache, content_hash, cache_path, ensure_build_dir, compile_to_blob_fn) come from env
|
||||
// Hidden vars (os, actorsym, init, core_path, shop_path, json, args) come from env
|
||||
// Engine is self-sufficient: defines its own compilation pipeline
|
||||
var ACTORDATA = actorsym
|
||||
var SYSYM = '__SYSTEM__'
|
||||
|
||||
@@ -14,13 +15,16 @@ var cases = {
|
||||
var dylib_ext = cases[os.platform()]
|
||||
|
||||
var MOD_EXT = '.cm'
|
||||
var ACTOR_EXT = '.ce'
|
||||
var ACTOR_EXT = '.ce'
|
||||
|
||||
var load_internal = os.load_internal
|
||||
function use_embed(name) {
|
||||
return load_internal("js_core_" + name + "_use")
|
||||
}
|
||||
|
||||
// These duplicate C builtins from runtime.c, but are needed because the GC can
|
||||
// lose properties on the global object after compaction. The runtime_env provides
|
||||
// a stable way for modules to access them via env_record linking.
|
||||
function logical(val1) {
|
||||
if (val1 == 0 || val1 == false || val1 == "false" || val1 == null)
|
||||
return false;
|
||||
@@ -47,11 +51,159 @@ function ends_with(str, suffix) {
|
||||
|
||||
var fd = use_embed('internal_fd')
|
||||
var js = use_embed('js')
|
||||
var crypto = use_embed('crypto')
|
||||
|
||||
// core_path and shop_path come from env (bootstrap.cm passes them through)
|
||||
// core_path and shop_path come from env (C runtime passes them through)
|
||||
// shop_path may be null if --core was used without --shop
|
||||
var packages_path = shop_path ? shop_path + '/packages' : null
|
||||
|
||||
// Self-sufficient initialization: content-addressed cache
|
||||
var use_cache = {}
|
||||
|
||||
function content_hash(content) {
|
||||
return text(crypto.blake2(content), 'h')
|
||||
}
|
||||
|
||||
function cache_path(hash) {
|
||||
if (!shop_path) return null
|
||||
return shop_path + '/build/' + hash
|
||||
}
|
||||
|
||||
function ensure_build_dir() {
|
||||
if (!shop_path) return null
|
||||
var dir = shop_path + '/build'
|
||||
if (!fd.is_dir(dir)) fd.mkdir(dir)
|
||||
return dir
|
||||
}
|
||||
|
||||
// Load a pipeline module from cache, with boot/ seed fallback
|
||||
function load_pipeline_module(name, env) {
|
||||
var source_path = core_path + '/' + name + '.cm'
|
||||
var source_blob = null
|
||||
var hash = null
|
||||
var cached = null
|
||||
var mcode_path = null
|
||||
var mcode_blob = null
|
||||
var mach_blob = null
|
||||
if (fd.is_file(source_path)) {
|
||||
source_blob = fd.slurp(source_path)
|
||||
hash = content_hash(source_blob)
|
||||
cached = cache_path(hash)
|
||||
if (cached && fd.is_file(cached))
|
||||
return mach_load(fd.slurp(cached), env)
|
||||
}
|
||||
// Boot seed fallback
|
||||
mcode_path = core_path + '/boot/' + name + '.cm.mcode'
|
||||
if (fd.is_file(mcode_path)) {
|
||||
mcode_blob = fd.slurp(mcode_path)
|
||||
mach_blob = mach_compile_mcode_bin(name, text(mcode_blob))
|
||||
return mach_load(mach_blob, env)
|
||||
}
|
||||
print("error: cannot load pipeline module: " + name + "\n")
|
||||
disrupt
|
||||
}
|
||||
|
||||
// Load compilation pipeline
|
||||
var pipeline_env = stone({use: use_embed})
|
||||
var tokenize_mod = load_pipeline_module('tokenize', pipeline_env)
|
||||
var parse_mod = load_pipeline_module('parse', pipeline_env)
|
||||
var fold_mod = load_pipeline_module('fold', pipeline_env)
|
||||
var mcode_mod = load_pipeline_module('mcode', pipeline_env)
|
||||
var streamline_mod = load_pipeline_module('streamline', pipeline_env)
|
||||
|
||||
use_cache['tokenize'] = tokenize_mod
|
||||
use_cache['parse'] = parse_mod
|
||||
use_cache['fold'] = fold_mod
|
||||
use_cache['mcode'] = mcode_mod
|
||||
use_cache['core/mcode'] = mcode_mod
|
||||
use_cache['streamline'] = streamline_mod
|
||||
use_cache['core/streamline'] = streamline_mod
|
||||
|
||||
// analyze: tokenize + parse + fold, check for errors
|
||||
function analyze(src, filename) {
|
||||
var tok_result = tokenize_mod(src, filename)
|
||||
var _ast = parse_mod(tok_result.tokens, src, filename, tokenize_mod)
|
||||
var _i = 0
|
||||
var prev_line = -1
|
||||
var prev_msg = null
|
||||
var e = null
|
||||
var msg = null
|
||||
var line = null
|
||||
var col = null
|
||||
var has_errors = _ast.errors != null && length(_ast.errors) > 0
|
||||
if (has_errors) {
|
||||
while (_i < length(_ast.errors)) {
|
||||
e = _ast.errors[_i]
|
||||
msg = e.message
|
||||
line = e.line
|
||||
col = e.column
|
||||
if (msg != prev_msg || line != prev_line) {
|
||||
if (line != null && col != null)
|
||||
print(`${filename}:${text(line)}:${text(col)}: error: ${msg}`)
|
||||
else
|
||||
print(`${filename}: error: ${msg}`)
|
||||
}
|
||||
prev_line = line
|
||||
prev_msg = msg
|
||||
_i = _i + 1
|
||||
}
|
||||
disrupt
|
||||
}
|
||||
return fold_mod(_ast)
|
||||
}
|
||||
|
||||
// Lazy-loaded verify_ir module (loaded on first use)
|
||||
var _verify_ir_mod = null
|
||||
|
||||
// Run AST through mcode pipeline -> register VM
|
||||
function run_ast_fn(name, ast, env) {
|
||||
var compiled = mcode_mod(ast)
|
||||
if (os._verify_ir) {
|
||||
if (_verify_ir_mod == null) {
|
||||
_verify_ir_mod = load_pipeline_module('verify_ir', pipeline_env)
|
||||
}
|
||||
compiled._verify = true
|
||||
compiled._verify_mod = _verify_ir_mod
|
||||
}
|
||||
var optimized = streamline_mod(compiled)
|
||||
if (optimized._verify) {
|
||||
delete optimized._verify
|
||||
delete optimized._verify_mod
|
||||
}
|
||||
var mcode_json = json.encode(optimized)
|
||||
var mach_blob = mach_compile_mcode_bin(name, mcode_json)
|
||||
return mach_load(mach_blob, env)
|
||||
}
|
||||
|
||||
// Run AST through mcode pipeline WITHOUT optimization -> register VM
|
||||
function run_ast_noopt_fn(name, ast, env) {
|
||||
var compiled = mcode_mod(ast)
|
||||
var mcode_json = json.encode(compiled)
|
||||
var mach_blob = mach_compile_mcode_bin(name, mcode_json)
|
||||
return mach_load(mach_blob, env)
|
||||
}
|
||||
|
||||
// Compile AST to blob without loading (for caching)
|
||||
function compile_to_blob(name, ast) {
|
||||
var compiled = mcode_mod(ast)
|
||||
var optimized = streamline_mod(compiled)
|
||||
return mach_compile_mcode_bin(name, json.encode(optimized))
|
||||
}
|
||||
|
||||
// If loaded directly by C runtime (not via bootstrap), convert args -> init
|
||||
var _program = null
|
||||
var _user_args = []
|
||||
var _j = 1
|
||||
var _init = init
|
||||
if (args != null && _init == null) {
|
||||
_program = args[0]
|
||||
while (_j < length(args)) {
|
||||
push(_user_args, args[_j])
|
||||
_j = _j + 1
|
||||
}
|
||||
_init = {program: _program, arg: _user_args}
|
||||
}
|
||||
|
||||
use_cache['core/os'] = os
|
||||
|
||||
// Extra env properties added as engine initializes (log, runtime fns, etc.)
|
||||
@@ -68,56 +220,43 @@ function use_core(path) {
|
||||
var result = null
|
||||
var script = null
|
||||
var ast = null
|
||||
var mcode_path = null
|
||||
var mcode_blob = null
|
||||
var _load_mod = null
|
||||
|
||||
// Build env: merge core_extras
|
||||
env = {use: use_core}
|
||||
arrfor(array(core_extras), function(k) { env[k] = core_extras[k] })
|
||||
env = stone(env)
|
||||
|
||||
var hash = null
|
||||
var cached_path = null
|
||||
var mach_blob = null
|
||||
var source_blob = null
|
||||
|
||||
// Check for pre-compiled .cm.mcode JSON IR (generated by regen)
|
||||
mcode_path = core_path + '/boot/' + replace(path, '/', '_') + '.cm.mcode'
|
||||
if (fd.is_file(mcode_path)) {
|
||||
mcode_blob = fd.slurp(mcode_path)
|
||||
hash = content_hash(mcode_blob)
|
||||
cached_path = cache_path(hash)
|
||||
if (cached_path && fd.is_file(cached_path)) {
|
||||
result = mach_load(fd.slurp(cached_path), env)
|
||||
} else {
|
||||
mach_blob = mach_compile_mcode_bin('core:' + path, text(mcode_blob))
|
||||
if (cached_path) {
|
||||
ensure_build_dir()
|
||||
fd.slurpwrite(cached_path, mach_blob)
|
||||
}
|
||||
result = mach_load(mach_blob, env)
|
||||
}
|
||||
use_cache[cache_key] = result
|
||||
return result
|
||||
}
|
||||
var file_path = null
|
||||
|
||||
// Compile from source .cm file
|
||||
var file_path = core_path + '/' + path + MOD_EXT
|
||||
file_path = core_path + '/' + path + MOD_EXT
|
||||
if (fd.is_file(file_path)) {
|
||||
source_blob = fd.slurp(file_path)
|
||||
hash = content_hash(source_blob)
|
||||
cached_path = cache_path(hash)
|
||||
if (cached_path && fd.is_file(cached_path)) {
|
||||
result = mach_load(fd.slurp(cached_path), env)
|
||||
} else {
|
||||
script = text(source_blob)
|
||||
ast = analyze(script, file_path)
|
||||
mach_blob = compile_to_blob_fn('core:' + path, ast)
|
||||
if (cached_path) {
|
||||
ensure_build_dir()
|
||||
fd.slurpwrite(cached_path, mach_blob)
|
||||
_load_mod = function() {
|
||||
source_blob = fd.slurp(file_path)
|
||||
hash = content_hash(source_blob)
|
||||
cached_path = cache_path(hash)
|
||||
if (cached_path && fd.is_file(cached_path)) {
|
||||
result = mach_load(fd.slurp(cached_path), env)
|
||||
} else {
|
||||
script = text(source_blob)
|
||||
ast = analyze(script, file_path)
|
||||
mach_blob = compile_to_blob('core:' + path, ast)
|
||||
if (cached_path) {
|
||||
ensure_build_dir()
|
||||
fd.slurpwrite(cached_path, mach_blob)
|
||||
}
|
||||
result = mach_load(mach_blob, env)
|
||||
}
|
||||
result = mach_load(mach_blob, env)
|
||||
} disruption {
|
||||
print("use('" + path + "'): failed to compile or load " + file_path + "\n")
|
||||
disrupt
|
||||
}
|
||||
_load_mod()
|
||||
use_cache[cache_key] = result
|
||||
return result
|
||||
}
|
||||
@@ -216,40 +355,44 @@ function actor_die(err)
|
||||
|
||||
//actor_mod.on_exception(actor_die)
|
||||
|
||||
_cell.args = init != null ? init : {}
|
||||
_cell.id = "newguy"
|
||||
_cell.args = _init != null ? _init : {}
|
||||
|
||||
function create_actor(desc) {
|
||||
var _desc = desc == null ? {id:guid()} : desc
|
||||
var actor = {}
|
||||
actor[ACTORDATA] = _desc
|
||||
stone(actor)
|
||||
return actor
|
||||
}
|
||||
|
||||
var $_ = {}
|
||||
$_.self = create_actor()
|
||||
|
||||
use_cache['core/json'] = json
|
||||
|
||||
// Create runtime_env early (empty) — filled after pronto loads.
|
||||
// Create runtime_env early (empty) -- filled after pronto loads.
|
||||
// Shop accesses it lazily (in inject_env, called at module-use time, not load time)
|
||||
// so it sees the filled version.
|
||||
var runtime_env = {}
|
||||
|
||||
// Populate core_extras with everything shop (and other core modules) need
|
||||
core_extras.use_cache = use_cache
|
||||
core_extras.core_path = core_path
|
||||
core_extras.shop_path = shop_path
|
||||
core_extras.analyze = analyze
|
||||
core_extras.run_ast_fn = run_ast_fn
|
||||
core_extras.run_ast_noopt_fn = run_ast_noopt_fn
|
||||
os.analyze = analyze
|
||||
os.run_ast_fn = run_ast_fn
|
||||
os.run_ast_noopt_fn = run_ast_noopt_fn
|
||||
core_extras.core_json = json
|
||||
core_extras.actor_api = $_
|
||||
core_extras.runtime_env = runtime_env
|
||||
core_extras.content_hash = content_hash
|
||||
core_extras.cache_path = cache_path
|
||||
core_extras.ensure_build_dir = ensure_build_dir
|
||||
core_extras.compile_to_blob = compile_to_blob
|
||||
|
||||
// NOW load shop — it receives all of the above via env
|
||||
// NOW load shop -- it receives all of the above via env
|
||||
var shop = use_core('internal/shop')
|
||||
var time = use_core('time')
|
||||
|
||||
@@ -259,7 +402,9 @@ var parallel = pronto.parallel
|
||||
var race = pronto.race
|
||||
var sequence = pronto.sequence
|
||||
|
||||
// Fill runtime_env (same object reference shop holds)
|
||||
// Fill runtime_env — includes duplicates of C builtins because the GC can
|
||||
// lose global object properties after compaction. Modules resolve these via
|
||||
// env_record, not the global.
|
||||
runtime_env.logical = logical
|
||||
runtime_env.some = some
|
||||
runtime_env.every = every
|
||||
@@ -370,7 +515,7 @@ REPLYTIMEOUT = config.reply_timeout
|
||||
replycc: the actor that is waiting for the reply
|
||||
target: ID of the actor that's supposed to receive the message. Only added to non direct sends (out of portals)
|
||||
return: reply ID so the replycc actor can know what callback to send the message to
|
||||
|
||||
|
||||
data: the actual content of the message
|
||||
}
|
||||
|
||||
@@ -441,7 +586,7 @@ $_.connection = function(callback, actor, config) {
|
||||
callback({type:"local"})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
callback()
|
||||
}
|
||||
|
||||
@@ -522,10 +667,10 @@ $_.start = function start(cb, program) {
|
||||
if (!program) return
|
||||
|
||||
var id = guid()
|
||||
var startup = {
|
||||
id,
|
||||
overling: $_.self,
|
||||
root,
|
||||
var startup = {
|
||||
id,
|
||||
overling: $_.self,
|
||||
root,
|
||||
program,
|
||||
}
|
||||
greeters[id] = cb
|
||||
@@ -716,7 +861,7 @@ stone(send)
|
||||
if (!_cell.args.id) _cell.id = guid()
|
||||
else _cell.id = _cell.args.id
|
||||
|
||||
$_.self[ACTORDATA].id = _cell.id
|
||||
$_.self = create_actor({id: _cell.id})
|
||||
|
||||
// Actor's timeslice for processing a single message
|
||||
function turn(msg)
|
||||
@@ -731,7 +876,7 @@ actor_mod.register_actor(_cell.id, turn, true, config.ar_timer)
|
||||
|
||||
if (config.actor_memory)
|
||||
js.mem_limit(config.actor_memory)
|
||||
|
||||
|
||||
if (config.stack_max)
|
||||
js.max_stacksize(config.system.stack_max);
|
||||
|
||||
@@ -821,12 +966,8 @@ function handle_message(msg) {
|
||||
|
||||
if (msg.type == "user") {
|
||||
letter = msg.data // what the sender really sent
|
||||
_ObjectDefineProperty(letter, HEADER, {
|
||||
value: msg, enumerable: false
|
||||
})
|
||||
_ObjectDefineProperty(letter, ACTORDATA, { // this is so is_actor == true
|
||||
value: { reply: msg.reply }, enumerable: false
|
||||
})
|
||||
letter[HEADER] = msg
|
||||
letter[ACTORDATA] = { reply: msg.reply }
|
||||
|
||||
if (msg.return) {
|
||||
fn = replies[msg.return]
|
||||
@@ -844,7 +985,7 @@ function handle_message(msg) {
|
||||
function enet_check()
|
||||
{
|
||||
if (portal) portal.service(handle_host)
|
||||
|
||||
|
||||
$_.delay(enet_check, ENETSERVICE);
|
||||
}
|
||||
|
||||
@@ -910,6 +1051,7 @@ $_.clock(_ => {
|
||||
}
|
||||
env.args = _cell.args.arg
|
||||
env.log = log
|
||||
env = stone(env)
|
||||
|
||||
var source_blob = fd.slurp(prog_path)
|
||||
var hash = content_hash(source_blob)
|
||||
@@ -923,7 +1065,7 @@ $_.clock(_ => {
|
||||
} else {
|
||||
script = text(source_blob)
|
||||
ast = analyze(script, prog_path)
|
||||
mach_blob = compile_to_blob_fn(prog, ast)
|
||||
mach_blob = compile_to_blob(prog, ast)
|
||||
if (cached_path) {
|
||||
ensure_build_dir()
|
||||
fd.slurpwrite(cached_path, mach_blob)
|
||||
|
||||
@@ -445,7 +445,8 @@ static JSValue js_os_dylib_close(JSContext *js, JSValue self, int argc, JSValue
|
||||
|
||||
/* Load a native .cm module from a dylib handle.
|
||||
Uses cell_rt_native_module_load from qbe_helpers.c */
|
||||
extern JSValue cell_rt_native_module_load(JSContext *ctx, void *dl_handle);
|
||||
extern JSValue cell_rt_native_module_load(JSContext *ctx, void *dl_handle, JSValue env);
|
||||
extern JSValue cell_rt_native_module_load_named(JSContext *ctx, void *dl_handle, const char *sym_name, JSValue env);
|
||||
|
||||
static JSValue js_os_native_module_load(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
{
|
||||
@@ -456,7 +457,27 @@ static JSValue js_os_native_module_load(JSContext *js, JSValue self, int argc, J
|
||||
if (!handle)
|
||||
return JS_ThrowTypeError(js, "First argument must be a dylib object");
|
||||
|
||||
return cell_rt_native_module_load(js, handle);
|
||||
JSValue env = (argc >= 2) ? argv[1] : JS_NULL;
|
||||
return cell_rt_native_module_load(js, handle, env);
|
||||
}
|
||||
|
||||
static JSValue js_os_native_module_load_named(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
{
|
||||
if (argc < 2)
|
||||
return JS_ThrowTypeError(js, "native_module_load_named requires (dylib, sym_name)");
|
||||
|
||||
void *handle = JS_GetOpaque(argv[0], js_dylib_class_id);
|
||||
if (!handle)
|
||||
return JS_ThrowTypeError(js, "First argument must be a dylib object");
|
||||
|
||||
const char *sym_name = JS_ToCString(js, argv[1]);
|
||||
if (!sym_name)
|
||||
return JS_EXCEPTION;
|
||||
|
||||
JSValue env = (argc >= 3) ? argv[2] : JS_NULL;
|
||||
JSValue result = cell_rt_native_module_load_named(js, handle, sym_name, env);
|
||||
JS_FreeCString(js, sym_name);
|
||||
return result;
|
||||
}
|
||||
|
||||
JSC_CCALL(os_print,
|
||||
@@ -634,7 +655,8 @@ static const JSCFunctionListEntry js_os_funcs[] = {
|
||||
MIST_FUNC_DEF(os, dylib_close, 1),
|
||||
MIST_FUNC_DEF(os, dylib_symbol, 2),
|
||||
MIST_FUNC_DEF(os, dylib_has_symbol, 2),
|
||||
MIST_FUNC_DEF(os, native_module_load, 1),
|
||||
MIST_FUNC_DEF(os, native_module_load, 2),
|
||||
MIST_FUNC_DEF(os, native_module_load_named, 3),
|
||||
MIST_FUNC_DEF(os, embedded_module, 1),
|
||||
MIST_FUNC_DEF(os, load_internal, 1),
|
||||
MIST_FUNC_DEF(os, internal_exists, 1),
|
||||
|
||||
364
internal/shop.cm
364
internal/shop.cm
@@ -339,6 +339,48 @@ Shop.save_lock = function(lock) {
|
||||
}
|
||||
|
||||
|
||||
// Shop configuration (shop.toml) with policy flags
|
||||
var _shop_config = null
|
||||
var _default_policy = {
|
||||
allow_dylib: true,
|
||||
allow_static: true,
|
||||
allow_mach: true,
|
||||
allow_compile: true
|
||||
}
|
||||
|
||||
Shop.load_config = function() {
|
||||
if (_shop_config) return _shop_config
|
||||
if (!global_shop_path) {
|
||||
_shop_config = {policy: object(_default_policy)}
|
||||
return _shop_config
|
||||
}
|
||||
var path = global_shop_path + '/shop.toml'
|
||||
if (!fd.is_file(path)) {
|
||||
_shop_config = {policy: object(_default_policy)}
|
||||
fd.slurpwrite(path, stone(blob(toml.encode(_shop_config))))
|
||||
return _shop_config
|
||||
}
|
||||
var content = text(fd.slurp(path))
|
||||
if (!length(content)) {
|
||||
_shop_config = {policy: object(_default_policy)}
|
||||
return _shop_config
|
||||
}
|
||||
_shop_config = toml.decode(content)
|
||||
if (!_shop_config.policy) _shop_config.policy = {}
|
||||
var keys = array(_default_policy)
|
||||
var i = 0
|
||||
for (i = 0; i < length(keys); i++) {
|
||||
if (_shop_config.policy[keys[i]] == null)
|
||||
_shop_config.policy[keys[i]] = _default_policy[keys[i]]
|
||||
}
|
||||
return _shop_config
|
||||
}
|
||||
|
||||
function get_policy() {
|
||||
var config = Shop.load_config()
|
||||
return config.policy
|
||||
}
|
||||
|
||||
// Get information about how to resolve a package
|
||||
// Local packages always start with /
|
||||
Shop.resolve_package_info = function(pkg) {
|
||||
@@ -439,7 +481,8 @@ function try_native_mod_dylib(pkg, stem) {
|
||||
if (!fd.is_file(dylib_path)) return null
|
||||
var handle = os.dylib_open(dylib_path)
|
||||
if (!handle) return null
|
||||
return os.native_module_load(handle)
|
||||
var sym = Shop.c_symbol_for_file(pkg, stem)
|
||||
return os.native_module_load_named(handle, sym)
|
||||
}
|
||||
|
||||
// Default capabilities injected into scripts
|
||||
@@ -508,51 +551,73 @@ function resolve_mod_fn(path, pkg) {
|
||||
var cached_mcode_path = null
|
||||
var _pkg_dir = null
|
||||
var _stem = null
|
||||
var policy = null
|
||||
|
||||
// Check for native .cm dylib at deterministic path first
|
||||
policy = get_policy()
|
||||
|
||||
// Compute _pkg_dir and _stem early so all paths can use them
|
||||
if (pkg) {
|
||||
_pkg_dir = get_packages_dir() + '/' + safe_package_path(pkg)
|
||||
if (starts_with(path, _pkg_dir + '/')) {
|
||||
_stem = fd.stem(text(path, length(_pkg_dir) + 1))
|
||||
native_result = try_native_mod_dylib(pkg, _stem)
|
||||
if (native_result != null) {
|
||||
return {_native: true, value: native_result}
|
||||
}
|
||||
_stem = text(path, length(_pkg_dir) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for native .cm dylib at deterministic path first
|
||||
if (policy.allow_dylib && pkg && _stem) {
|
||||
native_result = try_native_mod_dylib(pkg, _stem)
|
||||
if (native_result != null) {
|
||||
return {_native: true, value: native_result}
|
||||
}
|
||||
}
|
||||
|
||||
// Check cache for pre-compiled .mach blob
|
||||
cached = pull_from_cache(content_key)
|
||||
if (cached) {
|
||||
return cached
|
||||
if (policy.allow_mach) {
|
||||
cached = pull_from_cache(content_key)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
|
||||
// Check for cached mcode in content-addressed store (salted hash to distinguish from mach)
|
||||
cached_mcode_path = global_shop_path + '/build/' + content_hash(stone(blob(text(content_key) + "\nmcode")))
|
||||
if (fd.is_file(cached_mcode_path)) {
|
||||
mcode_json = text(fd.slurp(cached_mcode_path))
|
||||
compiled = mach_compile_mcode_bin(path, mcode_json)
|
||||
put_into_cache(content_key, compiled)
|
||||
return compiled
|
||||
if (policy.allow_compile) {
|
||||
cached_mcode_path = global_shop_path + '/build/' + content_hash(stone(blob(text(content_key) + "\nmcode")))
|
||||
if (fd.is_file(cached_mcode_path)) {
|
||||
mcode_json = text(fd.slurp(cached_mcode_path))
|
||||
compiled = mach_compile_mcode_bin(path, mcode_json)
|
||||
put_into_cache(content_key, compiled)
|
||||
return compiled
|
||||
}
|
||||
}
|
||||
|
||||
// Compile via full pipeline: analyze → mcode → streamline → serialize
|
||||
if (!_mcode_mod) _mcode_mod = Shop.use("mcode", null)
|
||||
if (!_streamline_mod) _streamline_mod = Shop.use("streamline", null)
|
||||
ast = analyze(content, path)
|
||||
ir = _mcode_mod(ast)
|
||||
optimized = _streamline_mod(ir)
|
||||
mcode_json = shop_json.encode(optimized)
|
||||
// Load compiler modules from use_cache directly (NOT via Shop.use, which
|
||||
// would re-enter resolve_locator → resolve_mod_fn → infinite recursion)
|
||||
if (policy.allow_compile) {
|
||||
if (!_mcode_mod) _mcode_mod = use_cache['core/mcode'] || use_cache['mcode']
|
||||
if (!_streamline_mod) _streamline_mod = use_cache['core/streamline'] || use_cache['streamline']
|
||||
if (!_mcode_mod || !_streamline_mod) {
|
||||
print(`error: compiler modules not loaded (mcode=${_mcode_mod != null}, streamline=${_streamline_mod != null})`)
|
||||
disrupt
|
||||
}
|
||||
ast = analyze(content, path)
|
||||
ir = _mcode_mod(ast)
|
||||
optimized = _streamline_mod(ir)
|
||||
mcode_json = shop_json.encode(optimized)
|
||||
|
||||
// Cache mcode (architecture-independent) in content-addressed store
|
||||
ensure_dir(global_shop_path + '/build')
|
||||
fd.slurpwrite(cached_mcode_path, stone(blob(mcode_json)))
|
||||
// Cache mcode (architecture-independent) in content-addressed store
|
||||
ensure_dir(global_shop_path + '/build')
|
||||
fd.slurpwrite(cached_mcode_path, stone(blob(mcode_json)))
|
||||
|
||||
// Cache mach blob
|
||||
compiled = mach_compile_mcode_bin(path, mcode_json)
|
||||
put_into_cache(content_key, compiled)
|
||||
// Cache mach blob
|
||||
compiled = mach_compile_mcode_bin(path, mcode_json)
|
||||
put_into_cache(content_key, compiled)
|
||||
|
||||
return compiled
|
||||
return compiled
|
||||
}
|
||||
|
||||
print(`Module ${path} could not be loaded: no artifact found or all methods blocked by policy`)
|
||||
disrupt
|
||||
}
|
||||
|
||||
// given a path and a package context
|
||||
@@ -653,6 +718,11 @@ function get_dylib_path(pkg, stem) {
|
||||
return global_shop_path + '/lib/' + safe_package_path(pkg) + '/' + stem + dylib_ext
|
||||
}
|
||||
|
||||
// Get the deterministic mach path for a module in lib/<pkg>/<stem>.mach
|
||||
function get_mach_path(pkg, stem) {
|
||||
return global_shop_path + '/lib/' + safe_package_path(pkg) + '/' + stem + '.mach'
|
||||
}
|
||||
|
||||
// Open a per-module dylib and return the dlopen handle
|
||||
function open_module_dylib(dylib_path) {
|
||||
if (open_dls[dylib_path]) return open_dls[dylib_path]
|
||||
@@ -682,6 +752,9 @@ function resolve_c_symbol(path, package_context) {
|
||||
var canon_pkg = null
|
||||
var mod_name = null
|
||||
var file_stem = null
|
||||
var policy = null
|
||||
|
||||
policy = get_policy()
|
||||
|
||||
if (explicit) {
|
||||
if (is_internal_path(explicit.path) && package_context && explicit.package != package_context)
|
||||
@@ -692,18 +765,20 @@ function resolve_c_symbol(path, package_context) {
|
||||
file_stem = replace(explicit.path, '.c', '')
|
||||
|
||||
// Check lib/ dylib first
|
||||
loader = try_dylib_symbol(sym, explicit.package, file_stem)
|
||||
if (loader) {
|
||||
return {
|
||||
symbol: loader,
|
||||
scope: SCOPE_PACKAGE,
|
||||
package: explicit.package,
|
||||
path: sym
|
||||
if (policy.allow_dylib) {
|
||||
loader = try_dylib_symbol(sym, explicit.package, file_stem)
|
||||
if (loader) {
|
||||
return {
|
||||
symbol: loader,
|
||||
scope: SCOPE_PACKAGE,
|
||||
package: explicit.package,
|
||||
path: sym
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then check internal/static
|
||||
if (os.internal_exists(sym)) {
|
||||
if (policy.allow_static && os.internal_exists(sym)) {
|
||||
return {
|
||||
symbol: function() { return os.load_internal(sym) },
|
||||
scope: SCOPE_PACKAGE,
|
||||
@@ -718,16 +793,18 @@ function resolve_c_symbol(path, package_context) {
|
||||
core_sym = make_c_symbol('core', path)
|
||||
|
||||
// Check lib/ dylib first for core
|
||||
loader = try_dylib_symbol(core_sym, 'core', path)
|
||||
if (loader) {
|
||||
return {
|
||||
symbol: loader,
|
||||
scope: SCOPE_CORE,
|
||||
path: core_sym
|
||||
if (policy.allow_dylib) {
|
||||
loader = try_dylib_symbol(core_sym, 'core', path)
|
||||
if (loader) {
|
||||
return {
|
||||
symbol: loader,
|
||||
scope: SCOPE_CORE,
|
||||
path: core_sym
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (os.internal_exists(core_sym)) {
|
||||
if (policy.allow_static && os.internal_exists(core_sym)) {
|
||||
return {
|
||||
symbol: function() { return os.load_internal(core_sym) },
|
||||
scope: SCOPE_CORE,
|
||||
@@ -740,16 +817,18 @@ function resolve_c_symbol(path, package_context) {
|
||||
// 1. Check own package (dylib first, then internal)
|
||||
sym = make_c_symbol(package_context, path)
|
||||
|
||||
loader = try_dylib_symbol(sym, package_context, path)
|
||||
if (loader) {
|
||||
return {
|
||||
symbol: loader,
|
||||
scope: SCOPE_LOCAL,
|
||||
path: sym
|
||||
if (policy.allow_dylib) {
|
||||
loader = try_dylib_symbol(sym, package_context, path)
|
||||
if (loader) {
|
||||
return {
|
||||
symbol: loader,
|
||||
scope: SCOPE_LOCAL,
|
||||
path: sym
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (os.internal_exists(sym)) {
|
||||
if (policy.allow_static && os.internal_exists(sym)) {
|
||||
return {
|
||||
symbol: function() { return os.load_internal(sym) },
|
||||
scope: SCOPE_LOCAL,
|
||||
@@ -768,17 +847,19 @@ function resolve_c_symbol(path, package_context) {
|
||||
mod_name = get_import_name(path)
|
||||
sym = make_c_symbol(canon_pkg, mod_name)
|
||||
|
||||
loader = try_dylib_symbol(sym, canon_pkg, mod_name)
|
||||
if (loader) {
|
||||
return {
|
||||
symbol: loader,
|
||||
scope: SCOPE_PACKAGE,
|
||||
package: canon_pkg,
|
||||
path: sym
|
||||
if (policy.allow_dylib) {
|
||||
loader = try_dylib_symbol(sym, canon_pkg, mod_name)
|
||||
if (loader) {
|
||||
return {
|
||||
symbol: loader,
|
||||
scope: SCOPE_PACKAGE,
|
||||
package: canon_pkg,
|
||||
path: sym
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (os.internal_exists(sym)) {
|
||||
if (policy.allow_static && os.internal_exists(sym)) {
|
||||
return {
|
||||
symbol: function() { return os.load_internal(sym) },
|
||||
scope: SCOPE_PACKAGE,
|
||||
@@ -792,16 +873,18 @@ function resolve_c_symbol(path, package_context) {
|
||||
// 3. Check core (dylib first, then internal)
|
||||
core_sym = make_c_symbol('core', path)
|
||||
|
||||
loader = try_dylib_symbol(core_sym, 'core', path)
|
||||
if (loader) {
|
||||
return {
|
||||
symbol: loader,
|
||||
scope: SCOPE_CORE,
|
||||
path: core_sym
|
||||
if (policy.allow_dylib) {
|
||||
loader = try_dylib_symbol(core_sym, 'core', path)
|
||||
if (loader) {
|
||||
return {
|
||||
symbol: loader,
|
||||
scope: SCOPE_CORE,
|
||||
path: core_sym
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (os.internal_exists(core_sym)) {
|
||||
if (policy.allow_static && os.internal_exists(core_sym)) {
|
||||
return {
|
||||
symbol: function() { return os.load_internal(core_sym) },
|
||||
scope: SCOPE_CORE,
|
||||
@@ -926,6 +1009,7 @@ function execute_module(info)
|
||||
env = inject_env(inject)
|
||||
pkg = file_info.package
|
||||
env.use = make_use_fn(pkg)
|
||||
env = stone(env)
|
||||
|
||||
// Load compiled bytecode with env
|
||||
used = mach_load(mod_resolve.symbol, env)
|
||||
@@ -961,6 +1045,7 @@ Shop.use = function use(path, package_context) {
|
||||
if (embedded) {
|
||||
embed_env = inject_env(SHOP_DEFAULT_INJECT)
|
||||
embed_env.use = make_use_fn(package_context)
|
||||
embed_env = stone(embed_env)
|
||||
use_cache[embed_key] = mach_load(embedded, embed_env)
|
||||
return use_cache[embed_key]
|
||||
}
|
||||
@@ -1392,6 +1477,8 @@ Shop.get_lib_dir = function() {
|
||||
return global_shop_path + '/lib'
|
||||
}
|
||||
|
||||
Shop.ensure_dir = ensure_dir
|
||||
|
||||
Shop.get_local_dir = function() {
|
||||
return global_shop_path + "/local"
|
||||
}
|
||||
@@ -1439,6 +1526,121 @@ Shop.get_dylib_path = function(pkg, stem) {
|
||||
return get_dylib_path(pkg, stem)
|
||||
}
|
||||
|
||||
// Get the deterministic mach path for a module (public API)
|
||||
Shop.get_mach_path = function(pkg, stem) {
|
||||
return get_mach_path(pkg, stem)
|
||||
}
|
||||
|
||||
// Load a module explicitly as mach bytecode, bypassing dylib resolution.
|
||||
// Returns the loaded module value. Disrupts if the module cannot be found.
|
||||
Shop.load_as_mach = function(path, pkg) {
|
||||
var locator = resolve_locator(path + '.cm', pkg)
|
||||
var file_path = null
|
||||
var content = null
|
||||
var content_key = null
|
||||
var cached = null
|
||||
var cached_mcode_path = null
|
||||
var mcode_json = null
|
||||
var compiled = null
|
||||
var ast = null
|
||||
var ir = null
|
||||
var optimized = null
|
||||
var pkg_dir = null
|
||||
var stem = null
|
||||
var mach_path = null
|
||||
var file_info = null
|
||||
var inject = null
|
||||
var env = null
|
||||
|
||||
if (!locator) { print('Module ' + path + ' not found'); disrupt }
|
||||
|
||||
file_path = locator.path
|
||||
content = text(fd.slurp(file_path))
|
||||
content_key = stone(blob(content))
|
||||
|
||||
// Try installed .mach in lib/
|
||||
if (pkg) {
|
||||
pkg_dir = get_packages_dir() + '/' + safe_package_path(pkg)
|
||||
if (starts_with(file_path, pkg_dir + '/')) {
|
||||
stem = text(file_path, length(pkg_dir) + 1)
|
||||
mach_path = get_mach_path(pkg, stem)
|
||||
if (fd.is_file(mach_path)) {
|
||||
compiled = fd.slurp(mach_path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try cached mach blob
|
||||
if (!compiled) {
|
||||
cached = pull_from_cache(content_key)
|
||||
if (cached) compiled = cached
|
||||
}
|
||||
|
||||
// Try cached mcode -> compile to mach
|
||||
if (!compiled) {
|
||||
cached_mcode_path = global_shop_path + '/build/' + content_hash(stone(blob(text(content_key) + "\nmcode")))
|
||||
if (fd.is_file(cached_mcode_path)) {
|
||||
mcode_json = text(fd.slurp(cached_mcode_path))
|
||||
compiled = mach_compile_mcode_bin(file_path, mcode_json)
|
||||
put_into_cache(content_key, compiled)
|
||||
}
|
||||
}
|
||||
|
||||
// Full compile from source
|
||||
if (!compiled) {
|
||||
if (!_mcode_mod) _mcode_mod = use_cache['core/mcode'] || use_cache['mcode']
|
||||
if (!_streamline_mod) _streamline_mod = use_cache['core/streamline'] || use_cache['streamline']
|
||||
if (!_mcode_mod || !_streamline_mod) {
|
||||
print('error: compiler modules not loaded')
|
||||
disrupt
|
||||
}
|
||||
ast = analyze(content, file_path)
|
||||
ir = _mcode_mod(ast)
|
||||
optimized = _streamline_mod(ir)
|
||||
mcode_json = shop_json.encode(optimized)
|
||||
cached_mcode_path = global_shop_path + '/build/' + content_hash(stone(blob(text(content_key) + "\nmcode")))
|
||||
ensure_dir(global_shop_path + '/build')
|
||||
fd.slurpwrite(cached_mcode_path, stone(blob(mcode_json)))
|
||||
compiled = mach_compile_mcode_bin(file_path, mcode_json)
|
||||
put_into_cache(content_key, compiled)
|
||||
}
|
||||
|
||||
// Load the mach blob with proper env
|
||||
file_info = Shop.file_info(file_path)
|
||||
inject = Shop.script_inject_for(file_info)
|
||||
env = inject_env(inject)
|
||||
env.use = make_use_fn(file_info.package)
|
||||
env = stone(env)
|
||||
return mach_load(compiled, env)
|
||||
}
|
||||
|
||||
// Load a module explicitly as a native dylib, bypassing mach resolution.
|
||||
// Returns the loaded module value, or null if no dylib exists.
|
||||
Shop.load_as_dylib = function(path, pkg) {
|
||||
var locator = resolve_locator(path + '.cm', pkg)
|
||||
var file_path = null
|
||||
var file_info = null
|
||||
var pkg_dir = null
|
||||
var stem = null
|
||||
var result = null
|
||||
var real_pkg = pkg
|
||||
|
||||
if (!locator) { print('Module ' + path + ' not found'); disrupt }
|
||||
|
||||
file_path = locator.path
|
||||
if (!real_pkg) {
|
||||
file_info = Shop.file_info(file_path)
|
||||
real_pkg = file_info.package
|
||||
}
|
||||
if (!real_pkg) return null
|
||||
|
||||
pkg_dir = get_packages_dir() + '/' + safe_package_path(real_pkg)
|
||||
if (!starts_with(file_path, pkg_dir + '/')) return null
|
||||
stem = text(file_path, length(pkg_dir) + 1)
|
||||
result = try_native_mod_dylib(real_pkg, stem)
|
||||
return result
|
||||
}
|
||||
|
||||
Shop.audit_packages = function() {
|
||||
var packages = Shop.list_packages()
|
||||
|
||||
@@ -1479,4 +1681,34 @@ Shop.parse_package = function(locator) {
|
||||
}
|
||||
}
|
||||
|
||||
Shop.use_native = function(path, package_context) {
|
||||
var src_path = path
|
||||
if (!starts_with(path, '/'))
|
||||
src_path = fd.realpath(path)
|
||||
if (!fd.is_file(src_path)) { print('File not found: ' + path); disrupt }
|
||||
|
||||
var file_info = Shop.file_info(src_path)
|
||||
var pkg = file_info.package || package_context
|
||||
|
||||
var sym_name = null
|
||||
if (pkg)
|
||||
sym_name = Shop.c_symbol_for_file(pkg, fd.basename(src_path))
|
||||
|
||||
var build = Shop.use('build', 'core')
|
||||
var dylib_path = build.compile_native(src_path, null, null, pkg)
|
||||
|
||||
var handle = os.dylib_open(dylib_path)
|
||||
if (!handle) { print('Failed to open native dylib: ' + dylib_path); disrupt }
|
||||
|
||||
// Build env with runtime functions and capabilities
|
||||
var inject = Shop.script_inject_for(file_info)
|
||||
var env = inject_env(inject)
|
||||
env.use = make_use_fn(pkg)
|
||||
env = stone(env)
|
||||
|
||||
if (sym_name)
|
||||
return os.native_module_load_named(handle, sym_name, env)
|
||||
return os.native_module_load(handle, env)
|
||||
}
|
||||
|
||||
return Shop
|
||||
2
link.ce
2
link.ce
@@ -77,7 +77,7 @@ if (cmd == 'list') {
|
||||
|
||||
pkg = args[1]
|
||||
|
||||
var _restore = null
|
||||
_restore = null
|
||||
if (link.remove(pkg)) {
|
||||
// Try to restore the original package
|
||||
log.console("Restoring " + pkg + "...")
|
||||
|
||||
8
link.cm
8
link.cm
@@ -4,17 +4,19 @@
|
||||
var toml = use('toml')
|
||||
var fd = use('fd')
|
||||
var blob = use('blob')
|
||||
var os = use('os')
|
||||
var runtime = use('runtime')
|
||||
|
||||
var global_shop_path = os.global_shop_path
|
||||
var global_shop_path = runtime.shop_path
|
||||
|
||||
// Get the links file path (in the global shop)
|
||||
function get_links_path() {
|
||||
if (!global_shop_path) return null
|
||||
return global_shop_path + '/link.toml'
|
||||
}
|
||||
|
||||
// Get the packages directory (in the global shop)
|
||||
function get_packages_dir() {
|
||||
if (!global_shop_path) return null
|
||||
return global_shop_path + '/packages'
|
||||
}
|
||||
|
||||
@@ -62,7 +64,7 @@ var link_cache = null
|
||||
Link.load = function() {
|
||||
if (link_cache) return link_cache
|
||||
var path = get_links_path()
|
||||
if (!fd.is_file(path)) {
|
||||
if (!path || !fd.is_file(path)) {
|
||||
link_cache = {}
|
||||
return link_cache
|
||||
}
|
||||
|
||||
4
list.ce
4
list.ce
@@ -50,9 +50,9 @@ if (args && length(args) > 0) {
|
||||
var links = link.load()
|
||||
var lock = shop.load_lock()
|
||||
|
||||
function print_deps(ctx, indent) {
|
||||
function print_deps(ctx, raw_indent) {
|
||||
var aliases = null
|
||||
indent = indent || ""
|
||||
var indent = raw_indent || ""
|
||||
deps = null
|
||||
var _read = function() {
|
||||
deps = pkg.dependencies(ctx)
|
||||
|
||||
45
mcode.cm
45
mcode.cm
@@ -291,6 +291,11 @@ var mcode = function(ast) {
|
||||
emit_3("add", _bp_dest, _bp_left, _bp_right)
|
||||
return null
|
||||
}
|
||||
// If either operand is a known number, concat is impossible
|
||||
if (is_known_number(_bp_ln) || is_known_number(_bp_rn)) {
|
||||
emit_numeric_binop("add")
|
||||
return null
|
||||
}
|
||||
// Unknown types: emit full dispatch
|
||||
var t0 = alloc_slot()
|
||||
var t1 = alloc_slot()
|
||||
@@ -844,7 +849,7 @@ var mcode = function(ast) {
|
||||
if (scope == null) {
|
||||
return null
|
||||
}
|
||||
var keys = array(scope)
|
||||
var keys = sort(array(scope))
|
||||
var _i = 0
|
||||
var name = null
|
||||
var v = null
|
||||
@@ -1217,7 +1222,7 @@ var mcode = function(ast) {
|
||||
}
|
||||
|
||||
// Binary expression compilation
|
||||
var gen_binary = function(node) {
|
||||
var gen_binary = function(node, target) {
|
||||
var kind = node.kind
|
||||
var left = node.left
|
||||
var right = node.right
|
||||
@@ -1272,7 +1277,8 @@ var mcode = function(ast) {
|
||||
// Standard binary ops
|
||||
left_slot = gen_expr(left, -1)
|
||||
right_slot = gen_expr(right, -1)
|
||||
dest = alloc_slot()
|
||||
// Use target slot for ops without multi-type dispatch (add has text+num paths)
|
||||
dest = (target >= 0 && kind != "+") ? target : alloc_slot()
|
||||
op = binop_map[kind]
|
||||
if (op == null) {
|
||||
op = "add"
|
||||
@@ -1426,9 +1432,9 @@ var mcode = function(ast) {
|
||||
return val_slot
|
||||
}
|
||||
|
||||
val_slot = gen_expr(right, -1)
|
||||
left_kind = left.kind
|
||||
|
||||
// For local name assignments, try to write directly to the var's slot
|
||||
if (left_kind == "name") {
|
||||
name = left.name
|
||||
level = left.level
|
||||
@@ -1438,17 +1444,30 @@ var mcode = function(ast) {
|
||||
if (level == 0 || level == -1) {
|
||||
slot = find_var(name)
|
||||
if (slot >= 0) {
|
||||
emit_2("move", slot, val_slot)
|
||||
} else if (level == -1) {
|
||||
val_slot = gen_expr(right, slot)
|
||||
if (val_slot != slot) {
|
||||
emit_2("move", slot, val_slot)
|
||||
}
|
||||
return val_slot
|
||||
}
|
||||
val_slot = gen_expr(right, -1)
|
||||
if (level == -1) {
|
||||
add_instr(["set_var", name, val_slot])
|
||||
}
|
||||
} else if (level > 0) {
|
||||
_lv = level - 1
|
||||
pstate = parent_states[length(parent_states) - 1 - _lv]
|
||||
pslot = find_var_in_saved(pstate, name)
|
||||
emit_3("put", val_slot, pslot, level)
|
||||
} else {
|
||||
val_slot = gen_expr(right, -1)
|
||||
if (level > 0) {
|
||||
_lv = level - 1
|
||||
pstate = parent_states[length(parent_states) - 1 - _lv]
|
||||
pslot = find_var_in_saved(pstate, name)
|
||||
emit_3("put", val_slot, pslot, level)
|
||||
}
|
||||
}
|
||||
} else if (left_kind == ".") {
|
||||
return val_slot
|
||||
}
|
||||
|
||||
val_slot = gen_expr(right, -1)
|
||||
if (left_kind == ".") {
|
||||
obj = left.left
|
||||
prop = left.right
|
||||
obj_slot = gen_expr(obj, -1)
|
||||
@@ -2045,7 +2064,7 @@ var mcode = function(ast) {
|
||||
}
|
||||
|
||||
// Binary operators (fallback)
|
||||
return gen_binary(expr)
|
||||
return gen_binary(expr, target)
|
||||
}
|
||||
|
||||
// Statement compilation
|
||||
|
||||
@@ -26,6 +26,7 @@ if get_option('force_gc')
|
||||
add_project_arguments('-DFORCE_GC_AT_MALLOC', language: 'c')
|
||||
endif
|
||||
|
||||
|
||||
deps = []
|
||||
|
||||
if host_machine.system() == 'darwin'
|
||||
|
||||
@@ -2,9 +2,11 @@ var package = {}
|
||||
var fd = use('fd')
|
||||
var toml = use('toml')
|
||||
var json = use('json')
|
||||
var os = use('os')
|
||||
var runtime = use('runtime')
|
||||
var link = use('link')
|
||||
|
||||
var global_shop_path = runtime.shop_path
|
||||
|
||||
// Cache for loaded configs to avoid toml re-parsing corruption
|
||||
var config_cache = {}
|
||||
|
||||
@@ -35,11 +37,11 @@ function get_path(name)
|
||||
if (starts_with(link_target, '/'))
|
||||
return link_target
|
||||
// Otherwise it's another package name, resolve that
|
||||
return os.global_shop_path + '/packages/' + replace(replace(link_target, '/', '_'), '@', '_')
|
||||
return global_shop_path + '/packages/' + replace(replace(link_target, '/', '_'), '@', '_')
|
||||
}
|
||||
|
||||
// Remote packages use nested directories, so don't transform slashes
|
||||
return os.global_shop_path + '/packages/' + replace(name, '@', '_')
|
||||
return global_shop_path + '/packages/' + replace(name, '@', '_')
|
||||
}
|
||||
|
||||
package.load_config = function(name)
|
||||
|
||||
2
parse.ce
2
parse.ce
@@ -6,4 +6,4 @@ var filename = args[0]
|
||||
var src = text(fd.slurp(filename))
|
||||
var result = tokenize(src, filename)
|
||||
var ast = parse(result.tokens, src, filename, tokenize)
|
||||
print(json.encode(ast))
|
||||
print(json.encode(ast, true))
|
||||
|
||||
2
parse.cm
2
parse.cm
@@ -2062,7 +2062,7 @@ 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_add_var(scope, name, {make: "function", fn_nr: scope.function_nr})
|
||||
if (name != null && sem_find_var(scope, name) == null) sem_add_var(scope, name, {make: "function", fn_nr: scope.function_nr})
|
||||
fn_nr_val = stmt.function_nr
|
||||
if (fn_nr_val == null) fn_nr_val = scope.function_nr
|
||||
fn_scope = make_scope(scope, fn_nr_val, {is_func: true})
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
// prettify_mcode.ce — reformat .mcode files to be human-readable
|
||||
// Usage: ./cell --dev prettify_mcode boot/tokenize.cm.mcode
|
||||
// ./cell --dev prettify_mcode boot/*.mcode
|
||||
|
||||
var fd = use("fd")
|
||||
var json = use("json")
|
||||
|
||||
if (length(args) == 0) {
|
||||
print("usage: cell prettify_mcode <file.mcode> [...]")
|
||||
disrupt
|
||||
}
|
||||
|
||||
// Collapse leaf arrays (instruction arrays) onto single lines
|
||||
var compact_arrays = function(json_text) {
|
||||
var lines = array(json_text, "\n")
|
||||
var result = []
|
||||
var i = 0
|
||||
var line = null
|
||||
var trimmed = null
|
||||
var collecting = false
|
||||
var collected = null
|
||||
var indent = null
|
||||
var is_leaf = null
|
||||
var j = 0
|
||||
var inner = null
|
||||
var parts = null
|
||||
var trailing = null
|
||||
var chars = null
|
||||
var k = 0
|
||||
|
||||
while (i < length(lines)) {
|
||||
line = lines[i]
|
||||
trimmed = trim(line)
|
||||
if (collecting == false && trimmed == "[") {
|
||||
collecting = true
|
||||
chars = array(line)
|
||||
k = 0
|
||||
while (k < length(chars) && chars[k] == " ") {
|
||||
k = k + 1
|
||||
}
|
||||
indent = text(line, 0, k)
|
||||
collected = []
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
if (collecting) {
|
||||
if (trimmed == "]" || trimmed == "],") {
|
||||
is_leaf = true
|
||||
j = 0
|
||||
while (j < length(collected)) {
|
||||
inner = trim(collected[j])
|
||||
if (starts_with(inner, "[") || starts_with(inner, "{")) {
|
||||
is_leaf = false
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
if (is_leaf && length(collected) > 0) {
|
||||
parts = []
|
||||
j = 0
|
||||
while (j < length(collected)) {
|
||||
inner = trim(collected[j])
|
||||
if (ends_with(inner, ",")) {
|
||||
inner = text(inner, 0, length(inner) - 1)
|
||||
}
|
||||
parts[] = inner
|
||||
j = j + 1
|
||||
}
|
||||
trailing = ""
|
||||
if (ends_with(trimmed, ",")) {
|
||||
trailing = ","
|
||||
}
|
||||
result[] = `${indent}[${text(parts, ", ")}]${trailing}`
|
||||
} else {
|
||||
result[] = `${indent}[`
|
||||
j = 0
|
||||
while (j < length(collected)) {
|
||||
result[] = collected[j]
|
||||
j = j + 1
|
||||
}
|
||||
result[] = line
|
||||
}
|
||||
collecting = false
|
||||
} else {
|
||||
collected[] = line
|
||||
}
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
result[] = line
|
||||
i = i + 1
|
||||
}
|
||||
return text(result, "\n")
|
||||
}
|
||||
|
||||
var i = 0
|
||||
var path = null
|
||||
var raw = null
|
||||
var obj = null
|
||||
var pretty = null
|
||||
var f = null
|
||||
while (i < length(args)) {
|
||||
path = args[i]
|
||||
if (!fd.is_file(path)) {
|
||||
print(`skip ${path} (not found)`)
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
raw = text(fd.slurp(path))
|
||||
obj = json.decode(raw)
|
||||
pretty = compact_arrays(json.encode(obj, null, 2))
|
||||
f = fd.open(path, "w")
|
||||
fd.write(f, pretty)
|
||||
fd.close(f)
|
||||
print(`prettified ${path}`)
|
||||
i = i + 1
|
||||
}
|
||||
795
qbe_emit.cm
795
qbe_emit.cm
File diff suppressed because it is too large
Load Diff
@@ -86,24 +86,27 @@ function pack(sources, archive_path, read_dir) {
|
||||
|
||||
function add_recursive(path) {
|
||||
var full_path = base_dir + "/" + path
|
||||
var st = null
|
||||
var list = null
|
||||
var data = null
|
||||
if (path == ".") full_path = base_dir
|
||||
if (read_dir == null && path != ".") full_path = path
|
||||
|
||||
var st = fd.stat(full_path)
|
||||
st = fd.stat(full_path)
|
||||
if (!st) {
|
||||
log.console("Could not stat " + full_path)
|
||||
return
|
||||
}
|
||||
|
||||
if (st.isDirectory) {
|
||||
var list = fd.readdir(full_path)
|
||||
list = fd.readdir(full_path)
|
||||
arrfor(list, function(item) {
|
||||
if (item == "." || item == "..") return
|
||||
var sub = path == "." ? item : path + "/" + item
|
||||
add_recursive(sub)
|
||||
})
|
||||
} else {
|
||||
var data = fd.slurp(full_path)
|
||||
data = fd.slurp(full_path)
|
||||
if (data) {
|
||||
writer.add_file(path, data)
|
||||
log.console("Added " + path)
|
||||
|
||||
125
regen.ce
125
regen.ce
@@ -1,125 +0,0 @@
|
||||
// regen.ce — regenerate .mcode bytecode files and pre-warm .mach cache
|
||||
|
||||
var fd = use("fd")
|
||||
var json = use("json")
|
||||
var crypto = use("crypto")
|
||||
var tokenize = use("tokenize")
|
||||
var parse = use("parse")
|
||||
var fold = use("fold")
|
||||
var mcode = use("mcode")
|
||||
var streamline = use("streamline")
|
||||
|
||||
// Pipeline files (tokenize/parse/fold/mcode/streamline) are only regenerated
|
||||
// with --all flag since they require a self-consistent compiler to bootstrap.
|
||||
var pipeline_files = [
|
||||
{src: "tokenize.cm", name: "tokenize", out: "boot/tokenize.cm.mcode"},
|
||||
{src: "parse.cm", name: "parse", out: "boot/parse.cm.mcode"},
|
||||
{src: "fold.cm", name: "fold", out: "boot/fold.cm.mcode"},
|
||||
{src: "mcode.cm", name: "mcode", out: "boot/mcode.cm.mcode"},
|
||||
{src: "streamline.cm", name: "streamline", out: "boot/streamline.cm.mcode"}
|
||||
]
|
||||
|
||||
var files = [
|
||||
{src: "qbe.cm", name: "qbe", out: "boot/qbe.cm.mcode"},
|
||||
{src: "qbe_emit.cm", name: "qbe_emit", out: "boot/qbe_emit.cm.mcode"},
|
||||
{src: "verify_ir.cm", name: "verify_ir", out: "boot/verify_ir.cm.mcode"},
|
||||
{src: "internal/bootstrap.cm", name: "bootstrap", out: "boot/bootstrap.cm.mcode"},
|
||||
{src: "internal/engine.cm", name: "engine", out: "boot/engine.cm.mcode"},
|
||||
{src: "boot/seed_bootstrap.cm", name: "seed_bootstrap", out: "boot/seed_bootstrap.cm.mcode"},
|
||||
{src: "fd.cm", name: "fd", out: "boot/fd.cm.mcode"},
|
||||
{src: "time.cm", name: "time", out: "boot/time.cm.mcode"},
|
||||
{src: "pronto.cm", name: "pronto", out: "boot/pronto.cm.mcode"},
|
||||
{src: "toml.cm", name: "toml", out: "boot/toml.cm.mcode"},
|
||||
{src: "link.cm", name: "link", out: "boot/link.cm.mcode"},
|
||||
{src: "toolchains.cm", name: "toolchains", out: "boot/toolchains.cm.mcode"},
|
||||
{src: "package.cm", name: "package", out: "boot/package.cm.mcode"},
|
||||
{src: "internal/shop.cm", name: "internal_shop", out: "boot/internal_shop.cm.mcode"}
|
||||
]
|
||||
|
||||
// Include pipeline files with --all flag
|
||||
var os = use('os')
|
||||
var regen_all = args != null && length(args) > 0 && args[0] == "--all"
|
||||
if (regen_all) {
|
||||
files = array(pipeline_files, files)
|
||||
}
|
||||
|
||||
// Resolve shop_path for cache writes
|
||||
var shop = os.getenv('CELL_SHOP')
|
||||
var home = null
|
||||
var cache_dir = null
|
||||
if (!shop) {
|
||||
home = os.getenv('HOME')
|
||||
if (home) {
|
||||
shop = home + '/.cell'
|
||||
}
|
||||
}
|
||||
if (shop) {
|
||||
cache_dir = shop + '/build'
|
||||
if (!fd.is_dir(cache_dir)) {
|
||||
fd.mkdir(cache_dir)
|
||||
}
|
||||
}
|
||||
|
||||
var i = 0
|
||||
var entry = null
|
||||
var src = null
|
||||
var tok_result = null
|
||||
var ast = null
|
||||
var folded = null
|
||||
var mcode_blob = null
|
||||
var hash = null
|
||||
var mach_blob = null
|
||||
var compiled = null
|
||||
var optimized = null
|
||||
var mcode_text = null
|
||||
var f = null
|
||||
var errs = null
|
||||
var ei = 0
|
||||
var e = null
|
||||
var had_errors = false
|
||||
var compact_mcode = null
|
||||
|
||||
while (i < length(files)) {
|
||||
entry = files[i]
|
||||
src = text(fd.slurp(entry.src))
|
||||
tok_result = tokenize(src, entry.src)
|
||||
ast = parse(tok_result.tokens, src, entry.src, tokenize)
|
||||
// Check for parse/semantic errors
|
||||
errs = ast.errors
|
||||
if (errs != null && length(errs) > 0) {
|
||||
ei = 0
|
||||
while (ei < length(errs)) {
|
||||
e = errs[ei]
|
||||
if (e.line != null) {
|
||||
print(`${entry.src}:${text(e.line)}:${text(e.column)}: error: ${e.message}`)
|
||||
} else {
|
||||
print(`${entry.src}: error: ${e.message}`)
|
||||
}
|
||||
ei = ei + 1
|
||||
}
|
||||
had_errors = true
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
folded = fold(ast)
|
||||
compiled = mcode(folded)
|
||||
optimized = streamline(compiled)
|
||||
mcode_text = json.encode(optimized)
|
||||
f = fd.open(entry.out, "w")
|
||||
fd.write(f, mcode_text)
|
||||
fd.close(f)
|
||||
print(`wrote ${entry.out}`)
|
||||
// Pre-warm .mach cache
|
||||
if (cache_dir) {
|
||||
mcode_blob = stone(blob(mcode_text))
|
||||
hash = text(crypto.blake2(mcode_blob), 'h')
|
||||
compact_mcode = json.encode(optimized)
|
||||
mach_blob = mach_compile_mcode_bin(entry.name, compact_mcode)
|
||||
fd.slurpwrite(cache_dir + '/' + hash, mach_blob)
|
||||
print(` cached ${hash}`)
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
if (had_errors) {
|
||||
print("regen aborted: fix errors above")
|
||||
}
|
||||
@@ -140,6 +140,7 @@ var commit_str = null
|
||||
var line = null
|
||||
var cflags = null
|
||||
var ldflags = null
|
||||
var _show_flags = null
|
||||
|
||||
for (i = 0; i < length(sorted); i++) {
|
||||
locator = sorted[i].locator
|
||||
@@ -199,7 +200,7 @@ for (i = 0; i < length(sorted); i++) {
|
||||
|
||||
// Show compilation inputs if requested (verbose)
|
||||
if (depth == 0) {
|
||||
var _show_flags = function() {
|
||||
_show_flags = function() {
|
||||
cflags = pkg.get_flags(locator, 'CFLAGS', target_triple)
|
||||
ldflags = pkg.get_flags(locator, 'LDFLAGS', target_triple)
|
||||
if (length(cflags) > 0 || length(ldflags) > 0) {
|
||||
|
||||
25
run_aot.ce
Normal file
25
run_aot.ce
Normal file
@@ -0,0 +1,25 @@
|
||||
// run_aot.ce — compile a .ce program to native dylib and run it
|
||||
//
|
||||
// Usage:
|
||||
// cell run_aot <program.ce>
|
||||
|
||||
var shop = use('internal/shop')
|
||||
var fd = use('fd')
|
||||
|
||||
if (length(args) < 1) {
|
||||
print('usage: cell run_aot <program.ce>')
|
||||
return
|
||||
}
|
||||
|
||||
var file = args[0]
|
||||
if (!fd.is_file(file)) {
|
||||
if (!ends_with(file, '.ce') && fd.is_file(file + '.ce'))
|
||||
file = file + '.ce'
|
||||
else {
|
||||
print('file not found: ' + file)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var abs = fd.realpath(file)
|
||||
shop.use_native(abs)
|
||||
271
source/cell.c
271
source/cell.c
@@ -12,8 +12,7 @@
|
||||
#include "cJSON.h"
|
||||
|
||||
#define BOOTSTRAP_MCODE "boot/bootstrap.cm.mcode"
|
||||
#define SEED_BOOTSTRAP_MCODE "boot/seed_bootstrap.cm.mcode"
|
||||
#define BOOTSTRAP_SRC "internal/bootstrap.cm"
|
||||
#define ENGINE_SRC "internal/engine.cm"
|
||||
#define CELL_SHOP_DIR ".cell"
|
||||
#define CELL_CORE_DIR "packages/core"
|
||||
|
||||
@@ -213,6 +212,37 @@ static char* load_core_file(const char *filename, size_t *out_size) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Try loading engine.cm from source-hash cache
|
||||
// Returns heap-allocated binary data and sets *out_size, or NULL on cache miss
|
||||
static char *try_engine_cache(size_t *out_size) {
|
||||
size_t src_size;
|
||||
char *src = load_core_file(ENGINE_SRC, &src_size);
|
||||
if (!src) return NULL;
|
||||
|
||||
char *hex = compute_blake2_hex(src, src_size);
|
||||
free(src);
|
||||
char *cpath = build_cache_path(hex);
|
||||
if (!cpath) { free(hex); return NULL; }
|
||||
free(hex);
|
||||
|
||||
FILE *fh = fopen(cpath, "rb");
|
||||
if (!fh) { free(cpath); return NULL; }
|
||||
free(cpath);
|
||||
|
||||
fseek(fh, 0, SEEK_END);
|
||||
long file_size = ftell(fh);
|
||||
fseek(fh, 0, SEEK_SET);
|
||||
char *data = malloc(file_size);
|
||||
if (data && fread(data, 1, file_size, fh) == (size_t)file_size) {
|
||||
fclose(fh);
|
||||
*out_size = file_size;
|
||||
return data;
|
||||
}
|
||||
free(data);
|
||||
fclose(fh);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get the core path for use by scripts
|
||||
const char* cell_get_core_path(void) {
|
||||
return core_path;
|
||||
@@ -254,26 +284,57 @@ void script_startup(cell_rt *prt)
|
||||
cell_rt *crt = JS_GetContextOpaque(js);
|
||||
JS_FreeValue(js, js_core_blob_use(js));
|
||||
|
||||
// Load pre-compiled bootstrap .mcode
|
||||
size_t boot_size;
|
||||
char *boot_data = load_core_file(BOOTSTRAP_MCODE, &boot_size);
|
||||
if (!boot_data) {
|
||||
printf("ERROR: Could not load bootstrap from %s!\n", core_path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try cache or compile mcode → binary
|
||||
// Try engine fast-path: load engine.cm from source-hash cache
|
||||
size_t bin_size;
|
||||
char *bin_data = load_or_cache_bootstrap(boot_data, boot_size, &bin_size);
|
||||
free(boot_data);
|
||||
char *bin_data = try_engine_cache(&bin_size);
|
||||
|
||||
if (!bin_data) {
|
||||
printf("ERROR: Failed to compile bootstrap mcode!\n");
|
||||
return;
|
||||
// Cold path: run bootstrap to seed cache, then retry
|
||||
size_t boot_size;
|
||||
char *boot_data = load_core_file(BOOTSTRAP_MCODE, &boot_size);
|
||||
if (!boot_data) {
|
||||
printf("ERROR: Could not load bootstrap from %s!\n", core_path);
|
||||
return;
|
||||
}
|
||||
size_t boot_bin_size;
|
||||
char *boot_bin = load_or_cache_bootstrap(boot_data, boot_size, &boot_bin_size);
|
||||
free(boot_data);
|
||||
if (!boot_bin) {
|
||||
printf("ERROR: Failed to compile bootstrap mcode!\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Build env for bootstrap (only needs os, core_path, shop_path)
|
||||
JSGCRef boot_env_ref;
|
||||
JS_AddGCRef(js, &boot_env_ref);
|
||||
boot_env_ref.val = JS_NewObject(js);
|
||||
JSValue btmp;
|
||||
btmp = js_core_os_use(js);
|
||||
JS_SetPropertyStr(js, boot_env_ref.val, "os", btmp);
|
||||
if (core_path) {
|
||||
btmp = JS_NewString(js, core_path);
|
||||
JS_SetPropertyStr(js, boot_env_ref.val, "core_path", btmp);
|
||||
}
|
||||
btmp = shop_path ? JS_NewString(js, shop_path) : JS_NULL;
|
||||
JS_SetPropertyStr(js, boot_env_ref.val, "shop_path", btmp);
|
||||
JSValue boot_env = JS_Stone(js, boot_env_ref.val);
|
||||
JS_DeleteGCRef(js, &boot_env_ref);
|
||||
|
||||
crt->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;
|
||||
|
||||
// Retry engine from cache
|
||||
bin_data = try_engine_cache(&bin_size);
|
||||
if (!bin_data) {
|
||||
printf("ERROR: Bootstrap ran but engine.cm not in cache!\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Create hidden environment
|
||||
// Note: evaluate allocating calls into temporaries before passing to
|
||||
// JS_SetPropertyStr, so env_ref.val is read AFTER GC may have moved it.
|
||||
// Build engine environment
|
||||
JSGCRef env_ref;
|
||||
JS_AddGCRef(js, &env_ref);
|
||||
env_ref.val = JS_NewObject(js);
|
||||
@@ -284,6 +345,7 @@ void script_startup(cell_rt *prt)
|
||||
JS_SetPropertyStr(js, env_ref.val, "json", tmp);
|
||||
|
||||
crt->actor_sym_ref.val = JS_NewObject(js);
|
||||
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));
|
||||
|
||||
// Always set init (even if null)
|
||||
@@ -310,7 +372,7 @@ void script_startup(cell_rt *prt)
|
||||
JSValue hidden_env = JS_Stone(js, env_ref.val);
|
||||
JS_DeleteGCRef(js, &env_ref);
|
||||
|
||||
// Run from binary
|
||||
// Run engine from binary
|
||||
crt->state = ACTOR_RUNNING;
|
||||
JSValue v = JS_RunMachBin(js, (const uint8_t *)bin_data, bin_size, hidden_env);
|
||||
free(bin_data);
|
||||
@@ -370,7 +432,6 @@ static void print_usage(const char *prog)
|
||||
printf(" --shop <path> Set shop path (overrides CELL_SHOP)\n");
|
||||
printf(" --dev Dev mode (shop=.cell, core=.)\n");
|
||||
printf(" --heap <size> Initial heap size (e.g. 256MB, 1GB)\n");
|
||||
printf(" --seed Use seed bootstrap (minimal, for regen)\n");
|
||||
printf(" --test [heap_size] Run C test suite\n");
|
||||
printf(" -h, --help Show this help message\n");
|
||||
printf("\nEnvironment:\n");
|
||||
@@ -401,9 +462,8 @@ int cell_init(int argc, char **argv)
|
||||
return run_test_suite(heap_size);
|
||||
}
|
||||
|
||||
/* Default: run script through bootstrap pipeline */
|
||||
/* Default: run script through engine pipeline */
|
||||
int arg_start = 1;
|
||||
int seed_mode = 0;
|
||||
size_t heap_size = 1024 * 1024; /* 1MB default */
|
||||
const char *shop_override = NULL;
|
||||
const char *core_override = NULL;
|
||||
@@ -424,9 +484,6 @@ int cell_init(int argc, char **argv)
|
||||
}
|
||||
core_override = argv[arg_start + 1];
|
||||
arg_start += 2;
|
||||
} else if (strcmp(argv[arg_start], "--seed") == 0) {
|
||||
seed_mode = 1;
|
||||
arg_start++;
|
||||
} else if (strcmp(argv[arg_start], "--heap") == 0) {
|
||||
if (arg_start + 1 >= argc) {
|
||||
printf("ERROR: --heap requires a size argument (e.g. 1GB, 256MB, 65536)\n");
|
||||
@@ -463,33 +520,15 @@ int cell_init(int argc, char **argv)
|
||||
|
||||
actor_initialize();
|
||||
|
||||
const char *boot_mcode = seed_mode ? SEED_BOOTSTRAP_MCODE : BOOTSTRAP_MCODE;
|
||||
size_t boot_size;
|
||||
char *boot_data = load_core_file(boot_mcode, &boot_size);
|
||||
if (!boot_data) {
|
||||
printf("ERROR: Could not load bootstrap from %s\n", core_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Try cache or compile mcode → binary
|
||||
size_t bin_size;
|
||||
char *bin_data = load_or_cache_bootstrap(boot_data, boot_size, &bin_size);
|
||||
free(boot_data);
|
||||
if (!bin_data) {
|
||||
printf("ERROR: Failed to compile bootstrap mcode\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
g_runtime = JS_NewRuntime();
|
||||
if (!g_runtime) {
|
||||
printf("Failed to create JS runtime\n");
|
||||
free(bin_data);
|
||||
return 1;
|
||||
}
|
||||
JSContext *ctx = JS_NewContextWithHeapSize(g_runtime, heap_size);
|
||||
if (!ctx) {
|
||||
printf("Failed to create JS context\n");
|
||||
free(bin_data); JS_FreeRuntime(g_runtime);
|
||||
JS_FreeRuntime(g_runtime);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -518,52 +557,122 @@ int cell_init(int argc, char **argv)
|
||||
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_SetActorSym(ctx, JS_DupValue(ctx, cli_rt->actor_sym_ref.val));
|
||||
|
||||
root_cell = cli_rt;
|
||||
|
||||
JS_FreeValue(ctx, js_core_blob_use(ctx));
|
||||
|
||||
JSGCRef env_ref;
|
||||
JS_AddGCRef(ctx, &env_ref);
|
||||
env_ref.val = JS_NewObject(ctx);
|
||||
JSValue tmp;
|
||||
tmp = js_core_os_use(ctx);
|
||||
JS_SetPropertyStr(ctx, env_ref.val, "os", tmp);
|
||||
tmp = JS_NewString(ctx, core_path);
|
||||
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));
|
||||
tmp = js_core_json_use(ctx);
|
||||
JS_SetPropertyStr(ctx, env_ref.val, "json", tmp);
|
||||
JS_SetPropertyStr(ctx, env_ref.val, "init", JS_NULL);
|
||||
JSGCRef args_ref;
|
||||
JS_AddGCRef(ctx, &args_ref);
|
||||
args_ref.val = JS_NewArray(ctx);
|
||||
for (int i = arg_start; i < argc; i++) {
|
||||
JSValue str = JS_NewString(ctx, argv[i]);
|
||||
JS_ArrayPush(ctx, &args_ref.val, str);
|
||||
}
|
||||
JS_SetPropertyStr(ctx, env_ref.val, "args", args_ref.val);
|
||||
JS_DeleteGCRef(ctx, &args_ref);
|
||||
JSValue hidden_env = JS_Stone(ctx, env_ref.val);
|
||||
JS_DeleteGCRef(ctx, &env_ref);
|
||||
|
||||
JSValue result = JS_RunMachBin(ctx, (const uint8_t *)bin_data, bin_size, hidden_env);
|
||||
free(bin_data);
|
||||
|
||||
int exit_code = 0;
|
||||
if (JS_IsException(result)) {
|
||||
JS_GetException(ctx);
|
||||
exit_code = 1;
|
||||
} else if (!JS_IsNull(result)) {
|
||||
const char *str = JS_ToCString(ctx, result);
|
||||
if (str) {
|
||||
printf("%s\n", str);
|
||||
JS_FreeCString(ctx, str);
|
||||
|
||||
// Try engine fast-path: load engine.cm from source-hash cache
|
||||
size_t bin_size;
|
||||
char *bin_data = try_engine_cache(&bin_size);
|
||||
|
||||
if (!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);
|
||||
if (!boot_data) {
|
||||
printf("ERROR: Could not load bootstrap from %s\n", core_path);
|
||||
return 1;
|
||||
}
|
||||
size_t boot_bin_size;
|
||||
char *boot_bin = load_or_cache_bootstrap(boot_data, boot_size, &boot_bin_size);
|
||||
free(boot_data);
|
||||
if (!boot_bin) {
|
||||
printf("ERROR: Failed to compile bootstrap mcode\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Build env for bootstrap (os, core_path, shop_path required;
|
||||
// args, json, actorsym provided for compatibility)
|
||||
JSGCRef boot_env_ref;
|
||||
JS_AddGCRef(ctx, &boot_env_ref);
|
||||
boot_env_ref.val = JS_NewObject(ctx);
|
||||
JSValue btmp;
|
||||
btmp = js_core_os_use(ctx);
|
||||
JS_SetPropertyStr(ctx, boot_env_ref.val, "os", btmp);
|
||||
btmp = JS_NewString(ctx, core_path);
|
||||
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));
|
||||
btmp = js_core_json_use(ctx);
|
||||
JS_SetPropertyStr(ctx, boot_env_ref.val, "json", btmp);
|
||||
JS_SetPropertyStr(ctx, boot_env_ref.val, "init", JS_NULL);
|
||||
JSGCRef boot_args_ref;
|
||||
JS_AddGCRef(ctx, &boot_args_ref);
|
||||
boot_args_ref.val = JS_NewArray(ctx);
|
||||
for (int i = arg_start; i < argc; i++) {
|
||||
JSValue str = JS_NewString(ctx, argv[i]);
|
||||
JS_ArrayPush(ctx, &boot_args_ref.val, str);
|
||||
}
|
||||
JS_SetPropertyStr(ctx, boot_env_ref.val, "args", boot_args_ref.val);
|
||||
JS_DeleteGCRef(ctx, &boot_args_ref);
|
||||
JSValue boot_env = JS_Stone(ctx, boot_env_ref.val);
|
||||
JS_DeleteGCRef(ctx, &boot_env_ref);
|
||||
|
||||
JSValue boot_result = JS_RunMachBin(ctx, (const uint8_t *)boot_bin, boot_bin_size, boot_env);
|
||||
free(boot_bin);
|
||||
if (JS_IsException(boot_result)) {
|
||||
JS_GetException(ctx);
|
||||
printf("ERROR: Bootstrap failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Retry engine from cache (new-style bootstrap seeds it)
|
||||
bin_data = try_engine_cache(&bin_size);
|
||||
if (!bin_data) {
|
||||
// Old-style bootstrap already ran the program — skip engine load
|
||||
goto check_actors;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// Build engine environment
|
||||
JSGCRef env_ref;
|
||||
JS_AddGCRef(ctx, &env_ref);
|
||||
env_ref.val = JS_NewObject(ctx);
|
||||
JSValue tmp;
|
||||
tmp = js_core_os_use(ctx);
|
||||
JS_SetPropertyStr(ctx, env_ref.val, "os", tmp);
|
||||
tmp = JS_NewString(ctx, core_path);
|
||||
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));
|
||||
tmp = js_core_json_use(ctx);
|
||||
JS_SetPropertyStr(ctx, env_ref.val, "json", tmp);
|
||||
JS_SetPropertyStr(ctx, env_ref.val, "init", JS_NULL);
|
||||
JSGCRef args_ref;
|
||||
JS_AddGCRef(ctx, &args_ref);
|
||||
args_ref.val = JS_NewArray(ctx);
|
||||
for (int i = arg_start; i < argc; i++) {
|
||||
JSValue str = JS_NewString(ctx, argv[i]);
|
||||
JS_ArrayPush(ctx, &args_ref.val, str);
|
||||
}
|
||||
JS_SetPropertyStr(ctx, env_ref.val, "args", args_ref.val);
|
||||
JS_DeleteGCRef(ctx, &args_ref);
|
||||
JSValue hidden_env = JS_Stone(ctx, env_ref.val);
|
||||
|
||||
JSValue result = JS_RunMachBin(ctx, (const uint8_t *)bin_data, bin_size, hidden_env);
|
||||
JS_DeleteGCRef(ctx, &env_ref);
|
||||
free(bin_data);
|
||||
|
||||
if (JS_IsException(result)) {
|
||||
JS_GetException(ctx);
|
||||
exit_code = 1;
|
||||
} else if (!JS_IsNull(result)) {
|
||||
const char *str = JS_ToCString(ctx, result);
|
||||
if (str) {
|
||||
printf("%s\n", str);
|
||||
JS_FreeCString(ctx, str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
check_actors:
|
||||
if (scheduler_actor_count() > 0) {
|
||||
scheduler_enable_quiescence();
|
||||
actor_loop();
|
||||
|
||||
854
source/mach.c
854
source/mach.c
File diff suppressed because it is too large
Load Diff
@@ -260,23 +260,56 @@ void cell_rt_store_index(JSContext *ctx, JSValue val, JSValue arr,
|
||||
|
||||
/* --- Intrinsic/global lookup --- */
|
||||
|
||||
/* Native module environment — set before executing a native module's cell_main.
|
||||
Contains runtime functions (starts_with, ends_with, etc.) and use(). */
|
||||
static JSGCRef g_native_env_ref;
|
||||
static int g_has_native_env = 0;
|
||||
|
||||
void cell_rt_set_native_env(JSContext *ctx, JSValue env) {
|
||||
if (!JS_IsNull(env) && !JS_IsStone(env)) {
|
||||
fprintf(stderr, "cell_rt_set_native_env: WARNING env not stone\n");
|
||||
}
|
||||
if (g_has_native_env)
|
||||
JS_DeleteGCRef(ctx, &g_native_env_ref);
|
||||
if (!JS_IsNull(env)) {
|
||||
JS_AddGCRef(ctx, &g_native_env_ref);
|
||||
g_native_env_ref.val = env;
|
||||
g_has_native_env = 1;
|
||||
} else {
|
||||
g_has_native_env = 0;
|
||||
}
|
||||
}
|
||||
|
||||
JSValue cell_rt_get_intrinsic(JSContext *ctx, const char *name) {
|
||||
/* Check native env first (runtime-provided functions like starts_with) */
|
||||
if (g_has_native_env) {
|
||||
JSValue v = JS_GetPropertyStr(ctx, g_native_env_ref.val, name);
|
||||
if (!JS_IsNull(v)) return v;
|
||||
}
|
||||
return JS_GetPropertyStr(ctx, ctx->global_obj, name);
|
||||
}
|
||||
|
||||
/* --- Closure access ---
|
||||
Slot 511 in each frame stores a pointer to the enclosing frame.
|
||||
Walking depth levels up the chain gives the target frame. */
|
||||
Slot 511 in each frame stores the magic ID (registry index) of the
|
||||
function that owns this frame. cell_rt_get/put_closure re-derive
|
||||
the enclosing frame from the function's GC ref at call time, so
|
||||
pointers stay valid even if GC moves frames. */
|
||||
|
||||
#define QBE_FRAME_OUTER_SLOT 511
|
||||
|
||||
static JSValue *derive_outer_fp(int magic);
|
||||
|
||||
JSValue cell_rt_get_closure(JSContext *ctx, void *fp, int64_t depth,
|
||||
int64_t slot) {
|
||||
JSValue *frame = (JSValue *)fp;
|
||||
for (int64_t d = 0; d < depth; d++) {
|
||||
void *outer = (void *)(uintptr_t)frame[QBE_FRAME_OUTER_SLOT];
|
||||
if (!outer) return JS_NULL;
|
||||
frame = (JSValue *)outer;
|
||||
/* fp[511] stores the magic ID (registry index) of the function
|
||||
that owns this frame. derive_outer_fp re-derives the enclosing
|
||||
frame from the function's GC ref, so it's always current even
|
||||
if GC moved the frame. */
|
||||
int magic = (int)(int64_t)frame[QBE_FRAME_OUTER_SLOT];
|
||||
frame = derive_outer_fp(magic);
|
||||
if (!frame) return JS_NULL;
|
||||
}
|
||||
return frame[slot];
|
||||
}
|
||||
@@ -285,13 +318,46 @@ void cell_rt_put_closure(JSContext *ctx, void *fp, JSValue val, int64_t depth,
|
||||
int64_t slot) {
|
||||
JSValue *frame = (JSValue *)fp;
|
||||
for (int64_t d = 0; d < depth; d++) {
|
||||
void *outer = (void *)(uintptr_t)frame[QBE_FRAME_OUTER_SLOT];
|
||||
if (!outer) return;
|
||||
frame = (JSValue *)outer;
|
||||
int magic = (int)(int64_t)frame[QBE_FRAME_OUTER_SLOT];
|
||||
frame = derive_outer_fp(magic);
|
||||
if (!frame) return;
|
||||
}
|
||||
frame[slot] = val;
|
||||
}
|
||||
|
||||
/* --- GC-managed AOT frame stack ---
|
||||
Each AOT function call pushes a GC ref so the GC can find and
|
||||
update frame pointers when it moves objects. cell_rt_refresh_fp
|
||||
re-derives the slot pointer after any GC-triggering call. */
|
||||
|
||||
#define MAX_AOT_DEPTH 256
|
||||
static JSGCRef g_aot_gc_refs[MAX_AOT_DEPTH];
|
||||
static int g_aot_depth = 0;
|
||||
|
||||
JSValue *cell_rt_enter_frame(JSContext *ctx, int64_t nr_slots) {
|
||||
if (g_aot_depth >= MAX_AOT_DEPTH)
|
||||
return NULL;
|
||||
JSFrameRegister *frame = alloc_frame_register(ctx, (int)nr_slots);
|
||||
if (!frame) return NULL;
|
||||
JSGCRef *ref = &g_aot_gc_refs[g_aot_depth];
|
||||
JS_AddGCRef(ctx, ref);
|
||||
ref->val = JS_MKPTR(frame);
|
||||
g_aot_depth++;
|
||||
return (JSValue *)frame->slots;
|
||||
}
|
||||
|
||||
JSValue *cell_rt_refresh_fp(JSContext *ctx) {
|
||||
(void)ctx;
|
||||
JSFrameRegister *frame = (JSFrameRegister *)JS_VALUE_GET_PTR(
|
||||
g_aot_gc_refs[g_aot_depth - 1].val);
|
||||
return (JSValue *)frame->slots;
|
||||
}
|
||||
|
||||
void cell_rt_leave_frame(JSContext *ctx) {
|
||||
g_aot_depth--;
|
||||
JS_DeleteGCRef(ctx, &g_aot_gc_refs[g_aot_depth]);
|
||||
}
|
||||
|
||||
/* --- Function creation and calling --- */
|
||||
|
||||
typedef JSValue (*cell_compiled_fn)(JSContext *ctx, void *fp);
|
||||
@@ -305,7 +371,8 @@ typedef JSValue (*cell_compiled_fn)(JSContext *ctx, void *fp);
|
||||
static struct {
|
||||
void *dl_handle;
|
||||
int fn_idx;
|
||||
void *outer_fp;
|
||||
JSGCRef frame_ref; /* independent GC ref for enclosing frame */
|
||||
int has_frame_ref;
|
||||
} g_native_fn_registry[MAX_NATIVE_FN];
|
||||
|
||||
static int g_native_fn_count = 0;
|
||||
@@ -313,6 +380,16 @@ static int g_native_fn_count = 0;
|
||||
/* Set before executing a native module's cell_main */
|
||||
static void *g_current_dl_handle = NULL;
|
||||
|
||||
/* Derive the outer frame's slots pointer from the closure's own GC ref.
|
||||
Each closure keeps an independent GC ref so the enclosing frame
|
||||
survives even after cell_rt_leave_frame pops the stack ref. */
|
||||
static JSValue *derive_outer_fp(int magic) {
|
||||
if (!g_native_fn_registry[magic].has_frame_ref) return NULL;
|
||||
JSFrameRegister *frame = (JSFrameRegister *)JS_VALUE_GET_PTR(
|
||||
g_native_fn_registry[magic].frame_ref.val);
|
||||
return (JSValue *)frame->slots;
|
||||
}
|
||||
|
||||
static JSValue cell_fn_trampoline(JSContext *ctx, JSValue this_val,
|
||||
int argc, JSValue *argv, int magic) {
|
||||
if (magic < 0 || magic >= g_native_fn_count)
|
||||
@@ -328,27 +405,44 @@ static JSValue cell_fn_trampoline(JSContext *ctx, JSValue this_val,
|
||||
if (!fn)
|
||||
return JS_ThrowTypeError(ctx, "native function %s not found in dylib", name);
|
||||
|
||||
/* Allocate frame: slot 0 = this, slots 1..argc = args */
|
||||
JSValue frame[512];
|
||||
memset(frame, 0, sizeof(frame));
|
||||
frame[0] = this_val;
|
||||
/* Allocate GC-managed frame: slot 0 = this, slots 1..argc = args */
|
||||
JSValue *fp = cell_rt_enter_frame(ctx, 512);
|
||||
if (!fp) return JS_EXCEPTION;
|
||||
fp[0] = this_val;
|
||||
for (int i = 0; i < argc && i < 510; i++)
|
||||
frame[1 + i] = argv[i];
|
||||
fp[1 + i] = argv[i];
|
||||
|
||||
/* Link to outer frame for closure access */
|
||||
frame[QBE_FRAME_OUTER_SLOT] = (JSValue)(uintptr_t)g_native_fn_registry[magic].outer_fp;
|
||||
/* Store the magic ID (registry index) so cell_rt_get/put_closure
|
||||
can re-derive the enclosing frame from the GC ref at call time,
|
||||
surviving GC moves */
|
||||
fp[QBE_FRAME_OUTER_SLOT] = (JSValue)(int64_t)magic;
|
||||
|
||||
return fn(ctx, frame);
|
||||
JSValue result = fn(ctx, fp);
|
||||
cell_rt_leave_frame(ctx);
|
||||
if (result == JS_EXCEPTION)
|
||||
return JS_EXCEPTION;
|
||||
return result;
|
||||
}
|
||||
|
||||
JSValue cell_rt_make_function(JSContext *ctx, int64_t fn_idx, void *outer_fp) {
|
||||
(void)outer_fp;
|
||||
if (g_native_fn_count >= MAX_NATIVE_FN)
|
||||
return JS_ThrowTypeError(ctx, "too many native functions (max %d)", MAX_NATIVE_FN);
|
||||
|
||||
int global_id = g_native_fn_count++;
|
||||
g_native_fn_registry[global_id].dl_handle = g_current_dl_handle;
|
||||
g_native_fn_registry[global_id].fn_idx = (int)fn_idx;
|
||||
g_native_fn_registry[global_id].outer_fp = outer_fp;
|
||||
|
||||
/* Create independent GC ref so the enclosing frame survives
|
||||
even after cell_rt_leave_frame pops the stack ref */
|
||||
if (g_aot_depth > 0) {
|
||||
JSGCRef *ref = &g_native_fn_registry[global_id].frame_ref;
|
||||
JS_AddGCRef(ctx, ref);
|
||||
ref->val = g_aot_gc_refs[g_aot_depth - 1].val;
|
||||
g_native_fn_registry[global_id].has_frame_ref = 1;
|
||||
} else {
|
||||
g_native_fn_registry[global_id].has_frame_ref = 0;
|
||||
}
|
||||
|
||||
return JS_NewCFunction2(ctx, (JSCFunction *)cell_fn_trampoline, "native_fn",
|
||||
255, JS_CFUNC_generic_magic, global_id);
|
||||
@@ -369,6 +463,7 @@ JSValue cell_rt_frame(JSContext *ctx, JSValue fn, int64_t nargs) {
|
||||
}
|
||||
|
||||
void cell_rt_setarg(JSValue frame_val, int64_t idx, JSValue val) {
|
||||
if (frame_val == JS_EXCEPTION) return;
|
||||
JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val);
|
||||
fr->slots[idx] = val;
|
||||
}
|
||||
@@ -474,18 +569,54 @@ int cell_rt_is_proxy(JSContext *ctx, JSValue v) {
|
||||
return fn->length == 2;
|
||||
}
|
||||
|
||||
/* --- 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;
|
||||
}
|
||||
|
||||
/* --- Disruption --- */
|
||||
|
||||
void cell_rt_disrupt(JSContext *ctx) {
|
||||
JS_ThrowTypeError(ctx, "type error in native code");
|
||||
}
|
||||
|
||||
/* --- set_var: set a variable in env_record or global --- */
|
||||
|
||||
void cell_rt_set_var(JSContext *ctx, JSValue val, const char *name) {
|
||||
JSValue key = JS_NewString(ctx, name);
|
||||
JS_SetProperty(ctx, ctx->global_obj, key, val);
|
||||
}
|
||||
|
||||
/* --- in: key in obj --- */
|
||||
|
||||
JSValue cell_rt_in(JSContext *ctx, JSValue key, JSValue obj) {
|
||||
int has = JS_HasProperty(ctx, obj, key);
|
||||
return JS_NewBool(ctx, has > 0);
|
||||
}
|
||||
|
||||
/* --- regexp: create regex from pattern and flags --- */
|
||||
|
||||
JSValue cell_rt_regexp(JSContext *ctx, const char *pattern, const char *flags) {
|
||||
JSValue argv[2];
|
||||
argv[0] = JS_NewString(ctx, pattern);
|
||||
argv[1] = JS_NewString(ctx, flags);
|
||||
JSValue re = js_regexp_constructor(ctx, JS_NULL, 2, argv);
|
||||
if (JS_IsException(re))
|
||||
return JS_EXCEPTION;
|
||||
return re;
|
||||
}
|
||||
|
||||
/* --- Module entry point ---
|
||||
Loads a native .cm module from a dylib handle.
|
||||
Looks up cell_main, builds a heap-allocated frame, sets
|
||||
g_current_dl_handle so closures register in the right module. */
|
||||
|
||||
JSValue cell_rt_native_module_load(JSContext *ctx, void *dl_handle) {
|
||||
JSValue cell_rt_native_module_load(JSContext *ctx, void *dl_handle, JSValue env) {
|
||||
cell_compiled_fn fn = (cell_compiled_fn)dlsym(dl_handle, "cell_main");
|
||||
if (!fn)
|
||||
return JS_ThrowTypeError(ctx, "cell_main not found in native module dylib");
|
||||
@@ -495,22 +626,27 @@ JSValue cell_rt_native_module_load(JSContext *ctx, void *dl_handle) {
|
||||
void *prev_handle = g_current_dl_handle;
|
||||
g_current_dl_handle = dl_handle;
|
||||
|
||||
/* Heap-allocate so closures created in cell_main can reference
|
||||
this frame after the module entry returns. */
|
||||
JSValue *frame = calloc(512, sizeof(JSValue));
|
||||
if (!frame) {
|
||||
/* Make env available for cell_rt_get_intrinsic lookups */
|
||||
cell_rt_set_native_env(ctx, env);
|
||||
|
||||
/* GC-managed frame for module execution */
|
||||
JSValue *fp = cell_rt_enter_frame(ctx, 512);
|
||||
if (!fp) {
|
||||
g_current_dl_handle = prev_handle;
|
||||
return JS_ThrowTypeError(ctx, "frame allocation failed");
|
||||
}
|
||||
|
||||
JSValue result = fn(ctx, frame);
|
||||
JSValue result = fn(ctx, fp);
|
||||
cell_rt_leave_frame(ctx); /* safe — closures have independent GC refs */
|
||||
g_current_dl_handle = prev_handle;
|
||||
if (result == JS_EXCEPTION)
|
||||
return JS_EXCEPTION;
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Load a native module from a dylib handle, trying a named symbol first.
|
||||
Falls back to cell_main if the named symbol is not found. */
|
||||
JSValue cell_rt_native_module_load_named(JSContext *ctx, void *dl_handle, const char *sym_name) {
|
||||
JSValue cell_rt_native_module_load_named(JSContext *ctx, void *dl_handle, const char *sym_name, JSValue env) {
|
||||
cell_compiled_fn fn = NULL;
|
||||
if (sym_name)
|
||||
fn = (cell_compiled_fn)dlsym(dl_handle, sym_name);
|
||||
@@ -522,19 +658,25 @@ JSValue cell_rt_native_module_load_named(JSContext *ctx, void *dl_handle, const
|
||||
void *prev_handle = g_current_dl_handle;
|
||||
g_current_dl_handle = dl_handle;
|
||||
|
||||
JSValue *frame = calloc(512, sizeof(JSValue));
|
||||
if (!frame) {
|
||||
/* Make env available for cell_rt_get_intrinsic lookups */
|
||||
cell_rt_set_native_env(ctx, env);
|
||||
|
||||
JSValue *fp = cell_rt_enter_frame(ctx, 512);
|
||||
if (!fp) {
|
||||
g_current_dl_handle = prev_handle;
|
||||
return JS_ThrowTypeError(ctx, "frame allocation failed");
|
||||
}
|
||||
|
||||
JSValue result = fn(ctx, frame);
|
||||
JSValue result = fn(ctx, fp);
|
||||
cell_rt_leave_frame(ctx); /* safe — closures have independent GC refs */
|
||||
g_current_dl_handle = prev_handle;
|
||||
if (result == JS_EXCEPTION)
|
||||
return JS_EXCEPTION;
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Backward-compat: uses RTLD_DEFAULT (works when dylib opened with RTLD_GLOBAL) */
|
||||
JSValue cell_rt_module_entry(JSContext *ctx) {
|
||||
void *handle = dlopen(NULL, RTLD_LAZY);
|
||||
return cell_rt_native_module_load(ctx, handle);
|
||||
return cell_rt_native_module_load(ctx, handle, JS_NULL);
|
||||
}
|
||||
|
||||
@@ -495,8 +495,8 @@ typedef enum MachOpcode {
|
||||
MACH_MOD, /* R(A) = R(B) % R(C) */
|
||||
MACH_POW, /* R(A) = R(B) ** R(C) */
|
||||
MACH_NEG, /* R(A) = -R(B) */
|
||||
MACH_INC, /* R(A) = R(B) + 1 */
|
||||
MACH_DEC, /* R(A) = R(B) - 1 */
|
||||
MACH__DEAD_INC, /* reserved — was MACH_INC, never emitted */
|
||||
MACH__DEAD_DEC, /* reserved — was MACH_DEC, never emitted */
|
||||
|
||||
/* Generic comparison (ABC) — used by legacy .mach */
|
||||
MACH_EQ, /* R(A) = (R(B) == R(C)) */
|
||||
@@ -671,8 +671,8 @@ static const char *mach_opcode_names[MACH_OP_COUNT] = {
|
||||
[MACH_MOD] = "mod",
|
||||
[MACH_POW] = "pow",
|
||||
[MACH_NEG] = "neg",
|
||||
[MACH_INC] = "inc",
|
||||
[MACH_DEC] = "dec",
|
||||
[MACH__DEAD_INC] = "dead_inc",
|
||||
[MACH__DEAD_DEC] = "dead_dec",
|
||||
[MACH_EQ] = "eq",
|
||||
[MACH_NEQ] = "neq",
|
||||
[MACH_LT] = "lt",
|
||||
@@ -850,9 +850,14 @@ static inline objhdr_t *chase(JSValue v) {
|
||||
updated to follow the chain to the live copy. */
|
||||
static inline void mach_resolve_forward(JSValue *slot) {
|
||||
if (JS_IsPtr(*slot)) {
|
||||
objhdr_t h = *(objhdr_t *)JS_VALUE_GET_PTR(*slot);
|
||||
if (objhdr_type(h) == OBJ_FORWARD) {
|
||||
*slot = JS_MKPTR(objhdr_fwd_ptr(h));
|
||||
objhdr_t *oh = (objhdr_t *)JS_VALUE_GET_PTR(*slot);
|
||||
if (objhdr_type(*oh) == OBJ_FORWARD) {
|
||||
do {
|
||||
objhdr_t *next = (objhdr_t *)objhdr_fwd_ptr(*oh);
|
||||
if (!next) break;
|
||||
oh = next;
|
||||
} while (objhdr_type(*oh) == OBJ_FORWARD);
|
||||
*slot = JS_MKPTR(oh);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1142,6 +1147,9 @@ struct JSContext {
|
||||
|
||||
JSValue current_exception;
|
||||
|
||||
/* Actor identity key — used by wota/nota PRIVATE serialization */
|
||||
JSValue actor_sym;
|
||||
|
||||
/* Stack overflow protection */
|
||||
// todo: want this, but should be a simple increment/decrement counter while frames are pushed
|
||||
size_t stack_depth;
|
||||
|
||||
@@ -324,6 +324,8 @@ JSContext *JS_NewContextWithHeapSize (JSRuntime *rt, size_t heap_size);
|
||||
void JS_FreeContext (JSContext *s);
|
||||
void *JS_GetContextOpaque (JSContext *ctx);
|
||||
void JS_SetContextOpaque (JSContext *ctx, void *opaque);
|
||||
void JS_SetActorSym (JSContext *ctx, JSValue sym);
|
||||
JSValue JS_GetActorSym (JSContext *ctx);
|
||||
JSRuntime *JS_GetRuntime (JSContext *ctx);
|
||||
/* use 0 to disable maximum stack size check */
|
||||
void JS_SetMaxStackSize (JSContext *ctx, size_t stack_size);
|
||||
@@ -588,7 +590,8 @@ JSValue JS_ParseJSON (JSContext *ctx, const char *buf, size_t buf_len,
|
||||
JSValue JS_ParseJSON2 (JSContext *ctx, const char *buf, size_t buf_len,
|
||||
const char *filename, int flags);
|
||||
JSValue JS_JSONStringify (JSContext *ctx, JSValue obj,
|
||||
JSValue replacer, JSValue space0);
|
||||
JSValue replacer, JSValue space0,
|
||||
JS_BOOL compact_arrays);
|
||||
|
||||
/* ============================================================
|
||||
9. Intrinsic Wrappers (JS_Cell* / JS_Array*)
|
||||
|
||||
362
source/runtime.c
362
source/runtime.c
@@ -595,6 +595,10 @@ int rec_resize (JSContext *ctx, JSValue *pobj, uint64_t new_mask) {
|
||||
/* Allocate new record with larger capacity - may trigger GC! */
|
||||
size_t slots_size = sizeof (slot) * (new_mask + 1);
|
||||
size_t total_size = sizeof (JSRecord) + slots_size;
|
||||
if (total_size >= 100000) {
|
||||
fprintf(stderr, "LARGE_REC_RESIZE: new_mask=%llu total=%zu old_mask=%llu\n",
|
||||
(unsigned long long)new_mask, total_size, (unsigned long long)old_mask);
|
||||
}
|
||||
|
||||
JSRecord *new_rec = js_malloc (ctx, total_size);
|
||||
if (!new_rec) {
|
||||
@@ -774,15 +778,31 @@ void *js_malloc (JSContext *ctx, size_t size) {
|
||||
size = (size + 7) & ~7;
|
||||
|
||||
#ifdef FORCE_GC_AT_MALLOC
|
||||
/* Force GC on every allocation for testing - but don't grow heap unless needed */
|
||||
int need_space = (uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end;
|
||||
if (ctx_gc(ctx, need_space, size) < 0) {
|
||||
JS_ThrowOutOfMemory(ctx);
|
||||
return NULL;
|
||||
}
|
||||
if ((uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end) {
|
||||
JS_ThrowOutOfMemory(ctx);
|
||||
return NULL;
|
||||
/* Force GC on every allocation for testing */
|
||||
{
|
||||
int need_space = (uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end;
|
||||
if (ctx_gc(ctx, need_space, size) < 0) {
|
||||
JS_ThrowOutOfMemory(ctx);
|
||||
return NULL;
|
||||
}
|
||||
/* If still no room after GC, grow and retry (same logic as normal path) */
|
||||
if ((uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end) {
|
||||
size_t live = (size_t)((uint8_t *)ctx->heap_free - (uint8_t *)ctx->heap_base);
|
||||
size_t need = live + size;
|
||||
size_t ns = ctx->current_block_size;
|
||||
while (ns < need && ns < buddy_max_block(&ctx->rt->buddy))
|
||||
ns *= 2;
|
||||
ctx->next_block_size = ns;
|
||||
if (ctx_gc (ctx, 1, size) < 0) {
|
||||
JS_ThrowOutOfMemory (ctx);
|
||||
return NULL;
|
||||
}
|
||||
ctx->next_block_size = ctx->current_block_size;
|
||||
if ((uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end) {
|
||||
JS_ThrowOutOfMemory (ctx);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
/* Check if we have space in current block */
|
||||
@@ -1311,9 +1331,6 @@ static inline int ptr_in_range (void *p, uint8_t *b, uint8_t *e) {
|
||||
return q >= b && q < e;
|
||||
}
|
||||
|
||||
static const char *gc_dbg_phase = "?";
|
||||
static void *gc_dbg_parent = NULL;
|
||||
|
||||
JSValue gc_copy_value (JSContext *ctx, JSValue v, uint8_t *from_base, uint8_t *from_end, uint8_t *to_base, uint8_t **to_free, uint8_t *to_end) {
|
||||
if (!JS_IsPtr (v)) return v;
|
||||
|
||||
@@ -1337,8 +1354,8 @@ JSValue gc_copy_value (JSContext *ctx, JSValue v, uint8_t *from_base, uint8_t *f
|
||||
}
|
||||
|
||||
if (type != OBJ_ARRAY && type != OBJ_TEXT && type != OBJ_RECORD && type != OBJ_FUNCTION && type != OBJ_FRAME) {
|
||||
fprintf (stderr, "gc_copy_value: invalid type %d at %p (hdr=0x%llx) phase=%s parent=%p\n",
|
||||
type, ptr, (unsigned long long)hdr, gc_dbg_phase, gc_dbg_parent);
|
||||
fprintf (stderr, "gc_copy_value: invalid type %d at %p (hdr=0x%llx)\n",
|
||||
type, ptr, (unsigned long long)hdr);
|
||||
fflush (stderr);
|
||||
abort ();
|
||||
}
|
||||
@@ -1359,6 +1376,18 @@ JSValue gc_copy_value (JSContext *ctx, JSValue v, uint8_t *from_base, uint8_t *f
|
||||
}
|
||||
}
|
||||
|
||||
/* Recursively scan a code tree's cpools to arbitrary nesting depth */
|
||||
static void gc_scan_code_tree (JSContext *ctx, JSCodeRegister *code,
|
||||
uint8_t *from_base, uint8_t *from_end,
|
||||
uint8_t *to_base, uint8_t **to_free, uint8_t *to_end) {
|
||||
for (uint32_t i = 0; i < code->cpool_count; i++)
|
||||
code->cpool[i] = gc_copy_value (ctx, code->cpool[i], from_base, from_end, to_base, to_free, to_end);
|
||||
code->name = gc_copy_value (ctx, code->name, from_base, from_end, to_base, to_free, to_end);
|
||||
for (uint32_t i = 0; i < code->func_count; i++)
|
||||
if (code->functions[i])
|
||||
gc_scan_code_tree (ctx, code->functions[i], from_base, from_end, to_base, to_free, to_end);
|
||||
}
|
||||
|
||||
/* Scan a copied object and update its internal references */
|
||||
void gc_scan_object (JSContext *ctx, void *ptr, uint8_t *from_base, uint8_t *from_end,
|
||||
uint8_t *to_base, uint8_t **to_free, uint8_t *to_end) {
|
||||
@@ -1382,8 +1411,9 @@ void gc_scan_object (JSContext *ctx, void *ptr, uint8_t *from_base, uint8_t *fro
|
||||
#endif
|
||||
/* Copy prototype */
|
||||
rec->proto = gc_copy_value (ctx, rec->proto, from_base, from_end, to_base, to_free, to_end);
|
||||
/* Copy table entries */
|
||||
for (uint32_t i = 0; i <= mask; i++) {
|
||||
/* Copy table entries — skip slot[0] which stores packed metadata
|
||||
(class_id | rec_id << 32), not JSValues */
|
||||
for (uint32_t i = 1; i <= mask; i++) {
|
||||
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, from_base, from_end, to_base, to_free, to_end);
|
||||
@@ -1397,26 +1427,11 @@ void gc_scan_object (JSContext *ctx, void *ptr, uint8_t *from_base, uint8_t *fro
|
||||
/* Scan the function name */
|
||||
fn->name = gc_copy_value (ctx, fn->name, from_base, from_end, to_base, to_free, to_end);
|
||||
if (fn->kind == JS_FUNC_KIND_REGISTER && fn->u.reg.code) {
|
||||
/* Register VM function - scan cpool (off-heap but contains JSValues) */
|
||||
JSCodeRegister *code = fn->u.reg.code;
|
||||
for (uint32_t i = 0; i < code->cpool_count; i++) {
|
||||
code->cpool[i] = gc_copy_value (ctx, code->cpool[i], from_base, from_end, to_base, to_free, to_end);
|
||||
}
|
||||
/* Scan function name */
|
||||
code->name = gc_copy_value (ctx, code->name, from_base, from_end, to_base, to_free, to_end);
|
||||
/* Scan code tree to arbitrary nesting depth */
|
||||
gc_scan_code_tree (ctx, fn->u.reg.code, from_base, from_end, to_base, to_free, to_end);
|
||||
/* Scan outer_frame and env_record */
|
||||
fn->u.reg.outer_frame = gc_copy_value (ctx, fn->u.reg.outer_frame, from_base, from_end, to_base, to_free, to_end);
|
||||
fn->u.reg.env_record = gc_copy_value (ctx, fn->u.reg.env_record, from_base, from_end, to_base, to_free, to_end);
|
||||
/* Recursively scan nested function cpools */
|
||||
for (uint32_t i = 0; i < code->func_count; i++) {
|
||||
if (code->functions[i]) {
|
||||
JSCodeRegister *nested = code->functions[i];
|
||||
for (uint32_t j = 0; j < nested->cpool_count; j++) {
|
||||
nested->cpool[j] = gc_copy_value (ctx, nested->cpool[j], from_base, from_end, to_base, to_free, to_end);
|
||||
}
|
||||
nested->name = gc_copy_value (ctx, nested->name, from_base, from_end, to_base, to_free, to_end);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1477,6 +1492,10 @@ void gc_scan_object (JSContext *ctx, void *ptr, uint8_t *from_base, uint8_t *fro
|
||||
allow_grow: if true, grow heap when recovery is poor
|
||||
alloc_size: the allocation that triggered GC — used to size the new block */
|
||||
int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) {
|
||||
#ifdef DUMP_GC_TIMING
|
||||
struct timespec gc_t0, gc_t1;
|
||||
clock_gettime(CLOCK_MONOTONIC, &gc_t0);
|
||||
#endif
|
||||
JSRuntime *rt = ctx->rt;
|
||||
size_t old_used = ctx->heap_free - ctx->heap_base;
|
||||
size_t old_heap_size = ctx->current_block_size;
|
||||
@@ -1516,9 +1535,6 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) {
|
||||
uint8_t *to_free = new_block;
|
||||
uint8_t *to_end = new_block + new_size;
|
||||
|
||||
gc_dbg_phase = "roots";
|
||||
gc_dbg_parent = NULL;
|
||||
|
||||
#ifdef VALIDATE_GC
|
||||
/* Pre-GC: walk live frame chain and check for bad slot values */
|
||||
if (JS_IsPtr (ctx->reg_current_frame)) {
|
||||
@@ -1603,6 +1619,9 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) {
|
||||
#endif
|
||||
ctx->current_exception = gc_copy_value (ctx, ctx->current_exception, from_base, from_end, to_base, &to_free, to_end);
|
||||
|
||||
/* Copy actor identity key */
|
||||
ctx->actor_sym = gc_copy_value (ctx, ctx->actor_sym, from_base, from_end, to_base, &to_free, to_end);
|
||||
|
||||
/* Copy class prototypes */
|
||||
#ifdef DUMP_GC_DETAIL
|
||||
printf(" roots: class_proto (count=%d)\n", ctx->class_count); fflush(stdout);
|
||||
@@ -1649,7 +1668,6 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) {
|
||||
}
|
||||
|
||||
/* Cheney scan: scan copied objects to find more references */
|
||||
gc_dbg_phase = "scan";
|
||||
uint8_t *scan = to_base;
|
||||
#ifdef DUMP_GC_DETAIL
|
||||
printf(" scan: to_base=%p to_free=%p to_end=%p\n", (void*)to_base, (void*)to_free, (void*)to_end);
|
||||
@@ -1666,7 +1684,6 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) {
|
||||
printf(" size=%zu\n", obj_size);
|
||||
fflush(stdout);
|
||||
#endif
|
||||
gc_dbg_parent = scan;
|
||||
gc_scan_object (ctx, scan, from_base, from_end, to_base, &to_free, to_end);
|
||||
scan += obj_size;
|
||||
}
|
||||
@@ -1682,6 +1699,17 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) {
|
||||
ctx->gc_bytes_copied += new_used;
|
||||
size_t recovered = old_used > new_used ? old_used - new_used : 0;
|
||||
|
||||
#ifdef DUMP_GC_TIMING
|
||||
clock_gettime(CLOCK_MONOTONIC, &gc_t1);
|
||||
double gc_ms = (gc_t1.tv_sec - gc_t0.tv_sec) * 1000.0 +
|
||||
(gc_t1.tv_nsec - gc_t0.tv_nsec) / 1e6;
|
||||
fprintf(stderr, "GC #%u: %.2f ms | copied %zu KB | old %zu KB -> new %zu KB | recovered %zu KB (%.0f%%)\n",
|
||||
ctx->gc_count, gc_ms,
|
||||
new_used / 1024, old_used / 1024, new_size / 1024,
|
||||
recovered / 1024,
|
||||
old_used > 0 ? (100.0 * recovered / old_used) : 0.0);
|
||||
#endif
|
||||
|
||||
ctx->heap_base = to_base;
|
||||
ctx->heap_free = to_free;
|
||||
ctx->heap_end = to_end;
|
||||
@@ -1700,11 +1728,18 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) {
|
||||
#endif
|
||||
|
||||
/* If <40% recovered, grow next block size for future allocations.
|
||||
First poor recovery: double. Consecutive poor: quadruple. */
|
||||
First poor recovery: double. Consecutive poor: quadruple.
|
||||
Skip under FORCE_GC_AT_MALLOC — forced GC on every allocation creates
|
||||
artificially poor recovery (no time to accumulate garbage), which
|
||||
would cause runaway exponential heap growth. */
|
||||
#ifdef DUMP_GC
|
||||
int will_grow = 0;
|
||||
#endif
|
||||
#ifdef FORCE_GC_AT_MALLOC
|
||||
if (0) {
|
||||
#else
|
||||
if (allow_grow && recovered > 0 && old_used > 0 && recovered < old_used * 2 / 5) {
|
||||
#endif
|
||||
size_t factor = ctx->gc_poor_streak >= 1 ? 4 : 2;
|
||||
size_t grown = new_size * factor;
|
||||
if (grown <= buddy_max_block(&ctx->rt->buddy)) {
|
||||
@@ -1860,6 +1895,7 @@ JSContext *JS_NewContextRawWithHeapSize (JSRuntime *rt, size_t heap_size) {
|
||||
|
||||
/* Initialize per-context execution state (moved from JSRuntime) */
|
||||
ctx->current_exception = JS_NULL;
|
||||
ctx->actor_sym = JS_NULL;
|
||||
|
||||
/* Initialize constant text pool (avoids overflow pages for common case) */
|
||||
{
|
||||
@@ -1941,6 +1977,14 @@ void JS_SetContextOpaque (JSContext *ctx, void *opaque) {
|
||||
ctx->user_opaque = opaque;
|
||||
}
|
||||
|
||||
void JS_SetActorSym (JSContext *ctx, JSValue sym) {
|
||||
ctx->actor_sym = sym;
|
||||
}
|
||||
|
||||
JSValue JS_GetActorSym (JSContext *ctx) {
|
||||
return ctx->actor_sym;
|
||||
}
|
||||
|
||||
void JS_SetClassProto (JSContext *ctx, JSClassID class_id, JSValue obj) {
|
||||
assert (class_id < ctx->class_count);
|
||||
set_value (ctx, &ctx->class_proto[class_id], obj);
|
||||
@@ -2299,7 +2343,17 @@ JSText *pretext_concat_value (JSContext *ctx, JSText *s, JSValue v) {
|
||||
JSText *p = JS_VALUE_GET_STRING (v);
|
||||
return pretext_concat (ctx, s, p, 0, (uint32_t)JSText_len (p));
|
||||
}
|
||||
/* Slow path: v needs conversion — root s across JS_ToString which can
|
||||
allocate and trigger GC */
|
||||
JSGCRef s_ref;
|
||||
JS_PushGCRef (ctx, &s_ref);
|
||||
s_ref.val = JS_MKPTR (s);
|
||||
|
||||
JSValue v1 = JS_ToString (ctx, v);
|
||||
|
||||
s = (JSText *)chase (s_ref.val); /* re-fetch after possible GC */
|
||||
JS_PopGCRef (ctx, &s_ref);
|
||||
|
||||
if (JS_IsException (v1)) return NULL;
|
||||
|
||||
if (MIST_IsImmediateASCII (v1)) {
|
||||
@@ -2407,28 +2461,37 @@ JSValue JS_NewStringLen (JSContext *ctx, const char *buf, size_t buf_len) {
|
||||
JSValue JS_ConcatString3 (JSContext *ctx, const char *str1, JSValue str2, const char *str3) {
|
||||
JSText *b;
|
||||
int len1, len3, str2_len;
|
||||
JSGCRef str2_ref;
|
||||
|
||||
if (!JS_IsText (str2)) {
|
||||
str2 = JS_ToString (ctx, str2);
|
||||
if (JS_IsException (str2)) goto fail;
|
||||
}
|
||||
|
||||
str2_len = js_string_value_len (str2);
|
||||
/* Root str2 — pretext_init/pretext_write8/pretext_concat_value allocate
|
||||
and can trigger GC, which would move the heap string str2 points to */
|
||||
JS_PushGCRef (ctx, &str2_ref);
|
||||
str2_ref.val = str2;
|
||||
|
||||
str2_len = js_string_value_len (str2_ref.val);
|
||||
len1 = strlen (str1);
|
||||
len3 = strlen (str3);
|
||||
|
||||
b = pretext_init (ctx, len1 + str2_len + len3);
|
||||
if (!b) goto fail;
|
||||
if (!b) goto fail_pop;
|
||||
|
||||
b = pretext_write8 (ctx, b, (const uint8_t *)str1, len1);
|
||||
if (!b) goto fail;
|
||||
b = pretext_concat_value (ctx, b, str2);
|
||||
if (!b) goto fail;
|
||||
if (!b) goto fail_pop;
|
||||
b = pretext_concat_value (ctx, b, str2_ref.val);
|
||||
if (!b) goto fail_pop;
|
||||
b = pretext_write8 (ctx, b, (const uint8_t *)str3, len3);
|
||||
if (!b) goto fail;
|
||||
if (!b) goto fail_pop;
|
||||
|
||||
JS_PopGCRef (ctx, &str2_ref);
|
||||
return pretext_end (ctx, b);
|
||||
|
||||
fail_pop:
|
||||
JS_PopGCRef (ctx, &str2_ref);
|
||||
fail:
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
@@ -2555,13 +2618,25 @@ int js_string_compare_value_nocase (JSContext *ctx, JSValue op1, JSValue op2) {
|
||||
|
||||
JSValue JS_ConcatString (JSContext *ctx, JSValue op1, JSValue op2) {
|
||||
if (unlikely (!JS_IsText (op1))) {
|
||||
/* Root op2 across JS_ToString which can trigger GC */
|
||||
JSGCRef op2_guard;
|
||||
JS_PushGCRef (ctx, &op2_guard);
|
||||
op2_guard.val = op2;
|
||||
op1 = JS_ToString (ctx, op1);
|
||||
op2 = op2_guard.val;
|
||||
JS_PopGCRef (ctx, &op2_guard);
|
||||
if (JS_IsException (op1)) {
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
}
|
||||
if (unlikely (!JS_IsText (op2))) {
|
||||
/* Root op1 across JS_ToString which can trigger GC */
|
||||
JSGCRef op1_guard;
|
||||
JS_PushGCRef (ctx, &op1_guard);
|
||||
op1_guard.val = op1;
|
||||
op2 = JS_ToString (ctx, op2);
|
||||
op1 = op1_guard.val;
|
||||
JS_PopGCRef (ctx, &op1_guard);
|
||||
if (JS_IsException (op2)) {
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
@@ -3095,7 +3170,6 @@ JSValue JS_GetOwnPropertyNames (JSContext *ctx, JSValue obj) {
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
|
||||
/* Reading slots is GC-safe - no allocation */
|
||||
JSRecord *rec = JS_VALUE_GET_OBJ (obj);
|
||||
mask = (uint32_t)objhdr_cap56 (rec->mist_hdr);
|
||||
|
||||
@@ -3107,22 +3181,31 @@ JSValue JS_GetOwnPropertyNames (JSContext *ctx, JSValue obj) {
|
||||
|
||||
if (count == 0) return JS_NewArrayLen (ctx, 0);
|
||||
|
||||
/* Collect keys into stack buffer (JSValues are just uint64_t) */
|
||||
JSValue *keys = alloca (count * sizeof (JSValue));
|
||||
/* Root obj — JS_NewArrayLen allocates and can trigger GC, which
|
||||
moves the record. Re-read keys from the moved record after. */
|
||||
JSGCRef obj_ref;
|
||||
JS_PushGCRef (ctx, &obj_ref);
|
||||
obj_ref.val = obj;
|
||||
|
||||
JSValue arr = JS_NewArrayLen (ctx, count);
|
||||
if (JS_IsException (arr)) {
|
||||
JS_PopGCRef (ctx, &obj_ref);
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
|
||||
/* Re-read record pointer after possible GC */
|
||||
rec = JS_VALUE_GET_OBJ (obj_ref.val);
|
||||
mask = (uint32_t)objhdr_cap56 (rec->mist_hdr);
|
||||
|
||||
uint32_t idx = 0;
|
||||
for (i = 1; i <= mask; i++) {
|
||||
JSValue k = rec->slots[i].key;
|
||||
if (JS_IsText (k)) keys[idx++] = k;
|
||||
}
|
||||
|
||||
/* Now allocate and fill - GC point, but keys are on stack */
|
||||
JSValue arr = JS_NewArrayLen (ctx, count);
|
||||
if (JS_IsException (arr)) return JS_EXCEPTION;
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
JS_SetPropertyNumber (ctx, arr, i, keys[i]);
|
||||
if (JS_IsText (k)) {
|
||||
JS_SetPropertyNumber (ctx, arr, idx++, k);
|
||||
}
|
||||
}
|
||||
|
||||
JS_PopGCRef (ctx, &obj_ref);
|
||||
return arr;
|
||||
}
|
||||
|
||||
@@ -3415,7 +3498,7 @@ int JS_SetPropertyKey (JSContext *ctx, JSValue this_obj, JSValue key, JSValue va
|
||||
JS_ThrowTypeError (ctx, "cannot modify frozen object");
|
||||
return -1;
|
||||
}
|
||||
return rec_set_own (ctx, rec, key, val);
|
||||
return rec_set_own (ctx, &this_obj, key, val);
|
||||
}
|
||||
|
||||
/* For string keys, use text directly as key */
|
||||
@@ -4111,17 +4194,22 @@ static JSValue JS_ToStringCheckObject (JSContext *ctx, JSValue val) {
|
||||
}
|
||||
|
||||
static JSValue JS_ToQuotedString (JSContext *ctx, JSValue val1) {
|
||||
JSValue val;
|
||||
int i, len;
|
||||
uint32_t c;
|
||||
JSText *b;
|
||||
char buf[16];
|
||||
JSGCRef val_ref;
|
||||
|
||||
val = JS_ToStringCheckObject (ctx, val1);
|
||||
JSValue val = JS_ToStringCheckObject (ctx, val1);
|
||||
if (JS_IsException (val)) return val;
|
||||
|
||||
/* Root val — pretext_init/pretext_putc allocate and can trigger GC,
|
||||
which would move the heap string val points to */
|
||||
JS_PushGCRef (ctx, &val_ref);
|
||||
val_ref.val = val;
|
||||
|
||||
/* Use js_string_value_len to handle both immediate and heap strings */
|
||||
len = js_string_value_len (val);
|
||||
len = js_string_value_len (val_ref.val);
|
||||
|
||||
b = pretext_init (ctx, len + 2);
|
||||
if (!b) goto fail;
|
||||
@@ -4129,7 +4217,7 @@ static JSValue JS_ToQuotedString (JSContext *ctx, JSValue val1) {
|
||||
b = pretext_putc (ctx, b, '\"');
|
||||
if (!b) goto fail;
|
||||
for (i = 0; i < len; i++) {
|
||||
c = js_string_value_get (val, i);
|
||||
c = js_string_value_get (val_ref.val, i);
|
||||
switch (c) {
|
||||
case '\t':
|
||||
c = 't';
|
||||
@@ -4168,8 +4256,10 @@ static JSValue JS_ToQuotedString (JSContext *ctx, JSValue val1) {
|
||||
}
|
||||
b = pretext_putc (ctx, b, '\"');
|
||||
if (!b) goto fail;
|
||||
JS_PopGCRef (ctx, &val_ref);
|
||||
return pretext_end (ctx, b);
|
||||
fail:
|
||||
JS_PopGCRef (ctx, &val_ref);
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
|
||||
@@ -5152,16 +5242,31 @@ JSValue js_regexp_toString (JSContext *ctx, JSValue this_val, int argc, JSValue
|
||||
JSText *b = pretext_init (ctx, 0);
|
||||
if (!b) return JS_EXCEPTION;
|
||||
|
||||
/* Root b across allocating calls (JS_GetProperty can trigger GC) */
|
||||
JSGCRef b_ref;
|
||||
JS_PushGCRef (ctx, &b_ref);
|
||||
b_ref.val = JS_MKPTR (b);
|
||||
|
||||
b = pretext_putc (ctx, b, '/');
|
||||
if (!b) return JS_EXCEPTION;
|
||||
if (!b) { JS_PopGCRef (ctx, &b_ref); return JS_EXCEPTION; }
|
||||
b_ref.val = JS_MKPTR (b);
|
||||
|
||||
pattern = JS_GetProperty (ctx, this_val, JS_KEY_source);
|
||||
b = (JSText *)chase (b_ref.val);
|
||||
b = pretext_concat_value (ctx, b, pattern);
|
||||
if (!b) return JS_EXCEPTION;
|
||||
if (!b) { JS_PopGCRef (ctx, &b_ref); return JS_EXCEPTION; }
|
||||
b_ref.val = JS_MKPTR (b);
|
||||
|
||||
b = pretext_putc (ctx, b, '/');
|
||||
if (!b) return JS_EXCEPTION;
|
||||
if (!b) { JS_PopGCRef (ctx, &b_ref); return JS_EXCEPTION; }
|
||||
b_ref.val = JS_MKPTR (b);
|
||||
|
||||
flags = JS_GetProperty (ctx, this_val, JS_KEY_flags);
|
||||
b = (JSText *)chase (b_ref.val);
|
||||
b = pretext_concat_value (ctx, b, flags);
|
||||
if (!b) return JS_EXCEPTION;
|
||||
if (!b) { JS_PopGCRef (ctx, &b_ref); return JS_EXCEPTION; }
|
||||
|
||||
JS_PopGCRef (ctx, &b_ref);
|
||||
return pretext_end (ctx, b);
|
||||
}
|
||||
|
||||
@@ -5484,7 +5589,10 @@ static JSValue cjson_to_jsvalue (JSContext *ctx, const cJSON *item) {
|
||||
arr_ref.val = JS_NewArrayLen (ctx, n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON *child = cJSON_GetArrayItem (item, i);
|
||||
JS_SetPropertyNumber (ctx, arr_ref.val, i, cjson_to_jsvalue (ctx, child));
|
||||
/* Evaluate recursive call before reading arr_ref.val — the call
|
||||
allocates and can trigger GC which moves the array */
|
||||
JSValue elem = cjson_to_jsvalue (ctx, child);
|
||||
JS_SetPropertyNumber (ctx, arr_ref.val, i, elem);
|
||||
}
|
||||
JSValue result = arr_ref.val;
|
||||
JS_DeleteGCRef (ctx, &arr_ref);
|
||||
@@ -5495,7 +5603,8 @@ static JSValue cjson_to_jsvalue (JSContext *ctx, const cJSON *item) {
|
||||
JS_AddGCRef (ctx, &obj_ref);
|
||||
obj_ref.val = JS_NewObject (ctx);
|
||||
for (cJSON *child = item->child; child; child = child->next) {
|
||||
JS_SetPropertyStr (ctx, obj_ref.val, child->string, cjson_to_jsvalue (ctx, child));
|
||||
JSValue val = cjson_to_jsvalue (ctx, child);
|
||||
JS_SetPropertyStr (ctx, obj_ref.val, child->string, val);
|
||||
}
|
||||
JSValue result = obj_ref.val;
|
||||
JS_DeleteGCRef (ctx, &obj_ref);
|
||||
@@ -5531,6 +5640,8 @@ typedef struct JSONStringifyContext {
|
||||
JSValue gap;
|
||||
JSValue empty;
|
||||
JSGCRef b_root; /* GC root for buffer - use JSC_B_GET/SET macros */
|
||||
BOOL compact_arrays;
|
||||
BOOL in_compact_array;
|
||||
} JSONStringifyContext;
|
||||
|
||||
/* Macros to access the buffer from the rooted JSValue */
|
||||
@@ -5635,7 +5746,7 @@ static int js_json_to_str (JSContext *ctx, JSONStringifyContext *jsc, JSValue ho
|
||||
}
|
||||
indent1_ref.val = JS_ConcatString (ctx, indent_ref.val, jsc->gap);
|
||||
if (JS_IsException (indent1_ref.val)) goto exception;
|
||||
if (!JS_IsEmptyString (jsc->gap)) {
|
||||
if (!JS_IsEmptyString (jsc->gap) && !jsc->in_compact_array) {
|
||||
sep_ref.val = JS_ConcatString3 (ctx, "\n", indent1_ref.val, "");
|
||||
if (JS_IsException (sep_ref.val)) goto exception;
|
||||
sep1_ref.val = js_new_string8 (ctx, " ");
|
||||
@@ -5650,12 +5761,32 @@ static int js_json_to_str (JSContext *ctx, JSONStringifyContext *jsc, JSValue ho
|
||||
if (ret < 0) goto exception;
|
||||
if (ret) {
|
||||
if (js_get_length64 (ctx, &len, val_ref.val)) goto exception;
|
||||
/* Check if this is a leaf array for compact mode.
|
||||
Leaf = every element is a primitive or string (no arrays or objects). */
|
||||
BOOL was_compact = jsc->in_compact_array;
|
||||
if (jsc->compact_arrays && !jsc->in_compact_array && !JS_IsEmptyString (jsc->gap)) {
|
||||
BOOL is_leaf = TRUE;
|
||||
for (i = 0; i < len && is_leaf; i++) {
|
||||
v = JS_GetPropertyNumber (ctx, val_ref.val, i);
|
||||
if (JS_IsException (v)) goto exception;
|
||||
if (JS_IsArray (v) > 0) {
|
||||
is_leaf = FALSE;
|
||||
} else if (mist_is_gc_object (v) && !JS_IsText (v)) {
|
||||
is_leaf = FALSE;
|
||||
}
|
||||
}
|
||||
if (is_leaf) jsc->in_compact_array = TRUE;
|
||||
}
|
||||
JSC_B_PUTC (jsc, '[');
|
||||
for (i = 0; i < len; i++) {
|
||||
if (i > 0) {
|
||||
JSC_B_PUTC (jsc, ',');
|
||||
}
|
||||
JSC_B_CONCAT (jsc, sep_ref.val);
|
||||
if (jsc->in_compact_array && !was_compact) {
|
||||
if (i > 0) JSC_B_PUTC (jsc, ' ');
|
||||
} else {
|
||||
JSC_B_CONCAT (jsc, sep_ref.val);
|
||||
}
|
||||
v = JS_GetPropertyNumber (ctx, val_ref.val, i);
|
||||
if (JS_IsException (v)) goto exception;
|
||||
v_ref.val = v; /* root v — JS_ToString below can trigger GC */
|
||||
@@ -5668,11 +5799,12 @@ static int js_json_to_str (JSContext *ctx, JSONStringifyContext *jsc, JSValue ho
|
||||
if (JS_IsNull (v)) v = JS_NULL;
|
||||
if (js_json_to_str (ctx, jsc, val_ref.val, v, indent1_ref.val)) goto exception;
|
||||
}
|
||||
if (len > 0 && !JS_IsEmptyString (jsc->gap)) {
|
||||
if (len > 0 && !JS_IsEmptyString (jsc->gap) && !jsc->in_compact_array) {
|
||||
JSC_B_PUTC (jsc, '\n');
|
||||
JSC_B_CONCAT (jsc, indent_ref.val);
|
||||
}
|
||||
JSC_B_PUTC (jsc, ']');
|
||||
jsc->in_compact_array = was_compact;
|
||||
} else {
|
||||
if (!JS_IsNull (jsc->property_list))
|
||||
tab_ref.val = jsc->property_list;
|
||||
@@ -5706,7 +5838,7 @@ static int js_json_to_str (JSContext *ctx, JSONStringifyContext *jsc, JSValue ho
|
||||
has_content = TRUE;
|
||||
}
|
||||
}
|
||||
if (has_content && !JS_IsEmptyString (jsc->gap)) {
|
||||
if (has_content && !JS_IsEmptyString (jsc->gap) && !jsc->in_compact_array) {
|
||||
JSC_B_PUTC (jsc, '\n');
|
||||
JSC_B_CONCAT (jsc, indent_ref.val);
|
||||
}
|
||||
@@ -5770,12 +5902,13 @@ exception:
|
||||
return -1;
|
||||
}
|
||||
|
||||
JSValue JS_JSONStringify (JSContext *ctx, JSValue obj, JSValue replacer, JSValue space0) {
|
||||
JSValue JS_JSONStringify (JSContext *ctx, JSValue obj, JSValue replacer, JSValue space0, BOOL compact_arrays) {
|
||||
JSONStringifyContext jsc_s, *jsc = &jsc_s;
|
||||
JSValue val, v, space, ret, wrapper;
|
||||
int res;
|
||||
int64_t i, j, n;
|
||||
JSGCRef obj_ref;
|
||||
JSLocalRef *saved_local_frame = JS_GetLocalFrame (ctx);
|
||||
|
||||
/* Root obj since GC can happen during stringify setup */
|
||||
JS_PushGCRef (ctx, &obj_ref);
|
||||
@@ -5787,9 +5920,23 @@ JSValue JS_JSONStringify (JSContext *ctx, JSValue obj, JSValue replacer, JSValue
|
||||
jsc->property_list = JS_NULL;
|
||||
jsc->gap = JS_NULL;
|
||||
jsc->empty = JS_KEY_empty;
|
||||
jsc->compact_arrays = compact_arrays;
|
||||
jsc->in_compact_array = FALSE;
|
||||
ret = JS_NULL;
|
||||
wrapper = JS_NULL;
|
||||
|
||||
/* Root all jsc fields that hold heap objects — GC can fire during
|
||||
stringify and would move these objects without updating the struct */
|
||||
JSLocalRef lr_stack, lr_plist, lr_gap, lr_replacer;
|
||||
lr_stack.ptr = &jsc->stack;
|
||||
JS_PushLocalRef (ctx, &lr_stack);
|
||||
lr_plist.ptr = &jsc->property_list;
|
||||
JS_PushLocalRef (ctx, &lr_plist);
|
||||
lr_gap.ptr = &jsc->gap;
|
||||
JS_PushLocalRef (ctx, &lr_gap);
|
||||
lr_replacer.ptr = &jsc->replacer_func;
|
||||
JS_PushLocalRef (ctx, &lr_replacer);
|
||||
|
||||
/* Root the buffer for GC safety */
|
||||
JS_PushGCRef (ctx, &jsc->b_root);
|
||||
{
|
||||
@@ -5869,6 +6016,8 @@ exception:
|
||||
done1:
|
||||
done:
|
||||
JS_PopGCRef (ctx, &jsc->b_root);
|
||||
/* Restore local refs pushed for jsc fields */
|
||||
ctx->top_local_ref = saved_local_frame;
|
||||
JS_PopGCRef (ctx, &obj_ref);
|
||||
return ret;
|
||||
}
|
||||
@@ -7020,9 +7169,26 @@ JSValue js_cell_text_codepoint (JSContext *ctx, JSValue this_val, int argc, JSVa
|
||||
* file. */
|
||||
|
||||
static JSText *pt_concat_value_to_string_free (JSContext *ctx, JSText *b, JSValue v) {
|
||||
/* Root b across JS_ToString which can allocate and trigger GC */
|
||||
JSGCRef b_ref, s_ref;
|
||||
JS_PushGCRef (ctx, &b_ref);
|
||||
b_ref.val = JS_MKPTR (b);
|
||||
|
||||
JSValue s = JS_ToString (ctx, v);
|
||||
if (JS_IsException (s)) return NULL;
|
||||
b = pretext_concat_value (ctx, b, s);
|
||||
if (JS_IsException (s)) {
|
||||
JS_PopGCRef (ctx, &b_ref);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Root s — pretext_concat_value can trigger GC and move the heap string */
|
||||
JS_PushGCRef (ctx, &s_ref);
|
||||
s_ref.val = s;
|
||||
|
||||
b = (JSText *)chase (b_ref.val); /* re-fetch after possible GC */
|
||||
b = pretext_concat_value (ctx, b, s_ref.val);
|
||||
|
||||
JS_PopGCRef (ctx, &s_ref);
|
||||
JS_PopGCRef (ctx, &b_ref);
|
||||
return b;
|
||||
}
|
||||
|
||||
@@ -8754,8 +8920,7 @@ static JSValue js_cell_array_sort (JSContext *ctx, JSValue this_val, int argc, J
|
||||
arr = JS_VALUE_GET_ARRAY (arr_ref.val);
|
||||
items[i] = arr->values[i];
|
||||
}
|
||||
} else if (JS_VALUE_GET_TAG (argv[1]) == JS_TAG_STRING
|
||||
|| JS_VALUE_GET_TAG (argv[1]) == JS_TAG_STRING_IMM) {
|
||||
} else if (JS_VALUE_IS_TEXT (argv[1])) {
|
||||
JSValue prop_key = js_key_from_string (ctx, argv[1]);
|
||||
/* Re-read items[i] (js_key_from_string no longer allocates, but re-read is harmless) */
|
||||
arr = JS_VALUE_GET_ARRAY (arr_ref.val);
|
||||
@@ -8787,7 +8952,7 @@ static JSValue js_cell_array_sort (JSContext *ctx, JSValue this_val, int argc, J
|
||||
if (key_tag == JS_TAG_INT || key_tag == JS_TAG_FLOAT64 || key_tag == JS_TAG_SHORT_FLOAT) {
|
||||
JS_ToFloat64 (ctx, &keys[i], key);
|
||||
if (i == 0) is_string = 0;
|
||||
} else if (key_tag == JS_TAG_STRING || key_tag == JS_TAG_STRING_IMM) {
|
||||
} else if (JS_VALUE_IS_TEXT (key)) {
|
||||
if (i == 0) {
|
||||
is_string = 1;
|
||||
str_keys = alloca (sizeof (char *) * len);
|
||||
@@ -11032,9 +11197,14 @@ static JSValue js_cell_json_encode (JSContext *ctx, JSValue this_val, int argc,
|
||||
if (argc < 1)
|
||||
return JS_ThrowTypeError (ctx, "json.encode requires at least 1 argument");
|
||||
|
||||
JSValue replacer = argc > 1 ? argv[1] : JS_NULL;
|
||||
JSValue space = argc > 2 ? argv[2] : JS_NewInt32 (ctx, 1);
|
||||
JSValue result = JS_JSONStringify (ctx, argv[0], replacer, space);
|
||||
BOOL pretty = argc <= 1 || JS_ToBool (ctx, argv[1]);
|
||||
JSValue space = pretty ? JS_NewInt32 (ctx, 2) : JS_NULL;
|
||||
JSValue replacer = JS_NULL;
|
||||
if (argc > 2 && JS_IsFunction (argv[2]))
|
||||
replacer = argv[2];
|
||||
else if (argc > 3 && JS_IsArray (argv[3]))
|
||||
replacer = argv[3];
|
||||
JSValue result = JS_JSONStringify (ctx, argv[0], replacer, space, pretty);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -11190,9 +11360,15 @@ static char *js_do_nota_decode (JSContext *js, JSValue *tmp, char *nota, JSValue
|
||||
case NOTA_SYM:
|
||||
nota = nota_read_sym (&b, nota);
|
||||
if (b == NOTA_PRIVATE) {
|
||||
JSValue inner;
|
||||
nota = js_do_nota_decode (js, &inner, nota, holder, JS_NULL, reviver);
|
||||
JSGCRef inner_ref;
|
||||
JS_PushGCRef (js, &inner_ref);
|
||||
inner_ref.val = JS_NULL;
|
||||
nota = js_do_nota_decode (js, &inner_ref.val, nota, holder, JS_NULL, reviver);
|
||||
JSValue obj = JS_NewObject (js);
|
||||
if (!JS_IsNull (js->actor_sym))
|
||||
JS_SetPropertyKey (js, obj, js->actor_sym, inner_ref.val);
|
||||
JS_CellStone (js, obj);
|
||||
JS_PopGCRef (js, &inner_ref);
|
||||
*tmp = obj;
|
||||
} else {
|
||||
switch (b) {
|
||||
@@ -11286,11 +11462,17 @@ static void nota_encode_value (NotaEncodeContext *enc, JSValueConst val, JSValue
|
||||
}
|
||||
|
||||
JSValue adata = JS_NULL;
|
||||
if (!JS_IsNull (ctx->actor_sym)) {
|
||||
int has = JS_HasPropertyKey (ctx, replaced_ref.val, ctx->actor_sym);
|
||||
if (has > 0) adata = JS_GetPropertyKey (ctx, replaced_ref.val, ctx->actor_sym);
|
||||
}
|
||||
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;
|
||||
@@ -11697,6 +11879,10 @@ static void wota_encode_value (WotaEncodeContext *enc, JSValueConst val, JSValue
|
||||
break;
|
||||
}
|
||||
JSValue adata = JS_NULL;
|
||||
if (!JS_IsNull (ctx->actor_sym)) {
|
||||
int has = JS_HasPropertyKey (ctx, replaced, ctx->actor_sym);
|
||||
if (has > 0) adata = JS_GetPropertyKey (ctx, replaced, ctx->actor_sym);
|
||||
}
|
||||
if (!JS_IsNull (adata)) {
|
||||
wota_write_sym (&enc->wb, WOTA_PRIVATE);
|
||||
wota_encode_value (enc, adata, replaced, JS_NULL);
|
||||
@@ -11753,9 +11939,15 @@ static char *decode_wota_value (JSContext *ctx, char *data_ptr, JSValue *out_val
|
||||
int scode;
|
||||
data_ptr = wota_read_sym (&scode, data_ptr);
|
||||
if (scode == WOTA_PRIVATE) {
|
||||
JSValue inner = JS_NULL;
|
||||
data_ptr = decode_wota_value (ctx, data_ptr, &inner, holder, JS_NULL, reviver);
|
||||
JSGCRef inner_ref;
|
||||
JS_PushGCRef (ctx, &inner_ref);
|
||||
inner_ref.val = JS_NULL;
|
||||
data_ptr = decode_wota_value (ctx, data_ptr, &inner_ref.val, holder, JS_NULL, reviver);
|
||||
JSValue obj = JS_NewObject (ctx);
|
||||
if (!JS_IsNull (ctx->actor_sym))
|
||||
JS_SetPropertyKey (ctx, obj, ctx->actor_sym, inner_ref.val);
|
||||
JS_CellStone (ctx, obj);
|
||||
JS_PopGCRef (ctx, &inner_ref);
|
||||
*out_val = obj;
|
||||
} else if (scode == WOTA_NULL) *out_val = JS_NULL;
|
||||
else if (scode == WOTA_FALSE) *out_val = JS_NewBool (ctx, 0);
|
||||
|
||||
@@ -310,7 +310,6 @@ void actor_free(cell_rt *actor)
|
||||
|
||||
int actor_count = lockless_shlen(actors);
|
||||
if (actor_count == 0) {
|
||||
fprintf(stderr, "all actors are dead\n");
|
||||
pthread_mutex_lock(&engine.lock);
|
||||
engine.shutting_down = 1;
|
||||
pthread_cond_broadcast(&engine.wake_cond);
|
||||
|
||||
@@ -1428,7 +1428,7 @@ TEST(stringify_json_object) {
|
||||
obj_ref.val = JS_NewObject(ctx);
|
||||
JSValue v1 = JS_NewInt32(ctx, 1);
|
||||
JS_SetPropertyStr(ctx, obj_ref.val, "a", v1);
|
||||
JSValue str = JS_JSONStringify(ctx, obj_ref.val, JS_NULL, JS_NULL);
|
||||
JSValue str = JS_JSONStringify(ctx, obj_ref.val, JS_NULL, JS_NULL, 0);
|
||||
JS_PopGCRef(ctx, &obj_ref);
|
||||
ASSERT(JS_IsText(str));
|
||||
const char *s = JS_ToCString(ctx, str);
|
||||
@@ -1444,7 +1444,7 @@ TEST(stringify_json_array) {
|
||||
arr_ref.val = JS_NewArray(ctx);
|
||||
JS_ArrayPush(ctx, &arr_ref.val, JS_NewInt32(ctx, 1));
|
||||
JS_ArrayPush(ctx, &arr_ref.val, JS_NewInt32(ctx, 2));
|
||||
JSValue str = JS_JSONStringify(ctx, arr_ref.val, JS_NULL, JS_NULL);
|
||||
JSValue str = JS_JSONStringify(ctx, arr_ref.val, JS_NULL, JS_NULL, 0);
|
||||
JS_PopGCRef(ctx, &arr_ref);
|
||||
ASSERT(JS_IsText(str));
|
||||
const char *s = JS_ToCString(ctx, str);
|
||||
|
||||
@@ -16,4 +16,4 @@ var ast = parse(result.tokens, src, filename, tokenize)
|
||||
var folded = fold(ast)
|
||||
var compiled = mcode(folded)
|
||||
var optimized = streamline(compiled)
|
||||
print(json.encode(optimized))
|
||||
print(json.encode(optimized, true))
|
||||
|
||||
@@ -185,9 +185,9 @@ var streamline = function(ir, log) {
|
||||
backward_types[slot] = typ
|
||||
} else if (existing != typ && existing != T_UNKNOWN) {
|
||||
if ((existing == T_INT || existing == T_FLOAT) && typ == T_NUM) {
|
||||
// Keep more specific
|
||||
backward_types[slot] = T_NUM
|
||||
} else if (existing == T_NUM && (typ == T_INT || typ == T_FLOAT)) {
|
||||
backward_types[slot] = typ
|
||||
// Keep wider T_NUM
|
||||
} else if ((existing == T_INT && typ == T_FLOAT) || (existing == T_FLOAT && typ == T_INT)) {
|
||||
backward_types[slot] = T_NUM
|
||||
} else {
|
||||
@@ -226,24 +226,15 @@ var streamline = function(ir, log) {
|
||||
// [pos1, type1, pos2, type2] for operand positions to merge.
|
||||
// =========================================================
|
||||
var param_rules = {
|
||||
add: [2, T_NUM, 3, T_NUM],
|
||||
subtract: [2, T_NUM, 3, T_NUM], multiply: [2, T_NUM, 3, T_NUM],
|
||||
divide: [2, T_NUM, 3, T_NUM], modulo: [2, T_NUM, 3, T_NUM],
|
||||
pow: [2, T_NUM, 3, T_NUM], negate: [2, T_NUM],
|
||||
eq_int: [2, T_INT, 3, T_INT], ne_int: [2, T_INT, 3, T_INT],
|
||||
lt_int: [2, T_INT, 3, T_INT], gt_int: [2, T_INT, 3, T_INT],
|
||||
le_int: [2, T_INT, 3, T_INT], ge_int: [2, T_INT, 3, T_INT],
|
||||
bitand: [2, T_INT, 3, T_INT], bitor: [2, T_INT, 3, T_INT],
|
||||
bitxor: [2, T_INT, 3, T_INT], shl: [2, T_INT, 3, T_INT],
|
||||
shr: [2, T_INT, 3, T_INT], ushr: [2, T_INT, 3, T_INT],
|
||||
bitnot: [2, T_INT],
|
||||
eq_float: [2, T_FLOAT, 3, T_FLOAT], ne_float: [2, T_FLOAT, 3, T_FLOAT],
|
||||
lt_float: [2, T_FLOAT, 3, T_FLOAT], gt_float: [2, T_FLOAT, 3, T_FLOAT],
|
||||
le_float: [2, T_FLOAT, 3, T_FLOAT], ge_float: [2, T_FLOAT, 3, T_FLOAT],
|
||||
concat: [2, T_TEXT, 3, T_TEXT],
|
||||
eq_text: [2, T_TEXT, 3, T_TEXT], ne_text: [2, T_TEXT, 3, T_TEXT],
|
||||
lt_text: [2, T_TEXT, 3, T_TEXT], gt_text: [2, T_TEXT, 3, T_TEXT],
|
||||
le_text: [2, T_TEXT, 3, T_TEXT], ge_text: [2, T_TEXT, 3, T_TEXT],
|
||||
eq_bool: [2, T_BOOL, 3, T_BOOL], ne_bool: [2, T_BOOL, 3, T_BOOL],
|
||||
not: [2, T_BOOL], and: [2, T_BOOL, 3, T_BOOL], or: [2, T_BOOL, 3, T_BOOL],
|
||||
store_index: [1, T_ARRAY, 2, T_INT], store_field: [1, T_RECORD],
|
||||
push: [1, T_ARRAY],
|
||||
@@ -310,11 +301,11 @@ var streamline = function(ir, log) {
|
||||
function: [1, T_FUNCTION], length: [1, T_INT],
|
||||
bitnot: [1, T_INT], bitand: [1, T_INT], bitor: [1, T_INT],
|
||||
bitxor: [1, T_INT], shl: [1, T_INT], shr: [1, T_INT], ushr: [1, T_INT],
|
||||
negate: [1, T_UNKNOWN], concat: [1, T_TEXT],
|
||||
negate: [1, T_NUM], concat: [1, T_TEXT],
|
||||
eq: [1, T_BOOL], ne: [1, T_BOOL], lt: [1, T_BOOL],
|
||||
le: [1, T_BOOL], gt: [1, T_BOOL], ge: [1, T_BOOL], in: [1, T_BOOL],
|
||||
add: [1, T_UNKNOWN], subtract: [1, T_UNKNOWN], multiply: [1, T_UNKNOWN],
|
||||
divide: [1, T_UNKNOWN], modulo: [1, T_UNKNOWN], pow: [1, T_UNKNOWN],
|
||||
add: [1, T_NUM], subtract: [1, T_NUM], multiply: [1, T_NUM],
|
||||
divide: [1, T_NUM], modulo: [1, T_NUM], pow: [1, T_NUM],
|
||||
move: [1, T_UNKNOWN], load_field: [1, T_UNKNOWN],
|
||||
load_index: [1, T_UNKNOWN], load_dynamic: [1, T_UNKNOWN],
|
||||
pop: [1, T_UNKNOWN], get: [1, T_UNKNOWN],
|
||||
@@ -509,6 +500,13 @@ var streamline = function(ir, log) {
|
||||
i = i + 2
|
||||
continue
|
||||
}
|
||||
if ((checked_type == T_INT || checked_type == T_FLOAT) && src_known == T_NUM) {
|
||||
// T_NUM could be int or float — not a mismatch, keep check
|
||||
slot_types[dest] = T_BOOL
|
||||
slot_types[src] = checked_type
|
||||
i = i + 2
|
||||
continue
|
||||
}
|
||||
nc = nc + 1
|
||||
instructions[i] = "_nop_tc_" + text(nc)
|
||||
jlen = length(next)
|
||||
@@ -578,6 +576,12 @@ var streamline = function(ir, log) {
|
||||
i = i + 2
|
||||
continue
|
||||
}
|
||||
if ((checked_type == T_INT || checked_type == T_FLOAT) && src_known == T_NUM) {
|
||||
// T_NUM could be int or float — not a mismatch, keep check
|
||||
slot_types[dest] = T_BOOL
|
||||
i = i + 2
|
||||
continue
|
||||
}
|
||||
nc = nc + 1
|
||||
instructions[i] = "_nop_tc_" + text(nc)
|
||||
nc = nc + 1
|
||||
|
||||
4
test.ce
4
test.ce
@@ -418,9 +418,9 @@ function run_tests(package_name, specific_test) {
|
||||
var src_path = prefix + '/' + f
|
||||
var src = text(fd.slurp(src_path))
|
||||
var ast = analyze(src, src_path)
|
||||
test_mod_noopt = run_ast_noopt_fn(mod_path + '_noopt', ast, {
|
||||
test_mod_noopt = run_ast_noopt_fn(mod_path + '_noopt', ast, stone({
|
||||
use: function(path) { return shop.use(path, use_pkg) }
|
||||
})
|
||||
}))
|
||||
} disruption {
|
||||
log.console(` DIFF: failed to load noopt module for ${f}`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user