7 Commits

Author SHA1 Message Date
John Alanbrook
7af8ffe4e0 attempt 2025-12-31 16:01:02 -06:00
John Alanbrook
8abee37622 persistent current frame 2025-12-31 14:28:43 -06:00
John Alanbrook
7c3cce1ce2 works 2025-12-31 12:45:32 -06:00
John Alanbrook
334f3a789b runs 2025-12-31 11:09:18 -06:00
John Alanbrook
e21cd4e70b tail call 2025-12-31 09:19:39 -06:00
John Alanbrook
41eb4bf6f7 bench 2025-12-30 22:56:31 -06:00
John Alanbrook
5f761cc7af wire callinternal to use trampoline 2025-12-30 16:58:06 -06:00
227 changed files with 44276 additions and 47034 deletions

6
.cell/lock.toml Normal file
View File

@@ -0,0 +1,6 @@
[modules]
[modules.extramath]
hash = "MCLZT3JABTAENS4WVXKGWJ7JPBLZER4YQ5VN2PE7ZD2Z4WYGTIMA===="
url = "https://gitea.pockle.world/john/extramath@master"
downloaded = "Monday June 2 12:07:20.42 PM -5 2025 AD"
commit = "84d81a19a8455bcf8dc494739e9e6d545df6ff2c"

View File

@@ -1,20 +1,9 @@
BasedOnStyle: GNU
Language: C
IndentWidth: 2
TabWidth: 2
UseTab: Never
ContinuationIndentWidth: 2
ContinuationIndentWidth: 2 # Indents continuation lines by 2 spaces
AllowShortFunctionsOnASingleLine: true
AllowShortBlocksOnASingleLine: true
AllowShortIfStatementsOnASingleLine: true
BreakBeforeBraces: Attach
ColumnLimit: 0
BreakFunctionDefinitionParameters: false
BinPackParameters: false
BinPackArguments: false
# --- Fix the "static T\nname(...)" style ---
AlwaysBreakAfterDefinitionReturnType: None
BreakAfterReturnType: None

1
.gitignore vendored
View File

@@ -1,6 +1,5 @@
.git/
.obj/
website/
bin/
build/
*.zip

View File

@@ -1,25 +0,0 @@
# Code style
All code is done with 2 spaces for indentation.
For cell script and its integration files, objects are preferred over classes, and preferrably limited use of prototypes, make objects sendable between actors (.ce files).
## cell script format
Cell script files end in .ce or .cm. Cell script is similar to Javascript but with some differences.
Variables are delcared with 'var'. Var behaves like let.
Constants are declared with 'def'.
!= and == are strict, there is no !== or ===.
There is no undefined, only null.
There are no classes, only objects and prototypes.
Prefer backticks for string interpolation. Otherwise, convering non strings with the text() function is required.
Everything should be lowercase.
There are no arraybuffers, only blobs, which work with bits. They must be stoned like stone(blob) before being read from.
## c format
For cell script integration files, everything should be declared static that can be. Most don't have headers at all. Files in a package are not shared between packages.
There is no undefined, so JS_IsNull and JS_NULL should be used only.
## how module loading is done in cell script
Within a package, a c file, if using the correct macros (CELL_USE_FUNCS etc), will be loaded as a module with its name; so png.c inside ac package is loaded as <package>/png, giving you access to its functions.

View File

@@ -9,8 +9,6 @@
CELL_SHOP = $(HOME)/.cell
CELL_CORE_PACKAGE = $(CELL_SHOP)/packages/core
maker: install
makecell:
cell pack core -o cell
cp cell /opt/homebrew/bin/
@@ -58,7 +56,7 @@ static:
# Bootstrap: build cell from scratch using meson (only needed once)
# Also installs core scripts to ~/.cell/core
bootstrap:
meson setup build_bootstrap -Dbuildtype=debug -Db_sanitize=address
meson setup build_bootstrap -Dbuildtype=debugoptimized
meson compile -C build_bootstrap
cp build_bootstrap/cell .
cp build_bootstrap/libcell_runtime.dylib .

99
add.ce
View File

@@ -1,103 +1,28 @@
// cell add <locator> [alias] - Add a dependency to the current package
//
// Usage:
// cell add <locator> Add a dependency using default alias
// cell add <locator> <alias> Add a dependency with custom alias
//
// This adds the dependency to cell.toml and installs it to the shop.
// cell add <locator> [alias] - Add and install a package with its dependencies
var shop = use('internal/shop')
var pkg = use('package')
var build = use('build')
var fd = use('fd')
var locator = null
var alias = null
array(args, function(arg) {
if (arg == '--help' || arg == '-h') {
log.console("Usage: cell add <locator> [alias]")
log.console("")
log.console("Add a dependency to the current package.")
log.console("")
log.console("Examples:")
log.console(" cell add gitea.pockle.world/john/prosperon")
log.console(" cell add gitea.pockle.world/john/cell-image image")
log.console(" cell add ../local-package")
$stop()
} else if (!starts_with(arg, '-')) {
if (!locator) {
locator = arg
} else if (!alias) {
alias = arg
}
}
})
if (!locator) {
if (args.length < 1) {
log.console("Usage: cell add <locator> [alias]")
log.console("Examples:")
log.console(" cell add gitea.pockle.world/john/prosperon@main")
log.console(" cell add github.com/user/repo@v1.0.0 myalias")
$stop()
return
}
var locator = args[0]
// Resolve relative paths to absolute paths
if (locator == '.' || starts_with(locator, './') || starts_with(locator, '../') || fd.is_dir(locator)) {
if (locator == '.' || locator.startsWith('./') || locator.startsWith('../') || fd.is_dir(locator)) {
var resolved = fd.realpath(locator)
if (resolved) {
locator = resolved
}
}
var alias = args.length > 1 ? args[1] : null
// Generate default alias from locator
if (!alias) {
// Use the last component of the locator as alias
var parts = array(locator, '/')
alias = parts[length(parts) - 1]
// Remove any version suffix
if (search(alias, '@') != null) {
alias = array(alias, '@')[0]
}
}
shop.get(locator, alias)
// Check we're in a package directory
var cwd = fd.realpath('.')
if (!fd.is_file(cwd + '/cell.toml')) {
log.error("Not in a package directory (no cell.toml found)")
$stop()
}
log.console("Adding " + locator + " as '" + alias + "'...")
// Add to local project's cell.toml
try {
pkg.add_dependency(null, locator, alias)
log.console(" Added to cell.toml")
} catch (e) {
log.error("Failed to update cell.toml: " + e)
$stop()
}
// Install to shop
try {
shop.get(locator)
shop.extract(locator)
// Build scripts
shop.build_package_scripts(locator)
// Build C code if any
try {
var target = build.detect_host_target()
build.build_dynamic(locator, target, 'release')
} catch (e) {
// Not all packages have C code
}
log.console(" Installed to shop")
} catch (e) {
log.error("Failed to install: " + e)
$stop()
}
log.console("Added " + alias + " (" + locator + ")")
$stop()
$stop()

View File

@@ -8,14 +8,14 @@ static JSClassID js_writer_class_id;
static void js_reader_finalizer(JSRuntime *rt, JSValue val) {
mz_zip_archive *zip = JS_GetOpaque(val, js_reader_class_id);
mz_zip_reader_end(zip);
js_free_rt(zip);
js_free_rt(rt,zip);
}
static void js_writer_finalizer(JSRuntime *rt, JSValue val) {
mz_zip_archive *zip = JS_GetOpaque(val, js_writer_class_id);
mz_zip_writer_finalize_archive(zip);
mz_zip_writer_end(zip);
js_free_rt(zip);
js_free_rt(rt,zip);
}
static JSClassDef js_reader_class = {
@@ -101,7 +101,7 @@ static JSValue js_miniz_compress(JSContext *js, JSValue this_val,
size_t in_len = 0;
const void *in_ptr = NULL;
if (JS_IsText(argv[0])) {
if (JS_IsString(argv[0])) {
/* String → UTF-8 bytes without the terminating NUL */
cstring = JS_ToCStringLen(js, &in_len, argv[0]);
if (!cstring)

261
bench.ce
View File

@@ -24,45 +24,63 @@ def MAX_BATCH_SIZE = 100000000 // 100M iterations max per batch
// Statistical functions
function median(arr) {
if (length(arr) == 0) return 0
var sorted = sort(arr)
var mid = floor(length(arr) / 2)
if (length(arr) % 2 == 0) {
if (arr.length == 0) return 0
var sorted = arr.slice().sort(function(a, b) { return a - b })
var mid = number.floor(arr.length / 2)
if (arr.length % 2 == 0) {
return (sorted[mid - 1] + sorted[mid]) / 2
}
return sorted[mid]
}
function mean(arr) {
if (length(arr) == 0) return 0
if (arr.length == 0) return 0
var sum = 0
arrfor(arr, function(val) {
sum += val
})
return sum / length(arr)
for (var i = 0; i < arr.length; i++) {
sum += arr[i]
}
return sum / arr.length
}
function stddev(arr, mean_val) {
if (length(arr) < 2) return 0
if (arr.length < 2) return 0
var sum_sq_diff = 0
arrfor(arr, function(val) {
var diff = val - mean_val
for (var i = 0; i < arr.length; i++) {
var diff = arr[i] - mean_val
sum_sq_diff += diff * diff
})
return math.sqrt(sum_sq_diff / (length(arr) - 1))
}
return math.sqrt(sum_sq_diff / (arr.length - 1))
}
function percentile(arr, p) {
if (length(arr) == 0) return 0
var sorted = sort(arr)
var idx = floor(arr) * p / 100
if (idx >= length(arr)) idx = length(arr) - 1
if (arr.length == 0) return 0
var sorted = arr.slice().sort(function(a, b) { return a - b })
var idx = number.floor(arr.length * p / 100)
if (idx >= arr.length) idx = arr.length - 1
return sorted[idx]
}
function min_val(arr) {
if (arr.length == 0) return 0
var m = arr[0]
for (var i = 1; i < arr.length; i++) {
if (arr[i] < m) m = arr[i]
}
return m
}
function max_val(arr) {
if (arr.length == 0) return 0
var m = arr[0]
for (var i = 1; i < arr.length; i++) {
if (arr[i] > m) m = arr[i]
}
return m
}
// Parse arguments similar to test.ce
function parse_args() {
if (length(args) == 0) {
if (args.length == 0) {
if (!testlib.is_valid_package('.')) {
log.console('No cell.toml found in current directory')
return false
@@ -81,7 +99,7 @@ function parse_args() {
}
if (args[0] == 'package') {
if (length(args) < 2) {
if (args.length < 2) {
log.console('Usage: cell bench package <name> [bench]')
log.console(' cell bench package all')
return false
@@ -97,7 +115,7 @@ function parse_args() {
var lock = shop.load_lock()
if (lock[name]) {
target_pkg = name
} else if (starts_with(name, '/') && testlib.is_valid_package(name)) {
} else if (name.startsWith('/') && testlib.is_valid_package(name)) {
target_pkg = name
} else {
if (testlib.is_valid_package('.')) {
@@ -114,7 +132,7 @@ function parse_args() {
}
}
if (length(args) >= 3) {
if (args.length >= 3) {
target_bench = args[2]
}
@@ -126,7 +144,7 @@ function parse_args() {
var bench_path = args[0]
// Normalize path - add benches/ prefix if not present
if (!starts_with(bench_path, 'benches/') && !starts_with(bench_path, '/')) {
if (!bench_path.startsWith('benches/') && !bench_path.startsWith('/')) {
if (!fd.is_file(bench_path + '.cm') && !fd.is_file(bench_path)) {
if (fd.is_file('benches/' + bench_path + '.cm') || fd.is_file('benches/' + bench_path)) {
bench_path = 'benches/' + bench_path
@@ -159,18 +177,19 @@ function collect_benches(package_name, specific_bench) {
var files = pkg.list_files(package_name)
var bench_files = []
arrfor(files, function(f) {
if (starts_with(f, "benches/") && ends_with(f, ".cm")) {
for (var i = 0; i < files.length; i++) {
var f = files[i]
if (f.startsWith("benches/") && f.endsWith(".cm")) {
if (specific_bench) {
var bench_name = text(f, 0, -3)
var bench_name = f.substring(0, f.length - 3)
var match_name = specific_bench
if (!starts_with(match_name, 'benches/')) match_name = 'benches/' + match_name
var match_base = ends_with(match_name, '.cm') ? text(match_name, 0, -3) : match_name
if (bench_name != match_base) return
if (!match_name.startsWith('benches/')) match_name = 'benches/' + match_name
var match_base = match_name.endsWith('.cm') ? match_name.substring(0, match_name.length - 3) : match_name
if (bench_name != match_base) continue
}
push(bench_files, f)
bench_files.push(f)
}
})
}
return bench_files
}
@@ -184,7 +203,7 @@ function calibrate_batch_size(bench_fn, is_batch) {
// Find a batch size that takes at least MIN_SAMPLE_NS
while (n < MAX_BATCH_SIZE) {
// Ensure n is a valid number before calling
if (!is_number(n) || n < 1) {
if (typeof n != 'number' || n < 1) {
n = 1
break
}
@@ -198,7 +217,7 @@ function calibrate_batch_size(bench_fn, is_batch) {
// Double the batch size
var new_n = n * 2
// Check if multiplication produced a valid number
if (!is_number(new_n) || new_n > MAX_BATCH_SIZE) {
if (typeof new_n != 'number' || new_n > MAX_BATCH_SIZE) {
n = MAX_BATCH_SIZE
break
}
@@ -206,12 +225,12 @@ function calibrate_batch_size(bench_fn, is_batch) {
}
// Adjust to target sample duration
if (dt > 0 && dt < TARGET_SAMPLE_NS && is_number(n) && is_number(dt)) {
if (dt > 0 && dt < TARGET_SAMPLE_NS && typeof n == 'number' && typeof dt == 'number') {
var calc = n * TARGET_SAMPLE_NS / dt
if (is_number(calc) && calc > 0) {
var target_n = floor(calc)
if (typeof calc == 'number' && calc > 0) {
var target_n = number.floor(calc)
// Check if floor returned a valid number
if (is_number(target_n) && target_n > 0) {
if (typeof target_n == 'number' && target_n > 0) {
if (target_n > MAX_BATCH_SIZE) target_n = MAX_BATCH_SIZE
if (target_n < MIN_BATCH_SIZE) target_n = MIN_BATCH_SIZE
n = target_n
@@ -220,7 +239,7 @@ function calibrate_batch_size(bench_fn, is_batch) {
}
// Safety check - ensure we always return a valid batch size
if (!is_number(n) || n < 1) {
if (typeof n != 'number' || n < 1) {
n = 1
}
@@ -235,7 +254,7 @@ function run_single_bench(bench_fn, bench_name) {
// 1. Object with { setup, run, teardown } - structured format
// 2. Function that accepts (n) - batch format
// 3. Function that accepts () - legacy format
var is_structured = is_object(bench_fn) && bench_fn.run
var is_structured = typeof bench_fn == 'object' && bench_fn.run
var is_batch = false
var batch_size = 1
var setup_fn = null
@@ -266,7 +285,7 @@ function run_single_bench(bench_fn, bench_name) {
batch_size = calibrate_batch_size(calibrate_fn, is_batch)
// Safety check for structured benchmarks
if (!is_number(batch_size) || batch_size < 1) {
if (typeof batch_size != 'number' || batch_size < 1) {
batch_size = 1
}
} else {
@@ -288,9 +307,8 @@ function run_single_bench(bench_fn, bench_name) {
// Warmup phase
for (var i = 0; i < WARMUP_BATCHES; i++) {
// Ensure batch_size is valid before warmup
if (!is_number(batch_size) || batch_size < 1) {
var type_str = is_null(batch_size) ? 'null' : is_number(batch_size) ? 'number' : is_text(batch_size) ? 'text' : is_object(batch_size) ? 'object' : is_array(batch_size) ? 'array' : is_function(batch_size) ? 'function' : is_logical(batch_size) ? 'logical' : 'unknown'
log.console(`WARNING: batch_size became ${type_str} = ${batch_size}, resetting to 1`)
if (typeof batch_size != 'number' || batch_size < 1) {
log.console(`WARNING: batch_size became ${typeof batch_size} = ${batch_size}, resetting to 1`)
batch_size = 1
}
@@ -314,7 +332,7 @@ function run_single_bench(bench_fn, bench_name) {
// Measurement phase - collect SAMPLES timing samples
for (var i = 0; i < SAMPLES; i++) {
// Double-check batch_size is valid (should never happen, but defensive)
if (!is_number(batch_size) || batch_size < 1) {
if (typeof batch_size != 'number' || batch_size < 1) {
batch_size = 1
}
@@ -330,7 +348,7 @@ function run_single_bench(bench_fn, bench_name) {
if (teardown_fn) teardown_fn(state)
var ns_per_op = is_batch ? duration / batch_size : duration
push(timings_per_op, ns_per_op)
timings_per_op.push(ns_per_op)
} else {
var start = os.now()
if (is_batch) {
@@ -341,15 +359,15 @@ function run_single_bench(bench_fn, bench_name) {
var duration = os.now() - start
var ns_per_op = is_batch ? duration / batch_size : duration
push(timings_per_op, ns_per_op)
timings_per_op.push(ns_per_op)
}
}
// Calculate statistics
var mean_ns = mean(timings_per_op)
var median_ns = median(timings_per_op)
var min_ns = reduce(timings_per_op, min)
var max_ns = reduce(timings_per_op, max)
var min_ns = min_val(timings_per_op)
var max_ns = max_val(timings_per_op)
var stddev_ns = stddev(timings_per_op, mean_ns)
var p95_ns = percentile(timings_per_op, 95)
var p99_ns = percentile(timings_per_op, 99)
@@ -357,20 +375,20 @@ function run_single_bench(bench_fn, bench_name) {
// Calculate ops/s from median
var ops_per_sec = 0
if (median_ns > 0) {
ops_per_sec = floor(1000000000 / median_ns)
ops_per_sec = number.floor(1000000000 / median_ns)
}
return {
name: bench_name,
batch_size: batch_size,
samples: SAMPLES,
mean_ns: round(mean_ns),
median_ns: round(median_ns),
min_ns: round(min_ns),
max_ns: round(max_ns),
stddev_ns: round(stddev_ns),
p95_ns: round(p95_ns),
p99_ns: round(p99_ns),
mean_ns: number.round(mean_ns),
median_ns: number.round(median_ns),
min_ns: number.round(min_ns),
max_ns: number.round(max_ns),
stddev_ns: number.round(stddev_ns),
p95_ns: number.round(p95_ns),
p99_ns: number.round(p99_ns),
ops_per_sec: ops_per_sec
}
}
@@ -378,17 +396,17 @@ function run_single_bench(bench_fn, bench_name) {
// Format nanoseconds for display
function format_ns(ns) {
if (ns < 1000) return `${ns}ns`
if (ns < 1000000) return `${round(ns / 1000 * 100) / 100}µs`
if (ns < 1000000000) return `${round(ns / 1000000 * 100) / 100}ms`
return `${round(ns / 1000000000 * 100) / 100}s`
if (ns < 1000000) return `${number.round(ns / 1000 * 100) / 100}µs`
if (ns < 1000000000) return `${number.round(ns / 1000000 * 100) / 100}ms`
return `${number.round(ns / 1000000000 * 100) / 100}s`
}
// Format ops/sec for display
function format_ops(ops) {
if (ops < 1000) return `${ops} ops/s`
if (ops < 1000000) return `${round(ops / 1000 * 100) / 100}K ops/s`
if (ops < 1000000000) return `${round(ops / 1000000 * 100) / 100}M ops/s`
return `${round(ops / 1000000000 * 100) / 100}G ops/s`
if (ops < 1000000) return `${number.round(ops / 1000 * 100) / 100}K ops/s`
if (ops < 1000000000) return `${number.round(ops / 1000000 * 100) / 100}M ops/s`
return `${number.round(ops / 1000000000 * 100) / 100}G ops/s`
}
// Run benchmarks for a package
@@ -401,13 +419,14 @@ function run_benchmarks(package_name, specific_bench) {
total: 0
}
if (length(bench_files) == 0) return pkg_result
if (bench_files.length == 0) return pkg_result
if (package_name) log.console(`Running benchmarks for ${package_name}`)
else log.console(`Running benchmarks for local package`)
arrfor(bench_files, function(f) {
var mod_path = text(f, 0, -3)
for (var i = 0; i < bench_files.length; i++) {
var f = bench_files[i]
var mod_path = f.substring(0, f.length - 3)
var file_result = {
name: f,
@@ -420,22 +439,24 @@ function run_benchmarks(package_name, specific_bench) {
bench_mod = shop.use(mod_path, use_pkg)
var benches = []
if (is_function(bench_mod)) {
push(benches, {name: 'main', fn: bench_mod})
} else if (is_object(bench_mod)) {
arrfor(array(bench_mod), function(k) {
if (is_function(bench_mod[k]))
push(benches, {name: k, fn: bench_mod[k]})
})
if (typeof bench_mod == 'function') {
benches.push({name: 'main', fn: bench_mod})
} else if (typeof bench_mod == 'object') {
for (var k in bench_mod) {
if (typeof bench_mod[k] == 'function') {
benches.push({name: k, fn: bench_mod[k]})
}
}
}
if (length(benches) > 0) {
if (benches.length > 0) {
log.console(` ${f}`)
arrfor(benches, function(b) {
for (var j = 0; j < benches.length; j++) {
var b = benches[j]
try {
var result = run_single_bench(b.fn, b.name)
result.package = pkg_result.package
push(file_result.benchmarks, result)
file_result.benchmarks.push(result)
pkg_result.total++
log.console(` ${result.name}`)
@@ -452,10 +473,10 @@ function run_benchmarks(package_name, specific_bench) {
name: b.name,
error: e.toString()
}
push(file_result.benchmarks, error_result)
file_result.benchmarks.push(error_result)
pkg_result.total++
}
})
}
}
} catch (e) {
log.console(` Error loading ${f}: ${e}`)
@@ -464,14 +485,14 @@ function run_benchmarks(package_name, specific_bench) {
name: "load_module",
error: `Error loading module: ${e}`
}
push(file_result.benchmarks, error_result)
file_result.benchmarks.push(error_result)
pkg_result.total++
}
if (length(file_result.benchmarks) > 0) {
push(pkg_result.files, file_result)
if (file_result.benchmarks.length > 0) {
pkg_result.files.push(file_result)
}
})
}
return pkg_result
}
@@ -481,29 +502,29 @@ var all_results = []
if (all_pkgs) {
if (testlib.is_valid_package('.')) {
push(all_results, run_benchmarks(null, null))
all_results.push(run_benchmarks(null, null))
}
var packages = shop.list_packages()
arrfor(packages, function(pkg) {
push(all_results, run_benchmarks(pkg, null))
})
for (var i = 0; i < packages.length; i++) {
all_results.push(run_benchmarks(packages[i], null))
}
} else {
push(all_results, run_benchmarks(target_pkg, target_bench))
all_results.push(run_benchmarks(target_pkg, target_bench))
}
// Calculate totals
var total_benches = 0
arrfor(all_results, function(result) {
total_benches += result.total
})
for (var i = 0; i < all_results.length; i++) {
total_benches += all_results[i].total
}
log.console(`----------------------------------------`)
log.console(`Benchmarks: ${total_benches} total`)
// Generate reports
function generate_reports() {
var timestamp = text(floor(time.number()))
var timestamp = text(number.floor(time.number()))
var report_dir = shop.get_reports_dir() + '/bench_' + timestamp
testlib.ensure_dir(report_dir)
@@ -513,28 +534,34 @@ Total benchmarks: ${total_benches}
=== SUMMARY ===
`
arrfor(all_results, function(pkg_res) {
if (pkg_res.total == 0) return
for (var i = 0; i < all_results.length; i++) {
var pkg_res = all_results[i]
if (pkg_res.total == 0) continue
txt_report += `Package: ${pkg_res.package}\n`
arrfor(pkg_res.files, function(f) {
for (var j = 0; j < pkg_res.files.length; j++) {
var f = pkg_res.files[j]
txt_report += ` ${f.name}\n`
arrfor(f.benchmarks, function(b) {
for (var k = 0; k < f.benchmarks.length; k++) {
var b = f.benchmarks[k]
if (b.error) {
txt_report += ` ERROR ${b.name}: ${b.error}\n`
} else {
txt_report += ` ${b.name}: ${format_ns(b.median_ns)}/op (${format_ops(b.ops_per_sec)})\n`
}
})
})
})
}
}
}
txt_report += `\n=== DETAILED RESULTS ===\n`
arrfor(all_results, function(pkg_res) {
if (pkg_res.total == 0) return
for (var i = 0; i < all_results.length; i++) {
var pkg_res = all_results[i]
if (pkg_res.total == 0) continue
arrfor(pkg_res.files, function(f) {
arrfor(f.benchmarks, function(b) {
if (b.error) return
for (var j = 0; j < pkg_res.files.length; j++) {
var f = pkg_res.files[j]
for (var k = 0; k < f.benchmarks.length; k++) {
var b = f.benchmarks[k]
if (b.error) continue
txt_report += `\n${pkg_res.package}::${b.name}\n`
txt_report += ` batch_size: ${b.batch_size} samples: ${b.samples}\n`
@@ -546,28 +573,30 @@ Total benchmarks: ${total_benches}
txt_report += ` p95: ${format_ns(b.p95_ns)}\n`
txt_report += ` p99: ${format_ns(b.p99_ns)}\n`
txt_report += ` ops/s: ${format_ops(b.ops_per_sec)}\n`
})
})
})
}
}
}
testlib.ensure_dir(report_dir)
fd.slurpwrite(`${report_dir}/bench.txt`, stone(blob(txt_report)))
fd.slurpwrite(`${report_dir}/bench.txt`, stone(new blob(txt_report)))
log.console(`Report written to ${report_dir}/bench.txt`)
// Generate JSON per package
arrfor(all_results, function(pkg_res) {
if (pkg_res.total == 0) return
for (var i = 0; i < all_results.length; i++) {
var pkg_res = all_results[i]
if (pkg_res.total == 0) continue
var pkg_benches = []
arrfor(pkg_res.files, function(f) {
arrfor(f.benchmarks, function(benchmark) {
push(pkg_benches, benchmark)
})
})
for (var j = 0; j < pkg_res.files.length; j++) {
var f = pkg_res.files[j]
for (var k = 0; k < f.benchmarks.length; k++) {
pkg_benches.push(f.benchmarks[k])
}
}
var json_path = `${report_dir}/${replace(pkg_res.package, /\//, '_')}.json`
fd.slurpwrite(json_path, stone(blob(json.encode(pkg_benches))))
})
var json_path = `${report_dir}/${pkg_res.package.replace(/\//g, '_')}.json`
fd.slurpwrite(json_path, stone(new blob(json.encode(pkg_benches))))
}
}
generate_reports()

View File

@@ -20,14 +20,14 @@ function make_shapes(n) {
for (var i = 0; i < n; i++) {
var o = { a: i }
o[`p${i}`] = i
push(out, o)
out.push(o)
}
return out
}
function make_packed_array(n) {
var a = []
for (var i = 0; i < n; i++) push(a, i)
for (var i = 0; i < n; i++) a.push(i)
return a
}
@@ -203,8 +203,8 @@ return {
var x = 0
for (var j = 0; j < n; j++) {
var a = []
for (var i = 0; i < 256; i++) push(a, i)
x = (x + length(a)) | 0
for (var i = 0; i < 256; i++) a.push(i)
x = (x + a.length) | 0
}
return blackhole(sink, x)
},
@@ -216,7 +216,7 @@ return {
for (var j = 0; j < n; j++) {
var s = ""
for (var i = 0; i < 16; i++) s = s + "x"
x = (x + length(s)) | 0
x = (x + s.length) | 0
}
return blackhole(sink, x)
},

View File

@@ -1,5 +1,5 @@
function mainThread() {
var maxDepth = max(6, Number(arg[0] || 16));
var maxDepth = number.max(6, Number(arg[0] || 16));
var stretchDepth = maxDepth + 1;
var check = itemCheck(bottomUpTree(stretchDepth));
@@ -7,7 +7,7 @@ function mainThread() {
var longLivedTree = bottomUpTree(maxDepth);
for (var depth = 4; depth <= maxDepth; depth += 2) {
for (let depth = 4; depth <= maxDepth; depth += 2) {
var iterations = 1 << maxDepth - depth + 4;
work(iterations, depth);
}
@@ -16,8 +16,8 @@ function mainThread() {
}
function work(iterations, depth) {
var check = 0;
for (var i = 0; i < iterations; i++)
let check = 0;
for (let i = 0; i < iterations; i++)
check += itemCheck(bottomUpTree(depth));
log.console(`${iterations}\t trees of depth ${depth}\t check: ${check}`);
}
@@ -34,8 +34,8 @@ function itemCheck(node) {
function bottomUpTree(depth) {
return depth > 0
? TreeNode(bottomUpTree(depth - 1), bottomUpTree(depth - 1))
: TreeNode(null, null);
? new TreeNode(bottomUpTree(depth - 1), bottomUpTree(depth - 1))
: new TreeNode(null, null);
}
mainThread()

View File

@@ -2,8 +2,8 @@ var blob = use('blob')
var math = use('math/radians')
function eratosthenes (n) {
var sieve = blob(n, true)
var sqrtN = whole(math.sqrt(n));
var sieve = new blob(n, true)
var sqrtN = number.whole(math.sqrt(n));
for (i = 2; i <= sqrtN; i++)
if (sieve.read_logical(i))
@@ -17,7 +17,7 @@ var sieve = eratosthenes(10000000);
stone(sieve)
var c = 0
for (var i = 0; i < length(sieve); i++)
for (var i = 0; i < sieve.length; i++)
if (sieve.read_logical(i)) c++
log.console(c)

View File

@@ -1,6 +1,6 @@
function fannkuch(n) {
var perm1 = [n]
for (var i = 0; i < n; i++) perm1[i] = i
for (let i = 0; i < n; i++) perm1[i] = i
var perm = [n]
var count = [n]
var f = 0, flips = 0, nperm = 0, checksum = 0
@@ -18,7 +18,7 @@ function fannkuch(n) {
while (k != 0) {
i = 0
while (2*i < k) {
var t = perm[i]; perm[i] = perm[k-i]; perm[k-i] = t
let t = perm[i]; perm[i] = perm[k-i]; perm[k-i] = t
i += 1
}
k = perm[0]
@@ -34,10 +34,10 @@ function fannkuch(n) {
log.console( checksum )
return flips
}
var p0 = perm1[0]
let p0 = perm1[0]
i = 0
while (i < r) {
var j = i + 1
let j = i + 1
perm1[i] = perm1[j]
i = j
}

View File

@@ -7,9 +7,9 @@ function fib(n) {
var now = time.number()
var arr = [1,2,3,4,5]
arrfor(arr, function(i) {
for (var i in arr) {
log.console(fib(28))
})
}
log.console(`elapsed: ${time.number()-now}`)

View File

@@ -109,12 +109,12 @@ function benchArrayOps() {
var pushTime = measureTime(function() {
var arr = [];
for (var i = 0; i < iterations.medium; i++) {
push(arr, i);
arr.push(i);
}
});
var arr = [];
for (var i = 0; i < 10000; i++) push(arr, i);
for (var i = 0; i < 10000; i++) arr.push(i);
var accessTime = measureTime(function() {
var sum = 0;
@@ -126,7 +126,7 @@ function benchArrayOps() {
var iterateTime = measureTime(function() {
var sum = 0;
for (var j = 0; j < 1000; j++) {
for (var i = 0; i < length(arr); i++) {
for (var i = 0; i < arr.length; i++) {
sum += arr[i];
}
}
@@ -151,12 +151,13 @@ function benchObjectCreation() {
});
function Point(x, y) {
return {x,y}
this.x = x;
this.y = y;
}
var defructorTime = measureTime(function() {
for (var i = 0; i < iterations.medium; i++) {
var p = Point(i, i * 2);
var p = new Point(i, i * 2);
}
});
@@ -198,19 +199,19 @@ function benchStringOps() {
var strings = [];
for (var i = 0; i < 1000; i++) {
push(strings, "string" + i);
strings.push("string" + i);
}
var joinTime = measureTime(function() {
for (var i = 0; i < iterations.complex; i++) {
var result = text(strings, ",");
var result = strings.join(",");
}
});
var splitTime = measureTime(function() {
var str = "a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p";
for (var i = 0; i < iterations.medium; i++) {
var parts = array(str, ",");
var parts = str.split(",");
}
});
@@ -238,7 +239,7 @@ function benchArithmetic() {
var result = 1.5;
for (var i = 0; i < iterations.simple; i++) {
result = math.sine(result) + math.cosine(i * 0.01);
result = math.sqrt(abs(result)) + 0.1;
result = math.sqrt(number.abs(result)) + 0.1;
}
});
@@ -269,13 +270,13 @@ function benchClosures() {
var closureCreateTime = measureTime(function() {
var funcs = [];
for (var i = 0; i < iterations.medium; i++) {
push(funcs, makeAdder(i));
funcs.push(makeAdder(i));
}
});
var adders = [];
for (var i = 0; i < 1000; i++) {
push(adders, makeAdder(i));
adders.push(makeAdder(i));
}
var closureCallTime = measureTime(function() {

View File

@@ -8,15 +8,15 @@ var w = h
log.console(`P4\n${w} ${h}`);
for (var y = 0; y < h; ++y) {
for (let y = 0; y < h; ++y) {
// Create a blob for the row - we need w bits
var row = blob(w);
var row = new blob(w);
for (var x = 0; x < w; ++x) {
for (let x = 0; x < w; ++x) {
zr = zi = tr = ti = 0;
cr = 2 * x / w - 1.5;
ci = 2 * y / h - 1;
for (var i = 0; i < iter && (tr + ti <= limit * limit); ++i) {
for (let i = 0; i < iter && (tr + ti <= limit * limit); ++i) {
zi = 2 * zr * zi + ci;
zr = tr - ti + cr;
tr = zr * zr;

View File

@@ -3,11 +3,17 @@ var SOLAR_MASS = 4 * pi * pi;
var DAYS_PER_YEAR = 365.24;
function Body(x, y, z, vx, vy, vz, mass) {
return {x, y, z, vx, vy, vz, mass};
this.x = x;
this.y = y;
this.z = z;
this.vx = vx;
this.vy = vy;
this.vz = vz;
this.mass = mass;
}
function Jupiter() {
return Body(
return new Body(
4.84143144246472090e+00,
-1.16032004402742839e+00,
-1.03622044471123109e-01,
@@ -19,7 +25,7 @@ function Jupiter() {
}
function Saturn() {
return Body(
return new Body(
8.34336671824457987e+00,
4.12479856412430479e+00,
-4.03523417114321381e-01,
@@ -31,7 +37,7 @@ function Saturn() {
}
function Uranus() {
return Body(
return new Body(
1.28943695621391310e+01,
-1.51111514016986312e+01,
-2.23307578892655734e-01,
@@ -43,7 +49,7 @@ function Uranus() {
}
function Neptune() {
return Body(
return new Body(
1.53796971148509165e+01,
-2.59193146099879641e+01,
1.79258772950371181e-01,
@@ -55,7 +61,7 @@ function Neptune() {
}
function Sun() {
return Body(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, SOLAR_MASS);
return new Body(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, SOLAR_MASS);
}
var bodies = Array(Sun(), Jupiter(), Saturn(), Uranus(), Neptune());
@@ -64,7 +70,7 @@ function offsetMomentum() {
var px = 0;
var py = 0;
var pz = 0;
var size = length(bodies);
var size = bodies.length;
for (var i = 0; i < size; i++) {
var body = bodies[i];
var mass = body.mass;
@@ -80,7 +86,7 @@ function offsetMomentum() {
}
function advance(dt) {
var size = length(bodies);
var size = bodies.length;
for (var i = 0; i < size; i++) {
var bodyi = bodies[i];
@@ -121,7 +127,7 @@ function advance(dt) {
function energy() {
var e = 0;
var size = length(bodies);
var size = bodies.length;
for (var i = 0; i < size; i++) {
var bodyi = bodies[i];

View File

@@ -9,7 +9,7 @@ var newarr = []
var accstr = ""
for (var i = 0; i < 10000; i++) {
accstr += i;
newarrpush(i.toString())
newarr.push(i.toString())
}
// Arrays to store timing results
var jsonDecodeTimes = [];
@@ -19,35 +19,34 @@ var notaDecodeTimes = [];
var notaSizes = [];
// Run 100 tests
for (var i = 0; i < 100; i++) {
for (let i = 0; i < 100; i++) {
// JSON Decode test
var start = os.now();
let start = os.now();
var jll = json.decode(ll);
jsonDecodeTimespush((os.now() - start) * 1000);
jsonDecodeTimes.push((os.now() - start) * 1000);
// JSON Encode test
start = os.now();
var jsonStr = JSON.stringify(jll);
jsonEncodeTimespush((os.now() - start) * 1000);
let jsonStr = JSON.stringify(jll);
jsonEncodeTimes.push((os.now() - start) * 1000);
// NOTA Encode test
start = os.now();
var nll = nota.encode(jll);
notaEncodeTimespush((os.now() - start) * 1000);
notaEncodeTimes.push((os.now() - start) * 1000);
// NOTA Decode test
start = os.now();
var oll = nota.decode(nll);
notaDecodeTimespush((os.now() - start) * 1000);
notaDecodeTimes.push((os.now() - start) * 1000);
}
// Calculate statistics
function getStats(arr) {
return {
avg: reduce(arr, (a,b) => a+b, 0) / length(arr),
min: reduce(arr, min),
max: reduce(arr, max)
};
def avg = arr.reduce((a, b) => a + b) / arr.length;
def min = number.min(...arr);
def max = number.max(...arr);
return { avg, min, max };
}
// Pretty print results

View File

@@ -1,13 +1,13 @@
def math = use('math/radians');
const math = require('math/radians');
function A(i,j) {
return 1/((i+j)*(i+j+1)/2+i+1);
}
function Au(u,v) {
for (var i=0; i<length(u); ++i) {
for (var i=0; i<u.length; ++i) {
var t = 0;
for (var j=0; j<length(u); ++j)
for (var j=0; j<u.length; ++j)
t += A(i,j) * u[j];
v[i] = t;
@@ -15,9 +15,9 @@ function Au(u,v) {
}
function Atu(u,v) {
for (var i=0; i<length(u); ++i) {
for (var i=0; i<u.length; ++i) {
var t = 0;
for (var j=0; j<length(u); ++j)
for (var j=0; j<u.length; ++j)
t += A(j,i) * u[j];
v[i] = t;

View File

@@ -14,18 +14,18 @@
// Helper to run a function repeatedly and measure total time in seconds.
// Returns elapsed time in seconds.
function measureTime(fn, iterations) {
var t1 = os.now();
for (var i = 0; i < iterations; i++) {
let t1 = os.now();
for (let i = 0; i < iterations; i++) {
fn();
}
var t2 = os.now();
let t2 = os.now();
return t2 - t1;
}
// We'll define a function that does `encode -> decode` for a given value:
function roundTripWota(value) {
var encoded = wota.encode(value);
var decoded = wota.decode(encoded);
let encoded = wota.encode(value);
let decoded = wota.decode(encoded);
// Not doing a deep compare here, just measuring performance.
// (We trust the test suite to verify correctness.)
}
@@ -63,9 +63,15 @@ def benchmarks = [
{
name: "Large Array (1k numbers)",
// A thousand random numbers
data: [ array(1000, i => i *0.5) ],
data: [ Array.from({length:1000}, (_, i) => i * 0.5) ],
iterations: 1000
},
{
name: "Large Binary Blob (256KB)",
// A 256KB ArrayBuffer
data: [ new Uint8Array(256 * 1024).buffer ],
iterations: 200
}
];
// Print a header
@@ -73,23 +79,28 @@ log.console("Wota Encode/Decode Benchmark");
log.console("===================\n");
// We'll run each benchmark scenario in turn.
arrfor(benchmarks, function(bench) {
var totalIterations = bench.iterations * length(bench.data);
for (let bench of benchmarks) {
// We'll measure how long it takes to do 'iterations' *for each test value*
// in bench.data. The total loop count is `bench.iterations * bench.data.length`.
// Then we compute an overall encode+decode throughput (ops/s).
let totalIterations = bench.iterations * bench.data.length;
// We'll define a function that does a roundTrip for *each* data item in bench.data
// to measure in one loop iteration. Then we multiply by bench.iterations.
function runAllData() {
arrfor(bench.data, roundTripWota)
for (let val of bench.data) {
roundTripWota(val);
}
}
var elapsedSec = measureTime(runAllData, bench.iterations);
var opsPerSec = (totalIterations / elapsedSec).toFixed(1);
let elapsedSec = measureTime(runAllData, bench.iterations);
let opsPerSec = (totalIterations / elapsedSec).toFixed(1);
log.console(`${bench.name}:`);
log.console(` Iterations: ${bench.iterations} × ${length(bench.data)} data items = ${totalIterations}`);
log.console(` Iterations: ${bench.iterations} × ${bench.data.length} data items = ${totalIterations}`);
log.console(` Elapsed: ${elapsedSec.toFixed(3)} s`);
log.console(` Throughput: ${opsPerSec} encode+decode ops/sec\n`);
})
}
// All done
log.console("Benchmark completed.\n");

View File

@@ -13,7 +13,7 @@
//
// Parse command line arguments
if (length(arg) != 2) {
if (arg.length != 2) {
log.console('Usage: cell benchmark_wota_nota_json.ce <LibraryName> <ScenarioName>');
$stop()
}
@@ -32,7 +32,7 @@ def libraries = [
decode: wota.decode,
// wota produces an ArrayBuffer. We'll count `buffer.byteLength` as size.
getSize(encoded) {
return length(encoded);
return encoded.length;
}
},
{
@@ -41,7 +41,7 @@ def libraries = [
decode: nota.decode,
// nota also produces an ArrayBuffer:
getSize(encoded) {
return length(encoded);
return encoded.length;
}
},
{
@@ -50,8 +50,9 @@ def libraries = [
decode: json.decode,
// json produces a JS string. We'll measure its UTF-16 code unit length
// as a rough "size". Alternatively, you could convert to UTF-8 for
// a more accurate byte size. Here we just use `string.length`.
getSize(encodedStr) {
return length(encodedStr);
return encodedStr.length;
}
}
];
@@ -97,7 +98,7 @@ def benchmarks = [
},
{
name: "large_array",
data: [ array(1000, i => i) ],
data: [ Array.from({length:1000}, (_, i) => i) ],
iterations: 1000
},
];
@@ -107,9 +108,9 @@ def benchmarks = [
////////////////////////////////////////////////////////////////////////////////
function measureTime(fn) {
var start = os.now();
let start = os.now();
fn();
var end = os.now();
let end = os.now();
return (end - start); // in seconds
}
@@ -127,19 +128,19 @@ function runBenchmarkForLibrary(lib, bench) {
// Pre-store the encoded results for all items so we can measure decode time
// in a separate pass. Also measure total size once.
var encodedList = [];
var totalSize = 0;
let encodedList = [];
let totalSize = 0;
// 1) Measure ENCODING
var encodeTime = measureTime(() => {
for (var i = 0; i < bench.iterations; i++) {
let encodeTime = measureTime(() => {
for (let i = 0; i < bench.iterations; i++) {
// For each data item, encode it
for (var j = 0; j < length(bench.data); j++) {
var e = lib.encode(bench.data[j]);
for (let j = 0; j < bench.data.length; j++) {
let e = lib.encode(bench.data[j]);
// store only in the very first iteration, so we can decode them later
// but do not store them every iteration or we blow up memory.
if (i == 0) {
push(encodedList, e);
encodedList.push(e);
totalSize += lib.getSize(e);
}
}
@@ -147,9 +148,13 @@ function runBenchmarkForLibrary(lib, bench) {
});
// 2) Measure DECODING
var decodeTime = measureTime(() => {
for (var i = 0; i < bench.iterations; i++) {
arrfor(encodedList, lib.decode)
let decodeTime = measureTime(() => {
for (let i = 0; i < bench.iterations; i++) {
// decode everything we stored during the first iteration
for (let e of encodedList) {
let decoded = lib.decode(e);
// not verifying correctness here, just measuring speed
}
}
});
@@ -161,18 +166,18 @@ function runBenchmarkForLibrary(lib, bench) {
////////////////////////////////////////////////////////////////////////////////
// Find the requested library and scenario
var lib = libraries[find(libraries, l => l.name == lib_name)];
var bench = benchmarks[find(benchmarks, b => b.name == scenario_name)];
var lib = libraries.find(l => l.name == lib_name);
var bench = benchmarks.find(b => b.name == scenario_name);
if (!lib) {
log.console('Unknown library:', lib_name);
log.console('Available libraries:', text(array(libraries, l => l.name), ', '));
log.console('Available libraries:', libraries.map(l => l.name).join(', '));
$stop()
}
if (!bench) {
log.console('Unknown scenario:', scenario_name);
log.console('Available scenarios:', text(array(benchmarks, b => b.name), ', '));
log.console('Available scenarios:', benchmarks.map(b => b.name).join(', '));
$stop()
}
@@ -180,7 +185,7 @@ if (!bench) {
var { encodeTime, decodeTime, totalSize } = runBenchmarkForLibrary(lib, bench);
// Output json for easy parsing by hyperfine or other tools
var totalOps = bench.iterations * length(bench.data);
var totalOps = bench.iterations * bench.data.length;
var result = {
lib: lib_name,
scenario: scenario_name,

View File

@@ -1,11 +1,9 @@
// cell build [<locator>] - Build dynamic libraries locally for the current machine
// cell build [options] - Build dynamic libraries locally for the current machine
//
// Usage:
// cell build Build dynamic libraries for all packages in shop
// cell build . Build dynamic library for current directory package
// cell build <locator> Build dynamic library for specific package
// cell build Build dynamic libraries for all packages
// cell build -p <pkg> Build dynamic library for specific package
// cell build -t <target> Cross-compile dynamic libraries for target platform
// cell build -b <type> Build type: release (default), debug, or minsize
var build = use('build')
var shop = use('internal/shop')
@@ -14,28 +12,25 @@ var fd = use('fd')
var target = null
var target_package = null
var buildtype = 'release'
var force_rebuild = false
var dry_run = false
var buildtype = 'debug'
for (var i = 0; i < length(args); i++) {
for (var i = 0; i < args.length; i++) {
if (args[i] == '-t' || args[i] == '--target') {
if (i + 1 < length(args)) {
if (i + 1 < args.length) {
target = args[++i]
} else {
log.error('-t requires a target')
$stop()
}
} else if (args[i] == '-p' || args[i] == '--package') {
// Legacy support for -p flag
if (i + 1 < length(args)) {
if (i + 1 < args.length) {
target_package = args[++i]
} else {
log.error('-p requires a package name')
$stop()
}
} else if (args[i] == '-b' || args[i] == '--buildtype') {
if (i + 1 < length(args)) {
if (i + 1 < args.length) {
buildtype = args[++i]
if (buildtype != 'release' && buildtype != 'debug' && buildtype != 'minsize') {
log.error('Invalid buildtype: ' + buildtype + '. Must be release, debug, or minsize')
@@ -45,30 +40,13 @@ for (var i = 0; i < length(args); i++) {
log.error('-b requires a buildtype (release, debug, minsize)')
$stop()
}
} else if (args[i] == '--force') {
force_rebuild = true
} else if (args[i] == '--dry-run') {
dry_run = true
} else if (args[i] == '--list-targets') {
log.console('Available targets:')
var targets = build.list_targets()
for (var t = 0; t < length(targets); t++) {
for (var t = 0; t < targets.length; t++) {
log.console(' ' + targets[t])
}
$stop()
} else if (!starts_with(args[i], '-') && !target_package) {
// Positional argument - treat as package locator
target_package = args[i]
}
}
// Resolve local paths to absolute paths
if (target_package) {
if (target_package == '.' || starts_with(target_package, './') || starts_with(target_package, '../') || fd.is_dir(target_package)) {
var resolved = fd.realpath(target_package)
if (resolved) {
target_package = resolved
}
}
}
@@ -80,16 +58,16 @@ if (!target) {
if (target && !build.has_target(target)) {
log.error('Invalid target: ' + target)
log.console('Available targets: ' + text(build.list_targets(), ', '))
log.console('Available targets: ' + build.list_targets().join(', '))
$stop()
}
var packages = shop.list_packages()
log.console('Preparing packages...')
arrfor(packages, function(package) {
if (package == 'core') return
for (var package of packages) {
if (package == 'core') continue
shop.extract(package)
})
}
if (target_package) {
// Build single package
@@ -110,7 +88,7 @@ if (target_package) {
var success = 0
var failed = 0
for (var i = 0; i < length(results); i++) {
for (var i = 0; i < results.length; i++) {
if (results[i].library) {
success++
} else if (results[i].error) {

295
build.cm
View File

@@ -28,15 +28,15 @@ function get_local_dir() {
// Replace sigils in a string
// Currently supports: $LOCAL -> .cell/local full path
function replace_sigils(str) {
return replace(str, '$LOCAL', get_local_dir())
return str.replaceAll('$LOCAL', get_local_dir())
}
// Replace sigils in an array of flags
function replace_sigils_array(flags) {
var result = []
arrfor(flags, function(flag) {
push(result, replace_sigils(flag))
})
for (var i = 0; i < flags.length; i++) {
result.push(replace_sigils(flags[i]))
}
return result
}
@@ -73,7 +73,7 @@ Build.detect_host_target = function() {
// ============================================================================
function content_hash(str) {
var bb = stone(blob(str))
var bb = stone(new blob(str))
return text(crypto.blake2(bb, 32), 'h')
}
@@ -83,12 +83,14 @@ function get_build_dir() {
function ensure_dir(path) {
if (fd.stat(path).isDirectory) return
var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
for (var i = 0; i < length(parts); i++) {
var parts = path.split('/')
var current = path.startsWith('/') ? '/' : ''
for (var i = 0; i < parts.length; i++) {
if (parts[i] == '') continue
current += parts[i] + '/'
if (!fd.stat(current).isDirectory) fd.mkdir(current)
if (!fd.stat(current).isDirectory) {
fd.mkdir(current)
}
}
}
@@ -105,7 +107,7 @@ Build.compile_file = function(pkg, file, target, buildtype = 'release') {
var src_path = pkg_dir + '/' + file
if (!fd.is_file(src_path)) {
throw Error('Source file not found: ' + src_path)
throw new Error('Source file not found: ' + src_path)
}
// Get flags (with sigil replacement)
@@ -121,32 +123,33 @@ Build.compile_file = function(pkg, file, target, buildtype = 'release') {
// Add buildtype-specific flags
if (buildtype == 'release') {
cmd_parts = array(cmd_parts, ['-O3', '-DNDEBUG'])
cmd_parts.push('-O3', '-DNDEBUG')
} else if (buildtype == 'debug') {
cmd_parts = array(cmd_parts, ['-O2', '-g'])
cmd_parts.push('-O2', '-g')
} else if (buildtype == 'minsize') {
cmd_parts = array(cmd_parts, ['-Os', '-DNDEBUG'])
cmd_parts.push('-Os', '-DNDEBUG')
}
push(cmd_parts, '-DCELL_USE_NAME=' + sym_name)
push(cmd_parts, '-I"' + pkg_dir + '"')
cmd_parts.push('-DCELL_USE_NAME=' + sym_name)
cmd_parts.push('-I"' + pkg_dir + '"')
// Add package CFLAGS (resolve relative -I paths)
arrfor(cflags, function(flag) {
if (starts_with(flag, '-I') && !starts_with(flag, '-I/')) {
flag = '-I"' + pkg_dir + '/' + text(flag, 2) + '"'
for (var i = 0; i < cflags.length; i++) {
var flag = cflags[i]
if (flag.startsWith('-I') && !flag.startsWith('-I/')) {
flag = '-I"' + pkg_dir + '/' + flag.substring(2) + '"'
}
push(cmd_parts, flag)
})
cmd_parts.push(flag)
}
// Add target CFLAGS
arrfor(target_cflags, function(flag) {
push(cmd_parts, flag)
})
for (var i = 0; i < target_cflags.length; i++) {
cmd_parts.push(target_cflags[i])
}
push(cmd_parts, '"' + src_path + '"')
cmd_parts.push('"' + src_path + '"')
var cmd_str = text(cmd_parts, ' ')
var cmd_str = cmd_parts.join(' ')
// Content hash: command + file content
var file_content = fd.slurp(src_path)
@@ -167,7 +170,7 @@ Build.compile_file = function(pkg, file, target, buildtype = 'release') {
log.console('Compiling ' + file)
var ret = os.system(full_cmd)
if (ret != 0) {
throw Error('Compilation failed: ' + file)
throw new Error('Compilation failed: ' + file)
}
return obj_path
@@ -179,10 +182,10 @@ Build.build_package = function(pkg, target = Build.detect_host_target(), exclude
var c_files = pkg_tools.get_c_files(pkg, target, exclude_main)
var objects = []
arrfor(c_files, function(file) {
var obj = Build.compile_file(pkg, file, target, buildtype)
push(objects, obj)
})
for (var i = 0; i < c_files.length; i++) {
var obj = Build.compile_file(pkg, c_files[i], target, buildtype)
objects.push(obj)
}
return objects
}
@@ -190,51 +193,24 @@ Build.build_package = function(pkg, target = Build.detect_host_target(), exclude
// ============================================================================
// Dynamic library building
// ============================================================================
// Compute link key from all inputs that affect the dylib output
function compute_link_key(objects, ldflags, target_ldflags, target, cc) {
// Sort objects for deterministic hash
var sorted_objects = sort(objects)
// Build a string representing all link inputs
var parts = []
push(parts, 'target:' + target)
push(parts, 'cc:' + cc)
arrfor(sorted_objects, function(obj) {
// Object paths are content-addressed, so the path itself is the hash
push(parts, 'obj:' + obj)
})
arrfor(ldflags, function(flag) {
push(parts, 'ldflag:' + flag)
})
arrfor(target_ldflags, function(flag) {
push(parts, 'target_ldflag:' + flag)
})
return content_hash(text(parts, '\n'))
}
// Build a dynamic library for a package
// Output goes to .cell/lib/<package_name>.<ext>
// Dynamic libraries do NOT link against core; undefined symbols are resolved at dlopen time
// Uses content-addressed store + symlink for caching
Build.build_dynamic = function(pkg, target = Build.detect_host_target(), buildtype = 'release') {
var objects = Build.build_package(pkg, target, true, buildtype) // exclude main.c
if (length(objects) == 0) {
if (objects.length == 0) {
log.console('No C files in ' + pkg)
return null
}
var lib_dir = shop.get_lib_dir()
var store_dir = lib_dir + '/store'
ensure_dir(lib_dir)
ensure_dir(store_dir)
var lib_name = shop.lib_name_for_package(pkg)
var dylib_ext = toolchains[target].system == 'windows' ? '.dll' : (toolchains[target].system == 'darwin' ? '.dylib' : '.so')
var stable_path = lib_dir + '/' + lib_name + dylib_ext
var lib_path = lib_dir + '/' + lib_name + dylib_ext
// Get link flags (with sigil replacement)
var ldflags = replace_sigils_array(pkg_tools.get_flags(pkg, 'LDFLAGS', target))
var target_ldflags = toolchains[target].c_link_args || []
@@ -242,92 +218,65 @@ Build.build_dynamic = function(pkg, target = Build.detect_host_target(), buildty
var pkg_dir = shop.get_package_dir(pkg)
var local_dir = get_local_dir()
var tc = toolchains[target]
// Resolve relative -L paths in ldflags for hash computation
var resolved_ldflags = []
arrfor(ldflags, function(flag) {
if (starts_with(flag, '-L') && !starts_with(flag, '-L/')) {
flag = '-L"' + pkg_dir + '/' + text(flag, 2) + '"'
}
push(resolved_ldflags, flag)
})
// Compute link key
var link_key = compute_link_key(objects, resolved_ldflags, target_ldflags, target, cc)
var store_path = store_dir + '/' + lib_name + '-' + link_key + dylib_ext
// Check if already linked in store
if (fd.is_file(store_path)) {
// Ensure symlink points to the store file
if (fd.is_link(stable_path)) {
var current_target = fd.readlink(stable_path)
if (current_target == store_path) {
// Already up to date
return stable_path
}
fd.unlink(stable_path)
} else if (fd.is_file(stable_path)) {
fd.unlink(stable_path)
}
fd.symlink(store_path, stable_path)
return stable_path
}
// Build link command
var cmd_parts = [cc, '-shared', '-fPIC']
// Platform-specific flags for undefined symbols (resolved at dlopen) and size optimization
if (tc.system == 'darwin') {
cmd_parts = array(cmd_parts, [
'-undefined', 'dynamic_lookup',
'-Wl,-dead_strip',
'-Wl,-install_name,' + stable_path,
'-Wl,-rpath,@loader_path/../local',
'-Wl,-rpath,' + local_dir
])
// Allow undefined symbols - they will be resolved when dlopen'd into the main executable
cmd_parts.push('-undefined', 'dynamic_lookup')
// Dead-strip unused code
cmd_parts.push('-Wl,-dead_strip')
// rpath for .cell/local libraries
cmd_parts.push('-Wl,-rpath,@loader_path/../local')
cmd_parts.push('-Wl,-rpath,' + local_dir)
} else if (tc.system == 'linux') {
cmd_parts = array(cmd_parts, [
'-Wl,--allow-shlib-undefined',
'-Wl,--gc-sections',
'-Wl,-rpath,$ORIGIN/../local',
'-Wl,-rpath,' + local_dir
])
// Allow undefined symbols at link time
cmd_parts.push('-Wl,--allow-shlib-undefined')
// Garbage collect unused sections
cmd_parts.push('-Wl,--gc-sections')
// rpath for .cell/local libraries
cmd_parts.push('-Wl,-rpath,$ORIGIN/../local')
cmd_parts.push('-Wl,-rpath,' + local_dir)
} else if (tc.system == 'windows') {
// Windows DLLs: use --allow-shlib-undefined for mingw
push(cmd_parts, '-Wl,--allow-shlib-undefined')
cmd_parts.push('-Wl,--allow-shlib-undefined')
}
// Add .cell/local to library search path
push(cmd_parts, '-L"' + local_dir + '"')
arrfor(objects, function(obj) {
push(cmd_parts, '"' + obj + '"')
})
cmd_parts.push('-L"' + local_dir + '"')
for (var i = 0; i < objects.length; i++) {
cmd_parts.push('"' + objects[i] + '"')
}
// Do NOT link against core library - symbols resolved at dlopen time
cmd_parts = array(cmd_parts, resolved_ldflags)
cmd_parts = array(cmd_parts, target_ldflags)
push(cmd_parts, '-o')
push(cmd_parts, '"' + store_path + '"')
var cmd_str = text(cmd_parts, ' ')
log.console('Linking ' + lib_name + dylib_ext)
// Add LDFLAGS (resolve relative -L paths)
for (var i = 0; i < ldflags.length; i++) {
var flag = ldflags[i]
if (flag.startsWith('-L') && !flag.startsWith('-L/')) {
flag = '-L"' + pkg_dir + '/' + flag.substring(2) + '"'
}
cmd_parts.push(flag)
}
for (var i = 0; i < target_ldflags.length; i++) {
cmd_parts.push(target_ldflags[i])
}
cmd_parts.push('-o', '"' + lib_path + '"')
var cmd_str = cmd_parts.join(' ')
log.console('Linking ' + lib_path)
var ret = os.system(cmd_str)
if (ret != 0) {
throw Error('Linking failed: ' + pkg)
throw new Error('Linking failed: ' + pkg)
}
// Update symlink to point to the new store file
if (fd.is_link(stable_path)) {
fd.unlink(stable_path)
} else if (fd.is_file(stable_path)) {
fd.unlink(stable_path)
}
fd.symlink(store_path, stable_path)
return stable_path
return lib_path
}
// ============================================================================
@@ -343,36 +292,38 @@ Build.build_static = function(packages, target = Build.detect_host_target(), out
var seen_flags = {}
// Compile all packages
arrfor(packages, function(pkg) {
for (var i = 0; i < packages.length; i++) {
var pkg = packages[i]
var is_core = (pkg == 'core')
// For core, include main.c; for others, exclude it
var objects = Build.build_package(pkg, target, !is_core, buildtype)
arrfor(objects, function(obj) {
push(all_objects, obj)
})
for (var j = 0; j < objects.length; j++) {
all_objects.push(objects[j])
}
// Collect LDFLAGS (with sigil replacement)
var ldflags = replace_sigils_array(pkg_tools.get_flags(pkg, 'LDFLAGS', target))
var pkg_dir = shop.get_package_dir(pkg)
// Deduplicate based on the entire LDFLAGS string for this package
var ldflags_key = pkg + ':' + text(ldflags, ' ')
var ldflags_key = pkg + ':' + ldflags.join(' ')
if (!seen_flags[ldflags_key]) {
seen_flags[ldflags_key] = true
arrfor(ldflags, function(flag) {
for (var j = 0; j < ldflags.length; j++) {
var flag = ldflags[j]
// Resolve relative -L paths
if (starts_with(flag, '-L') && !starts_with(flag, '-L/')) {
flag = '-L"' + pkg_dir + '/' + text(flag, 2) + '"'
if (flag.startsWith('-L') && !flag.startsWith('-L/')) {
flag = '-L"' + pkg_dir + '/' + flag.substring(2) + '"'
}
push(all_ldflags, flag)
})
all_ldflags.push(flag)
}
}
})
}
if (length(all_objects) == 0) {
throw Error('No object files to link')
if (all_objects.length == 0) {
throw new Error('No object files to link')
}
// Link
@@ -380,32 +331,32 @@ Build.build_static = function(packages, target = Build.detect_host_target(), out
var target_ldflags = toolchains[target].c_link_args || []
var exe_ext = toolchains[target].system == 'windows' ? '.exe' : ''
if (!ends_with(output, exe_ext) && exe_ext) {
if (!output.endsWith(exe_ext) && exe_ext) {
output = output + exe_ext
}
var cmd_parts = [cc]
arrfor(all_objects, function(obj) {
push(cmd_parts, '"' + obj + '"')
})
for (var i = 0; i < all_objects.length; i++) {
cmd_parts.push('"' + all_objects[i] + '"')
}
arrfor(all_ldflags, function(flag) {
push(cmd_parts, flag)
})
for (var i = 0; i < all_ldflags.length; i++) {
cmd_parts.push(all_ldflags[i])
}
arrfor(target_ldflags, function(flag) {
push(cmd_parts, flag)
})
for (var i = 0; i < target_ldflags.length; i++) {
cmd_parts.push(target_ldflags[i])
}
push(cmd_parts, '-o', '"' + output + '"')
cmd_parts.push('-o', '"' + output + '"')
var cmd_str = text(cmd_parts, ' ')
var cmd_str = cmd_parts.join(' ')
log.console('Linking ' + output)
var ret = os.system(cmd_str)
if (ret != 0) {
throw Error('Linking failed with command: ' + cmd_str)
throw new Error('Linking failed with command: ' + cmd_str)
}
log.console('Built ' + output)
@@ -424,30 +375,30 @@ Build.build_all_dynamic = function(target, buildtype = 'release') {
var results = []
// Build core first
if (find(packages, 'core') != null) {
if (packages.indexOf('core') >= 0) {
try {
var lib = Build.build_dynamic('core', target, buildtype)
push(results, { package: 'core', library: lib })
results.push({ package: 'core', library: lib })
} catch (e) {
log.error('Failed to build core: ' + text(e))
push(results, { package: 'core', error: e })
results.push({ package: 'core', error: e })
}
}
// Build other packages
arrfor(packages, function(pkg) {
if (pkg == 'core') return
for (var i = 0; i < packages.length; i++) {
var pkg = packages[i]
if (pkg == 'core') continue
try {
var lib = Build.build_dynamic(pkg, target, buildtype)
push(results, { package: pkg, library: lib })
results.push({ package: pkg, library: lib })
} catch (e) {
log.error('Failed to build ' + pkg + ': ')
log.console(e.message)
log.console(e.stack)
push(results, { package: pkg, error: e })
log.error(e)
results.push({ package: pkg, error: e })
}
})
}
return results
}

189
cellfs.cm
View File

@@ -17,7 +17,30 @@ var writepath = "."
function normalize_path(path) {
if (!path) return ""
// Remove leading/trailing slashes and normalize
return replace(path, /^\/+|\/+$/, "")
return path.replace(/^\/+|\/+$/g, "")
}
// Helper to get directory from path
function dirname(path) {
var idx = path.lastIndexOf("/")
if (idx == -1) return ""
return path.substring(0, idx)
}
// Helper to get basename from path
function basename(path) {
var idx = path.lastIndexOf("/")
if (idx == -1) return path
return path.substring(idx + 1)
}
// Helper to join paths
function join_paths(base, rel) {
base = base.replace(/\/+$/, "")
rel = rel.replace(/^\/+/, "")
if (!base) return rel
if (!rel) return base
return base + "/" + rel
}
// Check if a file exists in a specific mount
@@ -36,7 +59,7 @@ function mount_exists(mount, path) {
return false
}
} else { // fs
var full_path = fd.join_paths(mount.source, path)
var full_path = join_paths(mount.source, path)
try {
var st = fd.stat(full_path)
return st.isFile || st.isDirectory
@@ -63,7 +86,7 @@ function is_directory(path) {
return false;
}
} else { // fs
var full_path = fd.join_paths(mount.source, path)
var full_path = join_paths(mount.source, path)
try {
var st = fd.stat(full_path)
return st.isDirectory
@@ -79,50 +102,44 @@ function resolve(path, must_exist) {
path = normalize_path(path)
// Check for named mount
if (starts_with(path, "@")) {
var idx = search(path, "/")
if (path.startsWith("@")) {
var idx = path.indexOf("/")
var mount_name = ""
var rel_path = ""
if (idx == null) {
mount_name = text(path, 1)
if (idx == -1) {
mount_name = path.substring(1)
rel_path = ""
} else {
mount_name = text(path, 1, idx)
rel_path = text(path, idx + 1)
mount_name = path.substring(1, idx)
rel_path = path.substring(idx + 1)
}
// Find named mount
var mount = null
arrfor(mounts, function(m) {
for (var m of mounts) {
if (m.name == mount_name) {
mount = m
return true
break
}
}, false, true)
}
if (!mount) {
throw Error("Unknown mount point: @" + mount_name)
throw new Error("Unknown mount point: @" + mount_name)
}
return { mount: mount, path: rel_path }
}
// Search path
var found_mount = null
arrfor(mounts, function(mount) {
for (var mount of mounts) {
if (mount_exists(mount, path)) {
found_mount = { mount: mount, path: path }
return true
return { mount: mount, path: path }
}
}, false, true)
if (found_mount) {
return found_mount
}
if (must_exist) {
throw Error("File not found in any mount: " + path)
throw new Error("File not found in any mount: " + path)
}
}
@@ -157,8 +174,8 @@ function mount(source, name) {
mount_info.zip_blob = blob // keep blob alive
} else {
var zip = miniz.read(blob)
if (!is_object(zip) || !is_function(zip.count)) {
throw Error("Invalid archive file (not zip or qop): " + source)
if (!zip || typeof zip.count != 'function') {
throw new Error("Invalid archive file (not zip or qop): " + source)
}
mount_info.type = 'zip'
@@ -166,32 +183,36 @@ function mount(source, name) {
mount_info.zip_blob = blob // keep blob alive
}
} else {
throw Error("Unsupported mount source type: " + source)
throw new Error("Unsupported mount source type: " + source)
}
push(mounts, mount_info)
mounts.push(mount_info)
}
// Unmount
function unmount(name_or_source) {
mounts = filter(mounts, function(mount) {
return mount.name != name_or_source && mount.source != name_or_source
})
for (var i = 0; i < mounts.length; i++) {
if (mounts[i].name == name_or_source || mounts[i].source == name_or_source) {
mounts.splice(i, 1)
return
}
}
throw new Error("Mount not found: " + name_or_source)
}
// Read file
function slurp(path) {
var res = resolve(path, true)
if (!res) throw Error("File not found: " + path)
if (!res) throw new Error("File not found: " + path)
if (res.mount.type == 'zip') {
return res.mount.handle.slurp(res.path)
} else if (res.mount.type == 'qop') {
var data = res.mount.handle.read(res.path)
if (!data) throw Error("File not found in qop: " + path)
if (!data) throw new Error("File not found in qop: " + path)
return data
} else {
var full_path = fd.join_paths(res.mount.source, res.path)
var full_path = join_paths(res.mount.source, res.path)
return fd.slurp(full_path)
}
}
@@ -208,7 +229,7 @@ function slurpwrite(path, data) {
// Check existence
function exists(path) {
var res = resolve(path, false)
if (starts_with(path, "@")) {
if (path.startsWith("@")) {
return mount_exists(res.mount, res.path)
}
return res != null
@@ -217,7 +238,7 @@ function exists(path) {
// Stat
function stat(path) {
var res = resolve(path, true)
if (!res) throw Error("File not found: " + path)
if (!res) throw new Error("File not found: " + path)
if (res.mount.type == 'zip') {
var mod = res.mount.handle.mod(res.path)
@@ -228,14 +249,14 @@ function stat(path) {
}
} else if (res.mount.type == 'qop') {
var s = res.mount.handle.stat(res.path)
if (!s) throw Error("File not found in qop: " + path)
if (!s) throw new Error("File not found in qop: " + path)
return {
filesize: s.size,
modtime: s.modtime,
isDirectory: s.isDirectory
}
} else {
var full_path = fd.join_paths(res.mount.source, res.path)
var full_path = join_paths(res.mount.source, res.path)
var s = fd.stat(full_path)
return {
filesize: s.size,
@@ -247,7 +268,7 @@ function stat(path) {
// Get search paths
function searchpath() {
return array(mounts)
return mounts.slice()
}
// Mount a package using the shop system
@@ -261,7 +282,7 @@ function mount_package(name) {
var dir = shop.get_package_dir(name)
if (!dir) {
throw Error("Package not found: " + name)
throw new Error("Package not found: " + name)
}
mount(dir, name)
@@ -275,16 +296,16 @@ function match(str, pattern) {
function rm(path) {
var res = resolve(path, true)
if (res.mount.type != 'fs') throw Error("Cannot delete from non-fs mount")
if (res.mount.type != 'fs') throw new Error("Cannot delete from non-fs mount")
var full_path = fd.join_paths(res.mount.source, res.path)
var full_path = join_paths(res.mount.source, res.path)
var st = fd.stat(full_path)
if (st.isDirectory) fd.rmdir(full_path)
else fd.unlink(full_path)
}
function mkdir(path) {
var full = fd.join_paths(writepath, path)
var full = join_paths(writepath, path)
fd.mkdir(full)
}
@@ -303,7 +324,7 @@ function prefdir(org, app) {
function realdir(path) {
var res = resolve(path, false)
if (!res) return null
return fd.join_paths(res.mount.source, res.path)
return join_paths(res.mount.source, res.path)
}
function enumerate(path, recurse) {
@@ -316,21 +337,21 @@ function enumerate(path, recurse) {
var list = fd.readdir(curr_full)
if (!list) return
arrfor(list, function(item) {
for (var item of list) {
var item_rel = rel_prefix ? rel_prefix + "/" + item : item
push(results, item_rel)
results.push(item_rel)
if (recurse) {
var st = fd.stat(fd.join_paths(curr_full, item))
var st = fd.stat(join_paths(curr_full, item))
if (st.isDirectory) {
visit(fd.join_paths(curr_full, item), item_rel)
visit(join_paths(curr_full, item), item_rel)
}
}
})
}
}
if (res.mount.type == 'fs') {
var full = fd.join_paths(res.mount.source, res.path)
var full = join_paths(res.mount.source, res.path)
var st = fd.stat(full)
if (st && st.isDirectory) {
visit(full, "")
@@ -338,29 +359,29 @@ function enumerate(path, recurse) {
} else if (res.mount.type == 'qop') {
var all = res.mount.handle.list()
var prefix = res.path ? res.path + "/" : ""
var prefix_len = length(prefix)
var prefix_len = prefix.length
// Use a set to avoid duplicates if we are simulating directories
var seen = {}
arrfor(all, function(p) {
if (starts_with(p, prefix)) {
var rel = text(p, prefix_len)
if (length(rel) == 0) return
for (var p of all) {
if (p.startsWith(prefix)) {
var rel = p.substring(prefix_len)
if (rel.length == 0) continue
if (!recurse) {
var slash = search(rel, '/')
if (slash != null) {
rel = text(rel, 0, slash)
var slash = rel.indexOf('/')
if (slash != -1) {
rel = rel.substring(0, slash)
}
}
if (!seen[rel]) {
seen[rel] = true
push(results, rel)
results.push(rel)
}
}
})
}
}
return results
@@ -372,25 +393,17 @@ function globfs(globs, dir) {
var results = []
function check_neg(path) {
var result = false
arrfor(globs, function(g) {
if (starts_with(g, "!") && wildstar.match(text(g, 1), path, wildstar.WM_WILDSTAR)) {
result = true
return true
}
}, false, true)
return result
for (var g of globs) {
if (g.startsWith("!") && wildstar.match(g.substring(1), path, wildstar.WM_WILDSTAR)) return true;
}
return false;
}
function check_pos(path) {
var result = false
arrfor(globs, function(g) {
if (!starts_with(g, "!") && wildstar.match(g, path, wildstar.WM_WILDSTAR)) {
result = true
return true
}
}, false, true)
return result
for (var g of globs) {
if (!g.startsWith("!") && wildstar.match(g, path, wildstar.WM_WILDSTAR)) return true;
}
return false;
}
function visit(curr_full, rel_prefix) {
@@ -399,10 +412,10 @@ function globfs(globs, dir) {
var list = fd.readdir(curr_full)
if (!list) return
arrfor(list, function(item) {
for (var item of list) {
var item_rel = rel_prefix ? rel_prefix + "/" + item : item
var child_full = fd.join_paths(curr_full, item)
var child_full = join_paths(curr_full, item)
var st = fd.stat(child_full)
if (st.isDirectory) {
@@ -411,14 +424,14 @@ function globfs(globs, dir) {
}
} else {
if (!check_neg(item_rel) && check_pos(item_rel)) {
push(results, item_rel)
results.push(item_rel)
}
}
})
}
}
if (res.mount.type == 'fs') {
var full = fd.join_paths(res.mount.source, res.path)
var full = join_paths(res.mount.source, res.path)
var st = fd.stat(full)
if (st && st.isDirectory) {
visit(full, "")
@@ -426,18 +439,18 @@ function globfs(globs, dir) {
} else if (res.mount.type == 'qop') {
var all = res.mount.handle.list()
var prefix = res.path ? res.path + "/" : ""
var prefix_len = length(prefix)
var prefix_len = prefix.length
arrfor(all, function(p) {
if (starts_with(p, prefix)) {
var rel = text(p, prefix_len)
if (length(rel) == 0) return
for (var p of all) {
if (p.startsWith(prefix)) {
var rel = p.substring(prefix_len)
if (rel.length == 0) continue
if (!check_neg(rel) && check_pos(rel)) {
push(results, rel)
results.push(rel)
}
}
})
}
}
return results

226
clean.ce
View File

@@ -1,218 +1,26 @@
// cell clean [<scope>] - Remove cached material to force refetch/rebuild
//
// Usage:
// cell clean Clean build outputs for current directory package
// cell clean . Clean build outputs for current directory package
// cell clean <locator> Clean build outputs for specific package
// cell clean shop Clean entire shop
// cell clean world Clean all world packages
//
// Options:
// --build Remove build outputs only (default)
// --fetch Remove fetched sources only
// --all Remove both build outputs and fetched sources
// --deep Apply to full dependency closure
// --dry-run Show what would be deleted
// cell clean - Remove build artifacts from global shop
var shop = use('internal/shop')
var pkg = use('package')
var fd = use('fd')
var shop = use('internal/shop')
var scope = null
var clean_build = false
var clean_fetch = false
var deep = false
var dry_run = false
var build_dir = shop.get_shop_path() + '/build'
for (var i = 0; i < length(args); i++) {
if (args[i] == '--build') {
clean_build = true
} else if (args[i] == '--fetch') {
clean_fetch = true
} else if (args[i] == '--all') {
clean_build = true
clean_fetch = true
} else if (args[i] == '--deep') {
deep = true
} else if (args[i] == '--dry-run') {
dry_run = true
} else if (args[i] == '--help' || args[i] == '-h') {
log.console("Usage: cell clean [<scope>] [options]")
log.console("")
log.console("Remove cached material to force refetch/rebuild.")
log.console("")
log.console("Scopes:")
log.console(" <locator> Clean specific package")
log.console(" shop Clean entire shop")
log.console(" world Clean all world packages")
log.console("")
log.console("Options:")
log.console(" --build Remove build outputs only (default)")
log.console(" --fetch Remove fetched sources only")
log.console(" --all Remove both build outputs and fetched sources")
log.console(" --deep Apply to full dependency closure")
log.console(" --dry-run Show what would be deleted")
$stop()
} else if (!starts_with(args[i], '-')) {
scope = args[i]
}
if (!fd.is_dir(build_dir)) {
log.console("No build directory found at " + build_dir)
$stop()
return
}
// Default to --build if nothing specified
if (!clean_build && !clean_fetch) {
clean_build = true
log.console("Cleaning build artifacts...")
// Remove the build directory
try {
fd.rm(build_dir)
log.console("Build directory removed: " + build_dir)
} catch (e) {
log.error(e)
}
// Default scope to current directory
if (!scope) {
scope = '.'
}
log.console("Clean complete!")
// Resolve local paths for single package scope
var is_shop_scope = (scope == 'shop')
var is_world_scope = (scope == 'world')
if (!is_shop_scope && !is_world_scope) {
if (scope == '.' || starts_with(scope, './') || starts_with(scope, '../') || fd.is_dir(scope)) {
var resolved = fd.realpath(scope)
if (resolved) {
scope = resolved
}
}
}
var files_to_delete = []
var dirs_to_delete = []
// Gather packages to clean
var packages_to_clean = []
if (is_shop_scope) {
packages_to_clean = shop.list_packages()
} else if (is_world_scope) {
// For now, world is the same as shop
packages_to_clean = shop.list_packages()
} else {
// Single package
push(packages_to_clean, scope)
if (deep) {
try {
var deps = pkg.gather_dependencies(scope)
arrfor(deps, function(dep) {
push(packages_to_clean, dep)
})
} catch (e) {
// Skip if can't read dependencies
}
}
}
// Gather files to clean
var lib_dir = shop.get_lib_dir()
var build_dir = shop.get_build_dir()
var packages_dir = replace(shop.get_package_dir(''), /\/$/, '') // Get base packages dir
if (clean_build) {
if (is_shop_scope) {
// Clean entire build and lib directories
if (fd.is_dir(build_dir)) {
push(dirs_to_delete, build_dir)
}
if (fd.is_dir(lib_dir)) {
push(dirs_to_delete, lib_dir)
}
} else {
// Clean specific package libraries
arrfor(packages_to_clean, function(p) {
if (p == 'core') return
var lib_name = shop.lib_name_for_package(p)
var dylib_ext = '.dylib'
var lib_path = lib_dir + '/' + lib_name + dylib_ext
if (fd.is_file(lib_path)) {
push(files_to_delete, lib_path)
}
// Also check for .so and .dll
var so_path = lib_dir + '/' + lib_name + '.so'
var dll_path = lib_dir + '/' + lib_name + '.dll'
if (fd.is_file(so_path)) {
push(files_to_delete, so_path)
}
if (fd.is_file(dll_path)) {
push(files_to_delete, dll_path)
}
})
}
}
if (clean_fetch) {
if (is_shop_scope) {
// Clean entire packages directory (dangerous!)
if (fd.is_dir(packages_dir)) {
push(dirs_to_delete, packages_dir)
}
} else {
// Clean specific package directories
arrfor(packages_to_clean, function(p) {
if (p == 'core') return
var pkg_dir = shop.get_package_dir(p)
if (fd.is_dir(pkg_dir) || fd.is_link(pkg_dir)) {
push(dirs_to_delete, pkg_dir)
}
})
}
}
// Execute or report
if (dry_run) {
log.console("Would delete:")
if (length(files_to_delete) == 0 && length(dirs_to_delete) == 0) {
log.console(" (nothing to clean)")
} else {
arrfor(files_to_delete, function(f) {
log.console(" [file] " + f)
})
arrfor(dirs_to_delete, function(d) {
log.console(" [dir] " + d)
})
}
} else {
var deleted_count = 0
arrfor(files_to_delete, function(f) {
try {
fd.unlink(f)
log.console("Deleted: " + f)
deleted_count++
} catch (e) {
log.error("Failed to delete " + f + ": " + e)
}
})
arrfor(dirs_to_delete, function(d) {
try {
if (fd.is_link(d)) {
fd.unlink(d)
} else {
fd.rmdir(d, 1) // recursive
}
log.console("Deleted: " + d)
deleted_count++
} catch (e) {
log.error("Failed to delete " + d + ": " + e)
}
})
if (deleted_count == 0) {
log.console("Nothing to clean.")
} else {
log.console("")
log.console("Clean complete: " + text(deleted_count) + " item(s) deleted.")
}
}
$stop()
$stop()

View File

@@ -7,7 +7,7 @@ var fd = use('fd')
var http = use('http')
var miniz = use('miniz')
if (length(args) < 2) {
if (args.length < 2) {
log.console("Usage: cell clone <origin> <path>")
log.console("Clones a cell package to a local path and links it.")
$stop()
@@ -18,7 +18,7 @@ var origin = args[0]
var target_path = args[1]
// Resolve target path to absolute
if (target_path == '.' || starts_with(target_path, './') || starts_with(target_path, '../')) {
if (target_path == '.' || target_path.startsWith('./') || target_path.startsWith('../')) {
var resolved = fd.realpath(target_path)
if (resolved) {
target_path = resolved
@@ -27,12 +27,12 @@ if (target_path == '.' || starts_with(target_path, './') || starts_with(target_p
var cwd = fd.realpath('.')
if (target_path == '.') {
target_path = cwd
} else if (starts_with(target_path, './')) {
target_path = cwd + text(target_path, 1)
} else if (starts_with(target_path, '../')) {
} else if (target_path.startsWith('./')) {
target_path = cwd + target_path.substring(1)
} else if (target_path.startsWith('../')) {
// Go up one directory from cwd
var parent = fd.dirname(cwd)
target_path = parent + text(target_path, 2)
var parent = cwd.substring(0, cwd.lastIndexOf('/'))
target_path = parent + target_path.substring(2)
}
}
}
@@ -92,13 +92,14 @@ try {
for (var i = 0; i < count; i++) {
if (zip.is_directory(i)) continue
var filename = zip.get_filename(i)
var first_slash = search(filename, '/')
if (first_slash == null) continue
if (first_slash + 1 >= length(filename)) continue
var rel_path = text(filename, first_slash + 1)
var parts = filename.split('/')
if (parts.length <= 1) continue
// Skip the first directory (repo-commit prefix)
parts.shift()
var rel_path = parts.join('/')
var full_path = target_path + '/' + rel_path
var dir_path = fd.dirname(full_path)
var dir_path = full_path.substring(0, full_path.lastIndexOf('/'))
// Ensure directory exists
if (!fd.is_dir(dir_path)) {

View File

@@ -31,30 +31,30 @@ function print_help() {
// Parse a dot-notation key into path segments
function parse_key(key) {
return array(key, '.')
return key.split('.')
}
// Get a value from nested object using path
function get_nested(obj, path) {
var current = obj
arrfor(path, function(segment) {
if (is_null(current) || !is_object(current)) return null
for (var segment of path) {
if (!current || typeof current != 'object') return null
current = current[segment]
})
}
return current
}
// Set a value in nested object using path
function set_nested(obj, path, value) {
var current = obj
for (var i = 0; i < length(path) - 1; i++) {
for (var i = 0; i < path.length - 1; i++) {
var segment = path[i]
if (is_null(current[segment]) || !is_object(current[segment])) {
if (!current[segment] || typeof current[segment] != 'object') {
current[segment] = {}
}
current = current[segment]
}
current[path[length(path) - 1]] = value
current[path[path.length - 1]] = value
}
// Parse value string into appropriate type
@@ -64,7 +64,7 @@ function parse_value(str) {
if (str == 'false') return false
// Number (including underscores)
var num_str = replace(str, /_/g, '')
var num_str = str.replace(/_/g, '')
if (/^-?\d+$/.test(num_str)) return parseInt(num_str)
if (/^-?\d*\.\d+$/.test(num_str)) return parseFloat(num_str)
@@ -74,29 +74,29 @@ function parse_value(str) {
// Format value for display
function format_value(val) {
if (is_text(val)) return '"' + val + '"'
if (is_number(val) && val >= 1000) {
if (typeof val == 'string') return '"' + val + '"'
if (typeof val == 'number' && val >= 1000) {
// Add underscores to large numbers
return replace(val.toString(), /\B(?=(\d{3})+(?!\d))/g, '_')
return val.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '_')
}
return text(val)
}
// Print configuration tree recursively
function print_config(obj, prefix = '') {
arrfor(array(obj), function(key) {
for (var key in obj) {
var val = obj[key]
var full_key = prefix ? prefix + '.' + key : key
if (is_object(val))
if (isa(val, object))
print_config(val, full_key)
else
log.console(full_key + ' = ' + format_value(val))
})
}
}
// Main command handling
if (length(args) == 0) {
if (args.length == 0) {
print_help()
$stop()
return
@@ -110,9 +110,6 @@ if (!config) {
}
var command = args[0]
var key
var path
var value
switch (command) {
case 'help':
@@ -128,14 +125,14 @@ switch (command) {
break
case 'get':
if (length(args) < 2) {
if (args.length < 2) {
log.error("Usage: cell config get <key>")
$stop()
return
}
key = args[1]
path = parse_key(key)
value = get_nested(config, path)
var key = args[1]
var path = parse_key(key)
var value = get_nested(config, path)
if (value == null) {
log.error("Key not found: " + key)
@@ -146,9 +143,9 @@ switch (command) {
log.console(key + ' = ' + format_value(value))
}
break
case 'set':
if (length(args) < 3) {
if (args.length < 3) {
log.error("Usage: cell config set <key> <value>")
$stop()
return
@@ -164,8 +161,8 @@ switch (command) {
'ar_timer', 'actor_memory', 'net_service',
'reply_timeout', 'actor_max', 'stack_max'
]
if (find(valid_system_keys, path[1]) == null) {
log.error("Invalid system key. Valid keys: " + text(valid_system_keys, ', '))
if (!valid_system_keys.includes(path[1])) {
log.error("Invalid system key. Valid keys: " + valid_system_keys.join(', '))
$stop()
return
}
@@ -175,10 +172,10 @@ switch (command) {
pkg.save_config(config)
log.console("Set " + key + " = " + format_value(value))
break
case 'actor':
// Handle actor-specific configuration
if (length(args) < 3) {
if (args.length < 3) {
log.error("Usage: cell config actor <name> <command> [options]")
$stop()
return
@@ -193,7 +190,7 @@ switch (command) {
switch (actor_cmd) {
case 'list':
if (length(array(config.actors[actor_name])) == 0) {
if (array(config.actors[actor_name]).length == 0) {
log.console("No configuration for actor: " + actor_name)
} else {
log.console("# Configuration for actor: " + actor_name)
@@ -203,14 +200,14 @@ switch (command) {
break
case 'get':
if (length(args) < 4) {
if (args.length < 4) {
log.error("Usage: cell config actor <name> get <key>")
$stop()
return
}
key = args[3]
path = parse_key(key)
value = get_nested(config.actors[actor_name], path)
var key = args[3]
var path = parse_key(key)
var value = get_nested(config.actors[actor_name], path)
if (value == null) {
log.error("Key not found for actor " + actor_name + ": " + key)
@@ -220,21 +217,21 @@ switch (command) {
break
case 'set':
if (length(args) < 5) {
if (args.length < 5) {
log.error("Usage: cell config actor <name> set <key> <value>")
$stop()
return
}
key = args[3]
var key = args[3]
var value_str = args[4]
path = parse_key(key)
value = parse_value(value_str)
var path = parse_key(key)
var value = parse_value(value_str)
set_nested(config.actors[actor_name], path, value)
pkg.save_config(config)
log.console("Set actors." + actor_name + "." + key + " = " + format_value(value))
break
default:
log.error("Unknown actor command: " + actor_cmd)
log.console("Valid commands: list, get, set")

View File

@@ -231,7 +231,7 @@ JSValue js_crypto_unlock(JSContext *js, JSValue self, int argc, JSValue *argv) {
static const JSCFunctionListEntry js_crypto_funcs[] = {
JS_CFUNC_DEF("shared", 2, js_crypto_shared),
JS_CFUNC_DEF("blake2", 2, js_crypto_blake2),
JS_CFUNC_DEF("blake2", 1, js_crypto_blake2),
JS_CFUNC_DEF("sign", 2, js_crypto_sign),
JS_CFUNC_DEF("verify", 3, js_crypto_verify),
JS_CFUNC_DEF("lock", 3, js_crypto_lock),

View File

@@ -1,6 +1,8 @@
#include "cell.h"
JSC_CCALL(os_gc, JS_RunGC(JS_GetRuntime(js)) )
JSC_CCALL(os_mem_limit, JS_SetMemoryLimit(JS_GetRuntime(js), js2number(js,argv[0])))
JSC_CCALL(os_gc_threshold, JS_SetGCThreshold(JS_GetRuntime(js), js2number(js,argv[0])))
JSC_CCALL(os_max_stacksize, JS_SetMaxStackSize(JS_GetRuntime(js), js2number(js,argv[0])))
// Compute the approximate size of a single JS value in memory.
@@ -13,7 +15,8 @@ JSC_CCALL(os_calc_mem,
JS_SetPropertyStr(js,ret,"memory_used_size",number2js(js,mu.memory_used_size));
JS_SetPropertyStr(js,ret,"malloc_count",number2js(js,mu.malloc_count));
JS_SetPropertyStr(js,ret,"memory_used_count",number2js(js,mu.memory_used_count));
/* atom_count and atom_size removed - atoms are now just strings */
JS_SetPropertyStr(js,ret,"atom_count",number2js(js,mu.atom_count));
JS_SetPropertyStr(js,ret,"atom_size",number2js(js,mu.atom_size));
JS_SetPropertyStr(js,ret,"str_count",number2js(js,mu.str_count));
JS_SetPropertyStr(js,ret,"str_size",number2js(js,mu.str_size));
JS_SetPropertyStr(js,ret,"obj_count",number2js(js,mu.obj_count));
@@ -39,22 +42,20 @@ JSC_CCALL(os_calc_mem,
JSC_SSCALL(os_eval,
if (!str2) return JS_ThrowReferenceError(js, "Second argument should be the script.");
if (!str) return JS_ThrowReferenceError(js, "First argument should be the name of the script.");
JSValue bytecode = JS_Compile(js, str2, strlen(str2), str);
if (JS_IsException(bytecode)) return bytecode;
ret = JS_Integrate(js, bytecode, JS_NULL);
ret = JS_Eval(js,str2,strlen(str2),str, 0);
)
// Compile a string of JavaScript code into a function object.
JSC_SSCALL(js_compile,
if (!str2) return JS_ThrowReferenceError(js, "Second argument should be the script.");
if (!str) return JS_ThrowReferenceError(js, "First argument should be the name of the script.");
ret = JS_Compile(js, str2, strlen(str2), str);
ret = JS_Eval(js, str2, strlen(str2), str, JS_EVAL_FLAG_COMPILE_ONLY | JS_EVAL_FLAG_BACKTRACE_BARRIER);
)
// Link compiled bytecode with environment and execute.
JSC_CCALL(js_integrate,
JSValue env = (argc > 1 && !JS_IsNull(argv[1])) ? argv[1] : JS_NULL;
ret = JS_Integrate(js, argv[0], env);
// Evaluate a function object in the current QuickJS context.
JSC_CCALL(js_eval_compile,
JS_DupValue(js,argv[0]);
ret = JS_EvalFunction(js, argv[0]);
)
// Compile a function object into a bytecode blob.
@@ -91,10 +92,12 @@ JSC_CCALL(js_fn_info,
static const JSCFunctionListEntry js_js_funcs[] = {
MIST_FUNC_DEF(os, calc_mem, 0),
MIST_FUNC_DEF(os, mem_limit, 1),
MIST_FUNC_DEF(os, gc_threshold, 1),
MIST_FUNC_DEF(os, max_stacksize, 1),
MIST_FUNC_DEF(os, gc, 0),
MIST_FUNC_DEF(os, eval, 2),
MIST_FUNC_DEF(js, compile, 2),
MIST_FUNC_DEF(js, integrate, 2),
MIST_FUNC_DEF(js, eval_compile, 1),
MIST_FUNC_DEF(js, compile_blob, 1),
MIST_FUNC_DEF(js, compile_unblob, 1),
MIST_FUNC_DEF(js, disassemble, 1),

View File

@@ -230,10 +230,10 @@ var json = use('json')
Check type or prototype chain.
```javascript
is_number(42) // true
is_text("hi") // true
is_array([1,2]) // true
is_object({}) // true
isa(42, number) // true
isa("hi", text) // true
isa([1,2], array) // true
isa({}, object) // true
isa(child, parent) // true if parent is in prototype chain
```
@@ -270,7 +270,7 @@ Cell supports regex patterns in string functions, but not standalone regex objec
```javascript
text.search("hello world", /world/)
replace("hello", /l/g, "L")
text.replace("hello", /l/g, "L")
```
## Error Handling

View File

@@ -1,921 +0,0 @@
# Cell Functions
The intrinsics are constants and functions that are built into the language. The use statement is not needed to access them.
A programmer is not obliged to consult the list of intrinsics before naming a new variable or input.
New intrinsics may be added to Misty without breaking existing programs.
Constants
false
This is the value of 1 = 0. The false value is one of the two logical values.
true
This is the value of 1 = 1. The true value is one of the two logical values.
null
This is the value of 1 / 0. The null value is an empty immutable object. All attempts to obtain a value from null by refinement will produce null.
Any attempt to modify null will disrupt. Any attempt to call null as a function will disrupt.
null is the value of missing input values, missing fields in records, and invalid numbers. The |default operator can detect the presence of null and substitute another value.
pi
This is an approximation of the circle expression circumference / diameter, or to be precisely approximate, 3.1415926535897932.
Creator Functions
The creator functions are used to make new objects. Some of them can take various types. All of these functions can return null if their inputs are not suitable.
Array
array(number)
Make an array. All of the elements are initialized to null.
number is a non-negative integer, the intended length of the new array.
array(number, initial_value)
Make an array. All of the elements are initialized to initial_value.
number is a non-negative integer, the intended length of the new array.
If initial_value is a function, then the function is called for each element to produce initialization values. If the function has an arity of 1 or more, it is passed the element number.
array(array)
Copy. Make a mutable copy of the array.
array(array, function, reverse, exit)
Map. Call the function with each element of the array, collecting the return values in a new array. The function is passed each element and its element number.
function (element, element_nr)
If reverse is true, then it starts with the last element and works backwards.
If exit is not null, then when the function returns the exit value, then the array function returns early. The exit value will not be stored into the new array. If the array was processed normally, then the returned array will be shorter than the input array. If the array was processed in reverse, then the returned array will have the same length as the input array, and the first elements will be null. The elements in the new array that were not set due to an early exit will be set to null.
This is like the for function except that the return values are collected into a new array.
array(array, another_array)
Concat. Produce a new array that concatenates the array and another_array.
array(array, from, to)
Slice. Make a mutable copy of all or part of an array.
array: the array to copy
from: the position at which to start copying. Default: 0, the beginning. If negative, add length(array).
to: the position at which to stop copying. Default: length(array), the end. If negative, add length(array).
If, after adjustment, from and to are not valid integers in the proper range, then it returns null. from must be positive and less than or equal to to. to must be less than or equal to length(array).
array(record)
Keys. Make an array containing all of the text keys in the record. The keys are not guaranteed to be in any particular order.
array(text)
Split the text into grapheme clusters. A grapheme cluster is a Unicode codepoint and its contributing combining characters, if any.
array(text, separator)
Split the text into an array of subtexts. The separator can be a text or pattern.
array(text, length)
Dice the text into an array of subtexts of a given length.
Logical
logical(value)
if value = 0 \/ value = false \/ value = "false" \/ value = null
return false
fi
if value = 1 \/ value = true \/ value = "true"
return true
fi
return null
Number
number(logical)
The result is 1 or 0.
number(number)
The number is returned.
number(text, radix)
Convert a text to a number. The optional radix is an integer from 2 thru 37. (See Base 32.) The default radix is 10.
number(text, format)
number format
format radix separator decimal point
"" 10 .period
"n"
"u" _underbar
"d" ,comma
"s" space
"v" .period ,comma
"l" dependent on locale
"i" _underbar
"b" 2
"o" 8
"h" 16
"t" 32
"j" 0x- base 16
0o- base 8
0b- base 2
otherwise base 10
The number function converts a text into a number.
If it is unable to (possibly because of a formatting error), it returns null. The format character determines how the text is interpreted. If the format is not one of those listed, then null is returned.
Examples:
assign result: number("123,456,789.10", "d") # result is 123456789.1
assign result: number("123.456.789,10", "v") # result is 123456789.1
assign result: number("123.456.789,10", "d") # result is null
assign result: number("123 456 789.10", "s") # result is 123456789.1
assign result: number("12.350") # result is 12.35
assign result: number("12.350", "v") # result is 12350
assign result: number("12.350", "i") # result is null
assign result: number("666") # result is 666
assign result: number("666", "b") # result is null
assign result: number("666", "o") # result is 438
assign result: number("666", "h") # result is 1638
assign result: number("666", "t") # result is 6342
assign result: number("0666") # result is 666
Record
record(record)
Copy. Make a mutable copy.
record(record, another_record)
Combine. Make a copy of a record, and then put all the fields of another_record into the copy.
record(record, array_of_keys)
Select. Make a new record containing only the fields that are named by the array_of_keys.
record(array_of_keys)
Set. Make a record using the array as the source of the keys. Each field value is true.
record(array_of_keys, value)
Value Set. Make a record using the array as the source of the keys. Each field value is value.
record(array_of_keys, function)
Functional Value Set. Make a record using the array as the source of the keys. The function is called for each key, yielding the field values.
Text
text(array)
Convert an array to text. The array can contain text and unicode codepoints. All are concatenated together to make a single text.
text(array, separator)
Convert an array to text. The array can contain text and unicode codepoints. All are concatenated together to make a single text. The separator text is inserted between each piece. The default separator is the empty text.
text(number, radix)
Convert a number to text. The optional radix is an integer from 2 thru 37. (See Base 32.) The default radix is 10.
text(number, format)
The format of the format text is
format
format_separation format_style format_places
format_separation
""
digit
format_style
'b'
'c'
'e'
'h'
'i'
'l'
'n'
'o'
's'
't'
'u'
format_places
digit digit
digit
Convert a number to formatted text. The text function converts a number to a text. It takes a format text input.
A format text contains a style letter that controls how a text is produced from the number. It is optionally preceded by a separation digit, and optionally followed by a places digit. There are real styles and integer styles. If the format input is not a proper format text, then null is returned.
Separation is a character that is placed between digits to improve readability. If separation is 0, then there is no separation. If separation is 3, then a character is inserted before the quadrillions, trillions, billions, millions, and thousands.
Places is the number of places to display after the decimal point (in real styles) or the minimum number of digits to display with zero-fill (in integer styles). If places is 0, then as many digits as necessary are displayed. Places can be zero or one or two digits.
real style base default
separation default
places decimal
point separator
e exponential 10 0 0 .period
n number
s space 3 space
u underbar _underbar
d decimal 2 ,comma
c comma ,comma .period
l locale determined by the locale
The real format options are
"e" uses scientific notation. One digit is placed before the decimal point, and all of the remaining digits after, followed by e and the exponent.
"n" uses .period as the decimal point and no separator. It is the format used for numbers in Misty source programs and JSON. Scientific notation is used if the number value is extreme.
"s" uses .period as the decimal point and a space as the separator.
"u" uses .period as the decimal point and _underbar as the separator.
"d" uses .period as the decimal point and ,comma as the separator.
"c" uses ,comma as the decimal point and .period as the separator.
"l" depends on the locale to determine the characters to use as the decimal point and the separator.
The optional places determines the number of digits after the decimal point. The default is determined by the format, as seen in the table. If the places is 0, then the number of decimal places will be the fewest to exactly display the number without truncating. If the places is larger, then the field is padded if necessary with trailing 0.
The optional separation determines the spacing of the separator character. For example, to place a separator between billions, millions, and thousands (that is, every 3 digits) then separation should be 3. If separation is zero, then there is no separation. The default is determined by the style.
integer style base default
separation minimum
places separator
i integer 10 0 1 _underbar
b binary 2
o octal 8
h hexadecimal 16
t Base32 32
The integer styles first trunc the number. The fractional part of the number is ignored. The separation character is _underbar.
The optional places determines the minimum number of digits to show. More leading 0 may be shown if necessary. The default is determined by the format, as seen in the table.
The optional separation determines the spacing of the separator character. For example, to place a separator between billions, millions, and thousands (that is, every 3 digits) then separation should be 3. If separation is zero, then there is no separation. The default is determined by the format, as seen in the table.
Examples:
def data: 0123456789.1
assign result: text(data) # result is "123456789.1"
assign result: text(data, "n") # result is "123456789.1"
assign result: text(data, "3s4") # result is "123 456 789.1000"
assign result: text(data, "s") # result is "123 456 789.1"
assign result: text(data, "d2") # result is "123,456,789.10"
assign result: text(data, "4d0") # result is "1,2345,6789.1"
assign result: text(data, "v2") # result is "123.456.789,10"
assign result: text(data, "e") # result is "1.234567891e8"
assign result: text(data, "e4") # result is "1.2345e8"
assign result: text(data, "i") # result is "123456789"
assign result: text(data, "8b") # result is "111_01011011_11001101_00010101"
assign result: text(data, "o") # result is "726746425"
assign result: text(data, "h") # result is "75BCD15"
assign result: text(data, "t") # result is "3NQK8N"
assign result: text(12) # result is "12"
assign result: text(12, 8) # result is "14"
assign result: text(12, 32) # result is "C"
assign result: text(12, "4b8") # result is "0000_1100"
assign result: text(12, "o3") # result is "014"
assign result: text(12, "h4") # result is "000C"
assign result: text(12, "t2") # result is "0C"
text(text)
Return the text. The text is not altered.
text(text, from, to)
Make a copy of part of a text.
text: the text to copy.
from: the position at which to start copying. Default: 0, the beginning. If negative, add length(text).
to: the position at which to stop copying. Default: length(text), the end. If negative, add length(text).
If, after adjustment, from and to are not valid integers in the proper range, then it returns null. from must be positive and less than or equal to to. to must be less than or equal to length(text).
assign my_text: "miskatonic"
text(my_text, 0, 3) # "mis" # the first 3
text(my_text, 3, 6) # "kat" # from 3 to 6
text(my_text, 5) # "tonic" # exclude the first 5
text(my_text, 0, -4) # "miskat" # exclude the last 4
text(my_text, -3) # "nic" # the last 3
text(my_text, 0, 0) # ""
text(my_text, 10) # ""
text(my_text, 11) # null
text(my_text, 2, 1) # null
Sensory Functions
The sensory functions end with ?question mark. They always return a logical value.
actor?(value)
Is the value an actor address object?
actor?(me!) # true
actor?("actor") # false
actor?({actor: true}) # false
array?(value)
Is the value an array? If the value is an array, the result is true. Otherwise, the result is false.
array?(0) # false
array?({}) # false
array?([]) # true
not(array?([])) # false
array?(pattern (1- {letter digit "_-%"})) # false
array?(null) # false
array?("array") # false
blob?(value)
Is the value a blob? If the value is a blob, the result is true. Otherwise, the result is false.
blob?(0) # false
blob?("blob") # false
blob?(blob()) # true
character?(value)
Is the value a character? If the value is a text with a length of 1, then the result is true. Otherwise, the result is false.
character?(1) # false
character?("1") # true
character?("character") # false
character?("") # false
character?("\u{FFFE}") # true
character?() # false
character?("/q") # false
character?("\q") # true
character?(<<">>) # true
character?(null) # false
data?(value)
Is the value data? If the value is a text, number, logical, array, blob, or record, then the result is true. If the value is a function, pattern, or null, the result is false.
data?(0) # true
data?("") # true
data?(["0"]) # true
data?({}) # true
data?(null) # false
digit?(value)
Is the value a digit? If the value is a text with a length of 1 and is one of the 10 digit characters, then the result is true. Otherwise, the result is false.
digit?(0) # false
digit?("0") # true
digit?("9") # true
digit?("09") # false
digit?("digit") # false
digit?("") # false
digit?(1) # false
digit?(["0"]) # false
digit?("Z") # false
false?(value)
Is the value false?
fit?(number)
Is the number a fit number? A number is a fit number if it is an integer that fits in 56 bits. All fit numbers are integers in the range -36028797018963968 thru 36028797018963967. Only fit numbers can be given to the fit functions. Misty has additional integers that are too big to fit.
function?(value)
Is the value a function? If the value is a function, then the result is true. Otherwise, the result is false.
function?(0) # false
function?(function () (null)) # true
function?("function") # false
function?(null) # false
function?(function?) # true
integer?(value)
Is the value an integer? If the value is a number and if its fraction part is zero, then the result is true. Otherwise, the result is false.
integer?(0) # true
integer?(13 / 4) # false
integer?(16 / 4) # true
integer?(65.0000000) # true
integer?(65.0000001) # false
integer?(null) # false
integer?(true) # false
integer?(1) # true
integer?(36028797018963968) # true
integer?(1.00001e100) # true
letter?(value)
Is the value a letter? If the value is a text with a length of 1 and is a letter, then the result is true. Otherwise, the result is false.
letter?(0) # false
letter?("0") # false
letter?("letter") # false
letter?("l") # true
letter?("L") # true
letter?("") # false
letter?(null) # false
logical?(value)
Is the value a logical? A logical is either a false or a true. All other values are not logical.
logical?(false) # true
logical?(true) # true
logical?(0) # false
logical?() # false
logical?(null) # false
lower?(value)
Is the value a lower case letter? If the value is a text with a length of 1 and is a lower case letter, then the result is true. Otherwise, the result is false.
lower?(0) # false
lower?("0") # false
lower?("lower") # false
lower?("l") # true
lower?("L") # false
lower?("") # false
null?(value)
Is the value null? This does the same thing as value = null.
number?(value)
Is the value a number? If the value is a number, then the result is true. Otherwise, the result is false.
number?(0) # true
number?((13 / 4)) # true
number?((13 / 0)) # false
number?(98.6) # true
number?("0") # false
number?(1) # true
pattern?(value)
Is the value a pattern? If the value is a pattern, then the result is true. Otherwise, the result is false.
pattern?(pattern (1- {letter digit "_-%"})) # true
record?(value)
Is the value a record? If the value is a record, then the result is true. Otherwise, the result is false.
record?(0) # false
record?({}) # true
record?([]) # false
record?("record") # false
record?("{}") # false
record?(function () ({})) # false
record?(pattern (1- {letter digit "_-%"})) # false
record?(@) # true
stone?(value)
Is the value stone?
stone?("false") # true
stone?(9) # true
stone?(null) # true
stone?({}) # false
stone?(stone({})) # true
text?(value)
Is the value a text? If the value is a text, then the result is true. Otherwise, the result is false.
text?(0) # false
text?("0") # true
text?("number") # true
text?("") # true
text?(null) # false
true?(value)
Is the value true?
upper?(value)
Is the value an upper case letter? If the value is a text with a length of 1 and is an upper case letter, then the result is true. Otherwise, the result is false.
upper?(0) # false
upper?("0") # false
upper?("UPPER") # false
upper?("u") # false
upper?("U") # true
upper?("") # false
whitespace?(value)
Is the value whitespace? If the value is a nonempty text containing only whitespace characters, then the result is true. Otherwise, the result is false.
whitespace?(0) # false
whitespace?(32) # false
whitespace?(char(32)) # true
whitespace?("0") # false
whitespace?(" ") # true
whitespace?("\t") # true
whitespace?("\r") # true
whitespace?("\r\n") # true
whitespace?("space") # false
whitespace?(" ") # true
whitespace?(" L") # false
whitespace?("") # false
Standard Functions
abs(number)
Absolute value. Return the positive form of the number. If the input value is not a number, the result is null.
apply(function, array)
Apply. Execute the function and return its return value. Pass the elements of the array as input values. See proxy.
If the first input value is not a function, apply returns its first input value.
If length(array) is greater than length(function), it disrupts.
If the second argument is not an array, it is used as a single input value.
ceiling(number, place)
If place is 0 or null, the number is rounded up to the smallest integer that is greater than or equal to the number. If place is a small positive integer, then the number is rounded up to that decimal place.
Examples:
assign result: ceiling(12.3775) # result is 13
assign result: ceiling(12.3775, 0) # result is 13
assign result: ceiling(12.3775, 1) # result is 20
assign result: ceiling(12.3775, -2) # result is 12.38
assign result: ceiling(-12.3775, -2) # result is -12.37
assign result: ceiling(-12.3775) # result is -12
character(value)
If the value is a text, it returns the first character. If the value is a non-negative 32-bit integer, it returns the character from that codepoint. Otherwise, it returns the empty string.
codepoint(text)
The codepoint function returns the codepoint number of the first character of the text. If the input value is not a text, or if it is the empty text, then it returns null.
extract(text, pattern, from, to)
The text is matched to the pattern. If it does not match, the result is null. If the pattern does match, then the result is a record containing the saved fields.
fallback(requestor_array)
The fallback requestor factory returns a requestor function that tries each of the requestors in the requstor_array until it gets a success. When the requestor is called, it calls the first requestor in requestor_array. If that is eventually successful, its value is passed to the callback. But if that requestor fails, the next requestor is called, and so on. If none of the requestors is successful, then the fallback fails. If any one succeeds, then the fallback succeeds.
The fallback requestor returns a cancel function that can be called when the result is no longer needed.
filter(array, function)
The filter function calls a function for every element in the array, passing each element and its element number.
(element, element_nr)
When the function's return value is true, then the element is copied into a new array. If the function's return value is false, then the element is not copied into the new array. If the return value is not a logical, then the filter returns null.
It returns a new array. The length of the new array is between 0 thru length(array). It returns null if the function input is not a function.
Example:
def data: [0, 1.25, 2, 3.5, 4, 5.75]
def integers: filter(data, integer?) # integers is [0, 2, 4]
find(array, function, reverse, from)
Call the function for each element of the array, passing each element and its element number.
(element, element_nr)
If the function returns true, then find returns the element number of the current element.
If the second input value is not a function, then it is compared exactly to the elements.
If the reverse input value is true, then search begins at the end of the array and works backward.
The from input value gives the element number to search first. The default is 0 unless reverse is true, when the default is length(array) - 1.
find returns the element number of the found value. If nothing is found, find returns null.
floor(number, place)
If place is 0 or null, the number is rounded down to the greatest integer that is less than or equal to the number. If place is a small positive integer, then the number is rounded down to that many decimal places. For positive numbers, this is like discarding decimal places.
Examples:
assign result: floor(12.3775) # result is 12
assign result: floor(12.3775, -2) # result is 12.37
assign result: floor(-12.3775, 0) # result is -13
assign result: floor(-12.3775, -2) # result is -12.38
for(array, function, reverse, exit)
For each. Call the function with each element of the array. The function is passed each element and its element number.
(element, element_nr)
If reverse is true, then it starts with the last element and works backwards.
If exit is not null, then when the function returns the exit value, then the for function returns early. The exit value is usually true or false, but it may be anything. If exit is null, then every element is processed.
The for function returns null unless it did an early exit, when it returns the exit value.
format(text, collection, transformer)
The format function makes a new text with substitutions in an original text. A collection is either an array of texts or a record of texts.
A search is made for {left brace and }right brace in the text. If they are found, the middle text between them is examined. If the collection is an array, the middle text is used as a number, and then the matched {left brace and middle and }right brace are replaced with the text at that subscript in the array. If the collection is a record, and if the middle text is the key of a member of the collection with a text value, then the value of the member is used in the substitution. Unmatched text is not altered.
The text between {left brace and }right brace is broken on the :colon character. The left text will be used as a number or name to select a value from the collection. (The value need not be a text.)
If a transformer input is a function, then it is called with the collection[left text] and right text as inputs. If it returns a text, then the substitution is made.
If a transformer input is a record, then the right text is used to select a function from the transformer. That function is passed the value from the collection. If the return value is a text, that text will substitute. If there is no colon, then the empty text is used to select the function from the transformer. If the transformer does not produce a function, or if the function does not return a text, then no replacement occurs. If transformer[right text](collection[left text]) produces a text, then the substitution is made.
If the substitution is not made, and if collection[left text] is a number, then the right text is used as a format input in calling collection[left text].text(right text). If it returns a text, then the substitution is made.
Example:
var result: format("{0} in {1}!", ["Malmborg", "Plano"])
# result is "Malmborg in Plano!"
fraction(number)
The fraction function returns the fractional part of a number It returns null for non-numbers. Also see whole.
length(value)
The length function
value result
array number of elements
blob number of bits
logical null
function number of named inputs
null null
number null
record record.length()
text number of codepoints
Length. Find the length of an array in elements, a blob in bits, or a text in codepoints. For functions, it is the arity (or number of inputs).
If the value is a record containing a length field
If the length field contains a function, then length(my_record) has the same effect as my_record.length().
If the length field contains a number, return the number.
All other values produce null.
lower(text)
The lower function returns a text in which all uppercase characters are converted to lowercase.
Example:
assign result: lower("Carl Hollywood") # result is "carl hollywood"
max(number, number)
Maximum. The result is the larger of the two numbers. If either input value is not a number, the result is null.
max(3, 4) # 4
max can be used with min to constrain values within an acceptable range.
min(max(2, 3), 7) # 3
min(max(4, 3), 7) # 4
min(max(8, 3), 7) # 7
max(1, null) # null
max(null, 1) # null
min(number,number)
Minimum. The result is the smaller of the two numbers. If either input value is not a number, the result is null.
min(3, 4) # 3
modulo(dividend, divisor)
The result of modulo(dividend, divisor) is dividend - (divisor * floor(dividend / divisor)). The result has the sign of the divisor.
If dividend is 0, then the result is 0. If divisor is 0, or if either operand is not a number, then the result is null. dividend and divisor are not required to be integers.
If both input values are integers, and if the divisor is a positive power of two, then it is the same as and(dividend, divisor - 1).
neg(number)
Negate. Reverse the sign of a number. If the input value is not a number, the result is null. Note that the -minus sign is not used as a prefix negation operator.
normalize(text)
Unicode normalize. Return a text whose textual value is the same as text, but whose binary representation is in the specified Unicode normalization form. The two texts will display the same, but might not be equal.
not(logical)
Not. Return the opposite logical. If the input value is not a logical, it returns null.
parallel(requestor_array, throttle, need)
Parallel. The parallel requestor factory returns a resquestor function. When the requestor function is called with a callback function and a value, every requestor in the requestor_array is called with the value, and the results are placed in corresponding elements of the results array. This all happens in parallel. The value produced by the first element of the requestor_array provides the first element of the result. The completed results array is passed to the callback function.
By default, it starts all of the requestors in the requestor_array at once, each in its own turn so that they do not interfere with each other. This can shock some systems by unleashing a lot of demand all at once. To mitigate the shock, the optional throttle argument sets the maximum number of requestors running at a time. As requestors succeed or fail, waiting requestors can be started. The throttle is optional. If provided, the throttle is a number: the maximum number of requestors to have running at any time.
Ordinarily, the number of successes must be the same as the number of requestors in the requestor_array. If you need few successes, specify your need with the need argument. The need could be 1, meaning that 1 or more successes are needed. The need could be 0, meaning that no successes are needed. If the number of successes is greater than or equal to need, then the whole operation succeeds. The need must be between 0 and requestor_array.length.
The requestor function itself returns a cancel function that cancels all of the pending requestors from the requestor_array.
race(requestor_array, throttle, need)
Race. The race function is a requestor factory that takes an array of requestor functions and returns a requestor function that starts all of the requestors in the requestor_array at once.
By default, it starts all of the requestors in the requestor_array at once, each in its own turn so that they do not interfere with each other. This can shock some systems by unleashing a lot of demand at once. To mitigate the shock, the optional throttle argument sets the maximum number of requestors running at a time. As requestors succeed or fail, waiting requestors can be started.
By default, a single result is produced. If an array of results is need, specify the needed number of results in the need parameter. When the needed number of successful results is obtained, the operation ends. The results go into a sparce array aligned with the requestor_array, and unfinished requestors are cancelled. The need argument must be between 1 and requestor_array.length.
The returned requestor function returns a cancel function that cancels all of the pending requestors from the requestor_array.
reduce(array, function, initial, reverse)
Reduce. The reduce function takes a function that takes two input values and returns a value.
function (first, second) {
return ...
}
The function is called for each element of the array, passing the result of the previous iteration to the next iteration.
The initial value is optional. If present, the function is called for every element of the array.
If initial is null:
If length(array) is 0, then it returns null.
If length(array) is 1, then it returns array[0].
If length(array) is 2, it returns function(array[0], array[1]).
If length(array) is 3, it returns function(function(array[0], array[1]), array[2]).
And so on.
If initial is not null:
If length(array) is 0, then it returns initial.
If length(array) is 1, then it returns function(initial, array[0]).
If length(array) is 2, it returns function(function(initial, array[0]), array[1]).
If length(array) is 3, it returns function(function(function(initial, array[0]), array[1]), array[2]).
And so on.
If reverse is true, then the work begins at the end of the array and works backward.
Example:
def data: [1, 2, 3, 4, 5, 6, 7, 8, 9]
def total: reduce(data, '+) # total is 45
def product: reduce(data, '*) # product is 362880
remainder(dividend, divisor)
Remainder. For fit integers, the remainder is dividend - ((dividend // divisor) * divisor).
replace(text, target, replacement, limit)
Return a new text in which the target is replaced by the replacement.
text: the source text.
target: a text or pattern that should be replaced in the source.
replacement: text to replace the matched text, or a function that takes the matched text and the starting position, and returns the replacement text or null if it should be left alone.
limit: The maximum number of replacements. The default is all possible replacements. The limit includes null matches.
reverse(array)
The reverse function makes a new array or blob with the elements or bits in the opposite order.
It returns a new, reversed array or blob.
Example:
def data: ["I", "am", "Sam"]
def result: reverse(data) # the result is ["Sam", "am", "I"]
round(number, place)
If place is 0 or null, the number is rounded to the nearest integer. If place is a small integer, then the number is rounded to that many decimal places.
Examples:
round(12.3775) # 12
round(12.3775, -2) # 12.38
round(-12.3775, 0) # -12
round(-12.3775, 1) # -10
round(-12.3775, 2) # 0
round(-12.3775, -2) # -12.38
search(text, target, from)
Search the text for the target. If the target is found, return the character position of the left-most part of the match. If the search fails, return null.
text: the source text.
target: a text or pattern that should be found in the source.
from: The starting position for the search. The default is 0, the beginning of the text. If from is negative, it is added to length(text).
sequence(requestor_array)
Sequence. The sequence requestor factory that takes a requestor_array and returns a requestor function that starts all of the requestors in the requestor_array one at a time. The result of each becomes the input to the next. The last result is the result of the sequence.
sequence returns a requestor that returns a cancel function and processes each requestor in requestor_array one at a time. Each of those requestors is passed the result of the previous requestor as its value argument. If all succeed, then the sequence succeeds, giving the result of the last of the requestors. If any fail, then the sequence fails.The sequence succeeds if every requestor in the requestor_array succeeds
sign(number)
The sign function returns -1 if the number is negative, 0 if the number is exactly 0, 1 if the number is positive, and null if it is not a number.
sort(array, select)
The sort function produces a new array in which the values are sorted. Sort keys must be either all numbers or all texts. Any other type of key or any error in the key calculation will cause the sort to fail, returning null. The sort is ascending. The sort is stable, so the relative order of equal keys is preserved.
The optional select input determines how the sort key for each element is selected.
select type Sort key for array[index] Description
null array[index] The sort key is the element itself. This is useful for sorting simple arrays of numbers or texts.
text array[index][select] The sort key is the select field of each record element. This is useful for sorting arrays of records.
number array[index][select] The sort key is the select element of each array element. This is useful for sorting arrays of arrays.
array select[index] select is an array of the same length containing the sort keys.
It returns a new, sorted array.
Examples:
def foods: ["oats", "peas", "beans", "barley"]
def result: sort(foods) # result is ["barley", "beans", "oats", "peas"]
var stooges: [
{first: "Moe", last: "Howard"}
{first: "Joe", last: "DeRita"}
{first: "Shemp", last: "Howard"}
{first: "Larry", last: "Fine"}
{first: "Joe", last: "Besser"}
{first: "Curly", last: "Howard"}
]
assign stooges: sort(sort(stooges, "first"), "last")
# stooges is now [
# {first: "Joe", last: "Besser"}
# {first: "Joe", last: "DeRita"}
# {first: "Larry", last: "Fine"}
# {first: "Curly", last: "Howard"}
# {first: "Moe", last: "Howard"}
# {first: "Shemp", last: "Howard"}
# ]
assign stooges: sort(stooges, [50, 60, 20, 40, 10, 30])
# stooges is now [
# {first: "Moe", last: "Howard"}
# {first: "Larry", last: "Fine"}
# {first: "Shemp", last: "Howard"}
# {first: "Curly", last: "Howard"}
# {first: "Joe", last: "Besser"}
# {first: "Joe", last: "DeRita"}
# ]
stone(value)
Petrify the value, turning it into stone. Its contents are preserved, but it can no longer be modified by the assign statement or the blob.write functions. This is usually performed on arrays, records, and blobs. All other types are already stone. Attempting to turn a value that is stone to stone will have no effect.
The stone operation is deep. Any mutable objects in the value will also be turned to stone.
This can not be reversed. Immutable objects can never become mutable. A mutable copy can be made of an immutable object, but the original object remains immutable.
The stone function returns the value.
trim(text, reject)
The trim function removes selected characters from the ends of a text. The default is to remove control characters and spaces.
assign result: " Hello there ".trim() # result is "Hello there"
trunc(number, place)
The number is truncated toward zero. If the number is positive, the result is the same as floor(place). If the number is negative, the result is the same as ceiling(place).
If place is a small integer, then the number is rounded down to that many decimal places. This is like discarding decimal places.
Examples:
assign result: trunc(12.3775, 0) # result is 12
assign result: trunc(12.3775, 2) # result is 12.37
assign result: trunc(-12.3775) # result is -12
assign result: trunc(-12.3775, 2) # result is -12.37
turkish_lower(text)
Similar to lower, except that I goes to ıdotless i.
turkish_upper(text)
Similar to upper, except that i goes to İI dot.
upper(text)
The upper function returns a text in which all lowercase characters are converted to uppercase.
Example:
assign result: upper("Carl Hollywood") # result is "CARL HOLLYWOOD"
whole(number)
The whole function returns the whole part of a number. It returns null for non-numbers. Also see fraction.

View File

@@ -2,7 +2,7 @@
![image](wizard.png)
Cell is an actor-based scripting language for building concurrent applications. It combines a familiar C-like syntax with the actor model of computation, optimized for low memory usage and simplicity.
Cell is an actor-based scripting language for building concurrent applications. It combines a familiar JavaScript-like syntax with the actor model of computation.
## Key Features
@@ -35,7 +35,7 @@ cell hello
## Standard Library
- [text](library/text.md) — string manipulation
- [number](library/number.md) — numeric operations (functions are global: `floor()`, `max()`, etc.)
- [number](library/number.md) — numeric operations
- [array](library/array.md) — array utilities
- [object](library/object.md) — object utilities
- [blob](library/blob.md) — binary data

View File

@@ -46,98 +46,98 @@ number("0xff", "j") // 255
## Methods
### abs(n)
### number.abs(n)
Absolute value.
```javascript
abs(-5) // 5
abs(5) // 5
number.abs(-5) // 5
number.abs(5) // 5
```
### sign(n)
### number.sign(n)
Returns -1, 0, or 1.
```javascript
sign(-5) // -1
sign(0) // 0
sign(5) // 1
number.sign(-5) // -1
number.sign(0) // 0
number.sign(5) // 1
```
### floor(n, place)
### number.floor(n, place)
Round down.
```javascript
floor(4.9) // 4
floor(4.567, 2) // 4.56
number.floor(4.9) // 4
number.floor(4.567, 2) // 4.56
```
### ceiling(n, place)
### number.ceiling(n, place)
Round up.
```javascript
ceiling(4.1) // 5
ceiling(4.123, 2) // 4.13
number.ceiling(4.1) // 5
number.ceiling(4.123, 2) // 4.13
```
### round(n, place)
### number.round(n, place)
Round to nearest.
```javascript
round(4.5) // 5
round(4.567, 2) // 4.57
number.round(4.5) // 5
number.round(4.567, 2) // 4.57
```
### trunc(n, place)
### number.trunc(n, place)
Truncate toward zero.
```javascript
trunc(4.9) // 4
trunc(-4.9) // -4
number.trunc(4.9) // 4
number.trunc(-4.9) // -4
```
### whole(n)
### number.whole(n)
Get the integer part.
```javascript
whole(4.9) // 4
whole(-4.9) // -4
number.whole(4.9) // 4
number.whole(-4.9) // -4
```
### fraction(n)
### number.fraction(n)
Get the fractional part.
```javascript
fraction(4.75) // 0.75
number.fraction(4.75) // 0.75
```
### min(...values)
### number.min(...values)
Return the smallest value.
```javascript
min(3, 1, 4, 1, 5) // 1
number.min(3, 1, 4, 1, 5) // 1
```
### max(...values)
### number.max(...values)
Return the largest value.
```javascript
max(3, 1, 4, 1, 5) // 5
number.max(3, 1, 4, 1, 5) // 5
```
### remainder(dividend, divisor)
### number.remainder(dividend, divisor)
Compute remainder.
```javascript
remainder(17, 5) // 2
number.remainder(17, 5) // 2
```

View File

@@ -70,18 +70,18 @@ text.search("hello world", "xyz") // null
text.search("hello hello", "hello", 1) // 6
```
### text.replace(text, target, replacement, cap)
### text.replace(text, target, replacement, limit)
Replace occurrences of `target` with `replacement`. If `cap` is not specified, replaces all occurrences.
Replace occurrences of `target` with `replacement`.
```javascript
text.replace("hello", "l", "L") // "heLLo" (replaces all)
text.replace("hello", "l", "L", 1) // "heLlo" (replaces first only)
text.replace("hello", "l", "L") // "heLLo"
text.replace("hello", "l", "L", 1) // "heLlo"
// With function
text.replace("hello", "l", function(match, pos) {
return pos == 2 ? "L" : match
}) // "heLLo" (replaces all by default)
}) // "heLlo"
```
### text.format(text, collection, transformer)

View File

@@ -1,248 +0,0 @@
# Cell actor scripting language
Cell is a Misty [https://mistysystem.com](https://mistysystem.com) implementation.
## Memory
Values are 32 bit for 32 bit builds and 64 bit for 64 bit builds.
### 32 bit value
LSB = 0
payload is a 31 bit signed int
LSB = 01
payload is a 30 bit pointer
LSB = 11
next 3 bits = special tag. 27 bits of payload.
### 64 bit value
LSB = 0
payload is a 32 bit signed int, using high 32 bits
LSB = 01
payload is a 61 bit pointer
LSB = 101
Short float: a 61 bit double, with 3 less exponent bits
LSB = 11
Special tag: next 3 bits. 5 bits total. 59 bits of payload. 8 total special tags.
Special tags:
1: Bool. Payload is 0 or 1.
2: null. payload is 0.
3: exception.
4: string.
Immediate string. Next 3 low bits = length in bytes. Rest is string data. This allows for strings up to 7 ascii letters. Encoded in utf8.
## Numbers and math
Cell can be compiled with different levels of exactness for numeracy. Any number which cannot be represented exactly becomes "null". Any numeric operation which includes "null" results in "null".
Using short floats in a 64 bit system means you have doubles in the range of +- 10^38, not the full range of double. If you create a number out of that range, it's null.
You can also compile a 64 bit system with full precision doubles, but this will use more memory and may be slower.
You can also compile a 64 bit system with 32 bit floats, stored as a 32 bit int is. Again, out of the 32 bit float range = null.
You can compile without floating point support at all; 32 bit ints are then used for fixed point calculations.
Or, you can compile using Dec64, which is a 64 bit decimal floating point format, for exact precision.
## Objects
Objects are heap allocated, referenced by a pointer value. They are all preceded by an object header, the length of a word on the system.
### 64 bit build
56 bits capacity
1 bit memory reclamation flag: note that this obj has already been moved
2 bit reserved (per object)
1 bit stone: note that this obj is immutable
3 bit type: note the type of the object
1 bit: fwd: note that this obj is a forward linkage
Last bit ..1:
The forward type indicates that the object (an array, blob, pretext, or record) has grown beyond its capacity and is now residing at a new address. The remaining 63 bits contain the address of the enlarged object. Forward linkages are cleaned up by the memory reclaimer.
Type 7: C light C object
Header
Pointer
Capacity is an ID of a registered C type.
Pointer is a pointer to the opaque C object.
Type 0: Array
Header
Length
Element[]
Capacity is number of elements the array can hold. Length is number of elements in use. Number of words used by an array is capacity + 2.
Type 1: blob
Header
Length
Bit[]
Capacity is number of bits the blob can hold. Length is number of bits in use. Bits follow, from [0] to [capacity - 1], with [0] bit in the most significant position of word 2, and [63] in the least significant position of word 2. The last word is zero filled, if necessary.
Number of words used is (capacity + 63) // 64 + 2
Type 2: Text
Text has two forms, depending on if it is stone or not, which changes the meaning of its length word.
Header
Length(pretext) or Hash(text)
Character[0] and character[1]
Capacity of pretex is the number of characters it can hold. During stoning and reclamation, capacity is set to the length.
The capacity of a text is its length.
The length of a pretext is the number of characters it contains; it is not greater than the capacity.
Hash of a text is used for organizing records. If the hash is zero, it's not been computed yet. All texts in the immutable memory have hashes.
A text object contains UTF32 characters, packed two per word. If the number of characters is odd, the least significant half of the last word is zero filled.
The number of words used by a text is (capacity + 1) // 2 + 2
Type 3: Record
A record is an array of fields represented as key/value pairs. Fields are located by hashes of texts, using open addressing with linear probing and lazy deletion. The load factor is less than 0.5.
Header
Prototype
Length
Key[0]
Value[0]
Key[1]
Value[1]
...
The capacity is the number of fields the record can hold. It is a power of two minus one. It is at least twice the length.
The length is the number of fields that the record currently contains.
A field candidate number is identified by and(key.hash, capacity). In case of hash collision, advance to the next field. If this goes past the end, continue with field 1. Field 0 is reserved.
The "exception" special tag is used to mark deleted entries in the object map.
The number of words used by a record is (capacity + 1) * 2.
Prototypes are searched for for properties if one cannot be found on the record itself. Prototypes can have prototypes.
#### key[0] and value[0]
These are reserved for internal use, and skipped over during key probing.
The first 32 bits of key are used as a 32 bit integer key, if this object has ever been used as a key itself.
The last 32 bits are used as an opaque C class key. C types can be registered with the system, and each are assigned a monotonically increasing number. In the case that this object has a C type, then the bottom 32 bits of key[0] are not 0. If that is the case, then a pointer to its C object is stored in value[0].
#### Valid keys & Hashing
Keys are stored directly in object maps. There are three possibilities for a vaild key: an object text, an object record, or an immediate text.
In the case of an immediate text, the hash is computed on the fly using the fash64_hash_one function, before being used to look up the key in the object map. Direct value comparison is used to confirm the key.
For object texts (texts longer than 7 ascii chars), the hash is stored in the text object itself. When an object text is used as a key, a stone version is created and interned. Any program static texts reference this stoned, interned text. When looking up a heap text as a key, it is first discovered if it's in the interned table. If it's not, the key is not in the object (since all keys are interned). If it is, the interned version is returned to check against the object map. The hash of the interned text is used to look up the key in the object map, and then direct pointer comparison is used to confirm the key.
For record keys, these are unique; once a record is used as a key, it gets assigned a monotonically increasing 32 bit integer, stored in key[0]. When checking it in an object map, the integer is used directly as the key. If key[0] is 0, the record has not been used as a key yet. If it's not 0, fash64_hash_one is used to compute a hash of its ID, and then direct value pointer comparison is used to confirm.
### Text interning
Texts that cannot fit in an immediate, and which are used as an object key, create a stoned and interned version (the pointer which is used as the key). Any text literals are also stoned and interned.
The interning table is an open addressed hash, with a load of 0.8, using a robin hood value. Probing is done using the text hash, confirmation is done using length, and then memcmp of the text.
When the GC run, a new interned text table is created. Each text literal, and each text used as a key, is added to the new table, as the live objects are copied. This keeps the interning table from becoming a graveyard. Interned values are never deleted until a GC.
Type 4: Function
Header
Code
Outer
A function object has zero capacity and is always stone.
Code is a pointer to the code object that the function executes.
Outer is a pointer to the frame that created this function object.
Size is 3 words.
Type 5: Frame
Header
Function
Caller
Return address
The activation frame is created when a function is invoked to hold its linkages and state.
The capacity is the number of slots, including the inputs, variables, temporaries, and the four words of overhead. A frame, unlike the other types, is never stone.
The function is the address of the function object being called.
The caller is the address of the frame that is invoking the function.
The return address is the address of the instruction in the code that should be executed upon return.
Next come the input arguments, if any.
Then the variables closed over by the inner functions.
Then the variables that are not closed over, followed by the temporaries.
When a function returns, the caller is set to zero. This is a signal to the memory reclaimer that the frame can be reduced.
Type 6: Code
Header
Arity
Size
Closure size
Entry point
Disruption point
A code object exists in the actor's immutable memory. A code object never exists in mutable memory.
A code object has a zero capacity and is always stone.
The arity is the maximum number of inputs.
The size is the capacity of an activation frame that will execute this code.
The closure size is a reduced capacity for returned frames that survive memory reclamation.
The entry point is the address at which to begin execution.
The disruption point is the address of the disruption clause.
### opaque C objects
Records can have opaque C data attached to them.
A C class can register a GC clean up, and a GC trace function. The trace function is called when the record is encountered in the live object graph; and it should mark any values it wants to keep alive in that function.
The system maintains an array of live opaque C objects. When such an object is encountered, it marks it as live in the array. When the GC completes, it iterates this array and calls the GC clean up function for each C object in the array with alive=0. Alive is then cleared for the next GC cycle.
## 32 bit build
~3 bit type
1 bit stone
1 bit memory reclamation flag
27 bit capacity
Key differences here are
blob max capacity is 2**27 bits = 2**24 bytes = 16 MB [this likely needs addressed]
fwd is type ...0, and the pointer is 31 bits
other types are
111 array
101 object
011 blob
001
## Memory
Cell uses a single block of memory that it doles out as needed to the actors in its system.
Actors are given a block of memory in standard sizes using a doubling buddy memory manager. An actor is given an immutable data section on birth, as well as a mutable data section. When its mutable data becomes full, it requests a new one. Actors utilize their mutable memory with a simple bump allocation. If there is not sufficient memory available, the actor suspends and its status changes to exhausted.
The smallest block size is determined per platform, but it can be as small as 4KB on 64 bit systems.
The actor is then given a new block of memory of the same size, and it runs a garbage collector to reclaim memory. It uses the cheney copying algorithm. If a disappointing amount of memory was reclaimed, it is noted, and the actor is given a larger block of memory on the next request.

View File

@@ -0,0 +1,234 @@
// HTTP Download Actor
// Handles download requests and progress queries
var http = use('http');
var os = use('os');
// Actor state
var state = {
downloading: false,
current_url: null,
total_bytes: 0,
downloaded_bytes: 0,
start_time: 0,
error: null,
connection: null,
download_msg: null,
chunks: []
};
// Helper to calculate progress percentage
function get_progress() {
if (state.total_bytes == 0) {
return 0;
}
return number.round((state.downloaded_bytes / state.total_bytes) * 100);
}
// Helper to format status response
function get_status() {
if (!state.downloading) {
return {
status: 'idle',
error: state.error
};
}
var elapsed = os.now() - state.start_time;
var bytes_per_sec = elapsed > 0 ? state.downloaded_bytes / elapsed : 0;
return {
status: 'downloading',
url: state.current_url,
progress: get_progress(),
downloaded_bytes: state.downloaded_bytes,
total_bytes: state.total_bytes,
elapsed_seconds: elapsed,
bytes_per_second: number.round(bytes_per_sec)
};
}
// Main message receiver
$receiver(function(msg) {
switch (msg.type) {
case 'download':
if (state.downloading) {
send(msg, {
type: 'error',
error: 'Already downloading',
current_url: state.current_url
});
return;
}
if (!msg.url) {
send(msg, {
type: 'error',
error: 'No URL provided'
});
return;
}
// Start download
state.downloading = true;
state.current_url = msg.url;
state.total_bytes = 0;
state.downloaded_bytes = 0;
state.start_time = os.now();
state.error = null;
state.download_msg = msg;
state.chunks = [];
try {
// Start the connection
state.connection = http.fetch_start(msg.url, msg.options || {});
if (!state.connection) {
throw new Error('Failed to start download');
}
// Schedule the first chunk read
$delay(read_next_chunk, 0);
} catch (e) {
state.error = e.toString();
state.downloading = false;
send(msg, {
type: 'error',
error: state.error,
url: msg.url
});
}
break;
case 'status':
log.console(`got status request. current is ${get_status()}`)
send(msg, {
type: 'status_response',
...get_status()
});
break;
case 'cancel':
if (state.downloading) {
// Cancel the download
if (state.connection) {
http.fetch_close(state.connection);
state.connection = null;
}
state.downloading = false;
state.current_url = null;
state.download_msg = null;
state.chunks = [];
send(msg, {
type: 'cancelled',
message: 'Download cancelled',
url: state.current_url
});
} else {
send(msg, {
type: 'error',
error: 'No download in progress'
});
}
break;
default:
send(msg, {
type: 'error',
error: 'Unknown message type: ' + msg.type
});
}
});
// Non-blocking chunk reader
function read_next_chunk() {
if (!state.downloading || !state.connection) {
return;
}
try {
var chunk = http.fetch_read_chunk(state.connection);
if (chunk == null) {
// Download complete
finish_download();
return;
}
// Store chunk
state.chunks.push(chunk);
// Update progress
var info = http.fetch_info(state.connection);
state.downloaded_bytes = info.bytes_read;
if (info.headers_complete && info.content_length > 0) {
state.total_bytes = info.content_length;
}
// Schedule next chunk read
$delay(read_next_chunk, 0);
} catch (e) {
// Error during download
state.error = e.toString();
if (state.connection) {
http.fetch_close(state.connection);
}
if (state.download_msg) {
send(state.download_msg, {
type: 'error',
error: state.error,
url: state.current_url
});
}
// Reset state
state.downloading = false;
state.connection = null;
state.download_msg = null;
state.chunks = [];
}
}
// Complete the download and send result
function finish_download() {
if (state.connection) {
http.fetch_close(state.connection);
}
// Combine all chunks into single ArrayBuffer
var total_size = 0;
for (var i = 0; i < state.chunks.length; i++) {
total_size += state.chunks[i].byteLength;
}
var result = new ArrayBuffer(total_size);
var view = new Uint8Array(result);
var offset = 0;
for (var i = 0; i < state.chunks.length; i++) {
var chunk_view = new Uint8Array(state.chunks[i]);
view.set(chunk_view, offset);
offset += state.chunks[i].byteLength;
}
// Send complete message
if (state.download_msg) {
send(state.download_msg, {
type: 'complete',
url: state.current_url,
data: result,
size: result.byteLength,
duration: os.now() - state.start_time
});
}
// Reset state
state.downloading = false;
state.connection = null;
state.current_url = null;
state.download_msg = null;
state.chunks = [];
}

View File

@@ -10,7 +10,7 @@ var match_id = 0;
$portal(e => {
log.console("NAT server: received connection request");
if (!is_actor(e.actor))
if (!isa(e.actor, actor))
send(e, {reason: "Must provide the actor you want to connect."});
if (waiting_client) {

93
fash.c
View File

@@ -1,93 +0,0 @@
/*
Fash64: Douglas Crockford (2017-02-02)
64-bit hash that uses the high 64 bits of a 128-bit product for feedback.
Notes:
- Requires a way to get the high half of a 64x64->128 multiply.
- Uses __uint128_t when available; otherwise uses MSVC _umul128.
*/
#include <stdint.h>
#include <stddef.h>
typedef struct fash64_state {
uint64_t result;
uint64_t sum;
} fash64_state;
enum {
FASH64_PRIME_11 = 11111111111111111027ull,
FASH64_PRIME_8 = 8888888888888888881ull,
FASH64_PRIME_3 = 3333333333333333271ull
};
static inline void fash64_mul_hi_lo(uint64_t a, uint64_t b, uint64_t *hi, uint64_t *lo)
{
#if defined(__SIZEOF_INT128__)
__uint128_t p = (__uint128_t)a * (__uint128_t)b;
*lo = (uint64_t)p;
*hi = (uint64_t)(p >> 64);
#elif defined(_MSC_VER) && defined(_M_X64)
*lo = _umul128(a, b, hi);
#else
/* Portable fallback (no 128-bit type, no _umul128). */
uint64_t a0 = (uint32_t)a;
uint64_t a1 = a >> 32;
uint64_t b0 = (uint32_t)b;
uint64_t b1 = b >> 32;
uint64_t p00 = a0 * b0;
uint64_t p01 = a0 * b1;
uint64_t p10 = a1 * b0;
uint64_t p11 = a1 * b1;
uint64_t mid = (p00 >> 32) + (uint32_t)p01 + (uint32_t)p10;
*lo = (p00 & 0xffffffffull) | (mid << 32);
*hi = p11 + (p01 >> 32) + (p10 >> 32) + (mid >> 32);
#endif
}
static inline void fash64_begin(fash64_state *s)
{
s->result = (uint64_t)FASH64_PRIME_8;
s->sum = (uint64_t)FASH64_PRIME_3;
}
static inline void fash64_word(fash64_state *s, uint64_t word)
{
uint64_t high, low;
uint64_t mixed = s->result ^ word;
fash64_mul_hi_lo(mixed, (uint64_t)FASH64_PRIME_11, &high, &low);
s->sum += high;
s->result = low ^ s->sum;
}
static inline void fash64_block(fash64_state *s, const uint64_t *block, size_t word_count)
{
for (size_t i = 0; i < word_count; i++) fash64_word(s, block[i]);
}
static inline uint64_t fash64_end(const fash64_state *s)
{
return s->result;
}
/* Convenience one-shot helper */
static inline uint64_t fash64_hash_words(const uint64_t *words, size_t word_count, uint64_t extra_word)
{
fash64_state s;
fash64_begin(&s);
fash64_block(&s, words, word_count);
fash64_word(&s, extra_word);
return fash64_end(&s);
}
static inline uint64_t fash64_hash_one(uint64_t word)
{
uint64_t high, low;
uint64_t mixed = (uint64_t)FASH64_PRIME_8 ^ word;
fash64_mul_hi_lo(mixed, (uint64_t)FASH64_PRIME_11, &high, &low);
return low ^ ((uint64_t)FASH64_PRIME_3 + high);
}

0
fash.h
View File

23
fd.c
View File

@@ -50,7 +50,7 @@ JSC_SCALL(fd_open,
mode_t mode = 0644;
// Parse optional flags argument
if (argc > 1 && JS_IsText(argv[1])) {
if (argc > 1 && JS_IsString(argv[1])) {
const char *flag_str = JS_ToCString(js, argv[1]);
flags = 0;
@@ -78,7 +78,7 @@ JSC_CCALL(fd_write,
size_t len;
ssize_t wrote;
if (JS_IsText(argv[1])) {
if (JS_IsString(argv[1])) {
const char *data = JS_ToCStringLen(js, &len, argv[1]);
if (!data) return JS_EXCEPTION;
wrote = write(fd, data, len);
@@ -276,7 +276,7 @@ JSC_SCALL(fd_mkdir,
JSC_SCALL(fd_mv,
if (argc < 2)
ret = JS_ThrowTypeError(js, "fd.mv requires 2 arguments: old path and new path");
else if (!JS_IsText(argv[1]))
else if (!JS_IsString(argv[1]))
ret = JS_ThrowTypeError(js, "second argument must be a string (new path)");
else {
const char *new_path = JS_ToCString(js, argv[1]);
@@ -289,7 +289,7 @@ JSC_SCALL(fd_mv,
JSC_SCALL(fd_symlink,
if (argc < 2)
ret = JS_ThrowTypeError(js, "fd.symlink requires 2 arguments: target and link path");
else if (!JS_IsText(argv[1]))
else if (!JS_IsString(argv[1]))
ret = JS_ThrowTypeError(js, "second argument must be a string (link path)");
else {
const char *link_path = JS_ToCString(js, argv[1]);
@@ -502,9 +502,10 @@ JSC_SCALL(fd_readdir,
ret = JS_ThrowInternalError(js, "FindFirstFile failed for %s", path);
} else {
ret = JS_NewArray(js);
int i = 0;
do {
if (strcmp(ffd.cFileName, ".") == 0 || strcmp(ffd.cFileName, "..") == 0) continue;
JS_ArrayPush(js, ret,JS_NewString(js, ffd.cFileName));
JS_SetPropertyUint32(js, ret, i++, JS_NewString(js, ffd.cFileName));
} while (FindNextFile(hFind, &ffd) != 0);
FindClose(hFind);
}
@@ -514,9 +515,10 @@ JSC_SCALL(fd_readdir,
d = opendir(str);
if (d) {
ret = JS_NewArray(js);
int i = 0;
while ((dir = readdir(d)) != NULL) {
if (strcmp(dir->d_name, ".") == 0 || strcmp(dir->d_name, "..") == 0) continue;
JS_ArrayPush(js, ret, JS_NewString(js, dir->d_name));
JS_SetPropertyUint32(js, ret, i++, JS_NewString(js, dir->d_name));
}
closedir(d);
} else {
@@ -557,22 +559,19 @@ JSC_CCALL(fd_slurpwrite,
size_t len;
const char *data = js_get_blob_data(js, &len, argv[1]);
if (!data && len > 0)
if (data == (const char *)-1)
return JS_EXCEPTION;
const char *str = JS_ToCString(js, argv[0]);
if (!str) return JS_EXCEPTION;
int fd = open(str, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
JS_FreeCString(js, str);
JS_FreeCString(js, str);
if (fd < 0)
return JS_ThrowInternalError(js, "open failed for %s: %s", str, strerror(errno));
}
ssize_t written = write(fd, data, len);
close(fd);
JS_FreeCString(js, str);
if (written != (ssize_t)len)
return JS_ThrowInternalError(js, "write failed for %s: %s", str, strerror(errno));

62
fd.cm
View File

@@ -1,67 +1,31 @@
var fd = this
var wildstar = use('wildstar')
function last_pos(str, sep) {
var last = null
replace(str, sep, function(m, pos) {
last = pos
return m
})
return last
}
// Helper to join paths
function join_paths(base, rel) {
base = replace(base, /\/+$/, "")
rel = replace(rel, /^\/+/, "")
base = base.replace(/\/+$/, "")
rel = rel.replace(/^\/+/, "")
if (!base) return rel
if (!rel) return base
return base + "/" + rel
}
fd.join_paths = join_paths
fd.basename = function basename(path) {
var last = last_pos(path, '/')
if (last == null) return path
return text(path, last+1)
}
fd.dirname = function dirname(path) {
var last = last_pos(path, '/')
if (last == null) return ""
return text(path,0,last)
}
fd.stem = function stem(path) {
var last = last_pos(path, '.')
if (last == null) return path
return text(path,0,last)
}
fd.globfs = function(globs, dir) {
if (dir == null) dir = "."
var results = []
function check_neg(path) {
var found = false;
arrfor(globs, function(g) {
if (starts_with(g, "!") && wildstar.match(text(g, 1), path, wildstar.WM_WILDSTAR)) {
found = true;
return true;
}
}, null, true);
return found;
for (var g of globs) {
if (g.startsWith("!") && wildstar.match(g.substring(1), path, wildstar.WM_WILDSTAR)) return true;
}
return false;
}
function check_pos(path) {
var found = false;
arrfor(globs, function(g) {
if (!starts_with(g, "!") && wildstar.match(g, path, wildstar.WM_WILDSTAR)) {
found = true;
return true;
}
}, null, true);
return found;
for (var g of globs) {
if (!g.startsWith("!") && wildstar.match(g, path, wildstar.WM_WILDSTAR)) return true;
}
return false;
}
function visit(curr_full, rel_prefix) {
@@ -70,7 +34,7 @@ fd.globfs = function(globs, dir) {
var list = fd.readdir(curr_full)
if (!list) return
arrfor(list, function(item) {
for (var item of list) {
var item_rel = rel_prefix ? rel_prefix + "/" + item : item
var child_full = join_paths(curr_full, item)
@@ -82,10 +46,10 @@ fd.globfs = function(globs, dir) {
}
} else {
if (!check_neg(item_rel) && check_pos(item_rel)) {
push(results, item_rel)
results.push(item_rel)
}
}
});
}
}
var st = fd.stat(dir)

View File

@@ -39,7 +39,7 @@ JSC_SCALL(fd_open,
FileOptions flags = kFileRead;
// Parse optional flags argument
if (argc > 1 && JS_IsText(argv[1])) {
if (argc > 1 && JS_IsString(argv[1])) {
const char *flag_str = JS_ToCString(js, argv[1]);
flags = 0;
@@ -70,7 +70,7 @@ JSC_CCALL(fd_write,
size_t len;
int wrote;
if (JS_IsText(argv[1])) {
if (JS_IsString(argv[1])) {
const char *data = JS_ToCStringLen(js, &len, argv[1]);
if (!data) return JS_EXCEPTION;
wrote = pd_file->write(fd, data, (unsigned int)len);
@@ -202,7 +202,7 @@ JSC_SCALL(fd_mkdir,
JSC_SCALL(fd_mv,
if (argc < 2)
ret = JS_ThrowTypeError(js, "fd.mv requires 2 arguments: old path and new path");
else if (!JS_IsText(argv[1]))
else if (!JS_IsString(argv[1]))
ret = JS_ThrowTypeError(js, "second argument must be a string (new path)");
else {
const char *new_path = JS_ToCString(js, argv[1]);
@@ -216,7 +216,7 @@ JSC_SCALL(fd_mv,
JSC_SCALL(fd_symlink,
// Not supported
if (argc >= 2 && JS_IsText(argv[1])) {
if (argc >= 2 && JS_IsString(argv[1])) {
// consume arg
JS_FreeCString(js, JS_ToCString(js, argv[1]));
}

View File

@@ -13,7 +13,7 @@ var shop = use('internal/shop')
// Parse arguments
var target_pkg = null
for (var i = 0; i < length(args); i++) {
for (var i = 0; i < args.length; i++) {
if (args[i] == '--help' || args[i] == '-h') {
log.console("Usage: cell fetch [package]")
log.console("Fetch package zips from remote sources.")
@@ -24,7 +24,7 @@ for (var i = 0; i < length(args); i++) {
log.console("This command ensures that the zip files on disk match what's in")
log.console("the lock file. For local packages, this is a no-op.")
$stop()
} else if (!starts_with(args[i], '-')) {
} else if (!args[i].startsWith('-')) {
target_pkg = args[i]
}
}
@@ -35,55 +35,52 @@ var packages_to_fetch = []
if (target_pkg) {
// Fetch specific package
if (find(all_packages, target_pkg) == null) {
if (!all_packages.includes(target_pkg)) {
log.error("Package not found: " + target_pkg)
$stop()
}
push(packages_to_fetch, target_pkg)
packages_to_fetch.push(target_pkg)
} else {
// Fetch all packages
packages_to_fetch = all_packages
}
var remote_count = 0
arrfor(packages_to_fetch, function(pkg) {
var entry = lock[pkg]
if (pkg != 'core' && (!entry || entry.type != 'local'))
remote_count++
}, null, null)
log.console("Fetching " + text(packages_to_fetch.length) + " package(s)...")
if (remote_count > 0)
log.console(`Fetching ${text(remote_count)} remote package(s)...`)
var downloaded_count = 0
var cached_count = 0
var success_count = 0
var skip_count = 0
var fail_count = 0
arrfor(packages_to_fetch, function(pkg) {
for (var pkg of packages_to_fetch) {
var entry = lock[pkg]
// Skip local packages
if (entry && entry.type == 'local') {
skip_count++
continue
}
// Skip core (handled separately)
if (pkg == 'core') return
if (pkg == 'core') {
skip_count++
continue
}
var result = shop.fetch(pkg)
if (result.status == 'local') {
// Local packages are just symlinks, nothing to fetch
return
} else if (result.status == 'cached') {
cached_count++
} else if (result.status == 'downloaded') {
log.console(" Downloaded: " + pkg)
downloaded_count++
} else if (result.status == 'error') {
log.error(" Failed: " + pkg + (result.message ? " - " + result.message : ""))
if (result) {
if (result.zip_blob) {
log.console("Fetched: " + pkg)
success_count++
} else {
skip_count++
}
} else {
log.error("Failed to fetch: " + pkg)
fail_count++
}
}, null, null)
}
log.console("")
var parts = []
if (downloaded_count > 0) push(parts, `${text(downloaded_count)} downloaded`)
if (cached_count > 0) push(parts, `${text(cached_count)} cached`)
if (fail_count > 0) push(parts, `${text(fail_count)} failed`)
if (length(parts) == 0) push(parts, "nothing to fetch")
log.console("Fetch complete: " + text(parts, ", "))
log.console("Fetch complete: " + text(success_count) + " fetched, " + text(skip_count) + " skipped, " + text(fail_count) + " failed")
$stop()

View File

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

236
graph.ce
View File

@@ -1,236 +0,0 @@
// cell graph [<locator>] - Emit dependency graph
//
// Usage:
// cell graph Graph current directory package
// cell graph . Graph current directory package
// cell graph <locator> Graph specific package
// cell graph --world Graph all packages in shop (world set)
//
// Options:
// --format <fmt> Output format: tree (default), dot, json
// --resolved Show resolved view with links applied (default)
// --locked Show lock view without links
// --world Graph all packages in shop
var shop = use('internal/shop')
var pkg = use('package')
var link = use('link')
var fd = use('fd')
var json = use('json')
var target_locator = null
var format = 'tree'
var show_locked = false
var show_world = false
for (var i = 0; i < length(args); i++) {
if (args[i] == '--format' || args[i] == '-f') {
if (i + 1 < length(args)) {
format = args[++i]
if (format != 'tree' && format != 'dot' && format != 'json') {
log.error('Invalid format: ' + format + '. Must be tree, dot, or json')
$stop()
}
} else {
log.error('--format requires a format type')
$stop()
}
} else if (args[i] == '--resolved') {
show_locked = false
} else if (args[i] == '--locked') {
show_locked = true
} else if (args[i] == '--world') {
show_world = true
} else if (args[i] == '--help' || args[i] == '-h') {
log.console("Usage: cell graph [<locator>] [options]")
log.console("")
log.console("Emit the dependency graph.")
log.console("")
log.console("Options:")
log.console(" --format <fmt> Output format: tree (default), dot, json")
log.console(" --resolved Show resolved view with links applied (default)")
log.console(" --locked Show lock view without links")
log.console(" --world Graph all packages in shop")
$stop()
} else if (!starts_with(args[i], '-')) {
target_locator = args[i]
}
}
var links = show_locked ? {} : link.load()
// Get effective locator (after links)
function get_effective(locator) {
return links[locator] || locator
}
// Build graph data structure
var nodes = {}
var edges = []
function add_node(locator) {
if (nodes[locator]) return
var lock = shop.load_lock()
var lock_entry = lock[locator]
var link_target = links[locator]
var info = shop.resolve_package_info(locator)
nodes[locator] = {
id: locator,
effective: get_effective(locator),
linked: link_target != null,
local: info == 'local',
commit: lock_entry && lock_entry.commit ? text(lock_entry.commit, 0, 8) : null
}
}
function gather_graph(locator, visited) {
if (visited[locator]) return
visited[locator] = true
add_node(locator)
try {
var deps = pkg.dependencies(locator)
if (deps) {
arrfor(array(deps), function(alias) {
var dep_locator = deps[alias]
add_node(dep_locator)
push(edges, { from: locator, to: dep_locator, alias: alias })
gather_graph(dep_locator, visited)
})
}
} catch (e) {
// Package might not have dependencies
}
}
// Gather graph from roots
var roots = []
if (show_world) {
// Use all packages in shop as roots
var packages = shop.list_packages()
arrfor(packages, function(p) {
if (p != 'core') {
push(roots, p)
}
})
} else {
// Default to current directory
if (!target_locator) {
target_locator = '.'
}
// Resolve local paths
if (target_locator == '.' || starts_with(target_locator, './') || starts_with(target_locator, '../') || fd.is_dir(target_locator)) {
var resolved = fd.realpath(target_locator)
if (resolved) {
target_locator = resolved
}
}
push(roots, target_locator)
}
arrfor(roots, function(root) {
gather_graph(root, {})
})
// Output based on format
if (format == 'tree') {
function print_tree(locator, prefix, is_last, visited) {
if (visited[locator]) {
log.console(prefix + (is_last ? "\\-- " : "|-- ") + locator + " (circular)")
return
}
visited[locator] = true
var node = nodes[locator]
var suffix = ""
if (node.linked) suffix += " -> " + node.effective
if (node.commit) suffix += " @" + node.commit
if (node.local) suffix += " (local)"
log.console(prefix + (is_last ? "\\-- " : "|-- ") + locator + suffix)
// Get children
var children = []
arrfor(edges, function(e) {
if (e.from == locator) {
push(children, e)
}
})
for (var i = 0; i < length(children); i++) {
var child_prefix = prefix + (is_last ? " " : "| ")
print_tree(children[i].to, child_prefix, i == length(children) - 1, visited)
}
}
for (var i = 0; i < length(roots); i++) {
log.console(roots[i])
var children = []
arrfor(edges, function(e) {
if (e.from == roots[i]) {
push(children, e)
}
})
for (var j = 0; j < length(children); j++) {
print_tree(children[j].to, "", j == length(children) - 1, {})
}
if (i < length(roots) - 1) log.console("")
}
} else if (format == 'dot') {
log.console("digraph dependencies {")
log.console(" rankdir=TB;")
log.console(" node [shape=box];")
log.console("")
// Node definitions
arrfor(array(nodes), function(id) {
var node = nodes[id]
var label = id
if (node.commit) label += "\\n@" + node.commit
var attrs = 'label="' + label + '"'
if (node.linked) attrs += ', style=dashed'
if (node.local) attrs += ', color=blue'
// Safe node ID for dot
var safe_id = replace(id, /[^a-zA-Z0-9]/g, '_')
log.console(' ' + safe_id + ' [' + attrs + '];')
})
log.console("")
// Edges
arrfor(edges, function(e) {
var from_id = replace(e.from, /[^a-zA-Z0-9]/g, '_')
var to_id = replace(e.to, /[^a-zA-Z0-9]/g, '_')
var label = e.alias != e.to ? 'label="' + e.alias + '"' : ''
log.console(' ' + from_id + ' -> ' + to_id + (label ? ' [' + label + ']' : '') + ';')
})
log.console("}")
} else if (format == 'json') {
var output = {
nodes: [],
edges: []
}
arrfor(array(nodes), function(id) {
push(output.nodes, nodes[id])
})
output.edges = edges
log.console(json.encode(output))
}
$stop()

46
help.ce
View File

@@ -2,7 +2,7 @@
var fd = use('fd')
var command = length(args) > 0 ? args[0] : null
var command = args.length > 0 ? args[0] : null
// Display specific command help
if (command) {
@@ -27,41 +27,21 @@ if (stat && stat.isFile) {
log.console(content)
} else {
// Fallback if man file doesn't exist
log.console("cell - The Cell package manager")
log.console("cell - The Cell module system for Prosperon")
log.console("")
log.console("Usage: cell <command> [arguments]")
log.console("")
log.console("Package Management:")
log.console(" install <locator> Install a package and its dependencies")
log.console(" update [locator] Update packages from remote sources")
log.console(" remove <locator> Remove a package from the shop")
log.console(" add <locator> Add a dependency to current package")
log.console("Commands:")
log.console(" init Initialize a new Cell project")
log.console(" get Fetch and add a module dependency")
log.console(" update Update a dependency to a new version")
log.console(" vendor Copy all dependencies locally")
log.console(" build Compile all modules to bytecode")
log.console(" patch Create a patch for a module")
log.console(" config Manage system and actor configurations")
log.console(" help Show this help message")
log.console("")
log.console("Building:")
log.console(" build [locator] Build dynamic libraries for packages")
log.console(" clean [scope] Remove build artifacts")
log.console("")
log.console("Linking (Local Development):")
log.console(" link <origin> <target> Link a package to a local path")
log.console(" unlink <origin> Remove a package link")
log.console(" clone <origin> <path> Clone and link a package locally")
log.console("")
log.console("Information:")
log.console(" list [scope] List packages and dependencies")
log.console(" ls [locator] List modules and actors in a package")
log.console(" why <locator> Show reverse dependencies")
log.console(" search <query> Search for packages, modules, or actors")
log.console("")
log.console("Diagnostics:")
log.console(" resolve [locator] Print fully resolved dependency closure")
log.console(" graph [locator] Emit dependency graph (tree, dot, json)")
log.console(" verify [scope] Verify integrity and consistency")
log.console("")
log.console("Other:")
log.console(" help [command] Show help for a command")
log.console(" version Show cell version")
log.console("")
log.console("Run 'cell <command> --help' for more information on a command.")
log.console("Run 'cell help <command>' for more information on a command.")
}
$stop()
$stop()

View File

@@ -1,185 +1,62 @@
// cell install <locator> - Install a package to the shop
//
// Usage:
// cell install <locator> Install a package and its dependencies
// cell install . Install current directory package
//
// Options:
// --target <triple> Build for target platform
// --refresh Refresh floating refs before locking
// --dry-run Show what would be installed
// Does not modify the current project's cell.toml
var shop = use('internal/shop')
var build = use('build')
var pkg = use('package')
var fd = use('fd')
if (length(args) < 1) {
log.console("Usage: cell install <locator> [options]")
log.console("")
log.console("Options:")
log.console(" --target <triple> Build for target platform")
log.console(" --refresh Refresh floating refs before locking")
log.console(" --dry-run Show what would be installed")
$stop()
}
var locator = null
var target_triple = null
var refresh = false
var dry_run = false
for (var i = 0; i < length(args); i++) {
if (args[i] == '--target' || args[i] == '-t') {
if (i + 1 < length(args)) {
target_triple = args[++i]
} else {
log.error('--target requires a triple')
$stop()
}
} else if (args[i] == '--refresh') {
refresh = true
} else if (args[i] == '--dry-run') {
dry_run = true
} else if (args[i] == '--help' || args[i] == '-h') {
log.console("Usage: cell install <locator> [options]")
log.console("")
log.console("Install a package and its dependencies to the shop.")
log.console("")
log.console("Options:")
log.console(" --target <triple> Build for target platform")
log.console(" --refresh Refresh floating refs before locking")
log.console(" --dry-run Show what would be installed")
$stop()
} else if (!starts_with(args[i], '-')) {
locator = args[i]
}
}
if (!locator) {
if (args.length < 1) {
log.console("Usage: cell install <locator>")
$stop()
return
}
var locator = args[0]
// Resolve relative paths to absolute paths
// Local paths like '.' or '../foo' need to be converted to absolute paths
if (locator == '.' || starts_with(locator, './') || starts_with(locator, '../') || fd.is_dir(locator)) {
if (locator == '.' || locator.startsWith('./') || locator.startsWith('../') || fd.is_dir(locator)) {
var resolved = fd.realpath(locator)
if (resolved) {
locator = resolved
}
}
// Default target
if (!target_triple) {
target_triple = build.detect_host_target()
}
log.console("Installing " + locator + "...")
// Gather all packages that will be installed
var packages_to_install = []
var skipped_packages = []
var visited = {}
var pkg = use('package')
function gather_packages(pkg_locator) {
// Recursive install function that handles dependencies
function install_package(pkg_locator, visited) {
if (visited[pkg_locator]) return
visited[pkg_locator] = true
// Check if this is a local path that doesn't exist
if (starts_with(pkg_locator, '/') && !fd.is_dir(pkg_locator)) {
push(skipped_packages, pkg_locator)
log.console(" Skipping missing local package: " + pkg_locator)
return
}
push(packages_to_install, pkg_locator)
// Try to read dependencies
// First, add to lock.toml
shop.update(pkg_locator)
// Extract/symlink the package so we can read its cell.toml
shop.extract(pkg_locator)
// Now get direct dependencies and install them first
try {
// For packages not yet extracted, we need to update and extract first to read deps
var lock = shop.load_lock()
if (!lock[pkg_locator]) {
if (!dry_run) {
var update_result = shop.update(pkg_locator)
if (update_result) {
shop.extract(pkg_locator)
} else {
// Update failed - package might not be fetchable
log.console("Warning: Could not fetch " + pkg_locator)
return
}
}
} else {
// Package is in lock, ensure it's extracted
if (!dry_run) {
shop.extract(pkg_locator)
}
}
var deps = pkg.dependencies(pkg_locator)
if (deps) {
arrfor(array(deps), function(alias) {
for (var alias in deps) {
var dep_locator = deps[alias]
gather_packages(dep_locator)
})
log.console("Installing dependency " + dep_locator)
install_package(dep_locator, visited)
}
}
} catch (e) {
// Package might not have dependencies or cell.toml issue
if (!dry_run) {
log.console(`Warning: Could not read dependencies for ${pkg_locator}: ${e.message}`)
}
log.console("Warning: Could not read dependencies for " + pkg_locator + ": " + e.message)
}
// Build the package after all dependencies are installed
build.build_package(pkg_locator)
}
// Gather all packages
gather_packages(locator)
if (dry_run) {
log.console("Would install:")
arrfor(packages_to_install, function(p) {
var lock = shop.load_lock()
var exists = lock[p] != null
log.console(" " + p + (exists ? " (already installed)" : ""))
})
if (length(skipped_packages) > 0) {
log.console("")
log.console("Would skip (missing local paths):")
arrfor(skipped_packages, function(p) {
log.console(" " + p)
})
}
$stop()
}
// Install each package
function install_package(pkg_locator) {
// Update lock entry
shop.update(pkg_locator)
// Extract/symlink the package
shop.extract(pkg_locator)
// Build scripts
shop.build_package_scripts(pkg_locator)
// Build C code
try {
build.build_dynamic(pkg_locator, target_triple, 'release')
} catch (e) {
// Not all packages have C code
}
}
arrfor(packages_to_install, function(p) {
log.console(" Installing " + p + "...")
install_package(p)
})
var summary = "Installed " + text(length(packages_to_install)) + " package(s)."
if (length(skipped_packages) > 0) {
summary += " Skipped " + text(length(skipped_packages)) + " missing local path(s)."
}
log.console(summary)
install_package(locator, {})
log.console("Installed " + locator)
$stop()

View File

@@ -1,12 +1,19 @@
(function engine() {
// Hidden vars (os, actorsym, init, core_path) come from env
var ACTORDATA = actorsym
(function engine() {
var _cell = globalThis.cell
delete globalThis.cell
var ACTORDATA = _cell.hidden.actorsym
var SYSYM = '__SYSTEM__'
var _cell = {}
var hidden = _cell.hidden
var os = hidden.os;
_cell.os = null
var dylib_ext
_cell.id ??= "newguy"
switch(os.platform()) {
case 'Windows': dylib_ext = '.dll'; break;
case 'macOS': dylib_ext = '.dylib'; break;
@@ -18,10 +25,11 @@ var ACTOR_EXT = '.ce'
var load_internal = os.load_internal
function use_embed(name) {
return load_internal("js_" + name + "_use")
return load_internal(`js_${name}_use`)
}
function logical(val1) {
globalThis.logical = function(val1)
{
if (val1 == 0 || val1 == false || val1 == "false" || val1 == null)
return false;
if (val1 == 1 || val1 == true || val1 == "true")
@@ -29,36 +37,20 @@ function logical(val1) {
return null;
}
function some(arr, pred) {
return find(arr, pred) != null
}
function every(arr, pred) {
return find(arr, x => not(pred(x))) == null
}
function starts_with(str, prefix) {
return search(str, prefix) == 0
}
function ends_with(str, suffix) {
return search(str, suffix, -length(suffix)) != null
}
var js = use_embed('js')
var fd = use_embed('fd')
// Get the shop path from HOME environment
var home = os.getenv('HOME') || os.getenv('USERPROFILE')
if (!home) {
throw Error('Could not determine home directory')
throw new Error('Could not determine home directory')
}
var shop_path = home + '/.cell'
var packages_path = shop_path + '/packages'
var core_path = packages_path + '/core'
if (!fd.is_dir(core_path)) {
throw Error('Cell shop not found at ' + shop_path + '. Run "cell install" to set up.')
throw new Error('Cell shop not found at ' + shop_path + '. Run "cell install" to set up.')
}
var use_cache = {}
@@ -70,7 +62,7 @@ function use_core(path) {
if (use_cache[cache_key])
return use_cache[cache_key];
var sym = use_embed(replace(path, '/', '_'))
var sym = use_embed(path.replace('/','_'))
// Core scripts are in packages/core/
var file_path = core_path + '/' + path + MOD_EXT
@@ -80,7 +72,7 @@ function use_core(path) {
var script = text(script_blob)
var mod = `(function setup_module(use){${script}})`
var fn = js.eval('core:' + path, mod)
var result = call(fn,sym, [use_core])
var result = fn.call(sym, use_core);
use_cache[cache_key] = result;
return result;
}
@@ -91,7 +83,14 @@ function use_core(path) {
var blob = use_core('blob')
function actor() {
// Capture Object and Array methods before they're deleted
Object.prototype.toString = function()
{
return json.encode(this)
}
globalThis.actor = function()
{
}
@@ -99,8 +98,43 @@ var actor_mod = use_core('actor')
var wota = use_core('wota')
var nota = use_core('nota')
function is_actor(value) {
return is_object(value) && value[ACTORDATA]
globalThis.isa = function(value, master) {
if (master == null) return false
// isa(value, function) - check if function.prototype is in chain
if (typeof master == 'function') {
// Special type checks
if (master == stone) return is_stone(value)
if (master == number) return is_number(value)
if (master == text) return is_text(value)
if (master == logical) return is_logical(value)
if (master == array) return is_array(value)
if (master == object) return is_object(value)
if (master == fn) return is_function(value)
if (master == actor) return is_object(value) && value[ACTORDATA]
// Check prototype chain
if (master.prototype) {
var proto = _getPrototypeOf(value)
while (proto != null) {
if (proto == master.prototype) return true
proto = _getPrototypeOf(proto)
}
}
return false
}
// isa(object, master_object) - check prototype chain
if (typeof master == 'object') {
var proto = _getPrototypeOf(value)
while (proto != null) {
if (proto == master) return true
proto = _getPrototypeOf(proto)
}
return false
}
return false
}
var ENETSERVICE = 0.1
@@ -111,12 +145,12 @@ function caller_data(depth = 0)
var file = "nofile"
var line = 0
var caller = array(Error().stack, "\n")[1+depth]
var caller = new Error().stack.split("\n")[1+depth]
if (caller) {
var md = extract(caller, /\((.*)\:/)
var md = caller.match(/\((.*)\:/)
var m = md ? md[1] : "SCRIPT"
if (m) file = m
md = extract(caller, /\:(\d*)\)/)
md = caller.match(/\:(\d*)\)/)
m = md ? md[1] : 0
if (m) line = m
}
@@ -125,62 +159,52 @@ function caller_data(depth = 0)
}
function console_rec(line, file, msg) {
return `[${text(_cell.id, 0, 5)}] [${file}:${line}]: ${msg}\n`
return `[${_cell.id.slice(0,5)}] [${file}:${line}]: ${msg}\n`
// time: [${time.text("mb d yyyy h:nn:ss")}]
}
function log(name, args) {
globalThis.log = {}
log.console = function(msg)
{
var caller = caller_data(1)
var msg = args[0]
os.print(console_rec(caller.line, caller.file, msg))
}
switch(name) {
case 'console':
os.print(console_rec(caller.line, caller.file, msg))
break
case 'error':
msg = msg ?? Error()
if (is_proto(msg, Error))
msg = msg.name + ": " + msg.message + "\n" + msg.stack
os.print(console_rec(caller.line, caller.file, msg))
break
case 'system':
msg = "[SYSTEM] " + msg
os.print(console_rec(caller.line, caller.file, msg))
break
default:
log.console(`unknown log type: ${name}`)
break
}
log.error = function(msg = new Error())
{
var caller = caller_data(1)
if (msg instanceof Error)
msg = msg.name + ": " + msg.message + "\n" + msg.stack
os.print(console_rec(caller.line,caller.file,msg))
}
log.system = function(msg) {
msg = "[SYSTEM] " + msg
log.console(msg)
}
function disrupt(err)
{
if (is_function(err.toString)) {
os.print(err.toString())
os.print("\n")
os.print(err.stack)
}
if (overling) {
if (err) {
// with an err, this is a forceful disrupt
var reason = (is_proto(err, Error)) ? err.stack : err
var reason = (err instanceof Error) ? err.stack : err
report_to_overling({type:'disrupt', reason})
} else
report_to_overling({type:'stop'})
}
if (underlings) {
var unders = array(underlings)
arrfor(unders, function(id, index) {
for (var id of underlings) {
log.console(`calling on ${id} to disrupt too`)
$_.stop(create_actor({id}))
})
}
}
if (err) {
if (err.message)
log.console(err.message)
log.console(err);
if (err.stack)
log.console(err.stack)
}
@@ -188,12 +212,11 @@ function disrupt(err)
actor_mod.disrupt()
}
actor_mod.on_exception(disrupt)
_cell.args = init ?? {}
_cell.id = "newguy"
_cell.args = _cell.hidden.init
_cell.args ??= {}
_cell.id ??= "newguy"
function create_actor(desc = {id:guid()}) {
var actor = {}
@@ -214,37 +237,17 @@ var json = use_core('json')
var time = use_core('time')
var pronto = use_core('pronto')
var fallback = pronto.fallback
var parallel = pronto.parallel
var race = pronto.race
var sequence = pronto.sequence
// Create runtime environment for modules
var runtime_env = {
logical: logical,
some: some,
every: every,
starts_with: starts_with,
ends_with: ends_with,
actor: actor,
is_actor: is_actor,
log: log,
send: send,
fallback: fallback,
parallel: parallel,
race: race,
sequence: sequence
}
// Pass to os for shop to access
os.runtime_env = runtime_env
globalThis.fallback = pronto.fallback
globalThis.parallel = pronto.parallel
globalThis.race = pronto.race
globalThis.sequence = pronto.sequence
$_.time_limit = function(requestor, seconds)
{
if (!pronto.is_requestor(requestor))
throw Error('time_limit: first argument must be a requestor');
if (!is_number(seconds) || seconds <= 0)
throw Error('time_limit: seconds must be a positive number');
throw new Error('time_limit: first argument must be a requestor');
if (!isa(seconds, number) || seconds <= 0)
throw new Error('time_limit: seconds must be a positive number');
return function time_limit_requestor(callback, value) {
pronto.check_callback(callback, 'time_limit')
@@ -340,12 +343,12 @@ REPLYTIMEOUT = config.reply_timeout
function guid(bits = 256)
{
var guid = blob(bits, os.random)
var guid = new blob(bits, os.random)
stone(guid)
return text(guid,'h')
}
var HEADER = {}
var HEADER = key()
// takes a function input value that will eventually be called with the current time in number form.
$_.clock = function(fn) {
@@ -355,7 +358,7 @@ $_.clock = function(fn) {
})
}
var underlings = {} // this is more like "all actors that are notified when we die"
var underlings = new Set() // this is more like "all actors that are notified when we die"
var overling = null
var root = null
@@ -408,8 +411,8 @@ var portal_fn = null
// takes a function input value that will eventually be called with the current time in number form.
$_.portal = function(fn, port) {
if (portal) throw Error(`Already started a portal listening on ${portal.port}`)
if (!port) throw Error("Requires a valid port.")
if (portal) throw new Error(`Already started a portal listening on ${portal.port}`)
if (!port) throw new Error("Requires a valid port.")
log.system(`starting a portal on port ${port}`)
portal = enet.create_host({address: "any", port})
portal_fn = fn
@@ -422,16 +425,14 @@ function handle_host(e) {
peers[`${e.peer.address}:${e.peer.port}`] = e.peer
var queue = peer_queue.get(e.peer)
if (queue) {
arrfor(queue, (msg, index) => e.peer.send(nota.encode(msg)))
for (var msg of queue) e.peer.send(nota.encode(msg))
log.system(`sent ${msg} out of queue`)
peer_queue.delete(e.peer)
}
break
case "disconnect":
peer_queue.delete(e.peer)
arrfor(array(peers), function(id, index) {
if (peers[id] == e.peer) delete peers[id]
})
for (var id in peers) if (peers[id] == e.peer) delete peers[id]
log.system('portal got disconnect from ' + e.peer.address + ":" + e.peer.port)
break
case "receive":
@@ -441,15 +442,16 @@ function handle_host(e) {
data.replycc[ACTORDATA].port = e.peer.port
}
function populate_actor_addresses(obj) {
if (!is_object(obj)) return
if (!isa(obj, object)) return
if (obj[ACTORDATA] && !obj[ACTORDATA].address) {
obj[ACTORDATA].address = e.peer.address
obj[ACTORDATA].port = e.peer.port
}
arrfor(array(obj), function(key, index) {
if (key in obj)
for (var key in obj) {
if (object.has(obj, key)) {
populate_actor_addresses(obj[key])
})
}
}
}
if (data.data) populate_actor_addresses(data.data)
turn(data)
@@ -467,18 +469,20 @@ $_.receiver = function receiver(fn) {
receive_fn = fn
}
$_.start = function start(cb, program) {
$_.start = function start(cb, program, ...args) {
if (!program) return
var id = guid()
if (args.length == 1 && Array.isArray(args[0])) args = args[0]
var startup = {
id,
overling: $_.self,
root,
arg: args,
program,
}
greeters[id] = cb
push(message_queue, { startup })
message_queue.push({ startup })
}
// stops an underling or self.
@@ -487,10 +491,10 @@ $_.stop = function stop(actor) {
need_stop = true
return
}
if (!is_actor(actor))
throw Error('Can only call stop on an actor.')
if (is_null(underlings[actor[ACTORDATA].id]))
throw Error('Can only call stop on an underling or self.')
if (!isa(actor, actor))
throw new Error('Can only call stop on an actor.')
if (!underlings.has(actor[ACTORDATA].id))
throw new Error('Can only call stop on an underling or self.')
sys_msg(actor, {kind:"stop"})
}
@@ -502,6 +506,11 @@ $_.unneeded = function unneeded(fn, seconds) {
// schedules the invocation of a function after a specified amount of time.
$_.delay = function delay(fn, seconds = 0) {
if (seconds <= 0) {
$_.clock(fn)
return
}
function delay_turn() {
fn()
send_messages()
@@ -513,16 +522,16 @@ $_.delay = function delay(fn, seconds = 0) {
var enet = use_core('enet')
// causes this actor to stop when another actor stops.
var couplings = {}
var couplings = new Set()
$_.couple = function couple(actor) {
if (actor == $_.self) return // can't couple to self
couplings[actor[ACTORDATA].id] = true
couplings.add(actor[ACTORDATA].id)
sys_msg(actor, {kind:'couple', from: $_.self})
log.system(`coupled to ${actor}`)
}
function actor_prep(actor, send) {
push(message_queue, {actor,send});
message_queue.push({actor,send});
}
// Send a message immediately without queuing
@@ -538,9 +547,9 @@ function actor_send(actor, message) {
if (actor[HEADER] && !actor[HEADER].replycc) // attempting to respond to a message but sender is not expecting; silently drop
return
if (!is_actor(actor) && !is_actor(actor.replycc)) throw Error(`Must send to an actor object. Attempted send to ${actor}`)
if (!isa(actor, actor) && !isa(actor.replycc, actor)) throw new Error(`Must send to an actor object. Attempted send to ${actor}`)
if (!is_object(message)) throw Error('Must send an object record.')
if (typeof message != 'object') throw new Error('Must send an object record.')
// message to self
if (actor[ACTORDATA].id == _cell.id) {
@@ -551,7 +560,7 @@ function actor_send(actor, message) {
// message to actor in same flock
if (actor[ACTORDATA].id && actor_mod.mailbox_exist(actor[ACTORDATA].id)) {
var wota_blob = wota.encode(message)
// log.console(`sending wota blob of ${length(wota_blob)/8} bytes`)
// log.console(`sending wota blob of ${wota_blob.length/8} bytes`)
actor_mod.mailbox_push(actor[ACTORDATA].id, wota_blob)
return
}
@@ -589,39 +598,39 @@ var need_stop = false
// if we've been flagged to stop, bail out before doing anything
if (need_stop) {
disrupt()
message_queue = []
message_queue.length = 0
return
}
arrfor(message_queue, function(msg, index) {
for (var msg of message_queue) {
if (msg.startup) {
// now is the time to actually spin up the actor
actor_mod.createactor(msg.startup)
} else {
actor_send(msg.actor, msg.send)
}
})
}
message_queue = []
message_queue.length = 0
}
var replies = {}
function send(actor, message, reply) {
if (!is_object(actor))
throw Error(`Must send to an actor object. Provided: ${actor}`);
if (!is_object(message))
throw Error('Message must be an object')
var send_msg = {type:"user", data: message}
globalThis.send = function send(actor, message, reply) {
if (typeof actor != 'object')
throw new Error(`Must send to an actor object. Provided: ${actor}`);
if (typeof message != 'object')
throw new Error('Message must be an object')
var send = {type:"user", data: message}
if (actor[HEADER] && actor[HEADER].replycc) {
var header = actor[HEADER]
if (!header.replycc || !is_actor(header.replycc))
throw Error(`Supplied actor had a return, but it's not a valid actor! ${actor[HEADER]}`)
if (!header.replycc || !isa(header.replycc, actor))
throw new Error(`Supplied actor had a return, but it's not a valid actor! ${actor[HEADER]}`)
actor = header.replycc
send_msg.return = header.reply
send.return = header.reply
}
if (reply) {
@@ -633,12 +642,12 @@ function send(actor, message, reply) {
delete replies[id]
}
}, REPLYTIMEOUT)
send_msg.reply = id
send_msg.replycc = $_.self
send.reply = id
send.replycc = $_.self
}
// Instead of sending immediately, queue it
actor_prep(actor, send_msg);
actor_prep(actor,send);
}
stone(send)
@@ -657,6 +666,7 @@ function turn(msg)
}
//log.console(`FIXME: need to get main from config, not just set to true`)
//log.console(`FIXME: add freeze/unfreeze at this level, so we can do it (but scripts cannot)`)
actor_mod.register_actor(_cell.id, turn, true, config.ar_timer)
if (config.actor_memory)
@@ -705,7 +715,7 @@ function handle_actor_disconnect(id) {
delete greeters[id]
}
log.system(`actor ${id} disconnected`)
if (!is_null(couplings[id])) disrupt("coupled actor died") // couplings now disrupts instead of stop
if (couplings.has(id)) disrupt("coupled actor died") // couplings now disrupts instead of stop
}
function handle_sysym(msg)
@@ -720,7 +730,7 @@ function handle_sysym(msg)
var greeter = greeters[from[ACTORDATA].id]
if (greeter) greeter(msg.message)
if (msg.message.type == 'disrupt')
delete underlings[from[ACTORDATA].id]
underlings.delete(from[ACTORDATA].id)
break
case 'contact':
if (portal_fn) {
@@ -728,11 +738,11 @@ function handle_sysym(msg)
letter2[HEADER] = msg
delete msg.data
portal_fn(letter2)
} else throw Error('Got a contact message, but no portal is established.')
} else throw new Error('Got a contact message, but no portal is established.')
break
case 'couple': // from must be notified when we die
from = msg.from
underlings[from[ACTORDATA].id] = true
underlings.add(from[ACTORDATA].id)
log.system(`actor ${from} is coupled to me`)
break
}
@@ -793,32 +803,36 @@ if (!locator) {
}
if (!locator)
throw Error(`Main program ${_cell.args.program} could not be found`)
throw new Error(`Main program ${_cell.args.program} could not be found`)
stone(globalThis)
var rads = use_core("math/radians")
$_.clock(_ => {
// Get capabilities for the main program
var file_info = shop.file_info ? shop.file_info(locator.path) : null
var inject = shop.script_inject_for ? shop.script_inject_for(file_info) : []
// Build env object for injection
var env = {}
for (var i = 0; i < length(inject); i++) {
// Build values array for injection
var vals = []
for (var i = 0; i < inject.length; i++) {
var key = inject[i]
if (key && key[0] == '$') key = text(key, 1)
if (key == 'fd') env[key] = fd
else env[key] = $_[key]
if (key && key[0] == '$') key = key.substring(1)
if (key == 'fd') vals.push(fd)
else vals.push($_[key])
}
// Create use function bound to the program's package
var pkg = file_info ? file_info.package : null
var use_fn = function(path) { return shop.use(path, pkg) }
// Call with signature: setup_module(args, use, env)
// The script wrapper binds $delay, $start, etc. from env
var val = call(locator.symbol, null, [_cell.args.arg, use_fn, env])
// Call with signature: setup_module(args, use, ...capabilities)
// The script wrapper builds $_ from the injected capabilities for backward compatibility
var val = locator.symbol.call(null, _cell.args.arg, use_fn, ...vals)
if (val)
throw Error('Program must not return anything');
throw new Error('Program must not return anything');
})
})()

63
internal/json.c Normal file
View File

@@ -0,0 +1,63 @@
#include "cell.h"
static JSValue js_json_encode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
if (argc < 1) return JS_ThrowTypeError(ctx, "json.encode requires at least 1 argument");
JSValue global = JS_GetGlobalObject(ctx);
JSValue json = JS_GetPropertyStr(ctx, global, "JSON");
JSValue stringify = JS_GetPropertyStr(ctx, json, "stringify");
JSValue args[3];
args[0] = argv[0]; // value
args[1] = (argc > 1) ? argv[1] : JS_NULL; // replacer
args[2] = (argc > 2) ? argv[2] : JS_NewInt32(ctx, 1); // space, default 1
JSValue result = JS_Call(ctx, stringify, json, 3, args);
JS_FreeValue(ctx, stringify);
JS_FreeValue(ctx, json);
JS_FreeValue(ctx, global);
if (argc <= 2) JS_FreeValue(ctx, args[2]);
return result;
}
static JSValue js_json_decode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
if (argc < 1) return JS_ThrowTypeError(ctx, "json.decode requires at least 1 argument");
if (!JS_IsString(argv[0])) {
JSValue err = JS_NewError(ctx);
JS_DefinePropertyValueStr(ctx, err, "message",
JS_NewString(ctx, "couldn't parse text: not a string"),
JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE);
return JS_Throw(ctx, err);
}
JSValue global = JS_GetGlobalObject(ctx);
JSValue json = JS_GetPropertyStr(ctx, global, "JSON");
JSValue parse = JS_GetPropertyStr(ctx, json, "parse");
JSValue args[2];
args[0] = argv[0]; // text
args[1] = (argc > 1) ? argv[1] : JS_NULL; // reviver
JSValue result = JS_Call(ctx, parse, json, argc > 1 ? 2 : 1, args);
JS_FreeValue(ctx, parse);
JS_FreeValue(ctx, json);
JS_FreeValue(ctx, global);
return result;
}
static const JSCFunctionListEntry js_json_funcs[] = {
JS_CFUNC_DEF("encode", 1, js_json_encode),
JS_CFUNC_DEF("decode", 1, js_json_decode),
};
JSValue js_json_use(JSContext *js) {
JSValue export = JS_NewObject(js);
JS_SetPropertyFunctionList(js, export, js_json_funcs, sizeof(js_json_funcs)/sizeof(JSCFunctionListEntry));
return export;
}

385
internal/nota.c Executable file
View File

@@ -0,0 +1,385 @@
#include "cell.h"
#include "cell_internal.h"
#define NOTA_IMPLEMENTATION
#include "nota.h"
typedef struct NotaEncodeContext {
JSContext *ctx;
JSValue visitedStack;
NotaBuffer nb;
int cycle;
JSValue replacer;
} NotaEncodeContext;
static void nota_stack_push(NotaEncodeContext *enc, JSValueConst val)
{
JSContext *ctx = enc->ctx;
int len = JS_ArrayLength(ctx, enc->visitedStack);
JS_SetPropertyInt64(ctx, enc->visitedStack, len, JS_DupValue(ctx, val));
}
static void nota_stack_pop(NotaEncodeContext *enc)
{
JSContext *ctx = enc->ctx;
int len = JS_ArrayLength(ctx, enc->visitedStack);
JS_SetPropertyStr(ctx, enc->visitedStack, "length", JS_NewUint32(ctx, len - 1));
}
static int nota_stack_has(NotaEncodeContext *enc, JSValueConst val)
{
JSContext *ctx = enc->ctx;
int len = JS_ArrayLength(ctx, enc->visitedStack);
for (int i = 0; i < len; i++) {
JSValue elem = JS_GetPropertyUint32(ctx, enc->visitedStack, i);
if (JS_IsObject(elem) && JS_IsObject(val)) {
if (JS_StrictEq(ctx, elem, val)) {
JS_FreeValue(ctx, elem);
return 1;
}
}
JS_FreeValue(ctx, elem);
}
return 0;
}
static JSValue apply_replacer(NotaEncodeContext *enc, JSValueConst holder, JSValueConst key, JSValueConst val) {
if (JS_IsNull(enc->replacer)) return JS_DupValue(enc->ctx, val);
JSValue args[2] = { JS_DupValue(enc->ctx, key), JS_DupValue(enc->ctx, val) };
JSValue result = JS_Call(enc->ctx, enc->replacer, holder, 2, args);
JS_FreeValue(enc->ctx, args[0]);
JS_FreeValue(enc->ctx, args[1]);
if (JS_IsException(result)) return JS_DupValue(enc->ctx, val);
return result;
}
char *js_do_nota_decode(JSContext *js, JSValue *tmp, char *nota, JSValue holder, JSValue key, JSValue reviver) {
int type = nota_type(nota);
JSValue ret2;
long long n;
double d;
int b;
char *str;
uint8_t *blob;
switch(type) {
case NOTA_BLOB:
nota = nota_read_blob(&n, (char**)&blob, nota);
*tmp = js_new_blob_stoned_copy(js, blob, n);
free(blob);
break;
case NOTA_TEXT:
nota = nota_read_text(&str, nota);
*tmp = JS_NewString(js, str);
free(str);
break;
case NOTA_ARR:
nota = nota_read_array(&n, nota);
*tmp = JS_NewArray(js);
for (int i = 0; i < n; i++) {
nota = js_do_nota_decode(js, &ret2, nota, *tmp, JS_NewInt32(js, i), reviver);
JS_SetPropertyInt64(js, *tmp, i, ret2);
}
break;
case NOTA_REC:
nota = nota_read_record(&n, nota);
*tmp = JS_NewObject(js);
for (int i = 0; i < n; i++) {
nota = nota_read_text(&str, nota);
JSValue prop_key = JS_NewString(js, str);
nota = js_do_nota_decode(js, &ret2, nota, *tmp, prop_key, reviver);
JS_SetPropertyStr(js, *tmp, str, ret2);
JS_FreeValue(js, prop_key);
free(str);
}
break;
case NOTA_INT:
nota = nota_read_int(&n, nota);
*tmp = JS_NewInt64(js, n);
break;
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);
JSValue obj = JS_NewObject(js);
cell_rt *crt = JS_GetContextOpaque(js);
JS_SetProperty(js, obj, crt->actor_sym, inner);
*tmp = obj;
} else {
switch(b) {
case NOTA_NULL: *tmp = JS_NULL; break;
case NOTA_FALSE: *tmp = JS_NewBool(js, 0); break;
case NOTA_TRUE: *tmp = JS_NewBool(js, 1); break;
default: *tmp = JS_NULL; break;
}
}
break;
default:
case NOTA_FLOAT:
nota = nota_read_float(&d, nota);
*tmp = JS_NewFloat64(js, d);
break;
}
if (!JS_IsNull(reviver)) {
JSValue args[2] = { JS_DupValue(js, key), JS_DupValue(js, *tmp) };
JSValue revived = JS_Call(js, reviver, holder, 2, args);
JS_FreeValue(js, args[0]);
JS_FreeValue(js, args[1]);
if (!JS_IsException(revived)) {
JS_FreeValue(js, *tmp);
*tmp = revived;
} else {
JS_FreeValue(js, revived);
}
}
return nota;
}
static void nota_encode_value(NotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValueConst key) {
JSContext *ctx = enc->ctx;
JSValue replaced = apply_replacer(enc, holder, key, val);
int tag = JS_VALUE_GET_TAG(replaced);
switch (tag) {
case JS_TAG_INT:
case JS_TAG_FLOAT64: {
double d;
JS_ToFloat64(ctx, &d, replaced);
nota_write_number(&enc->nb, d);
break;
}
case JS_TAG_STRING: {
const char *str = JS_ToCString(ctx, replaced);
nota_write_text(&enc->nb, str);
JS_FreeCString(ctx, str);
break;
}
case JS_TAG_BOOL:
if (JS_VALUE_GET_BOOL(replaced)) nota_write_sym(&enc->nb, NOTA_TRUE);
else nota_write_sym(&enc->nb, NOTA_FALSE);
break;
case JS_TAG_NULL:
nota_write_sym(&enc->nb, NOTA_NULL);
break;
case JS_TAG_OBJECT: {
if (js_is_blob(ctx, replaced)) {
size_t buf_len;
void *buf_data = js_get_blob_data(ctx, &buf_len, replaced);
if (buf_data == -1) {
JS_FreeValue(ctx, replaced);
return; // JS_EXCEPTION will be handled by caller
}
nota_write_blob(&enc->nb, (unsigned long long)buf_len * 8, (const char*)buf_data);
break;
}
if (JS_IsArray(ctx, replaced)) {
if (nota_stack_has(enc, replaced)) {
enc->cycle = 1;
break;
}
nota_stack_push(enc, replaced);
int arr_len = JS_ArrayLength(ctx, replaced);
nota_write_array(&enc->nb, arr_len);
for (int i = 0; i < arr_len; i++) {
JSValue elem_val = JS_GetPropertyUint32(ctx, replaced, i);
JSValue elem_key = JS_NewInt32(ctx, i);
nota_encode_value(enc, elem_val, replaced, elem_key);
JS_FreeValue(ctx, elem_val);
JS_FreeValue(ctx, elem_key);
}
nota_stack_pop(enc);
break;
}
cell_rt *crt = JS_GetContextOpaque(ctx);
JSValue adata = JS_GetProperty(ctx, replaced, crt->actor_sym);
if (!JS_IsNull(adata)) {
nota_write_sym(&enc->nb, NOTA_PRIVATE);
nota_encode_value(enc, adata, replaced, JS_NULL);
JS_FreeValue(ctx, adata);
break;
}
JS_FreeValue(ctx, adata);
if (nota_stack_has(enc, replaced)) {
enc->cycle = 1;
break;
}
nota_stack_push(enc, replaced);
JSValue to_json = JS_GetPropertyStr(ctx, replaced, "toJSON");
if (JS_IsFunction(ctx, to_json)) {
JSValue result = JS_Call(ctx, to_json, replaced, 0, NULL);
JS_FreeValue(ctx, to_json);
if (!JS_IsException(result)) {
nota_encode_value(enc, result, holder, key);
JS_FreeValue(ctx, result);
} else {
nota_write_sym(&enc->nb, NOTA_NULL);
}
nota_stack_pop(enc);
break;
}
JS_FreeValue(ctx, to_json);
JSPropertyEnum *ptab;
uint32_t plen;
if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, replaced, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK) < 0) {
nota_write_sym(&enc->nb, NOTA_NULL);
nota_stack_pop(enc);
break;
}
uint32_t non_function_count = 0;
for (uint32_t i = 0; i < plen; i++) {
JSValue prop_val = JS_GetProperty(ctx, replaced, ptab[i].atom);
if (!JS_IsFunction(ctx, prop_val)) non_function_count++;
JS_FreeValue(ctx, prop_val);
}
nota_write_record(&enc->nb, non_function_count);
for (uint32_t i = 0; i < plen; i++) {
JSValue prop_val = JS_GetProperty(ctx, replaced, ptab[i].atom);
if (!JS_IsFunction(ctx, prop_val)) {
const char *prop_name = JS_AtomToCString(ctx, ptab[i].atom);
JSValue prop_key = JS_AtomToValue(ctx, ptab[i].atom);
nota_write_text(&enc->nb, prop_name);
nota_encode_value(enc, prop_val, replaced, prop_key);
JS_FreeCString(ctx, prop_name);
JS_FreeValue(ctx, prop_key);
}
JS_FreeValue(ctx, prop_val);
JS_FreeAtom(ctx, ptab[i].atom);
}
js_free(ctx, ptab);
nota_stack_pop(enc);
break;
}
default:
nota_write_sym(&enc->nb, NOTA_NULL);
break;
}
JS_FreeValue(ctx, replaced);
}
void *value2nota(JSContext *ctx, JSValue v) {
NotaEncodeContext enc_s, *enc = &enc_s;
enc->ctx = ctx;
enc->visitedStack = JS_NewArray(ctx);
enc->cycle = 0;
enc->replacer = JS_NULL;
nota_buffer_init(&enc->nb, 128);
nota_encode_value(enc, v, JS_NULL, JS_NewString(ctx, ""));
if (enc->cycle) {
JS_FreeValue(ctx, enc->visitedStack);
nota_buffer_free(&enc->nb);
return NULL;
}
JS_FreeValue(ctx, enc->visitedStack);
void *data_ptr = enc->nb.data;
enc->nb.data = NULL;
nota_buffer_free(&enc->nb);
return data_ptr;
}
JSValue nota2value(JSContext *js, void *nota) {
if (!nota) return JS_NULL;
JSValue ret;
JSValue holder = JS_NewObject(js);
js_do_nota_decode(js, &ret, nota, holder, JS_NewString(js, ""), JS_NULL);
JS_FreeValue(js, holder);
return ret;
}
static JSValue js_nota_tostring(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
size_t len;
void *nota = js_get_blob_data(ctx, &len, this_val);
if (nota == (void*)-1) return JS_EXCEPTION;
if (!nota) return JS_NULL;
JSValue decoded;
JSValue holder = JS_NewObject(ctx);
js_do_nota_decode(ctx, &decoded, (char*)nota, holder, JS_NewString(ctx, ""), JS_NULL);
JS_FreeValue(ctx, holder);
JSValue global = JS_GetGlobalObject(ctx);
JSValue json = JS_GetPropertyStr(ctx, global, "JSON");
JSValue stringify = JS_GetPropertyStr(ctx, json, "stringify");
JSValue args[3];
args[0] = decoded;
args[1] = JS_NULL;
args[2] = JS_NewInt32(ctx, 1);
JSValue result = JS_Call(ctx, stringify, json, 3, args);
JS_FreeValue(ctx, stringify);
JS_FreeValue(ctx, json);
JS_FreeValue(ctx, global);
JS_FreeValue(ctx, decoded);
JS_FreeValue(ctx, args[2]);
return result;
}
static JSValue js_nota_encode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
if (argc < 1) return JS_ThrowTypeError(ctx, "nota.encode requires at least 1 argument");
NotaEncodeContext enc_s, *enc = &enc_s;
enc->ctx = ctx;
enc->visitedStack = JS_NewArray(ctx);
enc->cycle = 0;
enc->replacer = (argc > 1 && JS_IsFunction(ctx, argv[1])) ? argv[1] : JS_NULL;
nota_buffer_init(&enc->nb, 128);
nota_encode_value(enc, argv[0], JS_NULL, JS_NewString(ctx, ""));
if (enc->cycle) {
JS_FreeValue(ctx, enc->visitedStack);
nota_buffer_free(&enc->nb);
return JS_ThrowReferenceError(ctx, "Tried to encode something to nota with a cycle.");
}
JS_FreeValue(ctx, enc->visitedStack);
size_t total_len = enc->nb.size;
void *data_ptr = enc->nb.data;
JSValue ret = js_new_blob_stoned_copy(ctx, (uint8_t*)data_ptr, total_len);
nota_buffer_free(&enc->nb);
return ret;
}
static JSValue js_nota_decode(JSContext *js, JSValueConst self, int argc, JSValueConst *argv) {
if (argc < 1) return JS_NULL;
size_t len;
unsigned char *nota = js_get_blob_data(js, &len, argv[0]);
if (nota == -1) return JS_EXCEPTION;
if (!nota) return JS_NULL;
JSValue reviver = (argc > 1 && JS_IsFunction(js, argv[1])) ? argv[1] : JS_NULL;
JSValue ret;
JSValue holder = JS_NewObject(js);
js_do_nota_decode(js, &ret, (char*)nota, holder, JS_NewString(js, ""), reviver);
JS_FreeValue(js, holder);
return ret;
}
static const JSCFunctionListEntry js_nota_funcs[] = {
JS_CFUNC_DEF("encode", 1, js_nota_encode),
JS_CFUNC_DEF("decode", 1, js_nota_decode),
};
JSValue js_nota_use(JSContext *js) {
JSValue export = JS_NewObject(js);
JS_SetPropertyFunctionList(js, export, js_nota_funcs, sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry));
return export;
}

View File

@@ -1,5 +1,4 @@
var toml = use('toml')
var json = use('json')
var fd = use('fd')
var http = use('http')
@@ -8,7 +7,6 @@ var time = use('time')
var js = use('js')
var crypto = use('crypto')
var blob = use('blob')
var pkg_tools = use('package')
var os = use('os')
var link = use('link')
@@ -30,9 +28,9 @@ function put_into_cache(content, obj)
function ensure_dir(path) {
if (fd.stat(path).isDirectory) return
var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
for (var i = 0; i < length(parts); i++) {
var parts = path.split('/')
var current = path.startsWith('/') ? '/' : ''
for (var i = 0; i < parts.length; i++) {
if (parts[i] == '') continue
current += parts[i] + '/'
if (!fd.stat(current).isDirectory) {
@@ -93,8 +91,8 @@ Shop.get_reports_dir = function() {
}
function get_import_package(name) {
var parts = array(name, '/')
if (length(parts) > 1)
var parts = name.split('/')
if (parts.length > 1)
return parts[0]
return null
@@ -102,24 +100,24 @@ function get_import_package(name) {
function is_internal_path(path)
{
return path && starts_with(path, 'internal/')
return path && path.startsWith('internal/')
}
function split_explicit_package_import(path)
{
if (!path) return null
var parts = array(path, '/')
var parts = path.split('/')
if (length(parts) < 2) return null
if (parts.length < 2) return null
var looks_explicit = starts_with(path, '/') || (parts[0] && search(parts[0], '.') != null)
var looks_explicit = path.startsWith('/') || (parts[0] && parts[0].includes('.'))
if (!looks_explicit) return null
// Find the longest prefix that is an installed package
for (var i = length(parts) - 1; i >= 1; i--) {
var pkg_candidate = text(array(parts, 0, i), '/')
var mod_path = text(array(parts, i), '/')
if (!mod_path || length(mod_path) == 0) continue
for (var i = parts.length - 1; i >= 1; i--) {
var pkg_candidate = parts.slice(0, i).join('/')
var mod_path = parts.slice(i).join('/')
if (!mod_path || mod_path.length == 0) continue
var candidate_dir = get_packages_dir() + '/' + safe_package_path(pkg_candidate)
if (fd.is_file(candidate_dir + '/cell.toml'))
@@ -143,7 +141,7 @@ function package_in_shop(package) {
function abs_path_to_package(package_dir)
{
if (!fd.is_file(package_dir + '/cell.toml'))
throw Error('Not a valid package directory (no cell.toml): ' + package_dir)
throw new Error('Not a valid package directory (no cell.toml): ' + package_dir)
var packages_prefix = get_packages_dir() + '/'
var core_dir = packages_prefix + core_package
@@ -160,15 +158,8 @@ function abs_path_to_package(package_dir)
}
}
if (starts_with(package_dir, packages_prefix))
return text(package_dir, length(packages_prefix))
// Check if this local path is the target of a link
// If so, return the canonical package name (link origin) instead
var link_origin = link.get_origin(package_dir)
if (link_origin) {
return link_origin
}
if (package_dir.startsWith(packages_prefix))
return package_dir.substring(packages_prefix.length)
// in this case, the dir is the package
if (package_in_shop(package_dir))
@@ -197,9 +188,9 @@ Shop.file_info = function(file) {
name: null
}
if (ends_with(file, MOD_EXT))
if (file.endsWith(MOD_EXT))
info.is_module = true
else if (ends_with(file, ACTOR_EXT))
else if (file.endsWith(ACTOR_EXT))
info.is_actor = true
// Find package directory and determine package name
@@ -208,11 +199,11 @@ Shop.file_info = function(file) {
info.package = abs_path_to_package(pkg_dir)
if (info.is_actor)
info.name = text(file, length(pkg_dir) + 1, length(file) - length(ACTOR_EXT))
info.name = file.substring(pkg_dir.length + 1, file.length - ACTOR_EXT.length)
else if (info.is_module)
info.name = text(file, length(pkg_dir) + 1, length(file) - length(MOD_EXT))
info.name = file.substring(pkg_dir.length + 1, file.length - MOD_EXT.length)
else
info.name = text(file, length(pkg_dir) + 1)
info.name = file.substring(pkg_dir.length + 1)
}
return info
@@ -220,9 +211,9 @@ Shop.file_info = function(file) {
function get_import_name(path)
{
var parts = array(path, '/')
if (length(parts) < 2) return null
return text(array(parts, 1), '/')
var parts = path.split('/')
if (parts.length < 2) return null
return parts.slice(1).join('/')
}
// Given a path like 'prosperon/sprite' and a package context,
@@ -248,14 +239,14 @@ function safe_package_path(pkg)
{
// For absolute paths, replace / with _ to create a valid directory name
// Also replace @ with _
if (pkg && starts_with(pkg, '/'))
return replace(replace(pkg, '/', '_'), '@', '_')
return replace(pkg, '@', '_')
if (pkg && pkg.startsWith('/'))
return pkg.replaceAll('/', '_').replaceAll('@', '_')
return pkg.replaceAll('@', '_')
}
function package_cache_path(pkg)
{
return global_shop_path + '/cache/' + replace(replace(pkg, '/', '_'), '@', '_')
return global_shop_path + '/cache/' + pkg.replaceAll('/', '_').replaceAll('@', '_')
}
function get_shared_lib_path()
@@ -275,7 +266,7 @@ Shop.load_lock = function() {
return {}
var content = text(fd.slurp(path))
if (!length(content)) return {}
if (!content.length) return {}
_lock = toml.decode(content)
@@ -285,26 +276,26 @@ Shop.load_lock = function() {
// Save lock.toml configuration (to global shop)
Shop.save_lock = function(lock) {
var path = global_shop_path + '/lock.toml'
fd.slurpwrite(path, stone(blob(toml.encode(lock))));
fd.slurpwrite(path, stone(new blob(toml.encode(lock))));
}
// Get information about how to resolve a package
// Local packages always start with /
Shop.resolve_package_info = function(pkg) {
if (starts_with(pkg, '/')) return 'local'
if (search(pkg, 'gitea') != null) return 'gitea'
if (pkg.startsWith('/')) return 'local'
if (pkg.includes('gitea')) return 'gitea'
return null
}
// Verify if a package name is valid and return status
Shop.verify_package_name = function(pkg) {
if (!pkg) throw Error("Empty package name")
if (pkg == 'local') throw Error("local is not a valid package name")
if (pkg == 'core') throw Error("core is not a valid package name")
if (!pkg) throw new Error("Empty package name")
if (pkg == 'local') throw new Error("local is not a valid package name")
if (pkg == 'core') throw new Error("core is not a valid package name")
if (search(pkg, '://') != null)
throw Error(`Invalid package name: ${pkg}; did you mean ${array(pkg, '://')[1]}?`)
if (pkg.includes('://'))
throw new Error(`Invalid package name: ${pkg}; did you mean ${pkg.split('://')[1]}?`)
}
// Convert module package to download URL
@@ -312,7 +303,7 @@ Shop.get_download_url = function(pkg, commit_hash) {
var info = Shop.resolve_package_info(pkg)
if (info == 'gitea') {
var parts = array(pkg, '/')
var parts = pkg.split('/')
var host = parts[0]
var user = parts[1]
var repo = parts[2]
@@ -328,7 +319,7 @@ Shop.get_api_url = function(pkg) {
var info = Shop.resolve_package_info(pkg)
if (info == 'gitea') {
var parts = array(pkg, '/')
var parts = pkg.split('/')
var host = parts[0]
var user = parts[1]
var repo = parts[2]
@@ -347,7 +338,7 @@ Shop.extract_commit_hash = function(pkg, response) {
var data = json.decode(response)
if (info == 'gitea') {
if (is_array(data))
if (isa(data, array))
data = data[0]
return data.commit && data.commit.id
}
@@ -362,6 +353,11 @@ var open_dls = {}
// These map to $_ properties in engine.cm
var SHOP_DEFAULT_INJECT = ['$self', '$overling', '$clock', '$delay', '$start', '$receiver', '$contact', '$portal', '$time_limit', '$couple', '$stop', '$unneeded', '$connection', '$fd']
function strip_dollar(name) {
if (name && name[0] == '$') return name.substring(1)
return name
}
// Decide what a given module is allowed to see.
// This is the capability gate - tweak as needed.
Shop.script_inject_for = function(file_info) {
@@ -378,45 +374,19 @@ Shop.get_script_capabilities = function(path) {
return Shop.script_inject_for(file_info)
}
function inject_env(inject) {
// Start with runtime functions from engine
var env = {}
var rt = my$_.os ? my$_.os.runtime_env : null
if (rt) {
for (var k in rt) {
env[k] = rt[k]
}
}
// Add capability injections
for (var i = 0; i < length(inject); i++) {
var inj = inject[i]
var key = trim(inj, '$')
if (key == 'fd') env[key] = fd
else env[key] = my$_[key]
}
return env
function inject_params(inject) {
if (!inject || !inject.length) return ''
return ', ' + inject.join(', ')
}
function inject_bindings_code(inject) {
var lines = []
// Runtime function bindings
var runtime_fns = ['logical', 'some', 'every', 'starts_with', 'ends_with',
'actor', 'is_actor', 'log', 'send',
'fallback', 'parallel', 'race', 'sequence']
for (var i = 0; i < length(runtime_fns); i++) {
var fn = runtime_fns[i]
push(lines, `var ${fn} = env["${fn}"];`)
function inject_values(inject) {
var vals = []
for (var i = 0; i < inject.length; i++) {
var key = strip_dollar(inject[i])
if (key == 'fd') vals.push(fd)
else vals.push(my$_[key])
}
// Capability bindings ($delay, $start, etc.)
for (var i = 0; i < length(inject); i++) {
var inj = inject[i]
var key = trim(inj, '$')
push(lines, `var $${key} = env["${key}"];`)
}
return text(lines, '\n')
return vals
}
// Build the use function for a specific package context
@@ -427,42 +397,38 @@ function make_use_fn_code(pkg_arg) {
// for script forms, path is the canonical path of the module
var script_form = function(path, script, pkg, inject) {
var pkg_arg = pkg ? `'${pkg}'` : 'null'
var binds = inject_bindings_code(inject)
var fn = `(function setup_module(args, use, env){
def arg = args;
def PACKAGE = ${pkg_arg};
${binds}
${script}
})`
var params = inject_params(inject)
var fn = `(function setup_module(args, use${params}){ def arg = args; def PACKAGE = ${pkg_arg}; ${script}})`
return fn
}
// Resolve module function, hashing it in the process
// path is the exact path to the script file
function resolve_mod_fn(path, pkg) {
if (!fd.is_file(path)) throw Error(`path ${path} is not a file`)
if (!fd.is_file(path)) throw new Error(`path ${path} is not a file`)
var file_info = Shop.file_info(path)
var file_pkg = file_info.package
var inject = Shop.script_inject_for(file_info)
var content = text(fd.slurp(path))
var script = script_form(path, content, file_pkg, inject);
var obj = pull_from_cache(stone(blob(script)))
var obj = pull_from_cache(stone(new blob(script)))
if (obj) {
var fn = js.compile_unblob(obj)
return js.integrate(fn, null)
return js.eval_compile(fn)
}
// Compile name is just for debug/stack traces
// var compile_name = pkg ? pkg + ':' + path : 'local:' + path
var compile_name = path
var fn = js.compile(compile_name, script)
put_into_cache(stone(blob(script)), js.compile_blob(fn))
return js.integrate(fn, null)
put_into_cache(stone(new blob(script)), js.compile_blob(fn))
return js.eval_compile(fn)
}
// given a path and a package context
@@ -497,7 +463,7 @@ function resolve_locator(path, ctx)
// If ctx is an absolute path (starts with /), use it directly
// Otherwise, look it up in the packages directory
var ctx_dir
if (starts_with(ctx, '/')) {
if (ctx.startsWith('/')) {
ctx_dir = ctx
} else {
ctx_dir = get_packages_dir() + '/' + safe_package_path(ctx)
@@ -544,17 +510,25 @@ function resolve_locator(path, ctx)
// Generate symbol name for a C module file
// Uses the same format as Shop.c_symbol_for_file
// Symbol names are based on canonical package names, not link targets
// Resolves linked packages to their actual target first
function make_c_symbol(pkg, file) {
var pkg_safe = replace(replace(replace(pkg, '/', '_'), '.', '_'), '-', '_')
var file_safe = replace(replace(replace(file, '/', '_'), '.', '_'), '-', '_')
// Check if this package is linked - if so, use the link target for symbol name
var link_target = link.get_target(pkg)
var resolved_pkg = link_target ? link_target : pkg
var pkg_safe = resolved_pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_')
var file_safe = file.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_')
return 'js_' + pkg_safe + '_' + file_safe + '_use'
}
// Get the library path for a package in .cell/lib
// Library names are based on canonical package names, not link targets
// Resolves linked packages to their actual target first
function get_lib_path(pkg) {
var lib_name = replace(replace(replace(pkg, '/', '_'), '.', '_'), '-', '_')
// Check if this package is linked - if so, use the link target
var link_target = link.get_target(pkg)
var resolved_pkg = link_target ? link_target : pkg
var lib_name = resolved_pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_')
return global_shop_path + '/lib/' + lib_name + dylib_ext
}
@@ -566,43 +540,34 @@ Shop.open_package_dylib = function(pkg) {
var link_target = link.get_target(pkg)
var resolved_pkg = link_target ? link_target : pkg
var pkg_dir;
if (starts_with(resolved_pkg, '/')) {
if (resolved_pkg.startsWith('/')) {
pkg_dir = resolved_pkg
} else {
pkg_dir = get_packages_dir() + '/' + safe_package_path(resolved_pkg)
}
var toml_path = pkg_dir + '/cell.toml'
if (fd.is_file(toml_path)) {
try {
var content = text(fd.slurp(toml_path))
var cfg = toml.decode(content)
if (cfg.dependencies) {
arrfor(array(cfg.dependencies), function(alias, i) {
for (var alias in cfg.dependencies) {
var dep_pkg = cfg.dependencies[alias]
try {
Shop.open_package_dylib(dep_pkg)
} catch (dep_e) {
// Dependency dylib load failed, continue with others
}
})
Shop.open_package_dylib(dep_pkg)
}
}
} catch (e) {
// Error reading toml, continue
// Ignore errors reading cell.toml
}
}
var dl_path = get_lib_path(pkg)
if (fd.is_file(dl_path)) {
if (!open_dls[dl_path]) {
try {
open_dls[dl_path] = os.dylib_open(dl_path)
} catch (e) {
dylib_visited[pkg] = false
throw e
}
open_dls[dl_path] = os.dylib_open(dl_path)
}
}
}
@@ -610,19 +575,20 @@ Shop.open_package_dylib = function(pkg) {
// Resolve a C symbol by searching:
// 1. If package_context is null, only check core internal symbols
// 2. Otherwise: own package (internal then dylib) -> other packages (internal then dylib) -> core (internal only)
// Core is never loaded as a dynamic library via dlopen
function resolve_c_symbol(path, package_context) {
var explicit = split_explicit_package_import(path)
if (explicit) {
if (is_internal_path(explicit.path) && package_context && explicit.package != package_context)
explicit = null
}
if (explicit) {
var sym = make_c_symbol(explicit.package, explicit.path)
if (os.internal_exists(sym)) {
return {
symbol: function() { return os.load_internal(sym) },
scope: SCOPE_PACKAGE,
// Core is never loaded as a dynamic library via dlopen
function resolve_c_symbol(path, package_context)
{
var explicit = split_explicit_package_import(path)
if (explicit) {
if (is_internal_path(explicit.path) && package_context && explicit.package != package_context)
explicit = null
}
if (explicit) {
var sym = make_c_symbol(explicit.package, explicit.path)
if (os.internal_exists(sym)) {
return {
symbol: function() { return os.load_internal(sym) },
scope: SCOPE_PACKAGE,
package: explicit.package,
path: sym
}
@@ -642,7 +608,7 @@ function resolve_c_symbol(path, package_context) {
// If no package context, only check core internal symbols
if (!package_context || package_context == 'core') {
path = replace(path, '/', '_')
path = path.replace('/', '_')
var core_sym = `js_${path}_use`
if (os.internal_exists(core_sym)) {
return {
@@ -653,7 +619,7 @@ function resolve_c_symbol(path, package_context) {
}
return null
}
// 1. Check own package first (internal, then dylib)
var sym = make_c_symbol(package_context, path)
if (os.internal_exists(sym)) {
@@ -674,10 +640,10 @@ function resolve_c_symbol(path, package_context) {
path: sym
}
}
if (is_internal_path(path))
return null
// 2. Check aliased package imports (e.g. 'prosperon/sprite')
var pkg_alias = get_import_package(path)
if (pkg_alias) {
@@ -685,7 +651,7 @@ function resolve_c_symbol(path, package_context) {
if (canon_pkg) {
var mod_name = get_import_name(path)
var sym = make_c_symbol(canon_pkg, mod_name)
// Check internal first
if (os.internal_exists(sym)) {
return {
@@ -695,7 +661,7 @@ function resolve_c_symbol(path, package_context) {
path: sym
}
}
// Then check dylib
Shop.open_package_dylib(canon_pkg)
var dl_path = get_lib_path(canon_pkg)
@@ -709,9 +675,9 @@ function resolve_c_symbol(path, package_context) {
}
}
}
// 3. Check core internal symbols (core is never a dynamic library)
var core_sym = `js_${replace(path, '/', '_')}_use`
var core_sym = `js_${path}_use`
if (os.internal_exists(core_sym)) {
return {
symbol: function() { return os.load_internal(core_sym) },
@@ -719,7 +685,7 @@ function resolve_c_symbol(path, package_context) {
path: core_sym
}
}
return null
}
@@ -728,13 +694,13 @@ var module_info_cache = {}
function resolve_module_info(path, package_context) {
var lookup_key = package_context ? package_context + ':' + path : ':' + path
if (module_info_cache[lookup_key])
return module_info_cache[lookup_key]
var c_resolve = resolve_c_symbol(path, package_context) || {scope:999}
var mod_resolve = resolve_locator(path + '.cm', package_context) || {scope:999}
var min_scope = min(c_resolve.scope, mod_resolve.scope)
var min_scope = number.min(c_resolve.scope, mod_resolve.scope)
if (min_scope == 999)
return null
@@ -801,14 +767,6 @@ function make_use_fn(pkg) {
}
}
// Call a C module loader and execute the entrypoint
function call_c_module(c_resolve) {
var mod = c_resolve.symbol()
// if (is_function(mod))
// return mod()
return mod
}
function execute_module(info)
{
var c_resolve = info.c_resolve
@@ -819,31 +777,26 @@ function execute_module(info)
if (mod_resolve.scope < 900) {
var context = null
if (c_resolve.scope < 900) {
context = call_c_module(c_resolve)
context = c_resolve.symbol(null, $_)
}
// Get file info to determine inject list
var file_info = Shop.file_info(mod_resolve.path)
var inject = Shop.script_inject_for(file_info)
var env = inject_env(inject)
var vals = inject_values(inject)
var pkg = file_info.package
var use_fn = make_use_fn(pkg)
// Call with signature: setup_module(args, use, env)
// Call with signature: setup_module(args, use, ...capabilities)
// args is null for module loading
used = call(mod_resolve.symbol, context, [null, use_fn, env])
used = mod_resolve.symbol.call(context, null, use_fn, ...vals)
} else if (c_resolve.scope < 900) {
// C only
used = call_c_module(c_resolve)
used = c_resolve.symbol(null, my$_)
} else {
throw Error(`Module ${info.path} could not be found`)
}
// if (is_function(used))
// throw Error('C module loader returned a function; did you forget to call it?')
if (!used)
throw Error(`Module ${info} returned null`)
throw new Error(`Module ${info.path} could not be found`)
} if (!used)
throw new Error(`Module ${info} returned null`)
// stone(used)
return used
@@ -853,7 +806,7 @@ function get_module(path, package_context) {
var info = resolve_module_info(path, package_context)
if (!info)
throw Error(`Module ${path} could not be found in ${package_context}`)
throw new Error(`Module ${path} could not be found in ${package_context}`)
return execute_module(info)
}
@@ -861,7 +814,7 @@ function get_module(path, package_context) {
Shop.use = function use(path, package_context) {
var info = resolve_module_info(path, package_context)
if (!info)
throw Error(`Module ${path} could not be found in ${package_context}`)
throw new Error(`Module ${path} could not be found in ${package_context}`)
if (use_cache[info.cache_key])
return use_cache[info.cache_key]
@@ -873,7 +826,7 @@ Shop.resolve_locator = resolve_locator
// Get cache path for a package and commit
function get_cache_path(pkg, commit) {
return global_shop_path + '/cache/' + replace(replace(pkg, '@','_'), '/','_') + '_' + commit + '.zip'
return global_shop_path + '/cache/' + pkg.replaceAll('@','_').replaceAll('/','_') + '_' + commit + '.zip'
}
function get_package_abs_dir(package)
@@ -902,19 +855,22 @@ function fetch_remote_hash(pkg) {
// Returns the zip blob or null on failure
function download_zip(pkg, commit_hash) {
var cache_path = get_cache_path(pkg, commit_hash)
var download_url = Shop.get_download_url(pkg, commit_hash)
if (!download_url) {
log.error("Could not determine download URL for " + pkg)
return null
}
log.console("Downloading from " + download_url)
try {
var zip_blob = http.fetch(download_url)
log.console(`putting to ${cache_path}`)
fd.slurpwrite(cache_path, zip_blob)
log.console("Cached to " + cache_path)
return zip_blob
} catch (e) {
log.error("Download failed for " + pkg + ": " + e)
log.error(e)
return null
}
}
@@ -929,58 +885,39 @@ function get_cached_zip(pkg, commit_hash) {
}
// Fetch: Ensure the zip on disk matches what's in the lock file
// For local packages, this is a no-op
// For local packages, this is a no-op (returns true)
// For remote packages, downloads the zip if not present or hash mismatch
// Returns: { status: 'local'|'cached'|'downloaded'|'error', message: string }
// Returns true on success
Shop.fetch = function(pkg) {
var lock = Shop.load_lock()
var lock_entry = lock[pkg]
var info = Shop.resolve_package_info(pkg)
if (info == 'local') {
return { status: 'local' }
}
if (info == 'local') return null
// No lock entry - can't fetch without knowing what commit
if (!lock_entry || !lock_entry.commit) {
return { status: 'error', message: "No lock entry for " + pkg + " - run update first" }
}
if (!lock_entry || !lock_entry.commit)
throw new Error("No lock entry for " + pkg + " - run update first")
var commit = lock_entry.commit
var expected_hash = lock_entry.zip_hash
// Check if we have the zip cached
var zip_blob = get_cached_zip(pkg, commit)
if (zip_blob) {
// If we have a hash on record, verify it
if (expected_hash) {
var actual_hash = text(crypto.blake2(zip_blob), 'h')
if (actual_hash == expected_hash) {
return { status: 'cached' }
}
log.console("Zip hash mismatch for " + pkg + ", re-fetching...")
} else {
// No hash stored yet - compute and store it
var actual_hash = text(crypto.blake2(zip_blob), 'h')
lock_entry.zip_hash = actual_hash
Shop.save_lock(lock)
return { status: 'cached' }
}
}
// Verify hash matches
var actual_hash = text(crypto.blake2(zip_blob), 'h')
if (actual_hash == expected_hash)
return true
log.console("Zip hash mismatch for " + pkg + ", re-fetching...")
}
// Download the zip
var new_zip = download_zip(pkg, commit)
if (!new_zip) {
return { status: 'error', message: "Failed to download " + pkg }
}
download_zip(pkg, commit)
// Store the hash
var new_hash = text(crypto.blake2(new_zip), 'h')
lock_entry.zip_hash = new_hash
Shop.save_lock(lock)
return { status: 'downloaded' }
return true
}
// Extract: Extract a package to its target directory
@@ -990,7 +927,7 @@ Shop.fetch = function(pkg) {
// Returns true on success
Shop.extract = function(pkg) {
var target_dir = get_package_abs_dir(pkg)
// Check if this package is linked
var link_target = link.get_target(pkg)
if (link_target) {
@@ -998,7 +935,7 @@ Shop.extract = function(pkg) {
link.sync_one(pkg, link_target)
return true
}
var info = Shop.resolve_package_info(pkg)
if (info == 'local') {
@@ -1011,33 +948,13 @@ Shop.extract = function(pkg) {
return true
}
// Check if already extracted at correct commit
var lock = Shop.load_lock()
var lock_entry = lock[pkg]
if (lock_entry && lock_entry.commit) {
var extracted_commit_file = target_dir + '/.cell_commit'
if (fd.is_file(extracted_commit_file)) {
var extracted_commit = trim(text(fd.slurp(extracted_commit_file)))
if (extracted_commit == lock_entry.commit) {
// Already extracted at this commit, skip
return true
}
}
}
var zip_blob = get_package_zip(pkg)
if (!zip_blob)
throw Error("No zip blob available for " + pkg)
throw new Error("No zip blob available for " + pkg)
// Extract zip for remote package
install_zip(zip_blob, target_dir)
// Write marker file with the extracted commit
if (lock_entry && lock_entry.commit) {
fd.slurpwrite(target_dir + '/.cell_commit', stone(blob(lock_entry.commit)))
}
return true
}
@@ -1069,20 +986,8 @@ Shop.update = function(pkg) {
log.console(`checking ${pkg}`)
if (info == 'local') {
// Check if local path exists
if (!fd.is_dir(pkg)) {
log.console(` Local path does not exist: ${pkg}`)
return null
}
// Local packages always get a lock entry
var new_entry = {
type: 'local',
updated: time.number()
}
lock[pkg] = new_entry
Shop.save_lock(lock)
return new_entry
if (info == 'local') return {
updated: time.number()
}
var local_commit = lock_entry ? lock_entry.commit : null
@@ -1091,14 +996,14 @@ Shop.update = function(pkg) {
log.console(`local commit: ${local_commit}`)
log.console(`remote commit: ${remote_commit}`)
if (local_commit == remote_commit)
return null
if (!remote_commit) {
log.error("Could not resolve commit for " + pkg)
return null
}
if (local_commit == remote_commit)
return null
var new_entry = {
type: info,
commit: remote_commit,
@@ -1113,36 +1018,29 @@ Shop.update = function(pkg) {
function install_zip(zip_blob, target_dir) {
var zip = miniz.read(zip_blob)
if (!zip) throw Error("Failed to read zip archive")
if (!zip) throw new Error("Failed to read zip archive")
if (fd.is_link(target_dir)) fd.unlink(target_dir)
if (fd.is_dir(target_dir)) fd.rmdir(target_dir, 1)
log.console("Extracting to " + target_dir)
ensure_dir(target_dir)
var count = zip.count()
var created_dirs = {}
for (var i = 0; i < count; i++) {
if (zip.is_directory(i)) continue
var filename = zip.get_filename(i)
var slash_pos = search(filename, '/')
if (slash_pos == null) continue
if (slash_pos + 1 >= length(filename)) continue
var rel_path = text(filename, slash_pos + 1)
var parts = filename.split('/')
if (parts.length <= 1) continue
parts.shift()
var rel_path = parts.join('/')
var full_path = target_dir + '/' + rel_path
var dir_path = fd.dirname(full_path)
if (!created_dirs[dir_path]) {
ensure_dir(dir_path)
created_dirs[dir_path] = true
}
var file_data = zip.slurp(filename)
stone(file_data)
fd.slurpwrite(full_path, file_data)
var dir_path = full_path.substring(0, full_path.lastIndexOf('/'))
ensure_dir(dir_path)
fd.slurpwrite(full_path, zip.slurp(filename))
}
}
@@ -1165,14 +1063,14 @@ Shop.get = function(pkg) {
if (!lock[pkg]) {
var info = Shop.resolve_package_info(pkg)
if (!info) {
throw Error("Invalid package: " + pkg)
throw new Error("Invalid package: " + pkg)
}
var commit = null
if (info != 'local') {
commit = fetch_remote_hash(pkg)
if (!commit) {
throw Error("Could not resolve commit for " + pkg)
throw new Error("Could not resolve commit for " + pkg)
}
}
@@ -1214,14 +1112,12 @@ Shop.module_reload = function(path, package) {
var old = use_cache[cache_key]
var newmod = get_module(path, package)
arrfor(array(newmod), function(i, idx) {
for (var i in newmod)
old[i] = newmod[i]
})
arrfor(array(old), function(i, idx) {
for (var i in old)
if (!(i in newmod))
old[i] = null
})
}
function get_package_scripts(package)
@@ -1229,10 +1125,10 @@ function get_package_scripts(package)
var files = pkg_tools.list_files(package)
var scripts = []
for (var i = 0; i < length(files); i++) {
for (var i = 0; i < files.length; i++) {
var file = files[i]
if (ends_with(file, '.cm') || ends_with(file, '.ce')) {
push(scripts, file)
if (file.endsWith('.cm') || file.endsWith('.ce')) {
scripts.push(file)
}
}
@@ -1245,9 +1141,8 @@ Shop.build_package_scripts = function(package)
var scripts = get_package_scripts(package)
var pkg_dir = get_package_abs_dir(package)
arrfor(scripts, function(script, i) {
for (var script of scripts)
resolve_mod_fn(pkg_dir + '/' + script, package)
})
}
Shop.list_packages = function()
@@ -1279,22 +1174,22 @@ Shop.get_package_dir = function(pkg) {
// e.g., c_symbol_for_file('gitea.pockle.world/john/prosperon', 'sprite.c')
// -> 'js_gitea_pockle_world_john_prosperon_sprite_use'
Shop.c_symbol_for_file = function(pkg, file) {
var pkg_safe = replace(replace(replace(pkg, '/', '_'), '.', '_'), '-', '_')
var file_safe = replace(replace(fd.stem(file), '/', '_'), '.', '_')
var pkg_safe = pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_')
var file_safe = file.substring(0, file.lastIndexOf('.')).replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_')
return 'js_' + pkg_safe + '_' + file_safe + '_use'
}
// Generate C symbol prefix for a package
// e.g., c_symbol_prefix('gitea.pockle.world/john/prosperon') -> 'js_gitea_pockle_world_john_prosperon_'
Shop.c_symbol_prefix = function(pkg) {
var pkg_safe = replace(replace(replace(pkg, '/', '_'), '.', '_'), '-', '_')
var pkg_safe = pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_')
return 'js_' + pkg_safe + '_'
}
// Get the library name for a package (without extension)
// e.g., 'gitea.pockle.world/john/prosperon' -> 'gitea_pockle_world_john_prosperon'
Shop.lib_name_for_package = function(pkg) {
return replace(replace(replace(pkg, '/', '_'), '.', '_'), '-', '_')
return pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_')
}
// Returns { ok: bool, results: [{pkg, ok, error}] }
@@ -1303,12 +1198,12 @@ Shop.audit_packages = function() {
var bad = []
arrfor(packages, function(package, i) {
if (package == 'core') return
if (fd.is_dir(package)) return
if (fetch_remote_hash(package)) return
push(bad, package)
})
for (var package of packages) {
if (package == 'core') continue
if (fd.is_dir(package)) continue
if (fetch_remote_hash(package)) continue
bad.push(package)
}
return bad
}
@@ -1320,16 +1215,16 @@ Shop.parse_package = function(locator) {
// Strip version suffix if present
var clean = locator
if (search(locator, '@') != null) {
clean = array(locator, '@')[0]
if (locator.includes('@')) {
clean = locator.split('@')[0]
}
var info = Shop.resolve_package_info(clean)
if (!info) return null
// Extract package name (last component of path)
var parts = array(clean, '/')
var name = parts[length(parts) - 1]
var parts = clean.split('/')
var name = parts[parts.length - 1]
return {
path: clean,

View File

@@ -24,7 +24,7 @@ function get_pkg_dir(package_name) {
if (!package_name) {
return fd.realpath('.')
}
if (starts_with(package_name, '/')) {
if (package_name.startsWith('/')) {
return package_name
}
var shop = use('internal/shop')
@@ -35,9 +35,9 @@ function get_pkg_dir(package_name) {
function ensure_dir(path) {
if (fd.is_dir(path)) return true
var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
for (var i = 0; i < length(parts); i++) {
var parts = path.split('/')
var current = path.startsWith('/') ? '/' : ''
for (var i = 0; i < parts.length; i++) {
if (parts[i] == '') continue
current += parts[i] + '/'
if (!fd.is_dir(current)) {

27
link.ce
View File

@@ -17,7 +17,7 @@ var shop = use('internal/shop')
var fd = use('fd')
var toml = use('toml')
if (length(args) < 1) {
if (args.length < 1) {
log.console("Usage: link <command> [args] or link [package] <target>")
log.console("Commands:")
log.console(" list List all active links")
@@ -35,25 +35,25 @@ var cmd = args[0]
if (cmd == 'list') {
var links = link.load()
var count = 0
arrfor(array(links), function(k) {
for (var k in links) {
log.console(k + " -> " + links[k])
count++
})
}
if (count == 0) log.console("No links.")
} else if (cmd == 'sync') {
log.console("Syncing links...")
var result = link.sync_all(shop)
log.console("Synced " + result.synced + " link(s)")
if (length(result.errors) > 0) {
if (result.errors.length > 0) {
log.console("Errors:")
for (var i = 0; i < length(result.errors); i++) {
for (var i = 0; i < result.errors.length; i++) {
log.console(" " + result.errors[i])
}
}
} else if (cmd == 'delete' || cmd == 'rm') {
if (length(args) < 2) {
if (args.length < 2) {
log.console("Usage: link delete <package>")
$stop()
return
@@ -92,7 +92,7 @@ if (cmd == 'list') {
}
var arg1 = args[start_idx]
var arg2 = (length(args) > start_idx + 1) ? args[start_idx + 1] : null
var arg2 = (args.length > start_idx + 1) ? args[start_idx + 1] : null
if (!arg1) {
log.console("Error: target or package required")
@@ -108,13 +108,13 @@ if (cmd == 'list') {
// Resolve target if it's a local path
if (target == '.' || fd.is_dir(target)) {
target = fd.realpath(target)
} else if (starts_with(target, './') || starts_with(target, '../')) {
} else if (target.startsWith('./') || target.startsWith('../')) {
// Relative path that doesn't exist yet - try to resolve anyway
var cwd = fd.realpath('.')
if (starts_with(target, './')) {
target = cwd + text(target, 1)
if (target.startsWith('./')) {
target = cwd + target.substring(1)
} else {
// For ../ paths, var fd.realpath handle it if possible
// For ../ paths, let fd.realpath handle it if possible
target = fd.realpath(target) || target
}
}
@@ -127,7 +127,7 @@ if (cmd == 'list') {
// Resolve path
if (target == '.' || fd.is_dir(target)) {
target = fd.realpath(target)
} else if (starts_with(target, './') || starts_with(target, '../')) {
} else if (target.startsWith('./') || target.startsWith('../')) {
target = fd.realpath(target) || target
}
@@ -158,7 +158,7 @@ if (cmd == 'list') {
}
// Validate: if target is a local path, it must have cell.toml
if (starts_with(target, '/')) {
if (target.startsWith('/')) {
if (!fd.is_file(target + '/cell.toml')) {
log.console("Error: " + target + " is not a valid package (no cell.toml)")
$stop()
@@ -171,7 +171,6 @@ if (cmd == 'list') {
link.add(pkg_name, target, shop)
} catch (e) {
log.console("Error: " + e.message)
log.error(e)
$stop()
return
}

128
link.cm
View File

@@ -21,9 +21,9 @@ function get_packages_dir() {
// return the safe path for the package
function safe_package_path(pkg) {
// For absolute paths, replace / with _ to create a valid directory name
if (pkg && starts_with(pkg, '/'))
return replace(replace(pkg, '/', '_'), '@', '_')
return replace(pkg, '@', '_')
if (pkg && pkg.startsWith('/'))
return pkg.replaceAll('/', '_').replaceAll('@', '_')
return pkg.replaceAll('@', '_')
}
function get_package_abs_dir(package) {
@@ -32,9 +32,9 @@ function get_package_abs_dir(package) {
function ensure_dir(path) {
if (fd.stat(path).isDirectory) return
var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
for (var i = 0; i < length(parts); i++) {
var parts = path.split('/')
var current = path.startsWith('/') ? '/' : ''
for (var i = 0; i < parts.length; i++) {
if (parts[i] == '') continue
current += parts[i] + '/'
if (!fd.stat(current).isDirectory) {
@@ -47,7 +47,7 @@ function ensure_dir(path) {
// If target is a local path (starts with /), return it directly
// If target is a package name, return the package directory
function resolve_link_target(target) {
if (starts_with(target, '/')) {
if (target.startsWith('/')) {
return target
}
// Target is another package - resolve to its directory
@@ -81,67 +81,34 @@ Link.save = function(links) {
link_cache = links
var cfg = { links: links }
var path = get_links_path()
var b = blob(toml.encode(cfg))
stone(b)
fd.slurpwrite(path, b)
fd.slurpwrite(path, new blob(toml.encode(cfg)))
}
Link.add = function(canonical, target, shop) {
// Validate canonical package exists in shop
var lock = shop.load_lock()
if (!lock[canonical]) {
throw Error('Package ' + canonical + ' is not installed. Install it first with: cell get ' + canonical)
throw new Error('Package ' + canonical + ' is not installed. Install it first with: cell get ' + canonical)
}
// Validate target is a valid package
if (starts_with(target, '/')) {
if (target.startsWith('/')) {
// Local path - must have cell.toml
if (!fd.is_file(target + '/cell.toml')) {
throw Error('Target ' + target + ' is not a valid package (no cell.toml)')
throw new Error('Target ' + target + ' is not a valid package (no cell.toml)')
}
} else {
// Remote package target - ensure it's installed
shop.get(target)
}
var links = Link.load()
links[canonical] = target
Link.save(links)
// Create the symlink immediately
Link.sync_one(canonical, target, shop)
// Install dependencies of the linked package
// Read the target's cell.toml to find its dependencies
var target_path = starts_with(target, '/') ? target : get_package_abs_dir(target)
var toml_path = target_path + '/cell.toml'
if (fd.is_file(toml_path)) {
try {
var content = text(fd.slurp(toml_path))
var cfg = toml.decode(content)
if (cfg.dependencies) {
arrfor(array(cfg.dependencies), function(alias) {
var dep_locator = cfg.dependencies[alias]
// Skip local dependencies that don't exist
if (starts_with(dep_locator, '/') && !fd.is_dir(dep_locator)) {
log.console(" Skipping missing local dependency: " + dep_locator)
return
}
// Install the dependency if not already in shop
try {
shop.get(dep_locator)
shop.extract(dep_locator)
} catch (e) {
log.console(` Warning: Could not install dependency ${dep_locator}: ${e.message}`)
log.error(e)
}
})
}
} catch (e) {
log.console(` Warning: Could not read dependencies from ${toml_path}`)
}
}
log.console("Linked " + canonical + " -> " + target)
return true
}
@@ -166,12 +133,12 @@ Link.remove = function(canonical) {
Link.clear = function() {
// Remove all symlinks first
var links = Link.load()
arrfor(array(links), function(canonical) {
for (var canonical in links) {
var target_dir = get_package_abs_dir(canonical)
if (fd.is_link(target_dir)) {
fd.unlink(target_dir)
}
})
}
Link.save({})
log.console("Cleared all links")
@@ -184,7 +151,7 @@ Link.sync_one = function(canonical, target, shop) {
var link_target = resolve_link_target(target)
// Ensure parent directories exist
var parent = fd.dirname(target_dir)
var parent = target_dir.substring(0, target_dir.lastIndexOf('/'))
ensure_dir(parent)
// Check current state
@@ -210,59 +177,33 @@ Link.sync_one = function(canonical, target, shop) {
return true
}
// Sync all links - ensure all symlinks are in place and dependencies are installed
// Sync all links - ensure all symlinks are in place
Link.sync_all = function(shop) {
var links = Link.load()
var count = 0
var errors = []
arrfor(array(links), function(canonical) {
for (var canonical in links) {
var target = links[canonical]
try {
// Validate target exists
var link_target = resolve_link_target(target)
if (!fd.is_dir(link_target)) {
push(errors, canonical + ': target ' + link_target + ' does not exist')
return
errors.push(canonical + ': target ' + link_target + ' does not exist')
continue
}
if (!fd.is_file(link_target + '/cell.toml')) {
push(errors, canonical + ': target ' + link_target + ' is not a valid package')
return
errors.push(canonical + ': target ' + link_target + ' is not a valid package')
continue
}
Link.sync_one(canonical, target, shop)
// Install dependencies of the linked package
var toml_path = link_target + '/cell.toml'
try {
var content = text(fd.slurp(toml_path))
var cfg = toml.decode(content)
if (cfg.dependencies) {
arrfor(array(cfg.dependencies), function(alias) {
var dep_locator = cfg.dependencies[alias]
// Skip local dependencies that don't exist
if (starts_with(dep_locator, '/') && !fd.is_dir(dep_locator)) {
return
}
// Install the dependency if not already in shop
try {
shop.get(dep_locator)
shop.extract(dep_locator)
} catch (e) {
// Silently continue - dependency may already be installed
}
})
}
} catch (e) {
// Could not read dependencies - continue anyway
}
count++
} catch (e) {
push(errors, canonical + ': ' + e.message)
errors.push(canonical + ': ' + e.message)
}
})
}
return { synced: count, errors: errors }
}
@@ -278,15 +219,4 @@ Link.get_target = function(canonical) {
return links[canonical] || null
}
// Get the canonical package name that links to this target (reverse lookup)
// Returns null if no package links to this target
Link.get_origin = function(target) {
var links = Link.load()
var found = null
arrfor(array(links), function(origin) {
if (links[origin] == target) found = origin
})
return found
}
return Link

216
list.ce
View File

@@ -1,169 +1,85 @@
// cell list [<scope>] - List packages and dependencies
//
// Usage:
// cell list List dependencies of current package
// cell list shop List all packages in shop with status
// cell list <locator> List dependency tree for a package
// list installed packages
// cell list -> list packages installed in this package
// cell list all -> list all packages (including those that are there due to installed packages)
// cell list package <name> -> list the packages for the package <name>
var shop = use('internal/shop')
var pkg = use('package')
var link = use('link')
var fd = use('fd')
var mode = 'local'
var target_pkg = null
if (args && length(args) > 0) {
if (args[0] == 'shop') {
mode = 'shop'
} else if (args[0] == '--help' || args[0] == '-h') {
log.console("Usage: cell list [<scope>]")
log.console("")
log.console("List packages and dependencies.")
log.console("")
log.console("Scopes:")
log.console(" (none) List dependencies of current package")
log.console(" shop List all packages in shop with status")
log.console(" <locator> List dependency tree for a package")
$stop()
} else {
mode = 'package'
target_pkg = args[0]
// Resolve local paths
if (target_pkg == '.' || starts_with(target_pkg, './') || starts_with(target_pkg, '../') || fd.is_dir(target_pkg)) {
var resolved = fd.realpath(target_pkg)
if (resolved) {
target_pkg = resolved
}
if (args && args.length > 0) {
if (args[0] == 'all') {
mode = 'all'
} else if (args[0] == 'shop') {
mode = 'shop'
} else if (args[0] == 'package') {
if (args.length < 2) {
log.console("Usage: cell list package <name>")
$stop()
return
}
mode = 'package'
target_pkg = args[1]
} else {
log.console("Usage:")
log.console(" cell list : list local packages")
log.console(" cell list all : list all recursive packages")
log.console(" cell list package <name>: list dependencies of <name>")
log.console(" cell list shop : list all packages in shop")
$stop()
return
}
}
}
var links = link.load()
var lock = shop.load_lock()
function print_deps(ctx, indent) {
indent = indent || ""
var deps
try {
deps = pkg.dependencies(ctx)
} catch (e) {
log.console(indent + " (could not read dependencies)")
return
}
if (!deps) {
log.console(indent + " (none)")
return
}
var aliases = array(deps)
aliases = sort(aliases)
if (length(aliases) == 0) {
log.console(indent + " (none)")
return
}
for (var i = 0; i < length(aliases); i++) {
var alias = aliases[i]
var locator = deps[alias]
var link_target = links[locator]
var lock_entry = lock[locator]
var line = indent + " " + alias
if (alias != locator) {
line += " -> " + locator
}
// Add status indicators
var status = []
if (link_target) {
push(status, "linked -> " + link_target)
}
if (lock_entry && lock_entry.commit) {
push(status, "@" + text(lock_entry.commit, 0, 8))
}
if (lock_entry && lock_entry.type == 'local') {
push(status, "local")
}
if (!lock_entry) {
push(status, "not installed")
}
if (length(status) > 0) {
line += " [" + text(status, ", ") + "]"
}
log.console(line)
}
}
if (mode == 'local') {
log.console("Dependencies:")
print_deps(null)
log.console("Installed Packages (Local):")
print_deps(null)
} else if (mode == 'package') {
log.console("Dependencies for " + target_pkg + ":")
print_deps(target_pkg)
// Resolve alias to canonical package path
var canon = shop.get_canonical_package(target_pkg, null)
if (!canon) {
log.console("Package '" + target_pkg + "' not found in local dependencies.")
} else {
log.console("Dependencies for " + target_pkg + " (" + canon + "):")
print_deps(canon)
}
} else if (mode == 'all') {
log.console("All Packages:")
var all = shop.list_packages(null)
// list_packages returns an array of package strings (locators)
// We want to perhaps sort them
all.sort()
for (var i = 0; i < all.length; i++) {
log.console(" " + all[i])
}
if (all.length == 0) log.console(" (none)")
} else if (mode == 'shop') {
log.console("Shop packages:")
log.console("")
log.console("Shop Packages:")
var all = shop.list_packages()
if (all.length == 0)
log.console(" (none)")
else
all.forEach(package => log.console(" " + package))
}
var packages = shop.list_packages()
if (length(packages) == 0) {
log.console(" (none)")
} else {
packages = sort(packages)
function print_deps(ctx) {
var deps = pkg.dependencies(ctx)
var aliases = []
for (var k in deps) aliases.push(k)
aliases.sort()
// Group by type
var local_pkgs = []
var linked_pkgs = []
var remote_pkgs = []
arrfor(packages, function(p) {
if (p == 'core') return
var lock_entry = lock[p]
var link_target = links[p]
if (link_target) {
push(linked_pkgs, p)
} else if (lock_entry && lock_entry.type == 'local') {
push(local_pkgs, p)
} else {
push(remote_pkgs, p)
}
})
if (length(linked_pkgs) > 0) {
log.console("Linked packages:")
arrfor(linked_pkgs, function(p) {
var target = links[p]
log.console(" " + p + " -> " + target)
})
log.console("")
if (aliases.length == 0) {
log.console(" (none)")
} else {
for (var i = 0; i < aliases.length; i++) {
var alias = aliases[i]
var locator = deps[alias]
log.console(" " + alias + " -> " + locator)
}
}
if (length(local_pkgs) > 0) {
log.console("Local packages:")
arrfor(local_pkgs, function(p) {
log.console(" " + p)
})
log.console("")
}
if (length(remote_pkgs) > 0) {
log.console("Remote packages:")
arrfor(remote_pkgs, function(p) {
var lock_entry = lock[p]
var commit = lock_entry && lock_entry.commit ? " @" + text(lock_entry.commit, 0, 8) : ""
log.console(" " + p + commit)
})
log.console("")
}
log.console("Total: " + text(length(packages)) + " package(s)")
}
}
$stop()

12
ls.ce
View File

@@ -11,22 +11,22 @@ var modules = package.list_modules(pkg)
var programs = package.list_programs(pkg)
log.console("Modules in " + pkg + ":")
modules = sort(modules)
if (length(modules) == 0) {
modules.sort()
if (modules.length == 0) {
log.console(" (none)")
} else {
for (var i = 0; i < length(modules); i++) {
for (var i = 0; i < modules.length; i++) {
log.console(" " + modules[i])
}
}
log.console("")
log.console("Programs in " + pkg + ":")
programs = sort(programs)
if (length(programs) == 0) {
programs.sort()
if (programs.length == 0) {
log.console(" (none)")
} else {
for (var i = 0; i < length(programs); i++) {
for (var i = 0; i < programs.length; i++) {
log.console(" " + programs[i])
}
}

View File

@@ -40,9 +40,9 @@ sources = []
src += [ # core
'monocypher.c',
'cell.c',
'suite.c',
'wildmatch.c',
'qjs_actor.c',
'qjs_wota.c',
'miniz.c',
'quickjs.c',
'libregexp.c', 'libunicode.c', 'cutils.c', 'dtoa.c'
@@ -51,6 +51,7 @@ src += [ # core
src += ['scheduler.c']
scripts = [
'internal/nota.c',
'debug/js.c',
'qop.c',
'wildstar.c',
@@ -58,6 +59,7 @@ scripts = [
'crypto.c',
'internal/kim.c',
'time.c',
'internal/nota.c',
'debug/debug.c',
'internal/os.c',
'fd.c',
@@ -65,7 +67,6 @@ scripts = [
'net/enet.c',
'wildstar.c',
'archive/miniz.c',
'source/cJSON.c'
]
foreach file: scripts

View File

@@ -70,7 +70,7 @@ static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, int ar
JSValue config_obj = argv[0];
JSValue addr_val = JS_GetPropertyStr(ctx, config_obj, "address");
const char *addr_str = JS_IsText(addr_val) ? JS_ToCString(ctx, addr_val) : NULL;
const char *addr_str = JS_IsString(addr_val) ? JS_ToCString(ctx, addr_val) : NULL;
JS_FreeValue(ctx, addr_val);
if (!addr_str)
@@ -145,7 +145,7 @@ static JSValue js_enet_host_service(JSContext *ctx, JSValueConst this_val, int a
ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
if (!host) return JS_EXCEPTION;
if (argc < 1 || !JS_IsFunction(argv[0])) return JS_ThrowTypeError(ctx, "Expected a callback function as first argument");
if (argc < 1 || !JS_IsFunction(ctx, argv[0])) return JS_ThrowTypeError(ctx, "Expected a callback function as first argument");
JSValue callback = JS_DupValue(ctx, argv[0]);
double secs;
@@ -245,7 +245,7 @@ static JSValue js_enet_host_broadcast(JSContext *ctx, JSValueConst this_val, int
size_t data_len = 0;
uint8_t *buf = NULL;
if (JS_IsText(argv[0])) {
if (JS_IsString(argv[0])) {
data_str = JS_ToCStringLen(ctx, &data_len, argv[0]);
if (!data_str) return JS_EXCEPTION;
} else if (js_is_blob(ctx,argv[0])) {
@@ -310,7 +310,7 @@ static JSValue js_enet_peer_send(JSContext *ctx, JSValueConst this_val, int argc
size_t data_len = 0;
uint8_t *buf = NULL;
if (JS_IsText(argv[0])) {
if (JS_IsString(argv[0])) {
data_str = JS_ToCStringLen(ctx, &data_len, argv[0]);
if (!data_str) return JS_EXCEPTION;
} else if (js_is_blob(ctx,argv[0])) {
@@ -437,8 +437,8 @@ static const JSCFunctionListEntry js_enet_host_funcs[] = {
JS_CFUNC_DEF("connect", 2, js_enet_host_connect),
JS_CFUNC_DEF("flush", 0, js_enet_host_flush),
JS_CFUNC_DEF("broadcast", 1, js_enet_host_broadcast),
// JS_CGETSET_DEF("port", js_enet_host_get_port, NULL),
// JS_CGETSET_DEF("address", js_enet_host_get_address, NULL),
JS_CGETSET_DEF("port", js_enet_host_get_port, NULL),
JS_CGETSET_DEF("address", js_enet_host_get_address, NULL),
};
static JSValue js_enet_peer_get_rtt(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
@@ -552,20 +552,20 @@ static const JSCFunctionListEntry js_enet_peer_funcs[] = {
JS_CFUNC_DEF("ping", 0, js_enet_peer_ping),
JS_CFUNC_DEF("throttle_configure", 3, js_enet_peer_throttle_configure),
JS_CFUNC_DEF("timeout", 3, js_enet_peer_timeout),
// JS_CGETSET_DEF("rtt", js_enet_peer_get_rtt, NULL),
// JS_CGETSET_DEF("incoming_bandwidth", js_enet_peer_get_incoming_bandwidth, NULL),
// JS_CGETSET_DEF("outgoing_bandwidth", js_enet_peer_get_outgoing_bandwidth, NULL),
// JS_CGETSET_DEF("last_send_time", js_enet_peer_get_last_send_time, NULL),
// JS_CGETSET_DEF("last_receive_time", js_enet_peer_get_last_receive_time, NULL),
// JS_CGETSET_DEF("mtu", js_enet_peer_get_mtu, NULL),
// JS_CGETSET_DEF("outgoing_data_total", js_enet_peer_get_outgoing_data_total, NULL),
// JS_CGETSET_DEF("incoming_data_total", js_enet_peer_get_incoming_data_total, NULL),
// JS_CGETSET_DEF("rtt_variance", js_enet_peer_get_rtt_variance, NULL),
// JS_CGETSET_DEF("packet_loss", js_enet_peer_get_packet_loss, NULL),
// JS_CGETSET_DEF("state", js_enet_peer_get_state, NULL),
// JS_CGETSET_DEF("reliable_data_in_transit", js_enet_peer_get_reliable_data_in_transit, NULL),
// JS_CGETSET_DEF("port", js_enet_peer_get_port, NULL),
// JS_CGETSET_DEF("address", js_enet_peer_get_address, NULL),
JS_CGETSET_DEF("rtt", js_enet_peer_get_rtt, NULL),
JS_CGETSET_DEF("incoming_bandwidth", js_enet_peer_get_incoming_bandwidth, NULL),
JS_CGETSET_DEF("outgoing_bandwidth", js_enet_peer_get_outgoing_bandwidth, NULL),
JS_CGETSET_DEF("last_send_time", js_enet_peer_get_last_send_time, NULL),
JS_CGETSET_DEF("last_receive_time", js_enet_peer_get_last_receive_time, NULL),
JS_CGETSET_DEF("mtu", js_enet_peer_get_mtu, NULL),
JS_CGETSET_DEF("outgoing_data_total", js_enet_peer_get_outgoing_data_total, NULL),
JS_CGETSET_DEF("incoming_data_total", js_enet_peer_get_incoming_data_total, NULL),
JS_CGETSET_DEF("rtt_variance", js_enet_peer_get_rtt_variance, NULL),
JS_CGETSET_DEF("packet_loss", js_enet_peer_get_packet_loss, NULL),
JS_CGETSET_DEF("state", js_enet_peer_get_state, NULL),
JS_CGETSET_DEF("reliable_data_in_transit", js_enet_peer_get_reliable_data_in_transit, NULL),
JS_CGETSET_DEF("port", js_enet_peer_get_port, NULL),
JS_CGETSET_DEF("address", js_enet_peer_get_address, NULL),
};
JSValue js_enet_use(JSContext *ctx)

View File

@@ -293,7 +293,7 @@ static int par_easycurl_to_memory(char const *url, par_byte **data, int *nbytes)
// Performs a blocking HTTP GET and returns a QuickJS Blob of the body
static JSValue js_fetch_picoparser(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 1 || !JS_IsText(argv[0]))
if (argc < 1 || !JS_IsString(argv[0]))
return JS_ThrowTypeError(ctx, "fetch: URL string required");
const char *url = JS_ToCString(ctx, argv[0]);

View File

@@ -135,7 +135,7 @@ static int parse_url(const char *url, char **host, int *port, char **path, int *
// Performs a blocking HTTP GET and returns a QuickJS Blob of the body
static JSValue js_fetch_playdate(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 1 || !JS_IsText(argv[0]))
if (argc < 1 || !JS_IsString(argv[0]))
return JS_ThrowTypeError(ctx, "fetch: URL string required");
if (!pd_network || !pd_network->http) {

View File

@@ -144,7 +144,7 @@ JSC_CCALL(socket_socket,
int protocol = 0;
// Parse domain
if (JS_IsText(argv[0])) {
if (JS_IsString(argv[0])) {
const char *domain_str = JS_ToCString(js, argv[0]);
if (strcmp(domain_str, "AF_INET") == 0) domain = AF_INET;
else if (strcmp(domain_str, "AF_INET6") == 0) domain = AF_INET6;
@@ -156,7 +156,7 @@ JSC_CCALL(socket_socket,
// Parse type
if (argc > 1) {
if (JS_IsText(argv[1])) {
if (JS_IsString(argv[1])) {
const char *type_str = JS_ToCString(js, argv[1]);
if (strcmp(type_str, "SOCK_STREAM") == 0) type = SOCK_STREAM;
else if (strcmp(type_str, "SOCK_DGRAM") == 0) type = SOCK_DGRAM;
@@ -297,7 +297,7 @@ JSC_CCALL(socket_send,
flags = js2number(js, argv[2]);
}
if (JS_IsText(argv[1])) {
if (JS_IsString(argv[1])) {
const char *data = JS_ToCStringLen(js, &len, argv[1]);
sent = send(sockfd, data, len, flags);
JS_FreeCString(js, data);
@@ -385,7 +385,7 @@ JSC_CCALL(socket_sendto,
to_len = sizeof(addr);
}
if (JS_IsText(argv[1])) {
if (JS_IsString(argv[1])) {
const char *data = JS_ToCStringLen(js, &len, argv[1]);
sent = sendto(sockfd, data, len, flags, to_addr, to_len);
JS_FreeCString(js, data);
@@ -525,7 +525,7 @@ JSC_CCALL(socket_setsockopt,
int optname = 0;
// Parse level
if (JS_IsText(argv[1])) {
if (JS_IsString(argv[1])) {
const char *level_str = JS_ToCString(js, argv[1]);
if (strcmp(level_str, "SOL_SOCKET") == 0) level = SOL_SOCKET;
else if (strcmp(level_str, "IPPROTO_TCP") == 0) level = IPPROTO_TCP;
@@ -537,7 +537,7 @@ JSC_CCALL(socket_setsockopt,
}
// Parse option name
if (JS_IsText(argv[2])) {
if (JS_IsString(argv[2])) {
const char *opt_str = JS_ToCString(js, argv[2]);
if (strcmp(opt_str, "SO_REUSEADDR") == 0) optname = SO_REUSEADDR;
else if (strcmp(opt_str, "SO_KEEPALIVE") == 0) optname = SO_KEEPALIVE;

34
pack.ce
View File

@@ -14,7 +14,7 @@ var output_name = 'app'
var target_package = null
var buildtype = 'debug'
if (length(args) < 1) {
if (args.length < 1) {
log.error('Usage: cell pack <package> [options]')
log.error('')
log.error('Options:')
@@ -22,30 +22,30 @@ if (length(args) < 1) {
log.error(' -t, --target <target> Cross-compile for target platform')
log.error(' -b, --buildtype <type> Build type: release, debug, minsize (default: release)')
log.error('')
log.error('Available targets: ' + text(build.list_targets(), ', '))
log.error('Available targets: ' + build.list_targets().join(', '))
$stop()
return
}
target_package = args[0]
for (var i = 1; i < length(args); i++) {
for (var i = 1; i < args.length; i++) {
if (args[i] == '-t' || args[i] == '--target') {
if (i + 1 < length(args)) {
if (i + 1 < args.length) {
target = args[++i]
} else {
log.error('-t requires a target')
$stop()
}
} else if (args[i] == '-o' || args[i] == '--output') {
if (i + 1 < length(args)) {
if (i + 1 < args.length) {
output_name = args[++i]
} else {
log.error('-o requires an output name')
$stop()
}
} else if (args[i] == '-b' || args[i] == '--buildtype') {
if (i + 1 < length(args)) {
if (i + 1 < args.length) {
buildtype = args[++i]
if (buildtype != 'release' && buildtype != 'debug' && buildtype != 'minsize') {
log.error('Invalid buildtype: ' + buildtype + '. Must be release, debug, or minsize')
@@ -63,7 +63,7 @@ for (var i = 1; i < length(args); i++) {
log.console(' -t, --target <target> Cross-compile for target platform')
log.console(' -b, --buildtype <type> Build type: release, debug, minsize (default: release)')
log.console('')
log.console('Available targets: ' + text(build.list_targets(), ', '))
log.console('Available targets: ' + build.list_targets().join(', '))
$stop()
} else {
log.error('Unknown option: ' + args[i])
@@ -79,7 +79,7 @@ if (!target) {
if (target && !build.has_target(target)) {
log.error('Invalid target: ' + target)
log.console('Available targets: ' + text(build.list_targets(), ', '))
log.console('Available targets: ' + build.list_targets().join(', '))
$stop()
}
@@ -87,29 +87,29 @@ if (target && !build.has_target(target)) {
var packages = ['core']
var deps = pkg_tools.gather_dependencies(target_package)
for (var i = 0; i < length(deps); i++) {
push(packages, deps[i])
for (var i = 0; i < deps.length; i++) {
packages.push(deps[i])
}
push(packages, target_package)
packages.push(target_package)
// Remove duplicates
var unique_packages = []
var seen = {}
for (var i = 0; i < length(packages); i++) {
for (var i = 0; i < packages.length; i++) {
if (!seen[packages[i]]) {
seen[packages[i]] = true
push(unique_packages, packages[i])
unique_packages.push(packages[i])
}
}
packages = unique_packages
log.console('Preparing packages...')
arrfor(packages, function(package) {
if (package == 'core') return
for (var package of packages) {
if (package == 'core') continue
shop.extract(package)
})
}
log.console('Building static binary from ' + text(length(packages)) + ' packages: ' + text(packages, ', '))
log.console('Building static binary from ' + text(packages.length) + ' packages: ' + packages.join(', '))
try {
var result = build.build_static(packages, target, output_name, buildtype)

View File

@@ -1,21 +1,17 @@
var package = {}
var fd = use('fd')
var toml = use('toml')
var json = use('json')
var os = use('os')
var link = use('link')
// Cache for loaded configs to avoid toml re-parsing corruption
var config_cache = {}
// Convert package name to a safe directory name
// For absolute paths (local packages), replace / with _
// For remote packages, keep slashes as they use nested directories
function safe_package_path(pkg) {
if (!pkg) return pkg
if (starts_with(pkg, '/'))
return replace(replace(pkg, '/', '_'), '@', '_')
return replace(pkg, '@', '_')
if (pkg.startsWith('/'))
return pkg.replaceAll('/', '_').replaceAll('@', '_')
return pkg.replaceAll('@', '_')
}
function get_path(name)
@@ -24,50 +20,19 @@ function get_path(name)
if (!name)
return fd.realpath('.')
// If name is already an absolute path, use it directly
if (starts_with(name, '/'))
if (name.startsWith('/'))
return name
// Check if this package is linked - if so, use the link target directly
// This avoids symlink-related issues with file reading
var link_target = link.get_target(name)
if (link_target) {
// If link target is a local path, use it directly
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, '/', '_'), '@', '_')
}
// Remote packages use nested directories, so don't transform slashes
return os.global_shop_path + '/packages/' + replace(name, '@', '_')
return os.global_shop_path + '/packages/' + name.replaceAll('@', '_')
}
package.load_config = function(name)
{
var config_path = get_path(name) + '/cell.toml'
if (!fd.is_file(config_path))
throw new Error(`${config_path} isn't a path`)
// Return cached config if available
if (config_cache[config_path])
return config_cache[config_path]
if (!fd.is_file(config_path)) {
throw Error(`${config_path} does not exist`)
}
var content = text(fd.slurp(config_path))
if (!content || length(trim(content)) == 0)
return {}
var result = toml.decode(content)
if (!result) {
return {}
}
// Deep copy to avoid toml module's shared state bug and cache it
result = json.decode(json.encode(result))
config_cache[config_path] = result
return result
return toml.decode(text(fd.slurp(config_path)))
}
package.save_config = function(name, config)
@@ -86,11 +51,11 @@ package.find_alias = function(name, locator)
var config = package.load_config(name)
if (!config.dependencies) return null
var found = null
arrfor(array(config.dependencies), function(alias) {
if (config.dependencies[alias] == locator) found = alias
})
return found
for (var alias in config.dependencies)
if (config.dependencies[alias] == locator)
return alias
return null
}
package.alias_to_package = function(name, alias)
@@ -130,15 +95,19 @@ package.find_package_dir = function(file)
var absolute = fd.realpath(file)
var dir = absolute
if (fd.is_file(dir))
dir = fd.dirname(dir)
if (fd.is_file(dir)) {
var last_slash = dir.lastIndexOf('/')
if (last_slash > 0) dir = dir.substring(0, last_slash)
}
while (dir && length(dir) > 0) {
while (dir && dir.length > 0) {
var toml_path = dir + '/cell.toml'
if (fd.is_file(toml_path)) {
return dir
}
dir = fd.dirname(dir)
var last_slash = dir.lastIndexOf('/')
if (last_slash <= 0) break
dir = dir.substring(0, last_slash)
}
return null
@@ -151,27 +120,20 @@ package.find_package_dir = function(file)
// Returns null if no alias is found for the given path
package.split_alias = function(name, path)
{
if (!path || length(path) == 0) {
if (!path || path.length == 0) {
return null
}
var parts = array(path, '/')
var parts = path.split('/')
var first_part = parts[0]
try {
var config = package.load_config(name)
if (!config) return null
var deps = config.dependencies
if (deps && deps[first_part]) {
var dep_locator = deps[first_part]
var remaining_path = text(array(parts, 1), '/')
return { package: dep_locator, path: remaining_path }
}
} catch (e) {
// Config doesn't exist or couldn't be loaded
var config = package.load_config(name)
if (config.dependencies && config.dependencies[first_part]) {
var dep_locator = config.dependencies[first_part]
var remaining_path = parts.slice(1).join('/')
return { package: dep_locator, path: remaining_path }
}
return null
}
@@ -187,13 +149,13 @@ package.gather_dependencies = function(name)
var deps = package.dependencies(pkg_name)
if (!deps) return
arrfor(array(deps), function(alias) {
for (var alias in deps) {
var locator = deps[alias]
if (!all_deps[locator]) {
all_deps[locator] = true
gather_recursive(locator)
}
})
}
}
gather_recursive(name)
@@ -209,10 +171,10 @@ package.list_files = function(pkg) {
var list = fd.readdir(current_dir)
if (!list) return
for (var i = 0; i < length(list); i++) {
for (var i = 0; i < list.length; i++) {
var item = list[i]
if (item == '.' || item == '..') continue
if (starts_with(item, '.')) continue
if (item.startsWith('.')) continue
// Skip build directories in root
@@ -223,7 +185,7 @@ package.list_files = function(pkg) {
if (st.isDirectory) {
walk(full_path, rel_path)
} else {
push(files, rel_path)
files.push(rel_path)
}
}
}
@@ -237,9 +199,9 @@ package.list_files = function(pkg) {
package.list_modules = function(name) {
var files = package.list_files(name)
var modules = []
for (var i = 0; i < length(files); i++) {
if (ends_with(files[i], '.cm')) {
push(modules, text(files[i], 0, -3))
for (var i = 0; i < files.length; i++) {
if (files[i].endsWith('.cm')) {
modules.push(files[i].substring(0, files[i].length - 3))
}
}
return modules
@@ -248,9 +210,9 @@ package.list_modules = function(name) {
package.list_programs = function(name) {
var files = package.list_files(name)
var programs = []
for (var i = 0; i < length(files); i++) {
if (ends_with(files[i], '.ce')) {
push(programs, text(files[i], 0, -3))
for (var i = 0; i < files.length; i++) {
if (files[i].endsWith('.ce')) {
programs.push(files[i].substring(0, files[i].length - 3))
}
}
return programs
@@ -267,13 +229,13 @@ package.get_flags = function(name, flag_type, target) {
// Base flags
if (config.compilation && config.compilation[flag_type]) {
var base = config.compilation[flag_type]
flags = array(flags, filter(array(base, /\s+/), function(f) { return length(f) > 0 }))
flags = flags.concat(base.split(/\s+/).filter(function(f) { return f.length > 0 }))
}
// Target-specific flags
if (target && config.compilation && config.compilation[target] && config.compilation[target][flag_type]) {
var target_flags = config.compilation[target][flag_type]
flags = array(flags, filter(array(target_flags, /\s+/), function(f) { return length(f) > 0 }))
flags = flags.concat(target_flags.split(/\s+/).filter(function(f) { return f.length > 0 }))
}
return flags
@@ -290,27 +252,31 @@ package.get_c_files = function(name, target, exclude_main) {
// Group files by their base name (without target suffix)
var groups = {} // base_key -> { generic: file, variants: { target: file } }
for (var i = 0; i < length(files); i++) {
for (var i = 0; i < files.length; i++) {
var file = files[i]
if (!ends_with(file, '.c') && !ends_with(file, '.cpp')) continue
if (!file.endsWith('.c') && !file.endsWith('.cpp')) continue
var ext = ends_with(file, '.cpp') ? '.cpp' : '.c'
var base = text(file, 0, -length(ext))
var name_part = fd.basename(base)
var dir_part = fd.dirname(base)
var dir = (dir_part && dir_part != '.') ? dir_part + '/' : ''
var ext = file.endsWith('.cpp') ? '.cpp' : '.c'
var base = file.substring(0, file.length - ext.length)
var dir = ''
var name_part = base
var slash = base.lastIndexOf('/')
if (slash >= 0) {
dir = base.substring(0, slash + 1)
name_part = base.substring(slash + 1)
}
// Check for target suffix
var is_variant = false
var variant_target = null
var generic_name = name_part
for (var t = 0; t < length(known_targets); t++) {
for (var t = 0; t < known_targets.length; t++) {
var suffix = '_' + known_targets[t]
if (ends_with(name_part, suffix)) {
if (name_part.endsWith(suffix)) {
is_variant = true
variant_target = known_targets[t]
generic_name = text(name_part, 0, -length(suffix))
generic_name = name_part.substring(0, name_part.length - suffix.length)
break
}
}
@@ -329,7 +295,7 @@ package.get_c_files = function(name, target, exclude_main) {
// Select appropriate file from each group
var result = []
arrfor(array(groups), function(key) {
for (var key in groups) {
var group = groups[key]
var selected = null
@@ -343,12 +309,14 @@ package.get_c_files = function(name, target, exclude_main) {
if (selected) {
// Skip main.c if requested
if (exclude_main) {
var basename = fd.basename(selected)
if (basename == 'main.c' || starts_with(basename, 'main_')) return
var basename = selected
var s = selected.lastIndexOf('/')
if (s >= 0) basename = selected.substring(s + 1)
if (basename == 'main.c' || basename.startsWith('main_')) continue
}
push(result, selected)
result.push(selected)
}
})
}
return result
}

224
parseq.cm Normal file
View File

@@ -0,0 +1,224 @@
// parseq.js (Misty edition)
// Douglas Crockford → adapted for Misty by ChatGPT, 20250529
// Better living thru eventuality!
/*
 The original parseq.js relied on the browser's setTimeout and ran in
 milliseconds. In Misty we may be given an optional @.delay capability
 (arguments[0]) and time limits are expressed in **seconds**. This rewrite
 removes the setTimeout dependency, uses the @.delay capability when it is
 present, and provides the factories described in the Misty specification:
    fallback, par_all, par_any, race, sequence
 Each factory returns a **requestor** function as described by the spec.
*/
def delay = arg[0] // may be null
// ———————————————————————————————————————— helpers
function make_reason (factory, excuse, evidence) {
def reason = new Error(`parseq.${factory}${excuse ? ': ' + excuse : ''}`)
reason.evidence = evidence
return reason
}
function is_requestor (fn) {
return typeof fn == 'function' && (fn.length == 1 || fn.length == 2)
}
function check_requestors (list, factory) {
if (!isa(list, array) || list.some(r => !is_requestor(r)))
throw make_reason(factory, 'Bad requestor list.', list)
}
function check_callback (cb, factory) {
if (typeof cb != 'function' || cb.length != 2)
throw make_reason(factory, 'Not a callback.', cb)
}
function schedule (fn, seconds) {
if (seconds == null || seconds <= 0) return fn()
if (typeof delay == 'function') return delay(fn, seconds)
throw make_reason('schedule', '@.delay capability required for timeouts.')
}
// ———————————————————————————————————————— core runner
function run (factory, requestors, initial, action, time_limit, throttle = 0) {
let cancel_list = new Array(requestors.length)
let next = 0
let timer_cancel
function cancel (reason = make_reason(factory, 'Cancel.')) {
if (timer_cancel) timer_cancel(), timer_cancel = null
if (!cancel_list) return
cancel_list.forEach(c => { try { if (typeof c == 'function') c(reason) } catch (_) {} })
cancel_list = null
}
function start_requestor (value) {
if (!cancel_list || next >= requestors.length) return
let idx = next++
def req = requestors[idx]
try {
cancel_list[idx] = req(function req_cb (val, reason) {
if (!cancel_list || idx == null) return
cancel_list[idx] = null
action(val, reason, idx)
idx = null
if (factory == 'sequence') start_requestor(val)
else if (throttle) start_requestor(initial)
}, value)
} catch (ex) {
action(null, ex, idx)
idx = null
if (factory == 'sequence') start_requestor(value)
else if (throttle) start_requestor(initial)
}
}
if (time_limit != null) {
if (typeof time_limit != 'number' || time_limit < 0)
throw make_reason(factory, 'Bad time limit.', time_limit)
if (time_limit > 0) timer_cancel = schedule(() => cancel(make_reason(factory, 'Timeout.', time_limit)), time_limit)
}
def concurrent = throttle ? number.min(throttle, requestors.length) : requestors.length
for (let i = 0; i < concurrent; i++) start_requestor(initial)
return cancel
}
// ———————————————————————————————————————— factories
function _normalize (collection, factory) {
if (isa(collection)) return { names: null, list: collection }
if (collection && typeof collection == 'object') {
def names = array(collection)
def list = names.map(k => collection[k]).filter(is_requestor)
return { names, list }
}
throw make_reason(factory, 'Expected array or record.', collection)
}
function _denormalize (names, list) {
if (!names) return list
def obj = meme(null)
names.forEach((k, i) => { obj[k] = list[i] })
return obj
}
function par_all (collection, time_limit, throttle) {
def factory = 'par_all'
def { names, list } = _normalize(collection, factory)
if (list.length == 0) return (cb, v) => cb(names ? {} : [])
check_requestors(list, factory)
return function par_all_req (cb, initial) {
check_callback(cb, factory)
let pending = list.length
def results = new Array(list.length)
def cancel = run(factory, list, initial, (val, reason, idx) => {
if (val == null) {
cancel(reason)
return cb(null, reason)
}
results[idx] = val
if (--pending == 0) cb(_denormalize(names, results))
}, time_limit, throttle)
return cancel
}
}
function par_any (collection, time_limit, throttle) {
def factory = 'par_any'
def { names, list } = _normalize(collection, factory)
if (list.length == 0) return (cb, v) => cb(names ? {} : [])
check_requestors(list, factory)
return function par_any_req (cb, initial) {
check_callback(cb, factory)
let pending = list.length
def successes = new Array(list.length)
def cancel = run(factory, list, initial, (val, reason, idx) => {
pending--
if (val != null) successes[idx] = val
if (successes.some(v => v != null)) {
if (!pending) cancel(make_reason(factory, 'Finished.'))
return cb(_denormalize(names, successes.filter(v => v != null)))
}
if (!pending) cb(null, make_reason(factory, 'No successes.'))
}, time_limit, throttle)
return cancel
}
}
function race (list, time_limit, throttle) {
def factory = throttle == 1 ? 'fallback' : 'race'
if (!isa(list, array) || list.length == 0)
throw make_reason(factory, 'No requestors.')
check_requestors(list, factory)
return function race_req (cb, initial) {
check_callback(cb, factory)
let done = false
def cancel = run(factory, list, initial, (val, reason, idx) => {
if (done) return
if (val != null) {
done = true
cancel(make_reason(factory, 'Loser.', idx))
cb(val)
} else if (--list.length == 0) {
done = true
cancel(reason)
cb(null, reason)
}
}, time_limit, throttle)
return cancel
}
}
function fallback (list, time_limit) {
return race(list, time_limit, 1)
}
function sequence (list, time_limit) {
def factory = 'sequence'
if (!isa(list, array)) throw make_reason(factory, 'Not an array.', list)
check_requestors(list, factory)
if (list.length == 0) return (cb, v) => cb(v)
return function sequence_req (cb, initial) {
check_callback(cb, factory)
let idx = 0
function next (value) {
if (idx >= list.length) return cb(value)
try {
list[idx++](function seq_cb (val, reason) {
if (val == null) return cb(null, reason)
next(val)
}, value)
} catch (ex) {
cb(null, ex)
}
}
next(initial)
}
}
return {
fallback,
par_all,
par_any,
race,
sequence
}

338
plan.md
View File

@@ -1,338 +0,0 @@
# Cell/QuickJS Refactoring Plan: Remove Atoms, Shapes, and Dual-Encoding
## Overview
Refactor `source/quickjs.c` to match `docs/memory.md` specification:
- Remove JSAtom system (171 references → ~41 remaining)
- Remove JSShape system (94 references) ✓
- Remove IC caches (shape-based inline caches) ✓
- Remove `is_wide_char` dual-encoding (18 locations) ✓
- Use JSValue texts directly as property keys
- Reference: `mquickjs.c` shows the target pattern
## Completed Phases
### Phase 1: Remove is_wide_char Remnants ✓
### Phase 2: Remove IC Caches ✓
### Phase 3: Remove JSShape System ✓
### Phase 4: Complete Property Access with JSValue Keys ✓
Completed:
- Removed JS_GC_OBJ_TYPE_JS_OBJECT fallbacks from OP_get_field
- Removed JS_GC_OBJ_TYPE_JS_OBJECT fallbacks from OP_put_field
- Removed JS_GC_OBJ_TYPE_JS_OBJECT fallbacks from OP_define_field
- Created emit_key() function that adds JSValue to cpool and emits index
---
## Phase 5: Convert JSAtom to JSValue Text (IN PROGRESS)
This is the core transformation. All identifier handling moves from atoms to JSValue.
### Completed Items
**Token and Parser Infrastructure:**
- [x] Change JSToken.u.ident.atom to JSToken.u.ident.str (JSValue)
- [x] Change parse_ident() to return JSValue
- [x] Create emit_key() function (cpool-based)
- [x] Create JS_KEY_* macros for common names (lines ~279-335 in quickjs.c)
- [x] Update all token.u.ident.atom references to .str
- [x] Create keyword lookup table (js_keywords[]) with string comparison
- [x] Rewrite update_token_ident() to use js_keyword_lookup()
- [x] Rewrite is_strict_future_keyword() to use JSValue
- [x] Update token_is_pseudo_keyword() to use JSValue and js_key_equal()
**Function Declaration Parsing:**
- [x] Update js_parse_function_decl() signature to use JSValue func_name
- [x] Update js_parse_function_decl2() to use JSValue func_name throughout
- [x] Update js_parse_function_check_names() to use JSValue
- [x] Convert JS_DupAtom/JS_FreeAtom to JS_DupValue/JS_FreeValue in function parsing
**Variable Definition and Lookup:**
- [x] Update find_global_var() to use JSValue and js_key_equal()
- [x] Update find_lexical_global_var() to use JSValue
- [x] Update find_lexical_decl() to use JSValue and js_key_equal()
- [x] Update js_define_var() to use JSValue
- [x] Update js_parse_check_duplicate_parameter() to use JSValue and js_key_equal()
- [x] Update js_parse_destructuring_var() to return JSValue
- [x] Update js_parse_var() to use JSValue for variable names
**Comparison Helpers:**
- [x] Create js_key_equal_str() for comparing JSValue with C string literals
- [x] Update is_var_in_arg_scope() to use js_key_equal/js_key_equal_str
- [x] Update has_with_scope() to use js_key_equal_str
- [x] Update closure variable comparisons (cv->var_name) to use js_key_equal_str
**Property Access:**
- [x] Fix JS_GetPropertyStr to create proper JSValue keys
- [x] Fix JS_SetPropertyInternal callers to use JS_KEY_* instead of JS_ATOM_*
### JS_KEY_* Macros Added
Compile-time immediate ASCII string constants (≤7 chars):
```c
JS_KEY_empty, JS_KEY_name, JS_KEY_message, JS_KEY_stack,
JS_KEY_errors, JS_KEY_Error, JS_KEY_cause, JS_KEY_length,
JS_KEY_value, JS_KEY_get, JS_KEY_set, JS_KEY_raw,
JS_KEY_flags, JS_KEY_source, JS_KEY_exec, JS_KEY_toJSON,
JS_KEY_eval, JS_KEY_this, JS_KEY_true, JS_KEY_false,
JS_KEY_null, JS_KEY_NaN, JS_KEY_default, JS_KEY_index,
JS_KEY_input, JS_KEY_groups, JS_KEY_indices, JS_KEY_let,
JS_KEY_var, JS_KEY_new, JS_KEY_of, JS_KEY_yield,
JS_KEY_async, JS_KEY_target, JS_KEY_from, JS_KEY_meta,
JS_KEY_as, JS_KEY_with
```
Runtime macro for strings >7 chars:
```c
#define JS_KEY_STR(ctx, str) JS_NewStringLen((ctx), (str), sizeof(str) - 1)
```
Helper function for comparing JSValue with C string literals:
```c
static JS_BOOL js_key_equal_str(JSValue a, const char *str);
```
### Remaining Work
#### 5.3 Update js_parse_property_name() ✓
- [x] Change return type from JSAtom* to JSValue*
- [x] Update all callers (js_parse_object_literal, etc.)
- [x] Updated get_lvalue(), put_lvalue(), js_parse_destructuring_element()
#### 5.4 Replace remaining emit_atom() calls with emit_key() ✓
- [x] Removed emit_atom wrapper function
- [x] Changed last emit_atom(JS_ATOM_this) to emit_key(JS_KEY_this)
#### 5.5 Update Variable Opcode Format in quickjs-opcode.h
- [ ] Change `atom` format opcodes to `key` format
- [ ] Change `atom_u8` and `atom_u16` to `key_u8` and `key_u16`
#### 5.6 Update VM Opcode Handlers ✓
These now read cpool indices and look up JSValue:
- [x] OP_check_var, OP_get_var_undef, OP_get_var
- [x] OP_put_var, OP_put_var_init, OP_put_var_strict
- [x] OP_set_name, OP_make_var_ref, OP_delete_var
- [x] OP_define_var, OP_define_func, OP_throw_error
- [x] OP_make_loc_ref, OP_make_arg_ref
- [x] OP_define_method, OP_define_method_computed
#### 5.7 Update resolve_scope_var() ✓
- [x] Changed signature to use JSValue var_name
- [x] Updated all comparisons to use js_key_equal()/js_key_equal_str()
- [x] Updated var_object_test() to use JSValue
- [x] Updated optimize_scope_make_global_ref() to use JSValue
- [x] Updated resolve_variables() callers to read from cpool
#### 5.8 Convert Remaining JS_ATOM_* Usages
Categories remaining:
- Some debug/print functions still use JSAtom
- Some function signatures not yet converted
- Will be addressed in Phase 7 cleanup
---
## Phase 6: Update Bytecode Serialization ✓
### 6.1 JS_WriteObjectTag Changes ✓
- [x] Changed JS_WriteObjectTag to use bc_put_key() directly for property keys
- [x] Removed JS_ValueToAtom/bc_put_atom path (was broken anyway)
- [x] cpool values serialized via JS_WriteObjectRec()
### 6.2 JS_ReadObject Changes ✓
- [x] Changed JS_ReadObjectTag to use bc_get_key() for property keys
- [x] Uses JS_SetPropertyInternal with JSValue keys
### 6.3 Opcode Format Updates ✓
- [x] Added OP_FMT_key_u8, OP_FMT_key_u16, OP_FMT_key_label_u16 formats
- [x] Updated variable opcodes to use key formats instead of atom formats
- [x] Updated bc_byte_swap() to handle new key formats
- [x] Updated JS_WriteFunctionBytecode() to skip key format opcodes
- [x] Updated JS_ReadFunctionBytecode() to skip key format opcodes
### 6.4 Version Bump ✓
- [x] Incremented BC_VERSION from 5 to 6
---
## Phase 7: Final Cleanup ✓
### 7.1 Additional Parser/Compiler Fixes ✓
- [x] Fixed TOK_IDENT case to use JSValue name, JS_DupValue, emit_key
- [x] Fixed TOK_TRY catch clause to use JSValue name
- [x] Fixed js_parse_statement_or_decl label_name to use JSValue
- [x] Fixed OP_scope_get_var "eval" check to use js_key_equal_str
- [x] Fixed js_parse_delete to use JSValue for name comparison
- [x] Fixed JSON parsing to use js_key_from_string for property names
- [x] Added js_key_from_string() helper function
- [x] Added JS_KEY__ret_, JS_KEY__eval_, JS_KEY__var_ for internal names
- [x] Updated add_closure_var, get_closure_var2, get_closure_var to use JSValue var_name
- [x] Updated set_closure_from_var to use JS_DupValue
- [x] Updated add_closure_variables to use JS_DupValue
- [x] Updated instantiate_hoisted_definitions to use fd_cpool_add for keys
- [x] Updated resolve_variables to use js_key_equal and fd_cpool_add
### 7.1.1 Property Access and Runtime Fixes ✓
- [x] Fixed JS_GetPropertyValue to use js_key_from_string instead of JS_ValueToAtom
- [x] Fixed JS_GetPropertyKey to use js_key_from_string for string keys
- [x] Fixed JS_SetPropertyKey to use js_key_from_string for string keys
- [x] Fixed JS_HasPropertyKey to use js_key_from_string for string keys
- [x] Fixed JS_DeletePropertyKey to use js_key_from_string for string keys
- [x] Updated JS_HasProperty signature to take JSValue prop
- [x] Fixed OP_get_ref_value handler to use JSValue key
- [x] Fixed OP_put_ref_value handler to use JSValue key
- [x] Updated free_func_def to use JS_FreeValue for JSValue fields
### 7.2 Remove JSAtom Type and Functions ✓
- [x] Removed most JS_ATOM_* constants (kept JS_ATOM_NULL, JS_ATOM_END for BC compat)
- [x] JS_NewAtomString now returns JSValue using js_key_new
- [x] JS_FreeAtom, JS_DupAtom are stubs (no-op for backward compat)
- [x] JS_AtomToValue, JS_ValueToAtom are stubs (minimal BC compat)
- [x] Replaced JS_ATOM_* usages with JS_KEY_* or JS_GetPropertyStr
### 7.3 Additional Runtime Fixes ✓
- [x] Fixed free_function_bytecode to use JS_FreeValueRT for JSValue fields
- [x] Fixed JS_SetPropertyFunctionList to use JSValue keys via find_key()
- [x] Fixed JS_InstantiateFunctionListItem to use JSValue keys
- [x] Fixed internalize_json_property to use JSValue names
- [x] Fixed emit_break and push_break_entry to use JSValue label_name
- [x] Implemented JS_Invoke to use JSValue method key
### 7.4 Remaining Stubs (kept for bytecode backward compatibility)
- JSAtom typedef (uint32_t) - used in BC serialization
- JS_ATOM_NULL, JS_ATOM_END - bytecode format markers
- JS_FreeAtom, JS_DupAtom - no-op stubs
- JS_FreeAtomRT, JS_DupAtomRT - no-op stubs
- Legacy BC reader (idx_to_atom array) - for reading old bytecode
---
## Current Build Status
**Build: SUCCEEDS** with warnings (unused variables, labels)
**Statistics:**
- JS_ATOM_* usages: Minimal (only BC serialization compat)
- Property access uses JS_KEY_* macros or JS_GetPropertyStr
- BC_VERSION: 6 (updated for new key-based format)
**What Works:**
- All property access via JSValue keys
- Keyword detection via string comparison
- Function declaration parsing with JSValue names
- Variable definition with JSValue names
- Closure variable tracking with JSValue names
- VM opcode handlers read cpool indices and look up JSValue
- resolve_scope_var() uses JSValue throughout
- js_parse_property_name() returns JSValue
- Bytecode serialization uses bc_put_key/bc_get_key for property keys
- Variable opcodes use key format (cpool indices)
- JSON parsing uses JSValue keys
- Internal variable names use JS_KEY__ret_, JS_KEY__eval_, JS_KEY__var_
- JS_SetPropertyFunctionList uses JSValue keys
- JS_Invoke uses JSValue method keys
- break/continue labels use JSValue
---
## Phase 8: Migrate to New Tagging System (IN PROGRESS)
**Problem**: `JS_VALUE_GET_TAG` returns `JS_TAG_PTR` for all pointers, but ~200 places check for obsolete tags like `JS_TAG_OBJECT`, `JS_TAG_STRING`, `JS_TAG_FUNCTION`, etc., which are never returned. This causes crashes.
**Target Design** (from memory.md):
- JSValue tags: Only `JS_TAG_INT`, `JS_TAG_PTR`, `JS_TAG_SHORT_FLOAT`, `JS_TAG_SPECIAL`
- Pointer types determined by `objhdr_t` header at offset 8 in heap objects
- mist_obj_type: `OBJ_ARRAY(0)`, `OBJ_BLOB(1)`, `OBJ_TEXT(2)`, `OBJ_RECORD(3)`, `OBJ_FUNCTION(4)`, etc.
### 8.1 Unified Heap Object Layout ✓
- [x] Updated mist_text structure to have objhdr_t at offset 8:
```c
typedef struct mist_text {
JSRefCountHeader _dummy_header; /* unused, for offset alignment */
uint32_t _pad; /* padding to align objhdr_t to offset 8 */
objhdr_t hdr; /* NOW at offset 8, like JSString */
word_t length;
word_t packed[];
} mist_text;
```
- [x] JSString already has objhdr_t at offset 8
### 8.2 Type-Checking Helper Functions ✓
Added lowercase internal helpers (to avoid conflict with quickjs.h declarations):
```c
static inline JS_BOOL js_is_gc_object(JSValue v) { return JS_IsPtr(v); }
static inline JSGCObjectTypeEnum js_get_gc_type(JSValue v) {
return ((JSGCObjectHeader *)JS_VALUE_GET_PTR(v))->gc_obj_type;
}
static inline JS_BOOL js_is_record(JSValue v) {
if (!JS_IsPtr(v)) return FALSE;
return js_get_gc_type(v) == JS_GC_OBJ_TYPE_RECORD;
}
static inline JS_BOOL js_is_array(JSValue v) {
if (!JS_IsPtr(v)) return FALSE;
return js_get_gc_type(v) == JS_GC_OBJ_TYPE_ARRAY;
}
static inline JS_BOOL js_is_function(JSValue v) {
if (!JS_IsPtr(v)) return FALSE;
return js_get_gc_type(v) == JS_GC_OBJ_TYPE_FUNCTION;
}
static inline JS_BOOL js_is_object(JSValue v) {
if (!JS_IsPtr(v)) return FALSE;
JSGCObjectTypeEnum t = js_get_gc_type(v);
return t == JS_GC_OBJ_TYPE_RECORD || t == JS_GC_OBJ_TYPE_ARRAY;
}
```
### 8.3 Updated Core Functions ✓
- [x] Updated JS_IsString to read objhdr_t from offset 8
- [x] Updated js_key_hash to read objhdr_t from offset 8
- [x] Updated js_key_equal to read objhdr_t from offset 8
- [x] Updated __JS_FreeValueRT to use objhdr_type for type dispatch
- [x] Updated JS_MarkValue, JS_MarkValueEdgeEx for GC
- [x] Added JS_SetPropertyValue function
- [x] Changed quickjs.h JS_IsFunction/JS_IsObject from inline to extern declarations
### 8.4 Tag Check Migration (PARTIAL)
Updated some critical tag checks:
- [x] Some JS_TAG_OBJECT checks → js_is_object() or js_is_record()
- [ ] Many more JS_TAG_OBJECT checks remain (~200 total)
- [ ] JS_TAG_FUNCTION checks → js_is_function()
- [ ] JS_TAG_STRING checks (some already use JS_IsString)
### 8.5 Remaining Work
- [ ] Fix ASAN memory corruption error (attempting free on address not malloc'd)
- Crash occurs in js_def_realloc during js_realloc_array
- Address is 112 bytes inside a JSFunctionDef allocation
- [ ] Complete remaining ~200 tag check migrations
- [ ] Add mist_hdr to JSFunction (optional, gc_obj_type already works)
- [ ] Remove obsolete tag definitions from quickjs.h:
- JS_TAG_STRING = -8
- JS_TAG_ARRAY = -6
- JS_TAG_FUNCTION = -5
- JS_TAG_FUNCTION_BYTECODE = -2
- JS_TAG_OBJECT = -1
### Current Status
**Build: SUCCEEDS** with warnings
**Runtime: CRASHES** with ASAN error:
```
==16122==ERROR: AddressSanitizer: attempting free on address which was not malloc()-ed
```
The crash occurs during test execution in `js_def_realloc` called from `js_realloc_array`.
Root cause not yet identified - likely a pointer being passed to realloc that wasn't allocated with malloc.
---
## Notes
- JSVarDef.var_name is JSValue
- JSClosureVar.var_name is JSValue
- JSGlobalVar.var_name is JSValue
- JSFunctionDef.func_name is JSValue
- BlockEnv.label_name is JSValue
- OP_get_field/put_field/define_field already use cpool index format
- JSRecord with open addressing is fully implemented
- js_key_hash and js_key_equal work with both immediate and heap text
- js_key_equal_str enables comparison with C string literals for internal names

View File

@@ -145,12 +145,12 @@ static void encode_js_value(json_encoder *enc, JSContext *js, JSValue val) {
double d = js2number(js, val);
if (d == (int)d) enc->writeInt(enc, (int)d);
else enc->writeDouble(enc, d);
} else if (JS_IsText(val)) {
} else if (JS_IsString(val)) {
size_t len;
const char *str = JS_ToCStringLen(js, &len, val);
enc->writeString(enc, str, len);
JS_FreeCString(js, str);
} else if (JS_IsArray(val)) {
} else if (JS_IsArray(js, val)) {
encode_js_array(enc, js, val);
} else if (JS_IsObject(val)) {
encode_js_object(enc, js, val);

View File

@@ -102,7 +102,7 @@ static void scores_cb(PDScoresList *scores, const char *errorMessage) {
JSC_SCALL(scoreboards_addScore,
if (!pd_scoreboards) return JS_ThrowInternalError(js, "scoreboards not initialized");
uint32_t value = (uint32_t)js2number(js, argv[1]);
if (argc > 2 && JS_IsFunction(argv[2])) {
if (argc > 2 && JS_IsFunction(js, argv[2])) {
g_scoreboard_js = js;
JS_FreeValue(js, g_add_score_callback);
g_add_score_callback = JS_DupValue(js, argv[2]);
@@ -112,7 +112,7 @@ JSC_SCALL(scoreboards_addScore,
JSC_SCALL(scoreboards_getPersonalBest,
if (!pd_scoreboards) return JS_ThrowInternalError(js, "scoreboards not initialized");
if (argc > 1 && JS_IsFunction(argv[1])) {
if (argc > 1 && JS_IsFunction(js, argv[1])) {
g_scoreboard_js = js;
JS_FreeValue(js, g_personal_best_callback);
g_personal_best_callback = JS_DupValue(js, argv[1]);
@@ -129,7 +129,7 @@ JSC_CCALL(scoreboards_freeScore,
JSC_CCALL(scoreboards_getScoreboards,
if (!pd_scoreboards) return JS_ThrowInternalError(js, "scoreboards not initialized");
if (argc > 0 && JS_IsFunction(argv[0])) {
if (argc > 0 && JS_IsFunction(js, argv[0])) {
g_scoreboard_js = js;
JS_FreeValue(js, g_boards_list_callback);
g_boards_list_callback = JS_DupValue(js, argv[0]);
@@ -145,7 +145,7 @@ JSC_CCALL(scoreboards_freeBoardsList,
JSC_SCALL(scoreboards_getScores,
if (!pd_scoreboards) return JS_ThrowInternalError(js, "scoreboards not initialized");
if (argc > 1 && JS_IsFunction(argv[1])) {
if (argc > 1 && JS_IsFunction(js, argv[1])) {
g_scoreboard_js = js;
JS_FreeValue(js, g_scores_callback);
g_scores_callback = JS_DupValue(js, argv[1]);

View File

@@ -4,22 +4,22 @@
// Time is in seconds.
function make_reason(factory, excuse, evidence) {
def reason = Error(`pronto.${factory}${excuse ? ': ' + excuse : ''}`)
def reason = new Error(`pronto.${factory}${excuse ? ': ' + excuse : ''}`)
reason.evidence = evidence
return reason
}
function is_requestor(fn) {
return is_function(fn) && (length(fn) == 1 || length(fn) == 2)
return typeof fn == 'function' && (fn.length == 1 || fn.length == 2)
}
function check_requestors(list, factory) {
if (!is_array(list) || some(list, r => !is_requestor(r)))
if (!isa(list, array) || list.some(r => !is_requestor(r)))
throw make_reason(factory, 'Bad requestor array.', list)
}
function check_callback(cb, factory) {
if (!is_function(cb) || length(cb) != 2)
if (typeof cb != 'function' || cb.length != 2)
throw make_reason(factory, 'Not a callback.', cb)
}
@@ -27,7 +27,7 @@ function check_callback(cb, factory) {
// Tries each requestor in order until one succeeds.
function fallback(requestor_array) {
def factory = 'fallback'
if (!is_array(requestor_array) || length(requestor_array) == 0)
if (!isa(requestor_array, array) || requestor_array.length == 0)
throw make_reason(factory, 'Empty requestor array.')
check_requestors(requestor_array, factory)
@@ -47,7 +47,7 @@ function fallback(requestor_array) {
function try_next() {
if (cancelled) return
if (index >= length(requestor_array)) {
if (index >= requestor_array.length) {
callback(null, make_reason(factory, 'All requestors failed.'))
return
}
@@ -79,25 +79,25 @@ function fallback(requestor_array) {
// Runs requestors in parallel, collecting all results.
function parallel(requestor_array, throttle, need) {
def factory = 'parallel'
if (!is_array(requestor_array))
if (!isa(requestor_array, array))
throw make_reason(factory, 'Not an array.', requestor_array)
check_requestors(requestor_array, factory)
def length = length(requestor_array)
def length = requestor_array.length
if (length == 0)
return function(callback, value) { callback([]) }
if (need == null) need = length
if (!is_number(need) || need < 0 || need > length)
if (typeof need != 'number' || need < 0 || need > length)
throw make_reason(factory, 'Bad need.', need)
if (throttle != null && (!is_number(throttle) || throttle < 1))
if (throttle != null && (typeof throttle != 'number' || throttle < 1))
throw make_reason(factory, 'Bad throttle.', throttle)
return function parallel_requestor(callback, value) {
check_callback(callback, factory)
def results = array(length)
def cancel_list = array(length)
def results = new Array(length)
def cancel_list = new Array(length)
var next_index = 0
var successes = 0
var failures = 0
@@ -106,8 +106,8 @@ function parallel(requestor_array, throttle, need) {
function cancel(reason) {
if (finished) return
finished = true
arrfor(cancel_list, c => {
try { if (is_function(c)) c(reason) } catch (_) {}
cancel_list.forEach(c => {
try { if (typeof c == 'function') c(reason) } catch (_) {}
})
}
@@ -153,8 +153,7 @@ function parallel(requestor_array, throttle, need) {
}
}
def concurrent = throttle ? min(throttle, length) : length
def concurrent = throttle ? number.min(throttle, length) : length
for (var i = 0; i < concurrent; i++) start_one()
return cancel
@@ -165,22 +164,22 @@ function parallel(requestor_array, throttle, need) {
// Runs requestors in parallel, returns first success(es).
function race(requestor_array, throttle, need) {
def factory = 'race'
if (!is_array(requestor_array) || length(requestor_array) == 0)
if (!isa(requestor_array, array) || requestor_array.length == 0)
throw make_reason(factory, 'Empty requestor array.')
check_requestors(requestor_array, factory)
def length = length(requestor_array)
def length = requestor_array.length
if (need == null) need = 1
if (!is_number(need) || need < 1 || need > length)
if (typeof need != 'number' || need < 1 || need > length)
throw make_reason(factory, 'Bad need.', need)
if (throttle != null && (!is_number(throttle) || throttle < 1))
if (throttle != null && (typeof throttle != 'number' || throttle < 1))
throw make_reason(factory, 'Bad throttle.', throttle)
return function race_requestor(callback, value) {
check_callback(callback, factory)
def results = array(length)
def cancel_list = array(length)
def results = new Array(length)
def cancel_list = new Array(length)
var next_index = 0
var successes = 0
var failures = 0
@@ -189,8 +188,8 @@ function race(requestor_array, throttle, need) {
function cancel(reason) {
if (finished) return
finished = true
arrfor(cancel_list, c => {
try { if (is_function(c)) c(reason) } catch (_) {}
cancel_list.forEach(c => {
try { if (typeof c == 'function') c(reason) } catch (_) {}
})
}
@@ -239,7 +238,7 @@ function race(requestor_array, throttle, need) {
}
}
def concurrent = throttle ? min(throttle, length) : length
def concurrent = throttle ? number.min(throttle, length) : length
for (var i = 0; i < concurrent; i++) start_one()
return cancel
@@ -250,11 +249,11 @@ function race(requestor_array, throttle, need) {
// Runs requestors one at a time, passing result to next.
function sequence(requestor_array) {
def factory = 'sequence'
if (!is_array(requestor_array))
if (!isa(requestor_array, array))
throw make_reason(factory, 'Not an array.', requestor_array)
check_requestors(requestor_array, factory)
if (length(requestor_array) == 0)
if (requestor_array.length == 0)
return function(callback, value) { callback(value) }
return function sequence_requestor(callback, value) {
@@ -273,7 +272,7 @@ function sequence(requestor_array) {
function run_next(val) {
if (cancelled) return
if (index >= length(requestor_array)) {
if (index >= requestor_array.length) {
callback(val)
return
}
@@ -305,7 +304,7 @@ function sequence(requestor_array) {
// Converts a unary function into a requestor.
function requestorize(unary) {
def factory = 'requestorize'
if (!is_function(unary))
if (typeof unary != 'function')
throw make_reason(factory, 'Not a function.', unary)
return function requestorized(callback, value) {
@@ -319,12 +318,45 @@ function requestorize(unary) {
}
}
// objectify(factory_fn)
// Converts a factory that takes arrays to one that takes objects.
function objectify(factory_fn) {
def factory = 'objectify'
if (typeof factory_fn != 'function')
throw make_reason(factory, 'Not a factory.', factory_fn)
return function objectified_factory(object_of_requestors, ...rest) {
if (!isa(object_of_requestors, object))
throw make_reason(factory, 'Expected an object.', object_of_requestors)
def keys = array(object_of_requestors)
def requestor_array = keys.map(k => object_of_requestors[k])
def inner_requestor = factory_fn(requestor_array, ...rest)
return function(callback, value) {
return inner_requestor(function(results, reason) {
if (results == null) {
callback(null, reason)
} else if (isa(results, array)) {
def obj = {}
keys.forEach((k, i) => { obj[k] = results[i] })
callback(obj, reason)
} else {
callback(results, reason)
}
}, value)
}
}
}
return {
fallback,
parallel,
race,
sequence,
requestorize,
objectify,
is_requestor,
check_callback
}

16
qop.c
View File

@@ -8,10 +8,10 @@ static void js_qop_archive_finalizer(JSRuntime *rt, JSValue val) {
qop_desc *qop = JS_GetOpaque(val, js_qop_archive_class_id);
if (qop) {
if (qop->hashmap) {
js_free_rt(qop->hashmap);
js_free_rt(rt, qop->hashmap);
}
qop_close(qop);
js_free_rt(qop);
js_free_rt(rt, qop);
}
}
@@ -34,8 +34,8 @@ static void js_qop_writer_finalizer(JSRuntime *rt, JSValue val) {
qop_writer *w = JS_GetOpaque(val, js_qop_writer_class_id);
if (w) {
if (w->fh) fclose(w->fh);
if (w->files) js_free_rt(w->files);
js_free_rt(w);
if (w->files) js_free_rt(rt, w->files);
js_free_rt(rt, w);
}
}
@@ -450,10 +450,10 @@ static const JSCFunctionListEntry js_qop_writer_funcs[] = {
static const JSCFunctionListEntry js_qop_funcs[] = {
MIST_FUNC_DEF(qop, open, 1),
MIST_FUNC_DEF(qop, write, 1),
JS_PROP_INT32_DEF("FLAG_NONE", QOP_FLAG_NONE, 0),
JS_PROP_INT32_DEF("FLAG_COMPRESSED_ZSTD", QOP_FLAG_COMPRESSED_ZSTD, 0),
JS_PROP_INT32_DEF("FLAG_COMPRESSED_DEFLATE", QOP_FLAG_COMPRESSED_DEFLATE, 0),
JS_PROP_INT32_DEF("FLAG_ENCRYPTED", QOP_FLAG_ENCRYPTED, 0),
JS_PROP_INT32_DEF("FLAG_NONE", QOP_FLAG_NONE, JS_PROP_ENUMERABLE),
JS_PROP_INT32_DEF("FLAG_COMPRESSED_ZSTD", QOP_FLAG_COMPRESSED_ZSTD, JS_PROP_ENUMERABLE),
JS_PROP_INT32_DEF("FLAG_COMPRESSED_DEFLATE", QOP_FLAG_COMPRESSED_DEFLATE, JS_PROP_ENUMERABLE),
JS_PROP_INT32_DEF("FLAG_ENCRYPTED", QOP_FLAG_ENCRYPTED, JS_PROP_ENUMERABLE),
};
JSValue js_qop_use(JSContext *js) {

View File

@@ -3,10 +3,10 @@ var qop = use('qop')
function print_usage() {
log.console("Usage: qopconv [OPTION...] FILE...")
log.console(" -u <archive> .. unpack archive")
log.console(" -l <archive> .. list contents of archive")
log.console(" -u <archive> ... unpack archive")
log.console(" -l <archive> ... list contents of archive")
log.console(" -d <dir> ....... change read dir when creating archives")
log.console(" <sources...> <archive> .. create archive from sources")
log.console(" <sources...> <archive> ... create archive from sources")
}
function list(archive_path) {
@@ -24,12 +24,12 @@ function list(archive_path) {
}
var files = archive.list()
arrfor(files, function(f) {
for (var f of files) {
var s = archive.stat(f)
// Format: index hash size path
// We don't have index/hash easily available in JS binding yet, just size/path
log.console(`${f} (${s.size} bytes)`)
})
}
archive.close()
}
@@ -48,26 +48,26 @@ function unpack(archive_path) {
}
var files = archive.list()
arrfor(files, function(f) {
for (var f of files) {
var data = archive.read(f)
if (data) {
// Ensure directory exists
var dir = fd.dirname(f)
var dir = f.substring(0, f.lastIndexOf('/'))
if (dir) {
// recursive mkdir
var parts = array(dir, '/')
var parts = dir.split('/')
var curr = "."
arrfor(parts, function(p) {
for (var p of parts) {
curr += "/" + p
try { fd.mkdir(curr) } catch(e) {}
})
}
}
var fh = fd.open(f, "w")
fd.write(fh, data)
fd.close(fh)
log.console("Extracted " + f)
}
})
}
archive.close()
}
@@ -89,11 +89,11 @@ function pack(sources, archive_path, read_dir) {
if (st.isDirectory) {
var list = fd.readdir(full_path)
arrfor(list, function(item) {
if (item == "." || item == "..") return
for (var item of list) {
if (item == "." || item == "..") continue
var sub = path == "." ? item : path + "/" + item
add_recursive(sub)
})
}
} else {
var data = fd.slurp(full_path)
if (data) {
@@ -103,22 +103,22 @@ function pack(sources, archive_path, read_dir) {
}
}
arrfor(sources, function(s) {
for (var s of sources) {
add_recursive(s)
})
}
writer.finalize()
log.console("Created " + archive_path)
}
if (!is_array(arg) || length(arg) < 1) {
if (typeof arg == 'undefined' || arg.length < 1) {
print_usage()
} else {
if (arg[0] == "-l") {
if (length(arg) < 2) print_usage()
if (arg.length < 2) print_usage()
else list(arg[1])
} else if (arg[0] == "-u") {
if (length(arg) < 2) print_usage()
if (arg.length < 2) print_usage()
else unpack(arg[1])
} else {
var sources = []
@@ -130,12 +130,12 @@ if (!is_array(arg) || length(arg) < 1) {
i = 2
}
for (; i < length(arg) - 1; i++) {
push(sources, arg[i])
for (; i < arg.length - 1; i++) {
sources.push(arg[i])
}
archive = arg[length(arg) - 1]
archive = arg[arg.length - 1]
if (length(sources) == 0) {
if (sources.length == 0) {
print_usage()
} else {
pack(sources, archive, read_dir)

View File

@@ -14,7 +14,7 @@ rnd.random_fit = function()
rnd.random_whole = function(num)
{
return floor(rnd.random() * num)
return number.floor(rnd.random() * num)
}
rnd.random_range = function(min,max)

101
remove.ce
View File

@@ -1,105 +1,24 @@
// cell remove <locator> - Remove a package from the shop
//
// Usage:
// cell remove <locator> Remove a package from the shop
// cell remove . Remove current directory package from shop
//
// Options:
// --prune Also remove packages no longer needed by any root
// --dry-run Show what would be removed
// cell remove <alias|path> - Remove a package from dependencies or shop
var shop = use('internal/shop')
var pkg = use('package')
var link = use('link')
var fd = use('fd')
var target_pkg = null
var prune = false
var dry_run = false
for (var i = 0; i < length(args); i++) {
if (args[i] == '--prune') {
prune = true
} else if (args[i] == '--dry-run') {
dry_run = true
} else if (args[i] == '--help' || args[i] == '-h') {
log.console("Usage: cell remove <locator> [options]")
log.console("")
log.console("Remove a package from the shop.")
log.console("")
log.console("Options:")
log.console(" --prune Also remove packages no longer needed by any root")
log.console(" --dry-run Show what would be removed")
$stop()
} else if (!starts_with(args[i], '-')) {
target_pkg = args[i]
}
}
if (!target_pkg) {
log.console("Usage: cell remove <locator> [options]")
if (args.length < 1) {
log.console("Usage: cell remove <alias|path>")
$stop()
return
}
var pkg = args[0]
// Resolve relative paths to absolute paths
if (target_pkg == '.' || starts_with(target_pkg, './') || starts_with(target_pkg, '../') || fd.is_dir(target_pkg)) {
var resolved = fd.realpath(target_pkg)
if (pkg == '.' || pkg.startsWith('./') || pkg.startsWith('../') || fd.is_dir(pkg)) {
var resolved = fd.realpath(pkg)
if (resolved) {
target_pkg = resolved
pkg = resolved
}
}
var packages_to_remove = [target_pkg]
if (prune) {
// Find packages no longer needed
// Get all dependencies of remaining packages
var lock = shop.load_lock()
var all_packages = shop.list_packages()
// Build set of all needed packages (excluding target)
var needed = {}
arrfor(all_packages, function(p) {
if (p == target_pkg || p == 'core') return
// Mark this package and its deps as needed
needed[p] = true
try {
var deps = pkg.gather_dependencies(p)
arrfor(deps, function(dep) {
needed[dep] = true
})
} catch (e) {
// Skip if can't read deps
}
})
// Find packages that are NOT needed
arrfor(all_packages, function(p) {
if (p == 'core') return
if (!needed[p] && find(packages_to_remove, p) == null) {
push(packages_to_remove, p)
}
})
}
if (dry_run) {
log.console("Would remove:")
arrfor(packages_to_remove, function(p) {
log.console(" " + p)
})
} else {
arrfor(packages_to_remove, function(p) {
// Remove any link for this package
if (link.is_linked(p)) {
link.remove(p)
}
// Remove from shop
shop.remove(p)
})
log.console("Removed " + text(length(packages_to_remove)) + " package(s).")
}
shop.remove(pkg)
$stop()

View File

@@ -1,196 +0,0 @@
// cell resolve [<locator>] - Print fully resolved dependency closure
//
// Usage:
// cell resolve Resolve current directory package
// cell resolve . Resolve current directory package
// cell resolve <locator> Resolve specific package
//
// Options:
// --target <triple> Annotate builds for target platform
// --locked Show lock state without applying links
// --refresh Refresh floating refs before printing
var shop = use('internal/shop')
var pkg = use('package')
var link = use('link')
var build = use('build')
var fd = use('fd')
var target_locator = null
var target_triple = null
var show_locked = false
var refresh_first = false
for (var i = 0; i < length(args); i++) {
if (args[i] == '--target' || args[i] == '-t') {
if (i + 1 < length(args)) {
target_triple = args[++i]
} else {
log.error('--target requires a triple')
$stop()
}
} else if (args[i] == '--locked') {
show_locked = true
} else if (args[i] == '--refresh') {
refresh_first = true
} else if (args[i] == '--help' || args[i] == '-h') {
log.console("Usage: cell resolve [<locator>] [options]")
log.console("")
log.console("Print the fully resolved dependency closure.")
log.console("")
log.console("Options:")
log.console(" --target <triple> Annotate builds for target platform")
log.console(" --locked Show lock state without applying links")
log.console(" --refresh Refresh floating refs before printing")
$stop()
} else if (!starts_with(args[i], '-')) {
target_locator = args[i]
}
}
// Default to current directory
if (!target_locator) {
target_locator = '.'
}
// Resolve local paths
if (target_locator == '.' || starts_with(target_locator, './') || starts_with(target_locator, '../') || fd.is_dir(target_locator)) {
var resolved = fd.realpath(target_locator)
if (resolved) {
target_locator = resolved
}
}
// Check if it's a valid package
if (!fd.is_file(target_locator + '/cell.toml')) {
// Try to find it in the shop
var pkg_dir = shop.get_package_dir(target_locator)
if (!fd.is_file(pkg_dir + '/cell.toml')) {
log.error("Not a valid package: " + target_locator)
$stop()
}
}
// Detect target if not specified
if (!target_triple) {
target_triple = build.detect_host_target()
}
var lock = shop.load_lock()
var links = link.load()
// Gather all dependencies recursively
var all_deps = {}
var visited = {}
function gather_deps(locator, depth) {
if (visited[locator]) return
visited[locator] = true
all_deps[locator] = { depth: depth }
try {
var deps = pkg.dependencies(locator)
if (deps) {
arrfor(array(deps), function(alias) {
var dep_locator = deps[alias]
gather_deps(dep_locator, depth + 1)
})
}
} catch (e) {
// Package might not have dependencies
}
}
gather_deps(target_locator, 0)
// Print header
log.console("Resolved dependency closure for: " + target_locator)
log.console("Target: " + target_triple)
log.console("")
// Sort by depth then alphabetically
var sorted = array(array(all_deps), function(locator) { return { locator: locator, depth: all_deps[locator].depth } })
sorted = sort(sorted, "locator")
sorted = sort(sorted, "depth")
for (var i = 0; i < length(sorted); i++) {
var locator = sorted[i].locator
var depth = sorted[i].depth
var indent = ""
for (var j = 0; j < depth; j++) indent += " "
// Get info about this package
var info = shop.resolve_package_info(locator)
var lock_entry = lock[locator]
var link_target = show_locked ? null : links[locator]
var effective_locator = link_target || locator
// Check status
var is_linked = link_target != null
var is_in_lock = lock_entry != null
var is_local = info == 'local'
// Check if fetched (package directory exists)
var pkg_dir = shop.get_package_dir(locator)
var is_fetched = fd.is_dir(pkg_dir) || fd.is_link(pkg_dir)
// Check if built (library exists)
var lib_dir = shop.get_lib_dir()
var lib_name = shop.lib_name_for_package(locator)
var dylib_ext = '.dylib' // TODO: detect from target
var lib_path = lib_dir + '/' + lib_name + dylib_ext
var is_built = fd.is_file(lib_path)
// Format output
var status_parts = []
if (is_linked) push(status_parts, "linked")
if (is_local) push(status_parts, "local")
if (!is_in_lock) push(status_parts, "not in lock")
if (!is_fetched) push(status_parts, "not fetched")
if (is_built) push(status_parts, "built")
var commit_str = ""
if (lock_entry && lock_entry.commit) {
commit_str = " @" + text(lock_entry.commit, 0, 8)
} else if (lock_entry && lock_entry.type == 'local') {
commit_str = " (local)"
}
var line = indent + locator + commit_str
if (is_linked && !show_locked) {
line += " -> " + link_target
}
if (length(status_parts) > 0) {
line += " [" + text(status_parts, ", ") + "]"
}
log.console(line)
// Show compilation inputs if requested (verbose)
if (depth == 0) {
try {
var cflags = pkg.get_flags(locator, 'CFLAGS', target_triple)
var ldflags = pkg.get_flags(locator, 'LDFLAGS', target_triple)
if (length(cflags) > 0 || length(ldflags) > 0) {
log.console(indent + " Compilation inputs:")
if (length(cflags) > 0) {
log.console(indent + " CFLAGS: " + text(cflags, ' '))
}
if (length(ldflags) > 0) {
log.console(indent + " LDFLAGS: " + text(ldflags, ' '))
}
}
} catch (e) {
// Skip if can't read config
}
}
}
log.console("")
log.console("Total: " + text(length(sorted)) + " package(s)")
$stop()

View File

@@ -4,14 +4,14 @@
var shop = use('internal/shop')
var pkg = use('package')
if (length(args) < 1) {
if (args.length < 1) {
log.console("Usage: cell search <query>")
log.console("Searches for packages, actors, or modules matching the query.")
$stop()
return
}
var query = args[0]
var query = args[0].toLowerCase()
var found_packages = []
var found_modules = []
var found_actors = []
@@ -19,34 +19,34 @@ var found_actors = []
// Search through all installed packages
var packages = shop.list_packages()
arrfor(packages, function(package_name) {
for (var package_name of packages) {
// Check if package name matches
if (search(package_name, query) != null) {
push(found_packages, package_name)
if (package_name.toLowerCase().includes(query)) {
found_packages.push(package_name)
}
// Search modules and actors within the package
try {
var modules = pkg.list_modules(package_name)
arrfor(modules, function(mod) {
if (search(mod, query) != null) {
push(found_modules, package_name + ':' + mod)
for (var mod of modules) {
if (mod.toLowerCase().includes(query)) {
found_modules.push(package_name + ':' + mod)
}
})
}
var actors = pkg.list_programs(package_name)
arrfor(actors, function(actor) {
if (search(actor, query) != null) {
push(found_actors, package_name + ':' + actor)
for (var actor of actors) {
if (actor.toLowerCase().includes(query)) {
found_actors.push(package_name + ':' + actor)
}
})
}
} catch (e) {
// Skip packages that can't be read
}
})
}
// Print results
var total = length(found_packages) + length(found_modules) + length(found_actors)
var total = found_packages.length + found_modules.length + found_actors.length
if (total == 0) {
log.console("No results found for '" + query + "'")
@@ -54,27 +54,27 @@ if (total == 0) {
log.console("Found " + text(total) + " result(s) for '" + query + "':")
log.console("")
if (length(found_packages) > 0) {
if (found_packages.length > 0) {
log.console("Packages:")
arrfor(found_packages, function(p) {
for (var p of found_packages) {
log.console(" " + p)
})
}
log.console("")
}
if (length(found_modules) > 0) {
if (found_modules.length > 0) {
log.console("Modules:")
arrfor(found_modules, function(m) {
for (var m of found_modules) {
log.console(" " + m)
})
}
log.console("")
}
if (length(found_actors) > 0) {
if (found_actors.length > 0) {
log.console("Actors:")
arrfor(found_actors, function(a) {
for (var a of found_actors) {
log.console(" " + a)
})
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,306 +0,0 @@
/*
Copyright (c) 2009-2017 Dave Gamble and cJSON contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#ifndef cJSON__h
#define cJSON__h
#ifdef __cplusplus
extern "C"
{
#endif
#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32))
#define __WINDOWS__
#endif
#ifdef __WINDOWS__
/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention. For windows you have 3 define options:
CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols
CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default)
CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol
For *nix builds that support visibility attribute, you can define similar behavior by
setting default visibility to hidden by adding
-fvisibility=hidden (for gcc)
or
-xldscope=hidden (for sun cc)
to CFLAGS
then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does
*/
#define CJSON_CDECL __cdecl
#define CJSON_STDCALL __stdcall
/* export symbols by default, this is necessary for copy pasting the C and header file */
#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS)
#define CJSON_EXPORT_SYMBOLS
#endif
#if defined(CJSON_HIDE_SYMBOLS)
#define CJSON_PUBLIC(type) type CJSON_STDCALL
#elif defined(CJSON_EXPORT_SYMBOLS)
#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL
#elif defined(CJSON_IMPORT_SYMBOLS)
#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL
#endif
#else /* !__WINDOWS__ */
#define CJSON_CDECL
#define CJSON_STDCALL
#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY)
#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type
#else
#define CJSON_PUBLIC(type) type
#endif
#endif
/* project version */
#define CJSON_VERSION_MAJOR 1
#define CJSON_VERSION_MINOR 7
#define CJSON_VERSION_PATCH 19
#include <stddef.h>
/* cJSON Types: */
#define cJSON_Invalid (0)
#define cJSON_False (1 << 0)
#define cJSON_True (1 << 1)
#define cJSON_NULL (1 << 2)
#define cJSON_Number (1 << 3)
#define cJSON_String (1 << 4)
#define cJSON_Array (1 << 5)
#define cJSON_Object (1 << 6)
#define cJSON_Raw (1 << 7) /* raw json */
#define cJSON_IsReference 256
#define cJSON_StringIsConst 512
/* The cJSON structure: */
typedef struct cJSON
{
/* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
struct cJSON *next;
struct cJSON *prev;
/* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
struct cJSON *child;
/* The type of the item, as above. */
int type;
/* The item's string, if type==cJSON_String and type == cJSON_Raw */
char *valuestring;
/* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */
int valueint;
/* The item's number, if type==cJSON_Number */
double valuedouble;
/* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
char *string;
} cJSON;
typedef struct cJSON_Hooks
{
/* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */
void *(CJSON_CDECL *malloc_fn)(size_t sz);
void (CJSON_CDECL *free_fn)(void *ptr);
} cJSON_Hooks;
typedef int cJSON_bool;
/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them.
* This is to prevent stack overflows. */
#ifndef CJSON_NESTING_LIMIT
#define CJSON_NESTING_LIMIT 1000
#endif
/* Limits the length of circular references can be before cJSON rejects to parse them.
* This is to prevent stack overflows. */
#ifndef CJSON_CIRCULAR_LIMIT
#define CJSON_CIRCULAR_LIMIT 10000
#endif
/* returns the version of cJSON as a string */
CJSON_PUBLIC(const char*) cJSON_Version(void);
/* Supply malloc, realloc and free functions to cJSON */
CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks);
/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */
/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */
CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value);
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length);
/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */
/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */
CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated);
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated);
/* Render a cJSON entity to text for transfer/storage. */
CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item);
/* Render a cJSON entity to text for transfer/storage without any formatting. */
CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item);
/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */
CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt);
/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */
/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */
CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format);
/* Delete a cJSON entity and all subentities. */
CJSON_PUBLIC(void) cJSON_Delete(cJSON *item);
/* Returns the number of items in an array (or object). */
CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array);
/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */
CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index);
/* Get item "string" from object. Case insensitive. */
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string);
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string);
CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string);
/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */
CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void);
/* Check item type and return its value */
CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item);
CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item);
/* These functions check the type of an item */
CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item);
/* These calls create a cJSON item of the appropriate type. */
CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean);
CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num);
CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string);
/* raw json */
CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw);
CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void);
/* Create a string where valuestring references a string so
* it will not be freed by cJSON_Delete */
CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string);
/* Create an object/array that only references it's elements so
* they will not be freed by cJSON_Delete */
CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child);
CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child);
/* These utilities create an Array of count items.
* The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/
CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count);
CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count);
CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count);
CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count);
/* Append item to the specified array/object. */
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item);
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item);
/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object.
* WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before
* writing to `item->string` */
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item);
/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item);
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item);
/* Remove/Detach items from Arrays/Objects. */
CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item);
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which);
CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which);
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string);
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string);
CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string);
CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string);
/* Update array items. */
CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement);
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem);
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem);
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object,const char *string,cJSON *newitem);
/* Duplicate a cJSON item */
CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse);
/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will
* need to be released. With recurse!=0, it will duplicate any children connected to the item.
* The item->next and ->prev pointers are always zero on return from Duplicate. */
/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal.
* case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */
CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive);
/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings.
* The input pointer json cannot point to a read-only address area, such as a string constant,
* but should point to a readable and writable address area. */
CJSON_PUBLIC(void) cJSON_Minify(char *json);
/* Helper functions for creating and adding items to an object at the same time.
* They return the added item or NULL on failure. */
CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean);
CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number);
CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string);
CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw);
CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name);
/* When assigning an integer value, it needs to be propagated to valuedouble too. */
#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number))
/* helper for the cJSON_SetNumberValue macro */
CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number);
#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number))
/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */
CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring);
/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/
#define cJSON_SetBoolValue(object, boolValue) ( \
(object != NULL && ((object)->type & (cJSON_False|cJSON_True))) ? \
(object)->type=((object)->type &(~(cJSON_False|cJSON_True)))|((boolValue)?cJSON_True:cJSON_False) : \
cJSON_Invalid\
)
/* Macro for iterating over an array or object */
#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next)
/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */
CJSON_PUBLIC(void *) cJSON_malloc(size_t size);
CJSON_PUBLIC(void) cJSON_free(void *object);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -2,6 +2,7 @@
#include <windows.h>
#endif
#define WOTA_IMPLEMENTATION
#include "wota.h"
#define STB_DS_IMPLEMENTATION
@@ -9,21 +10,15 @@
#include "cell.h"
#include "cell_internal.h"
#include "cJSON.h"
#define ENGINE "internal/engine.cm"
#define CELL_SHOP_DIR ".cell"
#define CELL_CORE_DIR "packages/core"
#include <math.h>
#include <signal.h>
#include <unistd.h>
#include <sys/stat.h>
/* Test suite declarations */
int run_c_test_suite(JSContext *ctx);
static int run_test_suite(size_t heap_size);
cell_rt *root_cell = NULL;
static char *core_path = NULL;
@@ -105,35 +100,6 @@ static char* load_core_file(const char *filename, size_t *out_size) {
return data;
}
static int print_json_errors(const char *json) {
if (!json) return 0;
cJSON *root = cJSON_Parse(json);
if (!root) return 0;
cJSON *errors = cJSON_GetObjectItemCaseSensitive(root, "errors");
if (!cJSON_IsArray(errors) || cJSON_GetArraySize(errors) == 0) {
cJSON_Delete(root);
return 0;
}
const char *filename = "<unknown>";
cJSON *fname = cJSON_GetObjectItemCaseSensitive(root, "filename");
if (cJSON_IsString(fname))
filename = fname->valuestring;
cJSON *e;
cJSON_ArrayForEach(e, errors) {
const char *msg = cJSON_GetStringValue(
cJSON_GetObjectItemCaseSensitive(e, "message"));
cJSON *line = cJSON_GetObjectItemCaseSensitive(e, "line");
cJSON *col = cJSON_GetObjectItemCaseSensitive(e, "column");
if (msg && cJSON_IsNumber(line) && cJSON_IsNumber(col))
fprintf(stderr, "%s:%d:%d: error: %s\n",
filename, (int)line->valuedouble, (int)col->valuedouble, msg);
else if (msg)
fprintf(stderr, "%s: error: %s\n", filename, msg);
}
cJSON_Delete(root);
return 1;
}
// Get the core path for use by scripts
const char* cell_get_core_path(void) {
return core_path;
@@ -148,15 +114,21 @@ void actor_disrupt(cell_rt *crt)
JSValue js_os_use(JSContext *js);
JSValue js_math_use(JSContext *js);
JSValue js_json_use(JSContext *js);
JSValue js_nota_use(JSContext *js);
JSValue js_wota_use(JSContext *js);
void script_startup(cell_rt *prt)
{
JSRuntime *rt = JS_NewRuntime();
JS_SetInterruptHandler(rt, (JSInterruptHandler *)actor_interrupt_cb, prt);
JSContext *js = JS_NewContext(rt);
JSRuntime *rt;
rt = JS_NewRuntime();
JSContext *js = JS_NewContextRaw(rt);
JS_SetInterruptHandler(rt, actor_interrupt_cb, prt);
JS_AddIntrinsicBaseObjects(js);
JS_AddIntrinsicEval(js);
JS_AddIntrinsicRegExp(js);
JS_AddIntrinsicJSON(js);
JS_AddIntrinsicMapSet(js);
JS_SetContextOpaque(js, prt);
prt->context = js;
@@ -164,7 +136,42 @@ void script_startup(cell_rt *prt)
cell_rt *crt = JS_GetContextOpaque(js);
JS_FreeValue(js, js_blob_use(js));
// Load and compile engine.cm
JSValue globalThis = JS_GetGlobalObject(js);
JSValue cell = JS_NewObject(js);
JS_SetPropertyStr(js,globalThis,"cell", cell);
JSValue hidden_fn = JS_NewObject(js);
JS_SetPropertyStr(js, cell, "hidden", hidden_fn);
JS_SetPropertyStr(js, hidden_fn, "os", js_os_use(js));
const char actorsym_script[] = "Symbol('actordata');";
JSValue actorsym = JS_Eval(js, actorsym_script, sizeof(actorsym_script)-1, "internal", 0);
JS_SetPropertyStr(js, hidden_fn, "actorsym", actorsym);
crt->actor_sym = JS_ValueToAtom(js, actorsym);
if (crt->init_wota) {
JS_SetPropertyStr(js, hidden_fn, "init", wota2value(js, crt->init_wota));
// init wota can now be freed
free(crt->init_wota);
crt->init_wota = NULL;
}
// Store the core path for scripts to use
JSValue js_cell = JS_GetPropertyStr(js, globalThis, "cell");
JSValue hidden = JS_GetPropertyStr(js, js_cell, "hidden");
if (core_path) {
JS_SetPropertyStr(js, hidden, "core_path", JS_NewString(js, core_path));
}
JS_FreeValue(js, hidden);
JS_FreeValue(js, js_cell);
JS_FreeValue(js, globalThis);
// Load engine.cm from the core directory
size_t engine_size;
char *data = load_core_file(ENGINE, &engine_size);
if (!data) {
@@ -172,42 +179,9 @@ void script_startup(cell_rt *prt)
return;
}
JSValue bytecode = JS_Compile(js, data, engine_size, ENGINE);
free(data);
if (JS_IsException(bytecode)) {
uncaught_exception(js, bytecode);
return;
}
// Create hidden environment
JSValue hidden_env = JS_NewObject(js);
JS_SetPropertyStr(js, hidden_env, "os", js_os_use(js));
JS_SetPropertyStr(js, hidden_env, "json", js_json_use(js));
JS_SetPropertyStr(js, hidden_env, "nota", js_nota_use(js));
JS_SetPropertyStr(js, hidden_env, "wota", js_wota_use(js));
crt->actor_sym = JS_NewObject(js);
JS_SetPropertyStr(js, hidden_env, "actorsym", JS_DupValue(js, crt->actor_sym));
// Always set init (even if null)
if (crt->init_wota) {
JS_SetPropertyStr(js, hidden_env, "init", wota2value(js, crt->init_wota));
free(crt->init_wota);
crt->init_wota = NULL;
} else {
JS_SetPropertyStr(js, hidden_env, "init", JS_NULL);
}
if (core_path) {
JS_SetPropertyStr(js, hidden_env, "core_path", JS_NewString(js, core_path));
}
// Stone the environment
hidden_env = JS_Stone(js, hidden_env);
// Integrate and run
crt->state = ACTOR_RUNNING;
JSValue v = JS_Integrate(js, bytecode, hidden_env);
JSValue v = JS_Eval(js, data, engine_size, ENGINE, 0);
free(data);
uncaught_exception(js, v);
crt->state = ACTOR_IDLE;
set_actor_state(crt);
@@ -227,559 +201,12 @@ static void signal_handler(int sig)
}
#endif
if (!str) return;
exit_handler();
}
/* Run the C test suite with minimal runtime setup */
static int run_test_suite(size_t heap_size)
{
JSRuntime *rt = JS_NewRuntime();
if (!rt) {
printf("Failed to create JS runtime\n");
return 1;
}
JSContext *ctx = JS_NewContextWithHeapSize(rt, heap_size);
if (!ctx) {
printf("Failed to create JS context\n");
JS_FreeRuntime(rt);
return 1;
}
int result = run_c_test_suite(ctx);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
return result;
}
/* Run an immediate script string */
static int run_eval(const char *script_or_file, int print_bytecode, int use_bootstrap_env)
{
if (!find_cell_shop()) return 1;
/* Check if argument is a file path */
struct stat st;
char *script = NULL;
char *allocated_script = NULL;
const char *filename = "<eval>";
if (stat(script_or_file, &st) == 0 && S_ISREG(st.st_mode)) {
/* It's a file, read its contents */
FILE *f = fopen(script_or_file, "r");
if (!f) {
printf("Failed to open file: %s\n", script_or_file);
return 1;
}
allocated_script = malloc(st.st_size + 1);
if (!allocated_script) {
fclose(f);
printf("Failed to allocate memory for script\n");
return 1;
}
size_t read_size = fread(allocated_script, 1, st.st_size, f);
fclose(f);
allocated_script[read_size] = '\0';
script = allocated_script;
filename = script_or_file;
} else {
/* Treat as inline script */
script = (char *)script_or_file;
}
JSRuntime *rt = JS_NewRuntime();
if (!rt) {
printf("Failed to create JS runtime\n");
free(allocated_script);
return 1;
}
JSContext *ctx = JS_NewContext(rt);
if (!ctx) {
printf("Failed to create JS context\n");
JS_FreeRuntime(rt);
free(allocated_script);
return 1;
}
int result = 0;
JSGCRef bytecode_ref;
JS_PushGCRef(ctx, &bytecode_ref);
bytecode_ref.val = JS_Compile(ctx, script, strlen(script), filename);
if (JS_IsException(bytecode_ref.val)) {
uncaught_exception(ctx, bytecode_ref.val);
JS_PopGCRef(ctx, &bytecode_ref);
result = 1;
} else {
if (print_bytecode) {
printf("=== Compiled Bytecode ===\n");
JS_DumpFunctionBytecode(ctx, bytecode_ref.val);
}
JSValue env = JS_NULL;
if (use_bootstrap_env) {
JSGCRef env_ref, json_ref, nota_ref, wota_ref;
JS_PushGCRef(ctx, &env_ref);
JS_PushGCRef(ctx, &json_ref);
JS_PushGCRef(ctx, &nota_ref);
JS_PushGCRef(ctx, &wota_ref);
env_ref.val = JS_NewObject(ctx);
/* Create modules with GC rooting, then stone them */
json_ref.val = js_json_use(ctx);
nota_ref.val = js_nota_use(ctx);
wota_ref.val = js_wota_use(ctx);
JS_SetPropertyStr(ctx, env_ref.val, "json", JS_Stone(ctx, json_ref.val));
JS_SetPropertyStr(ctx, env_ref.val, "nota", JS_Stone(ctx, nota_ref.val));
JS_SetPropertyStr(ctx, env_ref.val, "wota", JS_Stone(ctx, wota_ref.val));
env = JS_Stone(ctx, env_ref.val);
JS_PopGCRef(ctx, &wota_ref);
JS_PopGCRef(ctx, &nota_ref);
JS_PopGCRef(ctx, &json_ref);
JS_PopGCRef(ctx, &env_ref);
}
JSValue v = JS_Integrate(ctx, bytecode_ref.val, env);
JS_PopGCRef(ctx, &bytecode_ref);
if (JS_IsException(v)) {
uncaught_exception(ctx, v);
result = 1;
} else {
JS_FreeValue(ctx, v);
}
}
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
free(allocated_script);
return result;
}
int cell_init(int argc, char **argv)
{
/* Check for --test flag to run C test suite */
if (argc >= 2 && strcmp(argv[1], "--test") == 0) {
size_t heap_size = 64 * 1024; /* 64KB default */
if (argc >= 3) {
heap_size = strtoull(argv[2], NULL, 0);
/* Round up to power of 2 for buddy allocator */
size_t p = 1;
while (p < heap_size) p <<= 1;
heap_size = p;
}
return run_test_suite(heap_size);
}
/* Check for --ast flag to output AST JSON */
if (argc >= 3 && strcmp(argv[1], "--ast") == 0) {
const char *script_or_file = argv[2];
char *script = NULL;
char *allocated_script = NULL;
const char *filename = "<eval>";
struct stat st;
if (stat(script_or_file, &st) == 0 && S_ISREG(st.st_mode)) {
FILE *f = fopen(script_or_file, "r");
if (!f) {
printf("Failed to open file: %s\n", script_or_file);
return 1;
}
allocated_script = malloc(st.st_size + 1);
if (!allocated_script) {
fclose(f);
printf("Failed to allocate memory for script\n");
return 1;
}
size_t read_size = fread(allocated_script, 1, st.st_size, f);
fclose(f);
allocated_script[read_size] = '\0';
script = allocated_script;
filename = script_or_file;
} else {
script = (char *)script_or_file;
}
JSRuntime *rt = JS_NewRuntime();
if (!rt) {
printf("Failed to create JS runtime\n");
free(allocated_script);
return 1;
}
JSContext *ctx = JS_NewContext(rt);
if (!ctx) {
printf("Failed to create JS context\n");
JS_FreeRuntime(rt);
free(allocated_script);
return 1;
}
char *json = JS_AST(ctx, script, strlen(script), filename);
if (json) {
int has_errors = print_json_errors(json);
printf("%s\n", json);
free(json);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
free(allocated_script);
return has_errors ? 1 : 0;
} else {
printf("Failed to parse AST\n");
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
free(allocated_script);
return 1;
}
}
/* Check for --tokenize flag to output token array JSON */
if (argc >= 3 && strcmp(argv[1], "--tokenize") == 0) {
const char *script_or_file = argv[2];
char *script = NULL;
char *allocated_script = NULL;
const char *filename = "<eval>";
struct stat st;
if (stat(script_or_file, &st) == 0 && S_ISREG(st.st_mode)) {
FILE *f = fopen(script_or_file, "r");
if (!f) {
printf("Failed to open file: %s\n", script_or_file);
return 1;
}
allocated_script = malloc(st.st_size + 1);
if (!allocated_script) {
fclose(f);
printf("Failed to allocate memory for script\n");
return 1;
}
size_t read_size = fread(allocated_script, 1, st.st_size, f);
fclose(f);
allocated_script[read_size] = '\0';
script = allocated_script;
filename = script_or_file;
} else {
script = (char *)script_or_file;
}
JSRuntime *rt = JS_NewRuntime();
if (!rt) {
printf("Failed to create JS runtime\n");
free(allocated_script);
return 1;
}
JSContext *ctx = JS_NewContext(rt);
if (!ctx) {
printf("Failed to create JS context\n");
JS_FreeRuntime(rt);
free(allocated_script);
return 1;
}
char *json = JS_Tokenize(ctx, script, strlen(script), filename);
if (json) {
int has_errors = print_json_errors(json);
printf("%s\n", json);
free(json);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
free(allocated_script);
return has_errors ? 1 : 0;
} else {
printf("Failed to tokenize\n");
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
free(allocated_script);
return 1;
}
}
/* Check for --mach flag to output machine code JSON */
if (argc >= 3 && strcmp(argv[1], "--mach") == 0) {
const char *script_or_file = argv[2];
char *script = NULL;
char *allocated_script = NULL;
const char *filename = "<eval>";
struct stat st;
if (stat(script_or_file, &st) == 0 && S_ISREG(st.st_mode)) {
FILE *f = fopen(script_or_file, "r");
if (!f) {
printf("Failed to open file: %s\n", script_or_file);
return 1;
}
allocated_script = malloc(st.st_size + 1);
if (!allocated_script) {
fclose(f);
printf("Failed to allocate memory for script\n");
return 1;
}
size_t read_size = fread(allocated_script, 1, st.st_size, f);
fclose(f);
allocated_script[read_size] = '\0';
script = allocated_script;
filename = script_or_file;
} else {
script = (char *)script_or_file;
}
JSRuntime *rt = JS_NewRuntime();
if (!rt) {
printf("Failed to create JS runtime\n");
free(allocated_script);
return 1;
}
JSContext *ctx = JS_NewContext(rt);
if (!ctx) {
printf("Failed to create JS context\n");
JS_FreeRuntime(rt);
free(allocated_script);
return 1;
}
char *ast_json = JS_AST(ctx, script, strlen(script), filename);
if (!ast_json) {
printf("Failed to parse AST\n");
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
free(allocated_script);
return 1;
}
if (print_json_errors(ast_json)) {
free(ast_json);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
free(allocated_script);
return 1;
}
char *mach_json = JS_Mach(ctx, ast_json);
free(ast_json);
if (mach_json) {
if (print_json_errors(mach_json)) {
free(mach_json);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
free(allocated_script);
return 1;
}
printf("%s\n", mach_json);
free(mach_json);
} else {
printf("Failed to generate machine code\n");
}
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
free(allocated_script);
return mach_json ? 0 : 1;
}
/* Check for --vmcode flag to dump linked register VM bytecode */
if (argc >= 3 && strcmp(argv[1], "--vmcode") == 0) {
const char *script_or_file = argv[2];
char *script = NULL;
char *allocated_script = NULL;
const char *filename = "<eval>";
struct stat st;
if (stat(script_or_file, &st) == 0 && S_ISREG(st.st_mode)) {
FILE *f = fopen(script_or_file, "r");
if (!f) {
printf("Failed to open file: %s\n", script_or_file);
return 1;
}
allocated_script = malloc(st.st_size + 1);
if (!allocated_script) {
fclose(f);
printf("Failed to allocate memory for script\n");
return 1;
}
size_t read_size = fread(allocated_script, 1, st.st_size, f);
fclose(f);
allocated_script[read_size] = '\0';
script = allocated_script;
filename = script_or_file;
} else {
script = (char *)script_or_file;
}
JSRuntime *rt = JS_NewRuntime();
if (!rt) {
printf("Failed to create JS runtime\n");
free(allocated_script);
return 1;
}
JSContext *ctx = JS_NewContext(rt);
if (!ctx) {
printf("Failed to create JS context\n");
JS_FreeRuntime(rt);
free(allocated_script);
return 1;
}
char *ast_json = JS_AST(ctx, script, strlen(script), filename);
if (!ast_json) {
printf("Failed to parse AST\n");
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
free(allocated_script);
return 1;
}
if (print_json_errors(ast_json)) {
free(ast_json);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
free(allocated_script);
return 1;
}
char *mach_json = JS_Mach(ctx, ast_json);
free(ast_json);
if (mach_json) {
if (print_json_errors(mach_json)) {
free(mach_json);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
free(allocated_script);
return 1;
}
JS_DumpRegisterMach(ctx, mach_json, JS_NULL);
free(mach_json);
} else {
printf("Failed to generate machine code\n");
}
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
free(allocated_script);
return mach_json ? 0 : 1;
}
/* Check for --mach-run flag to generate and run machine code through register VM */
if (argc >= 3 && strcmp(argv[1], "--mach-run") == 0) {
const char *script_or_file = argv[2];
char *script = NULL;
char *allocated_script = NULL;
const char *filename = "<eval>";
struct stat st;
if (stat(script_or_file, &st) == 0 && S_ISREG(st.st_mode)) {
FILE *f = fopen(script_or_file, "r");
if (!f) {
printf("Failed to open file: %s\n", script_or_file);
return 1;
}
allocated_script = malloc(st.st_size + 1);
if (!allocated_script) {
fclose(f);
printf("Failed to allocate memory for script\n");
return 1;
}
size_t read_size = fread(allocated_script, 1, st.st_size, f);
fclose(f);
allocated_script[read_size] = '\0';
script = allocated_script;
filename = script_or_file;
} else {
script = (char *)script_or_file;
}
JSRuntime *rt = JS_NewRuntime();
if (!rt) {
printf("Failed to create JS runtime\n");
free(allocated_script);
return 1;
}
JSContext *ctx = JS_NewContext(rt);
if (!ctx) {
printf("Failed to create JS context\n");
JS_FreeRuntime(rt);
free(allocated_script);
return 1;
}
/* Parse to AST */
char *ast_json = JS_AST(ctx, script, strlen(script), filename);
if (!ast_json) {
printf("Failed to parse AST\n");
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
free(allocated_script);
return 1;
}
if (print_json_errors(ast_json)) {
free(ast_json);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
free(allocated_script);
return 1;
}
/* Generate machine code */
char *mach_json = JS_Mach(ctx, ast_json);
free(ast_json);
if (!mach_json) {
printf("Failed to generate machine code\n");
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
free(allocated_script);
return 1;
}
if (print_json_errors(mach_json)) {
free(mach_json);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
free(allocated_script);
return 1;
}
/* Execute through register VM */
JSValue result = JS_IntegrateRegister(ctx, mach_json, JS_NULL);
free(mach_json);
int exit_code = 0;
if (JS_IsException(result)) {
JSValue exc = JS_GetException(ctx);
const char *err_str = JS_ToCString(ctx, exc);
if (err_str) {
printf("Error: %s\n", err_str);
JS_FreeCString(ctx, err_str);
}
exit_code = 1;
} else if (!JS_IsNull(result)) {
/* Print result */
const char *str = JS_ToCString(ctx, result);
if (str) {
printf("%s\n", str);
JS_FreeCString(ctx, str);
}
}
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
free(allocated_script);
return exit_code;
}
/* Check for -e or --eval flag to run immediate script */
/* Also check for -p flag to print bytecode */
/* -s / --serializers flag provides json, nota, wota in env */
if (argc >= 3 && (strcmp(argv[1], "-e") == 0 || strcmp(argv[1], "--eval") == 0)) {
return run_eval(argv[2], 0, 0);
}
if (argc >= 3 && (strcmp(argv[1], "-p") == 0 || strcmp(argv[1], "--print-bytecode") == 0)) {
return run_eval(argv[2], 1, 0);
}
if (argc >= 3 && (strcmp(argv[1], "-s") == 0 || strcmp(argv[1], "--serializers") == 0)) {
return run_eval(argv[2], 0, 1);
}
int script_start = 1;
/* Find the cell shop at ~/.cell */
@@ -819,8 +246,10 @@ int cell_init(int argc, char **argv)
int JS_ArrayLength(JSContext *js, JSValue a)
{
int64_t len;
JS_GetLength(js, a, &len);
JSValue length = JS_GetPropertyStr(js, a, "length");
int len;
JS_ToInt32(js,&len,length);
JS_FreeValue(js,length);
return len;
}
@@ -854,6 +283,7 @@ void cell_trace_sethook(cell_hook)
int uncaught_exception(JSContext *js, JSValue v)
{
cell_rt *rt = JS_GetContextOpaque(js);
if (!JS_HasException(js)) {
JS_FreeValue(js,v);
return 1;
@@ -861,21 +291,26 @@ int uncaught_exception(JSContext *js, JSValue v)
JSValue exp = JS_GetException(js);
JSValue message = JS_GetPropertyStr(js, exp, "message");
const char *msg_str = JS_ToCString(js, message);
if (msg_str) {
printf("Exception: %s\n", msg_str);
JS_FreeCString(js, msg_str);
if (JS_IsNull(rt->on_exception)) {
const char *str = JS_ToCString(js, exp);
if (str) {
printf("Uncaught exception: %s\n", str);
JS_FreeCString(js, str);
}
JSValue stack = JS_GetPropertyStr(js, exp, "stack");
if (!JS_IsNull(stack)) {
const char *stack_str = JS_ToCString(js, stack);
if (stack_str) {
printf("Stack trace:\n%s\n", stack_str);
JS_FreeCString(js, stack_str);
}
}
JS_FreeValue(js, stack);
} else {
JSValue ret = JS_Call(js, rt->on_exception, JS_NULL, 1, &exp);
JS_FreeValue(js, ret);
}
JS_FreeValue(js, message);
JSValue stack = JS_GetPropertyStr(js, exp, "stack");
const char *stack_str = JS_ToCString(js, stack);
if (stack_str) {
printf("Stack:\n%s\n", stack_str);
JS_FreeCString(js, stack_str);
}
JS_FreeValue(js, stack);
JS_FreeValue(js, exp);
JS_FreeValue(js, v);

View File

@@ -28,13 +28,6 @@ JSValue number2js(JSContext *js, double g);
JSValue wota2value(JSContext *js, void *v);
void *value2wota(JSContext *js, JSValue v, JSValue replacer, size_t *bytes);
JSValue nota2value(JSContext *js, void *nota);
void *value2nota(JSContext *js, JSValue v);
JSValue js_json_use(JSContext *js);
JSValue js_nota_use(JSContext *js);
JSValue js_wota_use(JSContext *js);
#define CELL_HOOK_ENTER 1
#define CELL_HOOK_EXIT 2
typedef void (*cell_hook)(const char *name, int type);
@@ -43,7 +36,7 @@ void cell_trace_sethook(cell_hook);
// Macros to help with creating scripts
#define MIST_CFUNC_DEF(name, length, func1, props) { name, props, JS_DEF_CFUNC, 0, .u = { .func = { length, JS_CFUNC_generic, { .generic = func1 } } } }
#define MIST_FUNC_DEF(TYPE, FN, LEN) MIST_CFUNC_DEF(#FN, LEN, js_##TYPE##_##FN, 0)
#define MIST_FUNC_DEF(TYPE, FN, LEN) MIST_CFUNC_DEF(#FN, LEN, js_##TYPE##_##FN, JS_PROP_C_W_E)
#define PROTO_FUNC_DEF(TYPE, FN, LEN) MIST_CFUNC_DEF(#FN, LEN, js_##TYPE##_##FN, 0)
#define JS_SETSIG JSContext *js, JSValue self, JSValue val
@@ -75,6 +68,41 @@ void cell_trace_sethook(cell_hook);
JS_FreeCString(js,str); \
) \
#define MIST_CGETSET_BASE(name, fgetter, fsetter, props) { name, props, JS_DEF_CGETSET, 0, .u = { .getset = { .get = { .getter = fgetter }, .set = { .setter = fsetter } } } }
#define MIST_CGETSET_DEF(name, fgetter, fsetter) MIST_CGETSET_BASE(name, fgetter, fsetter, JS_PROP_CONFIGURABLE | JS_PROP_ENUMERABLE)
#define MIST_CGETET_HID(name, fgetter, fsetter) MIST_CGETSET_BASE(name, fgetter, fsetter, JS_PROP_CONFIGURABLE)
#define MIST_GET(name, fgetter) { #fgetter , JS_PROP_CONFIGURABLE | JS_PROP_ENUMERABLE, JS_DEF_CGETSET, 0, .u = { .getset = { .get = { .getter = js_##name##_get_##fgetter } } } }
#define CGETSET_ADD_NAME(ID, ENTRY, NAME) MIST_CGETSET_DEF(#NAME, js_##ID##_get_##ENTRY, js_##ID##_set_##ENTRY)
#define CGETSET_ADD(ID, ENTRY) MIST_CGETSET_DEF(#ENTRY, js_##ID##_get_##ENTRY, js_##ID##_set_##ENTRY)
#define CGETSET_ADD_HID(ID, ENTRY) MIST_CGETSET_BASE(#ENTRY, js_##ID##_get_##ENTRY, js_##ID##_set_##ENTRY, JS_PROP_CONFIGURABLE)
#define GETSETPAIR(ID, ENTRY, TYPE, FN) \
JSValue js_##ID##_set_##ENTRY (JS_SETSIG) { \
js2##ID (js, self)->ENTRY = js2##TYPE (js,val); \
{FN;} \
return JS_NULL; \
} \
\
JSValue js_##ID##_get_##ENTRY (JSContext *js, JSValue self) { \
return TYPE##2js(js,js2##ID (js, self)->ENTRY); \
} \
#define JSC_GETSET(ID, ENTRY, TYPE) GETSETPAIR( ID , ENTRY , TYPE , ; )
#define JSC_GETSET_APPLY(ID, ENTRY, TYPE) GETSETPAIR(ID, ENTRY, TYPE, ID##_apply(js2##ID (js, self));)
#define JSC_GETSET_CALLBACK(ID, ENTRY) \
JSValue js_##ID##_set_##ENTRY (JS_SETSIG) { \
JSValue fn = js2##ID (js, self)->ENTRY; \
if (!JS_IsNull(fn)) JS_FreeValue(js, fn); \
js2##ID (js, self)->ENTRY = JS_DupValue(js, val); \
return JS_NULL; \
}\
JSValue js_##ID##_get_##ENTRY (JSContext *js, JSValue self) { return JS_DupValue(js, js2##ID (js, self)->ENTRY); } \
#define JSC_GET(ID, ENTRY, TYPE) \
JSValue js_##ID##_get_##ENTRY (JSContext *js, JSValue self) { \
return TYPE##2js(js,js2##ID (js, self)->ENTRY); } \
#define QJSCLASS(TYPE, ...)\
JSClassID js_##TYPE##_id;\
static void js_##TYPE##_finalizer(JSRuntime *rt, JSValue val){\
@@ -151,12 +179,15 @@ JS_SetClassProto(js, js_##TYPE##_id, TYPE##_proto); \
#define QJSCLASSPREP_FUNCS_CTOR(TYPE, CTOR_ARGC) \
({ \
QJSCLASSPREP_FUNCS(TYPE); \
JSValue TYPE##_ctor = JS_NewCFunction2(js, js_##TYPE##_constructor, #TYPE, CTOR_ARGC, JS_CFUNC_generic, 0); \
JSValue TYPE##_ctor = JS_NewCFunction2(js, js_##TYPE##_constructor, #TYPE, CTOR_ARGC, JS_CFUNC_constructor, 0); \
JS_SetConstructor(js, TYPE##_ctor, TYPE##_proto); \
TYPE##_ctor; \
})
#define countof(x) (sizeof(x)/sizeof((x)[0]))
// Common macros for property access
#define JS_GETPROP(JS, TARGET, VALUE, PROP, TYPE) {\
JSValue __##PROP##__v = JS_GetPropertyStr(JS,VALUE,#PROP); \

View File

@@ -53,7 +53,7 @@ typedef struct cell_rt {
int main_thread_only;
int affinity;
JSValue actor_sym;
JSAtom actor_sym;
const char *name; // human friendly name
cell_hook trace_hook;
@@ -63,7 +63,7 @@ cell_rt *create_actor(void *wota);
const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar);
void actor_disrupt(cell_rt *actor);
JSValue actor_sym(cell_rt *actor);
JSAtom actor_sym(cell_rt *actor);
const char *send_message(const char *id, void *msg);
const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar);

View File

@@ -28,6 +28,10 @@
#include <stddef.h>
#endif
#ifndef NDEBUG
#include <assert.h>
#endif
struct list_head {
struct list_head *prev;
struct list_head *next;
@@ -82,6 +86,29 @@ static inline int list_empty(struct list_head *el)
return el->next == el;
}
/* Move all elements from 'src' to 'dst', leaving 'src' empty.
'dst' must be empty before this call. */
static inline void list_splice(struct list_head *dst, struct list_head *src)
{
#ifndef NDEBUG
assert(dst != src);
assert(list_empty(dst));
#endif
if (!list_empty(src)) {
struct list_head *first = src->next;
struct list_head *last = src->prev;
/* Link dst to src's elements */
dst->next = first;
dst->prev = last;
first->prev = dst;
last->next = dst;
/* Reinitialize src as empty */
init_list_head(src);
}
}
#define list_for_each(el, head) \
for(el = (head)->next; el != (head); el = el->next)

View File

@@ -111,7 +111,7 @@ JSC_CCALL(actor_on_exception,
)
JSC_CCALL(actor_clock,
if (!JS_IsFunction(argv[0]))
if (!JS_IsFunction(js, argv[0]))
return JS_ThrowReferenceError(js, "Argument must be a function.");
cell_rt *actor = JS_GetContextOpaque(js);
@@ -119,7 +119,7 @@ JSC_CCALL(actor_clock,
)
JSC_CCALL(actor_delay,
if (!JS_IsFunction(argv[0]))
if (!JS_IsFunction(js, argv[0]))
return JS_ThrowReferenceError(js, "Argument must be a function.");
cell_rt *actor = JS_GetContextOpaque(js);

606
source/qjs_blob.c Normal file
View File

@@ -0,0 +1,606 @@
#define BLOB_IMPLEMENTATION
#include "blob.h"
#include "cell.h"
// Get countof from macros if not defined
#ifndef countof
#define countof(x) (sizeof(x)/sizeof((x)[0]))
#endif
// Free function for blob
void blob_free(JSRuntime *rt, blob *b)
{
blob_destroy(b);
}
// Use QJSCLASS macro to generate class boilerplate
QJSCLASS(blob,)
// Constructor function for blob
static JSValue js_blob_constructor(JSContext *ctx, JSValueConst new_target,
int argc, JSValueConst *argv) {
blob *bd = NULL;
// new Blob()
if (argc == 0) {
// empty antestone blob
bd = blob_new(0);
}
// new Blob(capacity)
else if (argc == 1 && JS_IsNumber(argv[0])) {
int64_t capacity_bits;
if (JS_ToInt64(ctx, &capacity_bits, argv[0]) < 0) {
return JS_EXCEPTION;
}
if (capacity_bits < 0) capacity_bits = 0;
bd = blob_new((size_t)capacity_bits);
}
// new Blob(length, logical/random)
else if (argc == 2 && JS_IsNumber(argv[0])) {
int64_t length_bits;
if (JS_ToInt64(ctx, &length_bits, argv[0]) < 0) {
return JS_EXCEPTION;
}
if (length_bits < 0) length_bits = 0;
if (JS_IsBool(argv[1])) {
// Fill with all 0s or all 1s
int is_one = JS_ToBool(ctx, argv[1]);
bd = blob_new_with_fill((size_t)length_bits, is_one);
} else if (JS_IsFunction(ctx, argv[1])) {
/* Random function provided call it and use up to 56 bits at a time */
size_t bytes = (length_bits + 7) / 8;
bd = blob_new((size_t)length_bits);
if (bd) {
bd->length = length_bits;
/* Ensure the backing storage starts out zeroed */
memset(bd->data, 0, bytes);
size_t bits_written = 0;
while (bits_written < length_bits) {
JSValue randval = JS_Call(ctx, argv[1], JS_NULL, 0, NULL);
if (JS_IsException(randval)) {
blob_destroy(bd);
return JS_EXCEPTION;
}
int64_t fitval;
JS_ToInt64(ctx, &fitval, randval);
JS_FreeValue(ctx, randval);
/* Extract up to 56 bits from the random value */
size_t bits_to_use = length_bits - bits_written;
if (bits_to_use > 52) bits_to_use = 52;
/* Write bits from the random value */
for (size_t j = 0; j < bits_to_use; j++) {
size_t bit_pos = bits_written + j;
size_t byte_idx = bit_pos / 8;
size_t bit_idx = bit_pos % 8;
if (fitval & (1LL << j))
bd->data[byte_idx] |= (uint8_t)(1 << bit_idx);
else
bd->data[byte_idx] &= (uint8_t)~(1 << bit_idx);
}
bits_written += bits_to_use;
}
}
} else {
return JS_ThrowTypeError(ctx, "Second argument must be boolean or random function");
}
}
// new Blob(blob, from, to)
else if (argc >= 1 && JS_IsObject(argv[0])) {
// we try copying from another blob if it's of the same class
blob *src = js2blob(ctx, argv[0]);
if (!src) {
return JS_ThrowTypeError(ctx, "Blob constructor: argument 1 not a blob");
}
int64_t from = 0, to = (int64_t)src->length;
if (argc >= 2 && JS_IsNumber(argv[1])) {
JS_ToInt64(ctx, &from, argv[1]);
if (from < 0) from = 0;
}
if (argc >= 3 && JS_IsNumber(argv[2])) {
JS_ToInt64(ctx, &to, argv[2]);
if (to < from) to = from;
if (to > (int64_t)src->length) to = (int64_t)src->length;
}
bd = blob_new_from_blob(src, (size_t)from, (size_t)to);
}
// else fail
else {
return JS_ThrowTypeError(ctx, "Blob constructor: invalid arguments");
}
if (!bd) {
return JS_ThrowOutOfMemory(ctx);
}
return blob2js(ctx, bd);
}
// blob.write_bit(logical)
static JSValue js_blob_write_bit(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 1) {
return JS_ThrowTypeError(ctx, "write_bit(logical) requires 1 argument");
}
blob *bd = js2blob(ctx, this_val);
if (!bd) {
return JS_ThrowTypeError(ctx, "write_bit: not called on a blob");
}
// Handle numeric 0/1 or boolean
int bit_val;
if (JS_IsNumber(argv[0])) {
int32_t num;
JS_ToInt32(ctx, &num, argv[0]);
if (num != 0 && num != 1) {
return JS_ThrowTypeError(ctx, "write_bit: value must be true, false, 0, or 1");
}
bit_val = num;
} else {
bit_val = JS_ToBool(ctx, argv[0]);
}
if (blob_write_bit(bd, bit_val) < 0) {
return JS_ThrowTypeError(ctx, "write_bit: cannot write (maybe stone or OOM)");
}
return JS_NULL;
}
// blob.write_blob(second_blob)
static JSValue js_blob_write_blob(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 1) {
return JS_ThrowTypeError(ctx, "write_blob(second_blob) requires 1 argument");
}
blob *bd = js2blob(ctx, this_val);
if (!bd) {
return JS_ThrowTypeError(ctx, "write_blob: not called on a blob");
}
blob *second = js2blob(ctx, argv[0]);
if (!second) {
return JS_ThrowTypeError(ctx, "write_blob: argument must be a blob");
}
if (blob_write_blob(bd, second) < 0) {
return JS_ThrowTypeError(ctx, "write_blob: cannot write to stone blob or OOM");
}
return JS_NULL;
}
// blob.write_dec64(number)
static JSValue js_blob_write_number(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 1) {
return JS_ThrowTypeError(ctx, "write_dec64(number) requires 1 argument");
}
blob *bd = js2blob(ctx, this_val);
if (!bd)
return JS_ThrowTypeError(ctx, "write_dec64: not called on a blob");
// Get the number as a double and convert to DEC64
double d;
if (JS_ToFloat64(ctx, &d, argv[0]) < 0)
return JS_EXCEPTION;
if (blob_write_dec64(bd, d) < 0)
return JS_ThrowTypeError(ctx, "write_dec64: cannot write to stone blob or OOM");
return JS_NULL;
}
// blob.w16(value) - write a 16-bit value (short)
static JSValue js_blob_w16(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 1)
return JS_ThrowTypeError(ctx, "w16(value) requires 1 argument");
blob *bd = js2blob(ctx, this_val);
if (!bd)
return JS_ThrowTypeError(ctx, "w16: not called on a blob");
int32_t value;
if (JS_ToInt32(ctx, &value, argv[0]) < 0) return JS_EXCEPTION;
int16_t short_val = (int16_t)value;
if (blob_write_bytes(bd, &short_val, sizeof(int16_t)) < 0)
return JS_ThrowTypeError(ctx, "w16: cannot write to stone blob or OOM");
return JS_NULL;
}
// blob.wf(value) - write a float (32-bit)
static JSValue js_blob_wf(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 1)
return JS_ThrowTypeError(ctx, "wf(value) requires 1 argument");
blob *bd = js2blob(ctx, this_val);
if (!bd)
return JS_ThrowTypeError(ctx, "wf: not called on a blob");
double d;
if (JS_ToFloat64(ctx, &d, argv[0]) < 0) return JS_EXCEPTION;
float f = (float)d;
if (blob_write_bytes(bd, &f, sizeof(float)) < 0)
return JS_ThrowTypeError(ctx, "wf: cannot write to stone blob or OOM");
return JS_NULL;
}
// blob.write_fit(value, len)
static JSValue js_blob_write_fit(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 2)
return JS_ThrowTypeError(ctx, "write_fit(value, len) requires 2 arguments");
blob *bd = js2blob(ctx, this_val);
if (!bd)
return JS_ThrowTypeError(ctx, "write_fit: not called on a blob");
int64_t value;
int32_t len;
if (JS_ToInt64(ctx, &value, argv[0]) < 0) return JS_EXCEPTION;
if (JS_ToInt32(ctx, &len, argv[1]) < 0) return JS_EXCEPTION;
if (blob_write_fit(bd, value, len) < 0) {
return JS_ThrowTypeError(ctx, "write_fit: value doesn't fit in specified bits, stone blob, or OOM");
}
return JS_NULL;
}
// blob.write_kim(fit)
static JSValue js_blob_write_text(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 1)
return JS_ThrowTypeError(ctx, "write_kim(fit) requires 1 argument");
blob *bd = js2blob(ctx, this_val);
if (!bd)
return JS_ThrowTypeError(ctx, "write_kim: not called on a blob");
// Handle number or single character string
const char *str = JS_ToCString(ctx, argv[0]);
if (blob_write_text(bd, str) < 0) {
JS_FreeCString(ctx,str);
return JS_ThrowTypeError(ctx, "write_kim: cannot write to stone blob or OOM");
}
JS_FreeCString(ctx,str);
return JS_NULL;
}
// blob.write_pad(block_size)
static JSValue js_blob_write_pad(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 1)
return JS_ThrowTypeError(ctx, "write_pad(block_size) requires 1 argument");
blob *bd = js2blob(ctx, this_val);
if (!bd)
return JS_ThrowTypeError(ctx, "write_pad: not called on a blob");
int32_t block_size;
if (JS_ToInt32(ctx, &block_size, argv[0]) < 0) return JS_EXCEPTION;
if (blob_write_pad(bd, block_size) < 0)
return JS_ThrowTypeError(ctx, "write_pad: cannot write (stone blob, OOM, or invalid block size)");
return JS_NULL;
}
// blob.read_logical(from)
static JSValue js_blob_read_logical(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 1) {
return JS_ThrowTypeError(ctx, "read_logical(from) requires 1 argument");
}
blob *bd = js2blob(ctx, this_val);
if (!bd) {
return JS_ThrowTypeError(ctx, "read_logical: not called on a blob");
}
int64_t pos;
if (JS_ToInt64(ctx, &pos, argv[0]) < 0) {
return JS_ThrowInternalError(ctx, "must provide a positive bit");
}
if (pos < 0) {
return JS_ThrowRangeError(ctx, "read_logical: position must be non-negative");
}
int bit_val;
if (blob_read_bit(bd, (size_t)pos, &bit_val) < 0) {
return JS_ThrowTypeError(ctx, "read_logical: blob must be stone");
}
return JS_NewBool(ctx, bit_val);
}
// blob.read_blob(from, to)
static JSValue js_blob_read_blob(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
blob *bd = js2blob(ctx, this_val);
if (!bd) {
return JS_ThrowTypeError(ctx, "read_blob: not called on a blob");
}
if (!bd->is_stone) {
return JS_ThrowTypeError(ctx, "read_blob: blob must be stone");
}
int64_t from = 0;
int64_t to = bd->length;
if (argc >= 1) {
if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION;
if (from < 0) from = 0;
}
if (argc >= 2) {
if (JS_ToInt64(ctx, &to, argv[1]) < 0) return JS_EXCEPTION;
if (to > (int64_t)bd->length) to = bd->length;
}
blob *new_bd = blob_read_blob(bd, from, to);
if (!new_bd) {
return JS_ThrowOutOfMemory(ctx);
}
return blob2js(ctx, new_bd);
}
// blob.read_dec64(from)
static JSValue js_blob_read_number(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 1) {
return JS_ThrowTypeError(ctx, "read_dec64(from) requires 1 argument");
}
blob *bd = js2blob(ctx, this_val);
if (!bd) {
return JS_ThrowTypeError(ctx, "read_number: not called on a blob");
}
if (!bd->is_stone) {
return JS_ThrowTypeError(ctx, "read_number: blob must be stone");
}
double from;
if (JS_ToFloat64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION;
if (from < 0) return JS_ThrowRangeError(ctx, "read_number: position must be non-negative");
double d;
if (blob_read_dec64(bd, from, &d) < 0) {
return JS_ThrowRangeError(ctx, "read_number: out of range");
}
return JS_NewFloat64(ctx, d);
}
// blob.read_fit(from, len)
static JSValue js_blob_read_fit(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 2) {
return JS_ThrowTypeError(ctx, "read_fit(from, len) requires 2 arguments");
}
blob *bd = js2blob(ctx, this_val);
if (!bd) {
return JS_ThrowTypeError(ctx, "read_fit: not called on a blob");
}
if (!bd->is_stone) {
return JS_ThrowTypeError(ctx, "read_fit: blob must be stone");
}
int64_t from;
int32_t len;
if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION;
if (JS_ToInt32(ctx, &len, argv[1]) < 0) return JS_EXCEPTION;
if (from < 0) {
return JS_ThrowRangeError(ctx, "read_fit: position must be non-negative");
}
int64_t value;
if (blob_read_fit(bd, from, len, &value) < 0) {
return JS_ThrowRangeError(ctx, "read_fit: out of range or invalid length");
}
return JS_NewInt64(ctx, value);
}
// blob.read_text(from)
static JSValue js_blob_read_text(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
blob *bd = js2blob(ctx, this_val);
if (!bd) {
return JS_ThrowTypeError(ctx, "read_text: not called on a blob");
}
if (!bd->is_stone) {
return JS_ThrowTypeError(ctx, "read_text: blob must be stone");
}
int64_t from;
if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION;
char *text;
size_t bits_read;
if (blob_read_text(bd, from, &text, &bits_read) < 0) {
return JS_ThrowRangeError(ctx, "read_text: out of range or invalid encoding");
}
JSValue result = JS_NewString(ctx, text);
free(text);
return result;
}
// blob.pad?(from, block_size)
static JSValue js_blob_pad_q(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 2) {
return JS_ThrowTypeError(ctx, "pad?(from, block_size) requires 2 arguments");
}
blob *bd = js2blob(ctx, this_val);
if (!bd) {
return JS_ThrowTypeError(ctx, "pad?: not called on a blob");
}
if (!bd->is_stone) {
return JS_ThrowTypeError(ctx, "pad?: blob must be stone");
}
int64_t from;
int32_t block_size;
if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION;
if (JS_ToInt32(ctx, &block_size, argv[1]) < 0) return JS_EXCEPTION;
return JS_NewBool(ctx, blob_pad_check(bd, from, block_size));
}
// blob.stone()
static JSValue js_blob_stone(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
blob *bd = js2blob(ctx, this_val);
if (!bd) {
return JS_ThrowTypeError(ctx, "stone: not called on a blob");
}
if (!bd->is_stone) {
blob_make_stone(bd);
}
return JS_NULL;
}
static JSValue js_blob_stonep(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
blob *bd = js2blob(ctx, this_val);
if (!bd) {
return JS_ThrowTypeError(ctx, "stone: not called on a blob");
}
return JS_NewBool(ctx, bd->is_stone);
}
// blob.length getter
// Return number of bits in the blob
static JSValue js_blob_get_length(JSContext *ctx, JSValueConst this_val, int magic) {
blob *bd = js2blob(ctx, this_val);
if (!bd) {
return JS_ThrowTypeError(ctx, "length: not called on a blob");
}
return JS_NewInt64(ctx, bd->length);
}
// -----------------------------------------------------------------------------
// Exports list
// -----------------------------------------------------------------------------
static const JSCFunctionListEntry js_blob_funcs[] = {
// Write methods
JS_CFUNC_DEF("write_bit", 1, js_blob_write_bit),
JS_CFUNC_DEF("write_blob", 1, js_blob_write_blob),
JS_CFUNC_DEF("write_number", 1, js_blob_write_number),
JS_CFUNC_DEF("write_fit", 2, js_blob_write_fit),
JS_CFUNC_DEF("write_text", 1, js_blob_write_text),
JS_CFUNC_DEF("write_pad", 1, js_blob_write_pad),
JS_CFUNC_DEF("wf", 1, js_blob_wf),
JS_CFUNC_DEF("w16", 1, js_blob_w16),
// Read methods
JS_CFUNC_DEF("read_logical", 1, js_blob_read_logical),
JS_CFUNC_DEF("read_blob", 2, js_blob_read_blob),
JS_CFUNC_DEF("read_number", 1, js_blob_read_number),
JS_CFUNC_DEF("read_fit", 2, js_blob_read_fit),
JS_CFUNC_DEF("read_text", 1, js_blob_read_text),
JS_CFUNC_DEF("pad?", 2, js_blob_pad_q),
// Other methods
JS_CFUNC_DEF("stone", 0, js_blob_stone),
JS_CFUNC_DEF("stonep", 0, js_blob_stonep),
// Length property getter
JS_CGETSET_DEF("length", js_blob_get_length, NULL),
};
JSValue js_blob_use(JSContext *js) {
return QJSCLASSPREP_FUNCS_CTOR(blob, 3);
}
JSValue js_new_blob_stoned_copy(JSContext *js, void *data, size_t bytes)
{
blob *b = blob_new(bytes*8);
memcpy(b->data, data, bytes);
b->length = bytes * 8; // Set the actual length in bits
blob_make_stone(b);
return blob2js(js, b);
}
void *js_get_blob_data(JSContext *js, size_t *size, JSValue v)
{
blob *b = js2blob(js, v);
if (!b) {
JS_ThrowReferenceError(js, "get_blob_data: not called on a blob");
return -1;
}
if (!b->is_stone) {
JS_ThrowReferenceError(js, "attempted to read data from a non-stone blob");
return -1;
}
if (b->length % 8 != 0) {
JS_ThrowReferenceError(js, "attempted to read data from a non-byte aligned blob [length is %d]", b->length);
return -1;
}
*size = (b->length + 7) / 8; // Return actual byte size based on bit length
return b->data;
}
void *js_get_blob_data_bits(JSContext *js, size_t *bits, JSValue v)
{
blob *b = js2blob(js, v);
if (!b) {
JS_ThrowReferenceError(js, "get_blob_data_bits: not called on a blob");
return -1;
}
if (!b->is_stone) {
JS_ThrowReferenceError(js, "attempted to read data from a non-stone blob");
return -1;
}
if (!b->data) {
JS_ThrowReferenceError(js, "attempted to read data from an empty blob");
return -1;
}
if (b->length % 8 != 0) {
JS_ThrowReferenceError(js, "attempted to read data from a non-byte aligned blob");
return -1;
}
if (b->length == 0) {
JS_ThrowReferenceError(js, "attempted to read data from an empty blob");
return -1;
}
*bits = b->length;
return b->data;
}
int js_is_blob(JSContext *js, JSValue v)
{
blob *b = js2blob(js,v);
if (b) return 1;
return 0;
}

397
source/qjs_wota.c Normal file
View File

@@ -0,0 +1,397 @@
#include "cell.h"
#include "cell_internal.h"
#include "wota.h"
#include <stdlib.h>
typedef struct ObjectRef {
void *ptr;
struct ObjectRef *next;
} ObjectRef;
typedef struct WotaEncodeContext {
JSContext *ctx;
ObjectRef *visited_stack;
WotaBuffer wb;
int cycle;
JSValue replacer;
} WotaEncodeContext;
static void wota_stack_push(WotaEncodeContext *enc, JSValueConst val)
{
if (!JS_IsObject(val)) return;
ObjectRef *ref = malloc(sizeof(ObjectRef));
if (!ref) return;
ref->ptr = JS_VALUE_GET_PTR(val);
ref->next = enc->visited_stack;
enc->visited_stack = ref;
}
static void wota_stack_pop(WotaEncodeContext *enc)
{
if (!enc->visited_stack) return;
ObjectRef *top = enc->visited_stack;
enc->visited_stack = top->next;
free(top);
}
static int wota_stack_has(WotaEncodeContext *enc, JSValueConst val)
{
if (!JS_IsObject(val)) return 0;
void *ptr = JS_VALUE_GET_PTR(val);
ObjectRef *current = enc->visited_stack;
while (current) {
if (current->ptr == ptr) return 1;
current = current->next;
}
return 0;
}
static void wota_stack_free(WotaEncodeContext *enc)
{
while (enc->visited_stack) {
wota_stack_pop(enc);
}
}
static JSValue apply_replacer(WotaEncodeContext *enc, JSValueConst holder, JSAtom key, JSValueConst val)
{
if (JS_IsNull(enc->replacer)) return JS_DupValue(enc->ctx, val);
JSValue key_val = (key == JS_ATOM_NULL) ? JS_NULL : JS_AtomToValue(enc->ctx, key);
JSValue args[2] = { key_val, JS_DupValue(enc->ctx, val) };
JSValue result = JS_Call(enc->ctx, enc->replacer, holder, 2, args);
JS_FreeValue(enc->ctx, args[0]);
JS_FreeValue(enc->ctx, args[1]);
if (JS_IsException(result)) return JS_DupValue(enc->ctx, val);
return result;
}
static void wota_encode_value(WotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSAtom key);
static void encode_object_properties(WotaEncodeContext *enc, JSValueConst val, JSValueConst holder)
{
JSContext *ctx = enc->ctx;
JSPropertyEnum *ptab;
uint32_t plen;
if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, val, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK) < 0) {
wota_write_sym(&enc->wb, WOTA_NULL);
return;
}
uint32_t non_function_count = 0;
JSValue props[plen];
JSAtom atoms[plen];
for (uint32_t i = 0; i < plen; i++) {
JSValue prop_val = JS_GetProperty(ctx, val, ptab[i].atom);
if (!JS_IsFunction(ctx, prop_val)) {
atoms[non_function_count] = ptab[i].atom;
props[non_function_count++] = prop_val;
} else
JS_FreeValue(ctx, prop_val);
}
wota_write_record(&enc->wb, non_function_count);
for (uint32_t i = 0; i < non_function_count; i++) {
size_t plen;
const char *prop_name = JS_AtomToCStringLen(ctx, &plen, atoms[i]);
JSValue prop_val = props[i];
wota_write_text_len(&enc->wb, prop_name, plen);
wota_encode_value(enc, prop_val, val, atoms[i]);
JS_FreeCString(ctx, prop_name);
JS_FreeValue(ctx, prop_val);
}
for (int i = 0; i < plen; i++)
JS_FreeAtom(ctx, ptab[i].atom);
js_free(ctx, ptab);
}
static void wota_encode_value(WotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSAtom key)
{
JSContext *ctx = enc->ctx;
JSValue replaced;
if (!JS_IsNull(enc->replacer) && key != JS_ATOM_NULL)
replaced = apply_replacer(enc, holder, key, val);
else
replaced = JS_DupValue(enc->ctx, val);
int tag = JS_VALUE_GET_TAG(replaced);
switch (tag) {
case JS_TAG_INT: {
int32_t d;
JS_ToInt32(ctx, &d, replaced);
wota_write_int_word(&enc->wb, d);
break;
}
case JS_TAG_FLOAT64:
case JS_TAG_BIG_INT: {
double d;
if (JS_ToFloat64(ctx, &d, replaced) < 0) {
wota_write_sym(&enc->wb, WOTA_NULL);
break;
}
wota_write_float_word(&enc->wb, d);
break;
}
case JS_TAG_STRING: {
size_t plen;
const char *str = JS_ToCStringLen(ctx, &plen, replaced);
wota_write_text_len(&enc->wb, str ? str : "", str ? plen : 0);
JS_FreeCString(ctx, str);
break;
}
case JS_TAG_BOOL:
wota_write_sym(&enc->wb, JS_VALUE_GET_BOOL(replaced) ? WOTA_TRUE : WOTA_FALSE);
break;
case JS_TAG_NULL:
wota_write_sym(&enc->wb, WOTA_NULL);
break;
case JS_TAG_OBJECT: {
if (js_is_blob(ctx, replaced)) {
size_t buf_len;
void *buf_data = js_get_blob_data(ctx, &buf_len, replaced);
if (buf_data == (void *)-1) {
JS_FreeValue(ctx, replaced);
return; // JS_EXCEPTION will be handled by caller
}
if (buf_len == 0) {
wota_write_blob(&enc->wb, 0, "");
} else {
wota_write_blob(&enc->wb, (unsigned long long)buf_len * 8, (const char *)buf_data);
}
break;
}
if (JS_IsArray(ctx, replaced)) {
if (wota_stack_has(enc, replaced)) {
enc->cycle = 1;
break;
}
wota_stack_push(enc, replaced);
int64_t arr_len;
JS_GetLength(ctx, replaced, &arr_len);
wota_write_array(&enc->wb, arr_len);
for (int64_t i = 0; i < arr_len; i++) {
JSValue elem_val = JS_GetPropertyUint32(ctx, replaced, i);
JSAtom idx_atom = JS_NewAtomUInt32(ctx, (uint32_t)i);
wota_encode_value(enc, elem_val, replaced, idx_atom);
JS_FreeAtom(ctx, idx_atom);
JS_FreeValue(ctx, elem_val);
}
wota_stack_pop(enc);
break;
}
cell_rt *crt = JS_GetContextOpaque(ctx);
JSValue adata = JS_GetProperty(ctx, replaced, crt->actor_sym);
if (!JS_IsNull(adata)) {
wota_write_sym(&enc->wb, WOTA_PRIVATE);
wota_encode_value(enc, adata, replaced, JS_ATOM_NULL);
JS_FreeValue(ctx, adata);
break;
}
JS_FreeValue(ctx, adata);
if (wota_stack_has(enc, replaced)) {
enc->cycle = 1;
break;
}
wota_stack_push(enc, replaced);
JSValue to_json = JS_GetPropertyStr(ctx, replaced, "toJSON");
if (JS_IsFunction(ctx, to_json)) {
JSValue result = JS_Call(ctx, to_json, replaced, 0, NULL);
JS_FreeValue(ctx, to_json);
if (!JS_IsException(result)) {
wota_encode_value(enc, result, holder, key);
JS_FreeValue(ctx, result);
} else
wota_write_sym(&enc->wb, WOTA_NULL);
wota_stack_pop(enc);
break;
}
JS_FreeValue(ctx, to_json);
encode_object_properties(enc, replaced, holder);
wota_stack_pop(enc);
break;
}
default:
wota_write_sym(&enc->wb, WOTA_NULL);
break;
}
JS_FreeValue(ctx, replaced);
}
static char *decode_wota_value(JSContext *ctx, char *data_ptr, JSValue *out_val, JSValue holder, JSAtom key, JSValue reviver)
{
uint64_t first_word = *(uint64_t *)data_ptr;
int type = (int)(first_word & 0xffU);
switch (type) {
case WOTA_INT: {
long long val;
data_ptr = wota_read_int(&val, data_ptr);
*out_val = JS_NewInt64(ctx, val);
break;
}
case WOTA_FLOAT: {
double d;
data_ptr = wota_read_float(&d, data_ptr);
*out_val = JS_NewFloat64(ctx, d);
break;
}
case WOTA_SYM: {
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_ATOM_NULL, reviver);
JSValue obj = JS_NewObject(ctx);
cell_rt *crt = JS_GetContextOpaque(ctx);
JS_SetProperty(ctx, obj, crt->actor_sym, inner);
*out_val = obj;
} else if (scode == WOTA_NULL) *out_val = JS_NULL;
else if (scode == WOTA_FALSE) *out_val = JS_NewBool(ctx, 0);
else if (scode == WOTA_TRUE) *out_val = JS_NewBool(ctx, 1);
else *out_val = JS_NULL;
break;
}
case WOTA_BLOB: {
long long blen;
char *bdata = NULL;
data_ptr = wota_read_blob(&blen, &bdata, data_ptr);
*out_val = bdata ? js_new_blob_stoned_copy(ctx, (uint8_t *)bdata, (size_t)blen) : js_new_blob_stoned_copy(ctx, NULL, 0);
if (bdata) free(bdata);
break;
}
case WOTA_TEXT: {
char *utf8 = NULL;
data_ptr = wota_read_text(&utf8, data_ptr);
*out_val = JS_NewString(ctx, utf8 ? utf8 : "");
if (utf8) free(utf8);
break;
}
case WOTA_ARR: {
long long c;
data_ptr = wota_read_array(&c, data_ptr);
JSValue arr = JS_NewArray(ctx);
JS_SetLength(ctx, arr, c);
for (long long i = 0; i < c; i++) {
JSValue elem_val = JS_NULL;
JSAtom idx_atom = JS_NewAtomUInt32(ctx, (uint32_t)i);
data_ptr = decode_wota_value(ctx, data_ptr, &elem_val, arr, idx_atom, reviver);
JS_SetPropertyUint32(ctx, arr, i, elem_val);
JS_FreeAtom(ctx, idx_atom);
}
*out_val = arr;
break;
}
case WOTA_REC: {
long long c;
data_ptr = wota_read_record(&c, data_ptr);
JSValue obj = JS_NewObject(ctx);
for (long long i = 0; i < c; i++) {
char *tkey = NULL;
size_t key_len;
data_ptr = wota_read_text_len(&key_len, &tkey, data_ptr);
if (!tkey) continue; // invalid key
JSAtom prop_key = JS_NewAtomLen(ctx, tkey, key_len);
JSValue sub_val = JS_NULL;
data_ptr = decode_wota_value(ctx, data_ptr, &sub_val, obj, prop_key, reviver);
JS_SetProperty(ctx, obj, prop_key, sub_val);
JS_FreeAtom(ctx, prop_key);
free(tkey);
}
*out_val = obj;
break;
}
default:
data_ptr += 8;
*out_val = JS_NULL;
break;
}
if (!JS_IsNull(reviver)) {
JSValue key_val = (key == JS_ATOM_NULL) ? JS_NULL : JS_AtomToValue(ctx, key);
JSValue args[2] = { key_val, JS_DupValue(ctx, *out_val) };
JSValue revived = JS_Call(ctx, reviver, holder, 2, args);
JS_FreeValue(ctx, args[0]);
JS_FreeValue(ctx, args[1]);
if (!JS_IsException(revived)) {
JS_FreeValue(ctx, *out_val);
*out_val = revived;
} else
JS_FreeValue(ctx, revived);
}
return data_ptr;
}
void *value2wota(JSContext *ctx, JSValue v, JSValue replacer, size_t *bytes)
{
WotaEncodeContext enc_s, *enc = &enc_s;
enc->ctx = ctx;
enc->visited_stack = NULL;
enc->cycle = 0;
enc->replacer = replacer;
wota_buffer_init(&enc->wb, 16);
wota_encode_value(enc, v, JS_NULL, JS_ATOM_NULL);
if (enc->cycle) {
wota_stack_free(enc);
wota_buffer_free(&enc->wb);
return NULL;
}
wota_stack_free(enc);
size_t total_bytes = enc->wb.size * sizeof(uint64_t);
void *wota = realloc(enc->wb.data, total_bytes);
if (bytes) *bytes = total_bytes;
return wota;
}
JSValue wota2value(JSContext *ctx, void *wota)
{
JSValue result = JS_NULL;
JSValue holder = JS_NewObject(ctx);
decode_wota_value(ctx, wota, &result, holder, JS_ATOM_NULL, JS_NULL);
JS_FreeValue(ctx, holder);
return result;
}
static JSValue js_wota_encode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 1) return JS_ThrowTypeError(ctx, "wota.encode requires at least 1 argument");
size_t total_bytes;
void *wota = value2wota(ctx, argv[0], JS_IsFunction(ctx,argv[1]) ? argv[1] : JS_NULL, &total_bytes);
JSValue ret = js_new_blob_stoned_copy(ctx, wota, total_bytes);
free(wota);
return ret;
}
static JSValue js_wota_decode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 1) return JS_NULL;
size_t len;
uint8_t *buf = js_get_blob_data(ctx, &len, argv[0]);
if (buf == (uint8_t *)-1) return JS_EXCEPTION;
if (!buf || len == 0) return JS_ThrowTypeError(ctx, "No blob data present");
JSValue reviver = (argc > 1 && JS_IsFunction(ctx, argv[1])) ? argv[1] : JS_NULL;
char *data_ptr = (char *)buf;
JSValue result = JS_NULL;
JSValue holder = JS_NewObject(ctx);
JSAtom empty_atom = JS_NewAtom(ctx, "");
decode_wota_value(ctx, data_ptr, &result, holder, empty_atom, reviver);
JS_FreeAtom(ctx, empty_atom);
JS_FreeValue(ctx, holder);
return result;
}
static const JSCFunctionListEntry js_wota_funcs[] = {
JS_CFUNC_DEF("encode", 2, js_wota_encode),
JS_CFUNC_DEF("decode", 2, js_wota_decode),
};
JSValue js_wota_use(JSContext *ctx)
{
JSValue exports = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, exports, js_wota_funcs, sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0]));
return exports;
}

View File

@@ -38,8 +38,10 @@ DEF(def, "def")
DEF(this, "this")
DEF(delete, "delete")
DEF(void, "void")
DEF(typeof, "typeof")
DEF(new, "new")
DEF(in, "in")
DEF(instanceof, "instanceof")
DEF(do, "do")
DEF(while, "while")
DEF(for, "for")

View File

@@ -28,6 +28,7 @@ FMT(none)
FMT(none_int)
FMT(none_loc)
FMT(none_arg)
FMT(none_var_ref)
FMT(u8)
FMT(i8)
FMT(loc8)
@@ -41,16 +42,17 @@ FMT(npopx)
FMT(npop_u16)
FMT(loc)
FMT(arg)
FMT(var_ref)
FMT(u32)
FMT(i32)
FMT(const)
FMT(label)
FMT(atom)
FMT(atom_u8)
FMT(atom_u16)
FMT(atom_label_u8)
FMT(atom_label_u16)
FMT(label_u16)
FMT(key)
FMT(key_u8)
FMT(key_u16)
FMT(key_label_u16)
FMT(u8_u16) /* 1 byte + 2 bytes for upvalue access */
#undef FMT
#endif /* FMT */
@@ -66,12 +68,14 @@ DEF(invalid, 1, 0, 0, none) /* never emitted */
DEF( push_i32, 5, 0, 1, i32)
DEF( push_const, 5, 0, 1, const)
DEF( fclosure, 5, 0, 1, const) /* must follow push_const */
DEF(push_atom_value, 5, 0, 1, atom)
DEF( null, 1, 0, 1, none)
DEF( push_this, 1, 0, 1, none) /* only used at the start of a function */
DEF( push_false, 1, 0, 1, none)
DEF( push_true, 1, 0, 1, none)
DEF( object, 1, 0, 1, none)
DEF( special_object, 2, 0, 1, u8) /* only used at the start of a function */
DEF( rest, 3, 0, 1, u16) /* only used at the start of a function */
DEF( drop, 1, 1, 0, none) /* a -> */
DEF( nip, 1, 2, 1, none) /* a b -> b */
@@ -93,46 +97,53 @@ DEF( rot3r, 1, 3, 3, none) /* a b x -> x a b */
DEF( rot4l, 1, 4, 4, none) /* x a b c -> a b c x */
DEF( rot5l, 1, 5, 5, none) /* x a b c d -> a b c d x */
DEF(call_constructor, 3, 2, 1, npop) /* func new.target args -> ret. arguments are not counted in n_pop */
DEF( call, 3, 1, 1, npop) /* arguments are not counted in n_pop */
DEF( tail_call, 3, 1, 0, npop) /* arguments are not counted in n_pop */
DEF( call_method, 3, 2, 1, npop) /* arguments are not counted in n_pop */
DEF(tail_call_method, 3, 2, 0, npop) /* arguments are not counted in n_pop */
DEF( array_from, 3, 0, 1, npop) /* arguments are not counted in n_pop */
DEF( apply, 3, 3, 1, u16)
DEF( return, 1, 1, 0, none)
DEF( return_undef, 1, 0, 0, none)
DEF( throw, 1, 1, 0, none)
DEF( throw_error, 6, 0, 0, key_u8)
DEF( throw_error, 6, 0, 0, atom_u8)
DEF( eval, 5, 1, 1, npop_u16) /* func args... -> ret_val */
DEF( apply_eval, 3, 2, 1, u16) /* func array -> ret_eval */
DEF( regexp, 1, 2, 1, none) /* create a RegExp object from the pattern and a
bytecode string */
/* Global variable access - resolved by linker to get/set_global_slot */
DEF( check_var, 5, 0, 1, key) /* check if a variable exists - resolved by linker */
DEF( get_var_undef, 5, 0, 1, key) /* resolved by linker to get_global_slot */
DEF( get_var, 5, 0, 1, key) /* resolved by linker to get_global_slot */
DEF( put_var, 5, 1, 0, key) /* resolved by linker to set_global_slot */
DEF( put_var_init, 5, 1, 0, key) /* resolved by linker to set_global_slot */
DEF( put_var_strict, 5, 2, 0, key) /* resolved by linker to set_global_slot */
DEF( check_var, 5, 0, 1, atom) /* check if a variable exists */
DEF( get_var_undef, 5, 0, 1, atom) /* push undefined if the variable does not exist */
DEF( get_var, 5, 0, 1, atom) /* throw an exception if the variable does not exist */
DEF( put_var, 5, 1, 0, atom) /* must come after get_var */
DEF( put_var_init, 5, 1, 0, atom) /* must come after put_var. Used to initialize a global lexical variable */
DEF( put_var_strict, 5, 2, 0, atom) /* for strict mode variable write */
/* Global variable opcodes - resolved by linker to get/set_global_slot */
DEF( define_var, 6, 0, 0, key_u8)
DEF(check_define_var, 6, 0, 0, key_u8)
DEF( define_func, 6, 1, 0, key_u8)
DEF( get_field, 5, 1, 1, key)
DEF( get_field2, 5, 1, 2, key)
DEF( put_field, 5, 2, 0, key)
DEF( get_ref_value, 1, 2, 3, none)
DEF( put_ref_value, 1, 3, 0, none)
DEF( define_var, 6, 0, 0, atom_u8)
DEF(check_define_var, 6, 0, 0, atom_u8)
DEF( define_func, 6, 1, 0, atom_u8)
DEF( get_field, 5, 1, 1, atom)
DEF( get_field2, 5, 1, 2, atom)
DEF( put_field, 5, 2, 0, atom)
DEF( get_array_el, 1, 2, 1, none)
DEF( get_array_el2, 1, 2, 2, none) /* obj prop -> obj value */
DEF( get_array_el3, 1, 2, 3, none) /* obj prop -> obj prop1 value */
DEF( put_array_el, 1, 3, 0, none)
DEF( define_field, 5, 2, 1, key)
DEF( set_name, 5, 1, 1, key)
DEF( define_field, 5, 2, 1, atom)
DEF( set_name, 5, 1, 1, atom)
DEF(set_name_computed, 1, 2, 2, none)
DEF( set_proto, 1, 2, 1, none)
DEF(define_array_el, 1, 3, 2, none)
DEF( append, 1, 3, 2, none) /* append enumerated object, update length */
DEF(copy_data_properties, 2, 3, 3, u8)
DEF( define_method, 6, 2, 1, key_u8)
DEF( define_method, 6, 2, 1, atom_u8)
DEF(define_method_computed, 2, 3, 1, u8) /* must come after define_method */
DEF( define_class, 6, 2, 2, key_u8) /* parent ctor -> ctor proto */
DEF( define_class_computed, 6, 3, 3, key_u8) /* field_name parent ctor -> field_name ctor proto (class with computed name) */
DEF( define_class, 6, 2, 2, atom_u8) /* parent ctor -> ctor proto */
DEF( define_class_computed, 6, 3, 3, atom_u8) /* field_name parent ctor -> field_name ctor proto (class with computed name) */
DEF( get_loc, 3, 0, 1, loc)
DEF( put_loc, 3, 1, 0, loc) /* must come after get_loc */
@@ -140,11 +151,18 @@ DEF( set_loc, 3, 1, 1, loc) /* must come after put_loc */
DEF( get_arg, 3, 0, 1, arg)
DEF( put_arg, 3, 1, 0, arg) /* must come after get_arg */
DEF( set_arg, 3, 1, 1, arg) /* must come after put_arg */
DEF( get_var_ref, 3, 0, 1, var_ref)
DEF( put_var_ref, 3, 1, 0, var_ref) /* must come after get_var_ref */
DEF( set_var_ref, 3, 1, 1, var_ref) /* must come after put_var_ref */
DEF(set_loc_uninitialized, 3, 0, 0, loc)
DEF( get_loc_check, 3, 0, 1, loc)
DEF( put_loc_check, 3, 1, 0, loc) /* must come after get_loc_check */
DEF( put_loc_check_init, 3, 1, 0, loc)
DEF(get_loc_checkthis, 3, 0, 1, loc)
DEF(get_var_ref_check, 3, 0, 1, var_ref)
DEF(put_var_ref_check, 3, 1, 0, var_ref) /* must come after get_var_ref_check */
DEF(put_var_ref_check_init, 3, 1, 0, var_ref)
DEF( close_loc, 3, 0, 0, loc)
DEF( if_false, 5, 1, 0, label)
DEF( if_true, 5, 1, 0, label) /* must come after if_false */
DEF( goto, 5, 0, 0, label) /* must come after if_true */
@@ -153,8 +171,25 @@ DEF( gosub, 5, 0, 0, label) /* used to execute the finally block */
DEF( ret, 1, 1, 0, none) /* used to return from the finally block */
DEF( nip_catch, 1, 2, 1, none) /* catch ... a -> a */
DEF( to_object, 1, 1, 1, none)
//DEF( to_string, 1, 1, 1, none)
DEF( to_propkey, 1, 1, 1, none)
DEF( make_loc_ref, 7, 0, 2, atom_u16)
DEF( make_arg_ref, 7, 0, 2, atom_u16)
DEF(make_var_ref_ref, 7, 0, 2, atom_u16)
DEF( make_var_ref, 5, 0, 2, atom)
DEF( for_in_start, 1, 1, 1, none)
DEF( for_of_start, 1, 1, 3, none)
DEF( for_in_next, 1, 1, 3, none)
DEF( for_of_next, 2, 3, 5, u8)
DEF(iterator_check_object, 1, 1, 1, none)
DEF(iterator_get_value_done, 1, 2, 3, none) /* catch_offset obj -> catch_offset value done */
DEF( iterator_close, 1, 3, 0, none)
DEF( iterator_next, 1, 4, 4, none)
DEF( iterator_call, 2, 4, 5, u8)
/* arithmetic/logic operations */
DEF( neg, 1, 1, 1, none)
DEF( plus, 1, 1, 1, none)
@@ -167,8 +202,9 @@ DEF( inc_loc, 2, 0, 0, loc8)
DEF( add_loc, 2, 1, 0, loc8)
DEF( not, 1, 1, 1, none)
DEF( lnot, 1, 1, 1, none)
DEF( typeof, 1, 1, 1, none)
DEF( delete, 1, 2, 1, none)
DEF( delete_var, 5, 0, 1, key) /* deprecated - global object is immutable */
DEF( delete_var, 5, 0, 1, atom)
DEF( mul, 1, 2, 1, none)
DEF( mul_float, 1, 2, 1, none)
@@ -187,28 +223,15 @@ DEF( lt, 1, 2, 1, none)
DEF( lte, 1, 2, 1, none)
DEF( gt, 1, 2, 1, none)
DEF( gte, 1, 2, 1, none)
DEF( instanceof, 1, 2, 1, none)
DEF( in, 1, 2, 1, none)
DEF( eq, 1, 2, 1, none)
DEF( neq, 1, 2, 1, none)
DEF( strict_eq, 1, 2, 1, none)
DEF( strict_neq, 1, 2, 1, none)
DEF( and, 1, 2, 1, none)
DEF( xor, 1, 2, 1, none)
DEF( or, 1, 2, 1, none)
/* format template - format_string_cpool_idx(u32), expr_count(u16)
Note: n_push=2 ensures stack has room for temp [format_str, arr] pair,
even though we only leave 1 value (the result) on the stack. */
DEF(format_template, 7, 0, 1, npop_u16)
/* Upvalue access (closures via outer_frame chain) */
DEF( get_up, 4, 0, 1, u8_u16) /* depth:u8, slot:u16 -> value */
DEF( set_up, 4, 1, 0, u8_u16) /* value, depth:u8, slot:u16 -> */
/* Name resolution with bytecode patching */
DEF( get_name, 5, 0, 1, const) /* cpool_idx -> value, patches itself */
DEF( get_env_slot, 3, 0, 1, u16) /* slot -> value (patched from get_name) */
DEF( set_env_slot, 3, 1, 0, u16) /* value -> slot (patched from put_var) */
DEF(get_global_slot, 3, 0, 1, u16) /* slot -> value (patched from get_var) */
DEF(set_global_slot, 3, 1, 0, u16) /* value -> slot (patched from put_var) */
/* must be the last non short and non temporary opcode */
DEF( nop, 1, 0, 0, none)
@@ -221,13 +244,15 @@ def( label, 5, 0, 0, label) /* emitted in phase 1, removed in phase 3 *
/* the following opcodes must be in the same order as the 'with_x' and
get_var_undef, get_var and put_var opcodes */
def(scope_get_var_undef, 7, 0, 1, key_u16) /* emitted in phase 1, removed in phase 2 */
def( scope_get_var, 7, 0, 1, key_u16) /* emitted in phase 1, removed in phase 2 */
def( scope_put_var, 7, 1, 0, key_u16) /* emitted in phase 1, removed in phase 2 */
def(scope_delete_var, 7, 0, 1, key_u16) /* emitted in phase 1, removed in phase 2 */
def(scope_put_var_init, 7, 0, 2, key_u16) /* emitted in phase 1, removed in phase 2 */
def(scope_get_var_checkthis, 7, 0, 1, key_u16) /* emitted in phase 1, removed in phase 2, only used to return 'this' in derived class constructors */
def(get_field_opt_chain, 5, 1, 1, key) /* emitted in phase 1, removed in phase 2 */
def(scope_get_var_undef, 7, 0, 1, atom_u16) /* emitted in phase 1, removed in phase 2 */
def( scope_get_var, 7, 0, 1, atom_u16) /* emitted in phase 1, removed in phase 2 */
def( scope_put_var, 7, 1, 0, atom_u16) /* emitted in phase 1, removed in phase 2 */
def(scope_delete_var, 7, 0, 1, atom_u16) /* emitted in phase 1, removed in phase 2 */
def( scope_make_ref, 11, 0, 2, atom_label_u16) /* emitted in phase 1, removed in phase 2 */
def( scope_get_ref, 7, 0, 2, atom_u16) /* emitted in phase 1, removed in phase 2 */
def(scope_put_var_init, 7, 0, 2, atom_u16) /* emitted in phase 1, removed in phase 2 */
def(scope_get_var_checkthis, 7, 0, 1, atom_u16) /* emitted in phase 1, removed in phase 2, only used to return 'this' in derived class constructors */
def(get_field_opt_chain, 5, 1, 1, atom) /* emitted in phase 1, removed in phase 2 */
def(get_array_el_opt_chain, 1, 2, 1, none) /* emitted in phase 1, removed in phase 2 */
def( set_class_name, 5, 1, 1, u32) /* emitted in phase 1, removed in phase 2 */
@@ -277,6 +302,20 @@ DEF( set_arg0, 1, 1, 1, none_arg)
DEF( set_arg1, 1, 1, 1, none_arg)
DEF( set_arg2, 1, 1, 1, none_arg)
DEF( set_arg3, 1, 1, 1, none_arg)
DEF( get_var_ref0, 1, 0, 1, none_var_ref)
DEF( get_var_ref1, 1, 0, 1, none_var_ref)
DEF( get_var_ref2, 1, 0, 1, none_var_ref)
DEF( get_var_ref3, 1, 0, 1, none_var_ref)
DEF( put_var_ref0, 1, 1, 0, none_var_ref)
DEF( put_var_ref1, 1, 1, 0, none_var_ref)
DEF( put_var_ref2, 1, 1, 0, none_var_ref)
DEF( put_var_ref3, 1, 1, 0, none_var_ref)
DEF( set_var_ref0, 1, 1, 1, none_var_ref)
DEF( set_var_ref1, 1, 1, 1, none_var_ref)
DEF( set_var_ref2, 1, 1, 1, none_var_ref)
DEF( set_var_ref3, 1, 1, 1, none_var_ref)
DEF( get_length, 1, 1, 1, none)
DEF( if_false8, 2, 1, 0, label8)
DEF( if_true8, 2, 1, 0, label8) /* must come after if_false8 */
@@ -289,6 +328,7 @@ DEF( call2, 1, 1, 1, npopx)
DEF( call3, 1, 1, 1, npopx)
DEF( is_null, 1, 1, 1, none)
DEF( typeof_is_function, 1, 1, 1, none)
#endif
#undef DEF

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -270,7 +270,7 @@ void actor_free(cell_rt *actor)
JS_FreeValue(js, actor->message_handle);
JS_FreeValue(js, actor->on_exception);
JS_FreeValue(js, actor->unneeded);
JS_FreeValue(js, actor->actor_sym);
JS_FreeAtom(js, actor->actor_sym);
for (int i = 0; i < hmlen(actor->timers); i++) {
JS_FreeValue(js, actor->timers[i].value);
@@ -436,7 +436,7 @@ void actor_unneeded(cell_rt *actor, JSValue fn, double seconds)
if (actor->disrupt) return;
JS_FreeValue(actor->context, actor->unneeded);
if (!JS_IsFunction(fn)) {
if (!JS_IsFunction(actor->context, fn)) {
actor->unneeded = JS_NULL;
goto END;
}
@@ -497,7 +497,7 @@ cell_rt *create_actor(void *wota)
actor->message_handle = JS_NULL;
actor->unneeded = JS_NULL;
actor->on_exception = JS_NULL;
actor->actor_sym = JS_NULL;
actor->actor_sym = JS_ATOM_NULL;
arrsetcap(actor->letters, 5);

View File

@@ -122,7 +122,7 @@ void actor_free(cell_rt *actor)
JS_FreeValue(js, actor->message_handle);
JS_FreeValue(js, actor->on_exception);
JS_FreeValue(js, actor->unneeded);
JS_FreeValue(js, actor->actor_sym);
JS_FreeAtom(js, actor->actor_sym);
/* Free timer callbacks stored in actor */
for (int i = 0; i < hmlen(actor->timers); i++) {
@@ -332,7 +332,7 @@ cell_rt *create_actor(void *wota)
actor->message_handle = JS_NULL;
actor->unneeded = JS_NULL;
actor->on_exception = JS_NULL;
actor->actor_sym = JS_NULL;
actor->actor_sym = JS_ATOM_NULL;
arrsetcap(actor->letters, 5);

File diff suppressed because it is too large Load Diff

View File

@@ -1,94 +0,0 @@
QuickJS Mist Memory Format Refactoring
Current Status
The codebase is partially refactored but doesn't compile due to missing KeyId type definitions.
Incremental Refactoring Tasks
Phase 0: Fix Compilation (Prerequisite)
Define missing KeyId type as transitional typedef (will be replaced by JSValue later)
Define K_EMPTY, K_TOMB,
key_text()
,
key_is_text()
,
key_payload()
macros/functions
Verify build compiles and tests pass
Phase 1: New JSValue Encoding in quickjs.h
Add new LSB-based tag constants alongside existing tags
Add JS_TAG_SHORT_FLOAT for 61-bit truncated double
Add JS_TAG_STRING_ASCII for immediate 7-byte ASCII strings
Add new value extraction/creation macros
Add type check inline functions
Keep existing NaN-boxing code active (compile-time switch)
Phase 2: Short Float Implementation
Implement JS_NewFloat64_ShortFloat() with range checking
Implement JS_VALUE_GET_FLOAT64_ShortFloat() for decoding
Out-of-range values return JS_NULL
Prefer integer encoding when exact
Phase 3: Immediate ASCII String
Phase 3: Immediate ASCII String
Implement JS_TryNewImmediateASCII() for strings up to 7 chars
Implement JS_IsImmediateASCII() type check
Implement JS_GetImmediateASCIILen() and JS_GetImmediateASCIIChar()
Integrate with
JS_NewStringLen()
to try immediate first
Phase 4: Remove JSStringRope
Delete JSStringRope structure
Remove JS_TAG_STRING_ROPE handling
Update string concatenation to create immediate mist_text objects
Remove rope-related iterator functions
Phase 5: Refactor JSString to UTF-32 (mist_text)
Modify struct JSString to store UTF-32 characters only
Remove is_wide_char flag and 8.16 unions
Update
js_alloc_string
to allocate UTF-32 buffer
Update string creation functions (
js_new_string8
, etc.)
Update all string accessors to use UTF-32
Implement immediate-to-UTF32 conversion helper
Update string operations (
concat
,
compare
) to work on UTF-32
Phase 6: Replace KeyId with JSValue in Records
Change JSRecordEntry.key from KeyId to JSValue
Update
rec_hash_key()
to hash JSValue keys directly
Update
rec_find_slot()
for JSValue key comparison
Update
rec_get_own()
,
rec_get()
,
rec_set_own()
for JSValue keys
Remove KeyId typedef and related functions
Phase 7: Consolidate JSObject → JSRecord
Remove JSShape and JSShapeProperty structures
Remove shape hash table from JSRuntime
Update all property access to use JSRecord
Migrate JSObject users to JSRecord
Remove JSObject structure
Phase 8: Update GC for New Format
Update mark_children for JSRecord with JSValue keys
Update free_record for JSValue keys
Handle immediate values correctly (no marking needed)
Test for cycles and correct collection
Phase 9: C Class Storage in Slot 0
Implement slot 0 reservation for class_id and opaque pointer
Update JS_SetOpaque() / JS_GetOpaque()
Migrate existing class storage
Verification Checklist
Build compiles without errors
Existing tests pass
Property access works correctly
GC correctly handles cycles
Short float encoding/decoding verified
Immediate ASCII strings work

229
test.ce
View File

@@ -4,16 +4,12 @@ var fd = use('fd')
var time = use('time')
var json = use('json')
var blob = use('blob')
var dbg = use('js')
// run gc with dbg.gc()
if (!args) args = []
var target_pkg = null // null = current package
var target_test = null // null = all tests, otherwise specific test file
var all_pkgs = false
var gc_after_each_test = false
// Actor test support
def ACTOR_TEST_TIMEOUT = 30000 // 30 seconds timeout for actor tests
@@ -47,17 +43,7 @@ function get_current_package_name() {
// cell test package all - run all tests from all packages
function parse_args() {
var cleaned_args = []
for (var i = 0; i < length(args); i++) {
if (args[i] == '-g') {
gc_after_each_test = true
} else {
push(cleaned_args, args[i])
}
}
args = cleaned_args
if (length(args) == 0) {
if (args.length == 0) {
// cell test - run all tests for current package
if (!is_valid_package('.')) {
log.console('No cell.toml found in current directory')
@@ -78,7 +64,7 @@ function parse_args() {
}
if (args[0] == 'package') {
if (length(args) < 2) {
if (args.length < 2) {
log.console('Usage: cell test package <name> [test]')
log.console(' cell test package all')
return false
@@ -98,7 +84,7 @@ function parse_args() {
var lock = shop.load_lock()
if (lock[name]) {
target_pkg = name
} else if (starts_with(name, '/') && is_valid_package(name)) {
} else if (name.startsWith('/') && is_valid_package(name)) {
target_pkg = name
} else {
// Try to resolve as dependency alias from current package
@@ -116,7 +102,7 @@ function parse_args() {
}
}
if (length(args) >= 3) {
if (args.length >= 3) {
// cell test package <name> <test>
target_test = args[2]
}
@@ -129,7 +115,7 @@ function parse_args() {
var test_path = args[0]
// Normalize path - add tests/ prefix if not present and doesn't start with /
if (!starts_with(test_path, 'tests/') && !starts_with(test_path, '/')) {
if (!test_path.startsWith('tests/') && !test_path.startsWith('/')) {
// Check if file exists as-is first
if (!fd.is_file(test_path + '.cm') && !fd.is_file(test_path)) {
// Try with tests/ prefix
@@ -158,13 +144,14 @@ if (!parse_args()) {
function ensure_dir(path) {
if (fd.is_dir(path)) return true
var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
for (var i = 0; i < length(parts); i++) {
var parts = path.split('/')
var current = path.startsWith('/') ? '/' : ''
for (var i = 0; i < parts.length; i++) {
if (parts[i] == '') continue
current += parts[i] + '/'
if (!fd.is_dir(current))
if (!fd.is_dir(current)) {
fd.mkdir(current)
}
}
return true
}
@@ -174,7 +161,7 @@ function get_pkg_dir(package_name) {
if (!package_name) {
return fd.realpath('.')
}
if (starts_with(package_name, '/')) {
if (package_name.startsWith('/')) {
return package_name
}
return shop.get_package_dir(package_name)
@@ -189,23 +176,23 @@ function collect_actor_tests(package_name, specific_test) {
var files = pkg.list_files(package_name)
var actor_tests = []
for (var i = 0; i < length(files); i++) {
for (var i = 0; i < files.length; i++) {
var f = files[i]
// Check if file is in tests/ folder and is a .ce actor
if (starts_with(f, "tests/") && ends_with(f, ".ce")) {
if (f.startsWith("tests/") && f.endsWith(".ce")) {
// If specific test requested, filter
if (specific_test) {
var test_name = text(f, 0, -3) // remove .ce
var test_name = f.substring(0, f.length - 3) // remove .ce
var match_name = specific_test
if (!starts_with(match_name, 'tests/')) match_name = 'tests/' + match_name
if (!ends_with(match_name, '.ce')) match_name = match_name
if (!match_name.startsWith('tests/')) match_name = 'tests/' + match_name
if (!match_name.endsWith('.ce')) match_name = match_name
// Match without extension
var test_base = test_name
var match_base = ends_with(match_name, '.ce') ? text(match_name, 0, -3) : match_name
var match_base = match_name.endsWith('.ce') ? match_name.substring(0, match_name.length - 3) : match_name
if (test_base != match_base) continue
}
push(actor_tests,{
actor_tests.push({
package: package_name || "local",
file: f,
path: prefix + '/' + f
@@ -217,7 +204,7 @@ function collect_actor_tests(package_name, specific_test) {
// Spawn an actor test and track it
function spawn_actor_test(test_info) {
var test_name = text(test_info.file, 6, -3) // remove "tests/" and ".ce"
var test_name = test_info.file.substring(6, test_info.file.length - 3) // remove "tests/" and ".ce"
log.console(` [ACTOR] ${test_info.file}`)
var entry = {
@@ -231,14 +218,14 @@ function spawn_actor_test(test_info) {
try {
// Spawn the actor test - it should send back results
var actor_path = text(test_info.path, 0, -3) // remove .ce
var actor_path = test_info.path.substring(0, test_info.path.length - 3) // remove .ce
entry.actor = $start(actor_path)
push(pending_actor_tests, entry)
pending_actor_tests.push(entry)
} catch (e) {
entry.status = "failed"
entry.error = { message: `Failed to spawn actor: ${e}` }
entry.duration_ns = 0
push(actor_test_results, entry)
actor_test_results.push(entry)
log.console(` FAIL ${test_name}: `)
log.error(e)
}
@@ -260,31 +247,31 @@ function run_tests(package_name, specific_test) {
var files = pkg.list_files(package_name)
var test_files = []
for (var i = 0; i < length(files); i++) {
for (var i = 0; i < files.length; i++) {
var f = files[i]
// Check if file is in tests/ folder and is a .cm module (not .ce - those are actor tests)
if (starts_with(f, "tests/") && ends_with(f, ".cm")) {
if (f.startsWith("tests/") && f.endsWith(".cm")) {
// If specific test requested, filter
if (specific_test) {
var test_name = text(f, 0, -3) // remove .cm
var test_name = f.substring(0, f.length - 3) // remove .cm
var match_name = specific_test
if (!starts_with(match_name, 'tests/')) match_name = 'tests/' + match_name
if (!match_name.startsWith('tests/')) match_name = 'tests/' + match_name
// Match without extension
var match_base = ends_with(match_name, '.cm') ? text(match_name, 0, -3) : match_name
var match_base = match_name.endsWith('.cm') ? match_name.substring(0, match_name.length - 3) : match_name
if (test_name != match_base) continue
}
push(test_files, f)
test_files.push(f)
}
}
if (length(test_files) > 0) {
if (test_files.length > 0) {
if (package_name) log.console(`Running tests for ${package_name}`)
else log.console(`Running tests for local package`)
}
for (var i = 0; i < length(test_files); i++) {
for (var i = 0; i < test_files.length; i++) {
var f = test_files[i]
var mod_path = text(f, 0, -3) // remove .cm
var mod_path = f.substring(0, f.length - 3) // remove .cm
var file_result = {
name: f,
@@ -300,19 +287,19 @@ function run_tests(package_name, specific_test) {
test_mod = shop.use(mod_path, use_pkg)
var tests = []
if (is_function(test_mod)) {
push(tests, {name: 'main', fn: test_mod})
} else if (is_object(test_mod)) {
arrfor(array(test_mod), function(k) {
if (is_function(test_mod[k])) {
push(tests, {name: k, fn: test_mod[k]})
if (typeof test_mod == 'function') {
tests.push({name: 'main', fn: test_mod})
} else if (typeof test_mod == 'object') {
for (var k in test_mod) {
if (typeof test_mod[k] == 'function') {
tests.push({name: k, fn: test_mod[k]})
}
})
}
}
if (length(tests) > 0) {
if (tests.length > 0) {
log.console(` ${f}`)
for (var j = 0; j < length(tests); j++) {
for (var j = 0; j < tests.length; j++) {
var t = tests[j]
var test_entry = {
package: pkg_result.package,
@@ -325,9 +312,9 @@ function run_tests(package_name, specific_test) {
try {
var ret = t.fn()
if (is_text(ret)) {
throw Error(ret)
} else if (ret && (is_text(ret.message) || is_proto(ret, Error))) {
if (typeof ret == 'string') {
throw new Error(ret)
} else if (ret && (typeof ret.message == 'string' || ret instanceof Error)) {
throw ret
}
@@ -338,31 +325,28 @@ function run_tests(package_name, specific_test) {
} catch (e) {
test_entry.status = "failed"
test_entry.error = {
message: e,
message: e.toString(),
stack: e.stack || ""
}
if (e.name) test_entry.error.name = e.name
if (is_object(e) && e.message) {
if (typeof e == 'object' && e.message) {
test_entry.error.message = e.message
}
log.console(` FAIL ${t.name} ${test_entry.error.message}`)
if (test_entry.error.stack) {
log.console(` ${text(array(test_entry.error.stack, '\n'), '\n ')}`)
log.console(` ${test_entry.error.stack.split('\n').join('\n ')}`)
}
pkg_result.failed++
file_result.failed++
}
var end_time = time.number()
test_entry.duration_ns = round((end_time - start_time) * 1000000000)
test_entry.duration_ns = number.round((end_time - start_time) * 1000000000)
push(file_result.tests, test_entry)
file_result.tests.push(test_entry)
pkg_result.total++
if (gc_after_each_test) {
dbg.gc()
}
}
}
@@ -375,15 +359,12 @@ function run_tests(package_name, specific_test) {
duration_ns: 0,
error: { message: `Error loading module: ${e}` }
}
push(file_result.tests, test_entry)
file_result.tests.push(test_entry)
pkg_result.failed++
file_result.failed++
pkg_result.total++
if (gc_after_each_test) {
dbg.gc()
}
}
push(pkg_result.files, file_result)
pkg_result.files.push(file_result)
}
return pkg_result
}
@@ -394,25 +375,25 @@ var all_actor_tests = []
if (all_pkgs) {
// Run local first if we're in a valid package
if (is_valid_package('.')) {
push(all_results, run_tests(null, null))
all_actor_tests = array(all_actor_tests, collect_actor_tests(null, null))
all_results.push(run_tests(null, null))
all_actor_tests = all_actor_tests.concat(collect_actor_tests(null, null))
}
// Then all packages in lock
var packages = shop.list_packages()
for (var i = 0; i < length(packages); i++) {
push(all_results, run_tests(packages[i], null))
all_actor_tests = array(all_actor_tests, collect_actor_tests(packages[i], null))
for (var i = 0; i < packages.length; i++) {
all_results.push(run_tests(packages[i], null))
all_actor_tests = all_actor_tests.concat(collect_actor_tests(packages[i], null))
}
} else {
push(all_results, run_tests(target_pkg, target_test))
all_actor_tests = array(all_actor_tests, collect_actor_tests(target_pkg, target_test))
all_results.push(run_tests(target_pkg, target_test))
all_actor_tests = all_actor_tests.concat(collect_actor_tests(target_pkg, target_test))
}
// Spawn actor tests if any
if (length(all_actor_tests) > 0) {
log.console(`Running ${length(all_actor_tests)} actor test(s)...`)
for (var i = 0; i < length(all_actor_tests); i++) {
if (all_actor_tests.length > 0) {
log.console(`Running ${all_actor_tests.length} actor test(s)...`)
for (var i = 0; i < all_actor_tests.length; i++) {
spawn_actor_test(all_actor_tests[i])
}
}
@@ -421,7 +402,7 @@ if (length(all_actor_tests) > 0) {
function handle_actor_message(msg) {
var sender = msg.$sender
var found_idx = -1
for (var i = 0; i < length(pending_actor_tests); i++) {
for (var i = 0; i < pending_actor_tests.length; i++) {
if (pending_actor_tests[i].actor == sender) {
found_idx = i
break
@@ -431,26 +412,26 @@ function handle_actor_message(msg) {
if (found_idx == -1) return
var base_entry = pending_actor_tests[found_idx]
pending_actor_tests = array(array(pending_actor_tests, 0, found_idx), array(pending_actor_tests, found_idx + 1))
pending_actor_tests.splice(found_idx, 1)
var end_time = time.number()
var duration_ns = round((end_time - base_entry.start_time) * 1000000000)
var duration_ns = number.round((end_time - base_entry.start_time) * 1000000000)
var results = []
if (is_array(msg)) {
if (isa(msg, array)) {
results = msg
} else if (msg && is_array(msg.results)) {
} else if (msg && isa(msg.results, array)) {
results = msg.results
} else {
results = [msg]
}
for (var i = 0; i < length(results); i++) {
for (var i = 0; i < results.length; i++) {
var res = results[i] || {}
var entry = {
package: base_entry.package,
file: base_entry.file,
test: res.test || base_entry.test + (length(results) > 1 ? `#${i+1}` : ""),
test: res.test || base_entry.test + (results.length > 1 ? `#${i+1}` : ""),
status: "failed",
duration_ns: duration_ns
}
@@ -467,11 +448,7 @@ function handle_actor_message(msg) {
log.console(` FAIL ${entry.test}: ${entry.error.message}`)
}
push(actor_test_results, entry)
}
if (gc_after_each_test) {
dbg.gc()
actor_test_results.push(entry)
}
check_completion()
@@ -482,27 +459,27 @@ function check_timeouts() {
var now = time.number()
var timed_out = []
for (var i = length(pending_actor_tests) - 1; i >= 0; i--) {
for (var i = pending_actor_tests.length - 1; i >= 0; i--) {
var entry = pending_actor_tests[i]
var elapsed_ms = (now - entry.start_time) * 1000
if (elapsed_ms > ACTOR_TEST_TIMEOUT) {
push(timed_out, i)
timed_out.push(i)
}
}
for (var i = 0; i < length(timed_out); i++) {
for (var i = 0; i < timed_out.length; i++) {
var idx = timed_out[i]
var entry = pending_actor_tests[idx]
pending_actor_tests = array(array(pending_actor_tests, 0, idx), array(pending_actor_tests, idx + 1))
pending_actor_tests.splice(idx, 1)
entry.status = "failed"
entry.error = { message: "Test timed out" }
entry.duration_ns = ACTOR_TEST_TIMEOUT * 1000000
push(actor_test_results, entry)
actor_test_results.push(entry)
log.console(` TIMEOUT ${entry.test}`)
}
if (length(pending_actor_tests) > 0) {
if (pending_actor_tests.length > 0) {
$delay(check_timeouts, 1000)
}
check_completion()
@@ -512,7 +489,7 @@ function check_timeouts() {
var finalized = false
function check_completion() {
if (finalized) return
if (length(pending_actor_tests) > 0) return
if (pending_actor_tests.length > 0) return
finalized = true
finalize_results()
@@ -520,10 +497,10 @@ function check_completion() {
function finalize_results() {
// Add actor test results to all_results
for (var i = 0; i < length(actor_test_results); i++) {
for (var i = 0; i < actor_test_results.length; i++) {
var r = actor_test_results[i]
var pkg_result = null
for (var j = 0; j < length(all_results); j++) {
for (var j = 0; j < all_results.length; j++) {
if (all_results[j].package == r.package) {
pkg_result = all_results[j]
break
@@ -531,11 +508,11 @@ function finalize_results() {
}
if (!pkg_result) {
pkg_result = { package: r.package, files: [], total: 0, passed: 0, failed: 0 }
push(all_results, pkg_result)
all_results.push(pkg_result)
}
var file_result = null
for (var j = 0; j < length(pkg_result.files); j++) {
for (var j = 0; j < pkg_result.files.length; j++) {
if (pkg_result.files[j].name == r.file) {
file_result = pkg_result.files[j]
break
@@ -543,10 +520,10 @@ function finalize_results() {
}
if (!file_result) {
file_result = { name: r.file, tests: [], passed: 0, failed: 0 }
push(pkg_result.files, file_result)
pkg_result.files.push(file_result)
}
push(file_result.tests, r)
file_result.tests.push(r)
pkg_result.total++
if (r.status == "passed") {
pkg_result.passed++
@@ -559,7 +536,7 @@ function finalize_results() {
// Calculate totals
var totals = { total: 0, passed: 0, failed: 0 }
for (var i = 0; i < length(all_results); i++) {
for (var i = 0; i < all_results.length; i++) {
totals.total += all_results[i].total
totals.passed += all_results[i].passed
totals.failed += all_results[i].failed
@@ -574,9 +551,9 @@ function finalize_results() {
// If no actor tests, finalize immediately
var totals
if (length(all_actor_tests) == 0) {
if (all_actor_tests.length == 0) {
totals = { total: 0, passed: 0, failed: 0 }
for (var i = 0; i < length(all_results); i++) {
for (var i = 0; i < all_results.length; i++) {
totals.total += all_results[i].total
totals.passed += all_results[i].passed
totals.failed += all_results[i].failed
@@ -590,7 +567,7 @@ if (length(all_actor_tests) == 0) {
// Generate Reports function
function generate_reports(totals) {
var timestamp = text(floor(time.number()))
var timestamp = text(number.floor(time.number()))
var report_dir = shop.get_reports_dir() + '/test_' + timestamp
ensure_dir(report_dir)
@@ -600,24 +577,24 @@ Total: ${totals.total}, Passed: ${totals.passed}, Failed: ${totals.failed}
=== SUMMARY ===
`
for (var i = 0; i < length(all_results); i++) {
for (var i = 0; i < all_results.length; i++) {
var pkg_res = all_results[i]
if (pkg_res.total == 0) continue
txt_report += `Package: ${pkg_res.package}\n`
for (var j = 0; j < length(pkg_res.files); j++) {
for (var j = 0; j < pkg_res.files.length; j++) {
var f = pkg_res.files[j]
var status = f.failed == 0 ? "PASS" : "FAIL"
txt_report += ` [${status}] ${f.name} (${f.passed}/${length(f.tests)})\n`
txt_report += ` [${status}] ${f.name} (${f.passed}/${f.tests.length})\n`
}
}
txt_report += `\n=== FAILURES ===\n`
var has_failures = false
for (var i = 0; i < length(all_results); i++) {
for (var i = 0; i < all_results.length; i++) {
var pkg_res = all_results[i]
for (var j = 0; j < length(pkg_res.files); j++) {
for (var j = 0; j < pkg_res.files.length; j++) {
var f = pkg_res.files[j]
for (var k = 0; k < length(f.tests); k++) {
for (var k = 0; k < f.tests.length; k++) {
var t = f.tests[k]
if (t.status == "failed") {
has_failures = true
@@ -625,7 +602,7 @@ Total: ${totals.total}, Passed: ${totals.passed}, Failed: ${totals.failed}
if (t.error) {
txt_report += ` Message: ${t.error.message}\n`
if (t.error.stack) {
txt_report += ` Stack:\n${text(array(array(t.error.stack, '\n'), l => ` ${l}`), '\n')}\n`
txt_report += ` Stack:\n${t.error.stack.split('\n').map(function(l){return ` ${l}`}).join('\n')}\n`
}
}
txt_report += `\n`
@@ -636,13 +613,13 @@ Total: ${totals.total}, Passed: ${totals.passed}, Failed: ${totals.failed}
if (!has_failures) txt_report += `None\n`
txt_report += `\n=== DETAILED RESULTS ===\n`
for (var i = 0; i < length(all_results); i++) {
for (var i = 0; i < all_results.length; i++) {
var pkg_res = all_results[i]
if (pkg_res.total == 0) continue
for (var j = 0; j < length(pkg_res.files); j++) {
for (var j = 0; j < pkg_res.files.length; j++) {
var f = pkg_res.files[j]
for (var k = 0; k < length(f.tests); k++) {
for (var k = 0; k < f.tests.length; k++) {
var t = f.tests[k]
var dur = `${t.duration_ns || 0}ns`
var status = t.status == "passed" ? "PASS" : "FAIL"
@@ -651,29 +628,29 @@ Total: ${totals.total}, Passed: ${totals.passed}, Failed: ${totals.failed}
}
}
ensure_dir(report_dir)
fd.slurpwrite(`${report_dir}/test.txt`, stone(blob(txt_report)))
fd.slurpwrite(`${report_dir}/test.txt`, stone(new blob(txt_report)))
log.console(`Report written to ${report_dir}/test.txt`)
// Generate JSON per package
for (var i = 0; i < length(all_results); i++) {
for (var i = 0; i < all_results.length; i++) {
var pkg_res = all_results[i]
if (pkg_res.total == 0) continue
var pkg_tests = []
for (var j = 0; j < length(pkg_res.files); j++) {
for (var j = 0; j < pkg_res.files.length; j++) {
var f = pkg_res.files[j]
for (var k = 0; k < length(f.tests); k++) {
push(pkg_tests, f.tests[k])
for (var k = 0; k < f.tests.length; k++) {
pkg_tests.push(f.tests[k])
}
}
var json_path = `${report_dir}/${replace(pkg_res.package, /\//, '_')}.json`
fd.slurpwrite(json_path, stone(blob(json.encode(pkg_tests))))
var json_path = `${report_dir}/${pkg_res.package.replace(/\//g, '_')}.json`
fd.slurpwrite(json_path, stone(new blob(json.encode(pkg_tests))))
}
}
// If no actor tests, generate reports and stop immediately
if (length(all_actor_tests) == 0) {
if (all_actor_tests.length == 0) {
generate_reports(totals)
$stop()
} else {

View File

@@ -4,34 +4,34 @@ var os = use('os');
function assert(condition, message) {
if (!condition) {
throw Error(message || "Assertion failed");
throw new Error(message || "Assertion failed");
}
}
function assertEqual(actual, expected, message) {
if (actual != expected) {
throw Error(message || "Expected " + expected + ", got " + actual);
throw new Error(message || "Expected " + expected + ", got " + actual);
}
}
return {
test_create_empty_blob: function() {
var b = Blob();
assertEqual(length(b), 0, "Empty blob should have length 0");
var b = new Blob();
assertEqual(b.length, 0, "Empty blob should have length 0");
},
test_create_blob_with_capacity: function() {
var b = Blob(100);
assertEqual(length(b), 0, "New blob with capacity should still have length 0");
var b = new Blob(100);
assertEqual(b.length, 0, "New blob with capacity should still have length 0");
},
test_write_and_read_single_bit: function() {
var b = Blob();
var b = new Blob();
b.write_bit(true);
b.write_bit(false);
b.write_bit(1);
b.write_bit(0);
assertEqual(length(b), 4, "Should have 4 bits after writing");
assertEqual(b.length, 4, "Should have 4 bits after writing");
stone(b);
assertEqual(b.read_logical(0), true, "First bit should be true");
@@ -41,7 +41,7 @@ return {
},
test_out_of_range_read_throws_error: function() {
var b = Blob();
var b = new Blob();
b.write_bit(true);
stone(b);
@@ -63,7 +63,7 @@ return {
},
test_write_and_read_numbers: function() {
var b = Blob();
var b = new Blob();
b.write_number(3.14159);
b.write_number(-42);
b.write_number(0);
@@ -77,7 +77,7 @@ return {
},
test_write_and_read_text: function() {
var b = Blob();
var b = new Blob();
b.write_text("Hello");
b.write_text("World");
b.write_text("🎉");
@@ -87,15 +87,15 @@ return {
},
test_write_and_read_blobs: function() {
var b1 = Blob();
var b1 = new Blob();
b1.write_bit(true);
b1.write_bit(false);
b1.write_bit(true);
var b2 = Blob(10);
var b2 = new Blob(10);
b2.write_blob(b1);
b2.write_bit(false);
assertEqual(length(b2), 4, "Combined blob should have 4 bits");
assertEqual(b2.length, 4, "Combined blob should have 4 bits");
stone(b2);
assertEqual(b2.read_logical(0), true);
@@ -105,37 +105,37 @@ return {
},
test_blob_copy_constructor: function() {
var b1 = Blob();
var b1 = new Blob();
b1.write_bit(true);
b1.write_bit(false);
b1.write_bit(true);
b1.write_bit(true);
stone(b1);
var b2 = Blob(b1);
var b2 = new Blob(b1);
stone(b2);
assertEqual(length(b2), 4, "Copied blob should have same length");
assertEqual(b2.length, 4, "Copied blob should have same length");
assertEqual(b2.read_logical(0), true);
assertEqual(b2.read_logical(3), true);
},
test_blob_partial_copy_constructor: function() {
var b1 = Blob();
var b1 = new Blob();
for (var i = 0; i < 10; i++) {
b1.write_bit(i % 2 == 0);
}
stone(b1);
var b2 = Blob(b1, 2, 7);
var b2 = new Blob(b1, 2, 7);
stone(b2);
assertEqual(length(b2), 5, "Partial copy should have 5 bits");
assertEqual(b2.length, 5, "Partial copy should have 5 bits");
assertEqual(b2.read_logical(0), true);
assertEqual(b2.read_logical(2), true);
},
test_create_blob_with_fill: function() {
var b1 = Blob(8, true);
var b2 = Blob(8, false);
var b1 = new Blob(8, true);
var b2 = new Blob(8, false);
stone(b1);
stone(b2);
@@ -150,7 +150,7 @@ return {
var sequence = [true, false, true, true, false];
var index = 0;
var b = Blob(5, function() {
var b = new Blob(5, function() {
return sequence[index++] ? 1 : 0;
});
@@ -161,13 +161,13 @@ return {
},
test_write_pad_and_check_padding: function() {
var b = Blob();
var b = new Blob();
b.write_bit(true);
b.write_bit(false);
b.write_bit(true);
b.write_pad(8);
assertEqual(length(b), 8, "Should be padded to 8 bits");
assertEqual(b.length, 8, "Should be padded to 8 bits");
stone(b);
assert(b['pad?'](3, 8), "Should detect valid padding at position 3");
@@ -175,7 +175,7 @@ return {
},
test_read_blob_from_stone_blob: function() {
var b1 = Blob();
var b1 = new Blob();
for (var i = 0; i < 16; i++) {
b1.write_bit(i % 3 == 0);
}
@@ -183,14 +183,14 @@ return {
var b2 = b1.read_blob(4, 12);
stone(b2);
assertEqual(length(b2), 8, "Read blob should have 8 bits");
assertEqual(b2.length, 8, "Read blob should have 8 bits");
assertEqual(b2.read_logical(2), true);
assertEqual(b2.read_logical(5), true);
},
test_stone_blob_is_immutable: function() {
var b = Blob();
var b = new Blob();
b.write_bit(true);
stone(b);
@@ -204,7 +204,7 @@ return {
},
test_multiple_stone_calls_are_safe: function() {
var b = Blob();
var b = new Blob();
b.write_bit(true);
assert(!stone.p(b), "Blob should not be a stone before stone() call");
stone(b);
@@ -218,7 +218,7 @@ return {
test_invalid_constructor_arguments: function() {
var threw = false;
try {
var b = Blob("invalid");
var b = new Blob("invalid");
} catch (e) {
threw = true;
}
@@ -226,7 +226,7 @@ return {
},
test_write_bit_validation: function() {
var b = Blob();
var b = new Blob();
b.write_bit(0);
b.write_bit(1);
@@ -240,7 +240,7 @@ return {
},
test_complex_data_round_trip: function() {
var b = Blob();
var b = new Blob();
b.write_text("Test");
b.write_number(123.456);
@@ -248,31 +248,31 @@ return {
b.write_bit(false);
b.write_number(-999.999);
var originalLength = length(b);
var originalLength = b.length;
stone(b);
var b2 = Blob(b);
var b2 = new Blob(b);
stone(b2);
assertEqual(length(b2), originalLength, "Copy should have same length");
assertEqual(b2.length, originalLength, "Copy should have same length");
assertEqual(b2.read_text(0), "Test", "First text should match");
},
test_zero_capacity_blob: function() {
var b = Blob(0);
assertEqual(length(b), 0, "Zero capacity blob should have length 0");
var b = new Blob(0);
assertEqual(b.length, 0, "Zero capacity blob should have length 0");
b.write_bit(true);
assertEqual(length(b), 1, "Should expand when writing");
assertEqual(b.length, 1, "Should expand when writing");
},
test_large_blob_handling: function() {
var b = Blob();
var b = new Blob();
var testSize = 1000;
for (var i = 0; i < testSize; i++) {
b.write_bit(i % 7 == 0);
}
assertEqual(length(b), testSize, "Should have " + testSize + " bits");
assertEqual(b.length, testSize, "Should have " + testSize + " bits");
stone(b);
assertEqual(b.read_logical(0), true, "Bit 0 should be true");
@@ -282,7 +282,7 @@ return {
},
test_non_stone_blob_read_methods_should_throw: function() {
var b = Blob();
var b = new Blob();
b.write_bit(true);
b.write_number(42);
b.write_text("test");
@@ -329,14 +329,14 @@ return {
},
test_empty_text_write_and_read: function() {
var b = Blob();
var b = new Blob();
b.write_text("");
stone(b);
assertEqual(b.read_text(0), "", "Empty string should round-trip");
},
test_invalid_read_positions: function() {
var b = Blob();
var b = new Blob();
b.write_number(42);
stone(b);

View File

@@ -12,7 +12,7 @@ return {
fd.write(f, bigdata)
fd.close(f)
var data = blob()
var data = new blob
var st = time.number()
var f2 = fd.open(tmp, 'r')
var chunksize = 1024 // reduced for test
@@ -20,7 +20,11 @@ return {
while(true) {
var chunk = fd.read(f2, chunksize);
data.write_blob(chunk);
if (length(chunk) < chunksize * 8) break;
// chunk.length is in bits, chunksize is bytes?
// fd.read usually takes bytes. Blob.length is bits.
// If chunk is blob, length is bits.
// fd.read returns blob.
if (chunk.length < chunksize * 8) break;
}
fd.close(f2)
log.console(`read took ${time.number()-st}`)

View File

@@ -15,7 +15,7 @@ return {
}
$receiver(tree => {
var child_reqs = array(tree.children, child => cb => {
var child_reqs = tree.children.map(child => cb => {
$start(e => send(e.actor, child, cb), "tests/comments")
})
@@ -30,10 +30,7 @@ return {
send(tree, reason)
}
var obj = object(result.comment)
obj.children = result.children
obj.time = time.text()
send(tree, obj)
send(tree, { ...result.comment, children: result.children, time: time.text() })
})
})
}

View File

@@ -9,7 +9,7 @@ function load_comment_from_api_requestor(id) {
}
$receiver(tree => {
var child_reqs = array(tree.children, child => cb => {
var child_reqs = tree.children.map(child => cb => {
$start(e => send(e.actor, child, cb), "tests/comments") // Note: recursively calls itself? Original used "tests/comments"
// We should probably change this to "tests/comments_actor" if it's recursive
})
@@ -25,9 +25,6 @@ $receiver(tree => {
send(tree, reason)
}
var obj = object(result.comment)
obj.children = result.children
obj.time = time.text()
send(tree, obj)
send(tree, { ...result.comment, children: result.children, time: time.text() })
})
})

View File

@@ -4,14 +4,14 @@ var time = use('time')
return {
test_guid: function() {
var st = time.number()
var guid = blob(256, $random_fit)
var guid = new blob(256, $random_fit)
stone(guid)
var btime = time.number()-st
st = time.number()
guid = text(guid,'h')
st = time.number()-st
log.console(`took ${btime*1000000} us to make blob; took ${st*1000000} us to make it text`)
log.console(lower(guid))
log.console(length(guid))
log.console(guid.toLowerCase())
log.console(guid.length)
}
}

View File

@@ -1,6 +1,6 @@
return {
test_hang: function() {
log.console(`Going to start hanging .. (disabled)`)
log.console(`Going to start hanging ... (disabled)`)
// while(1) {
// // hang!

View File

@@ -3,5 +3,5 @@ var http = use('http')
return function() {
var url = "http://example.com"
var b2 = http.fetch(url)
if (length(b2) == 0) throw "Empty response"
if (b2.length == 0) throw "Empty response"
}

View File

@@ -54,9 +54,9 @@ return {
var reader = miniz.read(zip_blob)
var listed = reader.list()
if (length(listed) != reader.count())
if (listed.length != reader.count())
throw "list/count mismatch"
if (length(listed) != 2)
if (listed.length != 2)
throw "unexpected entry count"
} finally {
try { fd.unlink(ZIP_PATH) } catch(e) {}

View File

@@ -7,8 +7,8 @@ var EPSILON = 1e-12
function stone_if_needed(b) { if (!stone.p(b)) stone(b) }
function bytes_to_blob(bytes) {
var b = blob()
for (var i = 0; i < length(bytes); i++) {
var b = new blob()
for (var i = 0; i < bytes.length; i++) {
var byte = bytes[i]
for (var bit = 7; bit >= 0; bit--) b.write_bit((byte >> bit) & 1)
}
@@ -20,11 +20,11 @@ function deepCompare(expected, actual, path) {
path = path || ''
if (expected == actual) return { passed: true, messages: [] };
if (is_number(expected) && is_number(actual)) {
if (typeof expected == 'number' && typeof actual == 'number') {
if (isNaN(expected) && isNaN(actual))
return { passed: true, messages: [] };
var diff = abs(expected - actual);
var diff = number.abs(expected - actual);
if (diff <= EPSILON)
return { passed: true, messages: [] };
@@ -37,47 +37,49 @@ function deepCompare(expected, actual, path) {
};
}
if (is_blob(expected) && is_blob(actual)) {
if ((expected instanceof blob) && (actual instanceof blob)) {
stone_if_needed(expected); stone_if_needed(actual)
if (length(expected) != length(actual))
return { passed: false, messages: [`blob length mismatch at ${path}: ${length(expected)} vs ${length(actual)}`] }
for (var i = 0; i < length(expected); i++) {
if (expected.length != actual.length)
return { passed: false, messages: [`blob length mismatch at ${path}: ${expected.length} vs ${actual.length}`] }
for (var i = 0; i < expected.length; i++) {
if (expected.read_logical(i) != actual.read_logical(i))
return { passed: false, messages: [`blob bit mismatch at ${path}[${i}]`] }
}
return { passed: true, messages: [] }
}
if (is_array(expected) && is_array(actual)) {
if (length(expected) != length(actual))
if (isa(expected, array) && isa(actual, array)) {
if (expected.length != actual.length)
return {
passed: false,
messages: [`Array length mismatch at ${path}: expected ${length(expected)}, got ${length(actual)}`]
messages: [`Array length mismatch at ${path}: expected ${expected.length}, got ${actual.length}`]
};
var messages = [];
arrfor(expected, function(val, i) {
var result = deepCompare(val, actual[i], `${path}[${i}]`);
if (!result.passed)
messages = array(messages, result.messages)
})
return { passed: length(messages) == 0, messages: messages };
let messages = [];
for (let i = 0; i < expected.length; i++) {
var result = deepCompare(expected[i], actual[i], `${path}[${i}]`);
if (!result.passed) {
for(var m of result.messages) messages.push(m);
}
}
return { passed: messages.length == 0, messages: messages };
}
if (is_object(expected) && is_object(actual)) {
var expKeys = sort(array(expected))
var actKeys = sort(array(actual))
if (isa(expected, object) && isa(actual, object)) {
var expKeys = array(expected).sort();
var actKeys = array(actual).sort();
if (JSON.stringify(expKeys) != JSON.stringify(actKeys))
return {
passed: false,
messages: [`Object keys mismatch at ${path}: expected ${expKeys}, got ${actKeys}`]
};
var messages = [];
arrfor(expKeys, function(key) {
let messages = [];
for (let key of expKeys) {
var result = deepCompare(expected[key], actual[key], `${path}.${key}`);
if (!result.passed)
messages = array(messages, result.messages)
})
return { passed: length(messages) == 0, messages: messages };
if (!result.passed) {
for(var m of result.messages) messages.push(m);
}
}
return { passed: messages.length == 0, messages: messages };
}
return {
@@ -89,7 +91,7 @@ function deepCompare(expected, actual, path) {
function makeTest(test) {
return function() {
var encoded = test.replacer ? nota.encode(test.input, test.replacer) : nota.encode(test.input);
if (!is_blob(encoded)){
if (!(encoded instanceof blob)){
throw "encode() should return blob";
}
@@ -102,14 +104,14 @@ function makeTest(test) {
var compareResult = deepCompare(expected, decoded);
if (!compareResult.passed) {
throw text(compareResult.messages, '; ');
throw compareResult.messages.join('; ');
}
};
}
var testarr = []
for (var i = 0; i < 500; i++) {
push(testarr, 1)
testarr.push(1)
}
var testCases = [
@@ -153,18 +155,18 @@ var testCases = [
str: "test",
obj: { x: true }
}] },
{ name: 'empty_buffer', input: blob() },
{ name: 'empty_buffer', input: new blob() },
{ name: 'nested_empty_array', input: [[]] },
{ name: 'empty_key_value', input: { "": "" } },
{ name: 'small_float', input: 1e-10 },
{ name: 'replacer_multiply', input: { a: 1, b: 2 },
replacer: (key, value) => is_number(value) ? value * 2 : value,
replacer: (key, value) => typeof value == 'number' ? value * 2 : value,
expected: { a: 2, b: 4 } },
{ name: 'replacer_string_append', input: { x: "test", y: 5 },
replacer: (key, value) => key == 'x' ? value + "!" : value,
expected: { x: "test!", y: 5 } },
{ name: 'reviver_multiply', input: { a: 1, b: 2 },
reviver: (key, value) => is_number(value) ? value * 3 : value,
reviver: (key, value) => typeof value == 'number' ? value * 3 : value,
expected: { a: 3, b: 6 } },
{ name: 'reviver_increment', input: { x: "test", y: 10 },
reviver: (key, value) => key == 'y' ? value + 1 : value,
@@ -172,7 +174,7 @@ var testCases = [
];
var tests = {};
for (var i = 0; i < length(testCases); i++) {
for (var i = 0; i < testCases.length; i++) {
var t = testCases[i];
tests[t.name] = makeTest(t);
}

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