From 8d449e6fc6827ebb34f559b9d856ec29a71c7124 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 20 Feb 2026 12:40:49 -0600 Subject: [PATCH 1/4] better compiler warnings adn errors --- analyze.cm | 144 ++++++++++++++++++++++++++++ index.cm | 4 +- internal/engine.cm | 130 +++++++++++++++++++++++++- internal/shop.cm | 19 ++++ parse.cm | 7 ++ query.ce | 137 ++++++++++++++++++++------- query.cm | 55 ++--------- source/cell.c | 11 ++- streamline.ce | 37 +++++++- streamline.cm | 228 +++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 679 insertions(+), 93 deletions(-) create mode 100644 analyze.cm diff --git a/analyze.cm b/analyze.cm new file mode 100644 index 00000000..992f2833 --- /dev/null +++ b/analyze.cm @@ -0,0 +1,144 @@ +// analyze.cm — Static analysis over index data. +// +// All functions take an index object (from index.cm) and return structured results. +// Does not depend on streamline — operates purely on source-semantic data. + +var analyze = {} + +// Find all references to a name, with optional scope filter. +// scope: "top" (enclosing == null), "fn" (enclosing != null), null (all) +analyze.find_refs = function(idx, name, scope) { + var hits = [] + var i = 0 + var ref = null + while (i < length(idx.references)) { + ref = idx.references[i] + if (ref.name == name) { + if (scope == null) { + hits[] = ref + } else if (scope == "top" && ref.enclosing == null) { + hits[] = ref + } else if (scope == "fn" && ref.enclosing != null) { + hits[] = ref + } + } + i = i + 1 + } + return hits +} + +// Find all . usage patterns (channel analysis). +// Only counts unshadowed uses (name not declared as local var in scope). +analyze.channels = function(idx, name) { + var channels = {} + var summary = {} + var i = 0 + var cs = null + var callee = null + var prop = null + var prefix_dot = name + "." + while (i < length(idx.call_sites)) { + cs = idx.call_sites[i] + callee = cs.callee + if (callee != null && starts_with(callee, prefix_dot)) { + prop = text(callee, length(prefix_dot), length(callee)) + if (channels[prop] == null) { + channels[prop] = [] + } + channels[prop][] = {span: cs.span} + if (summary[prop] == null) { + summary[prop] = 0 + } + summary[prop] = summary[prop] + 1 + } + i = i + 1 + } + return {channels: channels, summary: summary} +} + +// Find declarations by name, with optional kind filter. +// kind: "var", "def", "fn", "param", or null (any) +analyze.find_decls = function(idx, name, kind) { + var hits = [] + var i = 0 + var sym = null + while (i < length(idx.symbols)) { + sym = idx.symbols[i] + if (sym.name == name) { + if (kind == null || sym.kind == kind) { + hits[] = sym + } + } + i = i + 1 + } + return hits +} + +// Find intrinsic usage by name. +analyze.find_intrinsic = function(idx, name) { + var hits = [] + var i = 0 + var ref = null + if (idx.intrinsic_refs == null) return hits + while (i < length(idx.intrinsic_refs)) { + ref = idx.intrinsic_refs[i] + if (ref.name == name) { + hits[] = ref + } + i = i + 1 + } + return hits +} + +// Call sites with >4 args — always a compile error (max arity is 4). +analyze.excess_args = function(idx) { + var hits = [] + var i = 0 + var cs = null + while (i < length(idx.call_sites)) { + cs = idx.call_sites[i] + if (cs.args_count > 4) { + hits[] = {span: cs.span, callee: cs.callee, args_count: cs.args_count} + } + i = i + 1 + } + return hits +} + +// Extract module export shape from index data (for cross-module analysis). +analyze.module_summary = function(idx) { + var exports = {} + var i = 0 + var j = 0 + var exp = null + var sym = null + var found = false + if (idx.exports == null) return {exports: exports} + while (i < length(idx.exports)) { + exp = idx.exports[i] + found = false + if (exp.symbol_id != null) { + j = 0 + while (j < length(idx.symbols)) { + sym = idx.symbols[j] + if (sym.symbol_id == exp.symbol_id) { + if (sym.kind == "fn" && sym.params != null) { + exports[exp.name] = {type: "function", arity: length(sym.params)} + } else { + exports[exp.name] = {type: sym.kind} + } + found = true + break + } + j = j + 1 + } + } + if (!found) { + exports[exp.name] = {type: "unknown"} + } + i = i + 1 + } + return {exports: exports} +} + +return analyze diff --git a/index.cm b/index.cm index 81874b63..82e95608 100644 --- a/index.cm +++ b/index.cm @@ -201,7 +201,9 @@ var index_ast = function(ast, tokens, filename) { if (node.expression.left != null && node.expression.left.kind == "name") { callee_name = node.expression.left.name } - if (node.expression.right != null && node.expression.right.name != null) { + if (is_text(node.expression.right)) { + callee_name = (callee_name != null ? callee_name + "." : "") + node.expression.right + } else if (node.expression.right != null && node.expression.right.name != null) { callee_name = (callee_name != null ? callee_name + "." : "") + node.expression.right.name } } diff --git a/internal/engine.cm b/internal/engine.cm index 81815b53..4a035cfa 100644 --- a/internal/engine.cm +++ b/internal/engine.cm @@ -182,9 +182,99 @@ function analyze(src, filename) { // Lazy-loaded verify_ir module (loaded on first use) var _verify_ir_mod = null +// Module summary extraction for cross-program analysis. +// Scans mcode IR for use() call patterns and attaches summaries. +// _summary_resolver is set after shop loads (null during bootstrap). +var _summary_resolver = null + +function extract_module_summaries(compiled) { + if (_summary_resolver == null) return null + var instrs = null + var summaries = [] + var i = 0 + var j = 0 + var n = 0 + var instr = null + var prev = null + var op = null + var use_slots = {} + var frame_map = {} + var arg_map = {} + var val_slot = 0 + var f_slot = 0 + var path = null + var result_slot = 0 + var summary = null + + if (compiled.main == null) return null + instrs = compiled.main.instructions + if (instrs == null) return null + n = length(instrs) + + // Pass 1: find access(slot, {make:"intrinsic", name:"use"}) + i = 0 + while (i < n) { + instr = instrs[i] + if (is_array(instr) && instr[0] == "access") { + if (is_object(instr[2]) && instr[2].make == "intrinsic" && instr[2].name == "use") { + use_slots[text(instr[1])] = true + } + } + i = i + 1 + } + + // Pass 2: find frame(frame_slot, use_slot), setarg with string, invoke + i = 0 + while (i < n) { + instr = instrs[i] + if (is_array(instr)) { + op = instr[0] + if (op == "frame" || op == "goframe") { + if (use_slots[text(instr[2])] == true) { + frame_map[text(instr[1])] = true + } + } else if (op == "setarg") { + if (frame_map[text(instr[1])] == true) { + val_slot = instr[3] + j = i - 1 + while (j >= 0) { + prev = instrs[j] + if (is_array(prev) && prev[0] == "access" && prev[1] == val_slot && is_text(prev[2])) { + arg_map[text(instr[1])] = prev[2] + break + } + j = j - 1 + } + } + } else if (op == "invoke" || op == "tail_invoke") { + f_slot = instr[1] + path = arg_map[text(f_slot)] + if (path != null) { + result_slot = instr[2] + summary = _summary_resolver(path) + if (summary != null) { + summaries[] = {slot: result_slot, summary: summary} + } + } + } + } + i = i + 1 + } + + if (length(summaries) > 0) return summaries + return null +} + // Run AST through mcode pipeline -> register VM function run_ast_fn(name, ast, env) { var compiled = mcode_mod(ast) + var ms = null + var optimized = null + var _di = 0 + var _diag = null + var _has_errors = false + var mcode_json = null + var mach_blob = null if (os._verify_ir) { if (_verify_ir_mod == null) { _verify_ir_mod = load_pipeline_module('verify_ir', pipeline_env) @@ -192,13 +282,31 @@ function run_ast_fn(name, ast, env) { compiled._verify = true compiled._verify_mod = _verify_ir_mod } - var optimized = streamline_mod(compiled) + if (!_no_warn) { + compiled._warn = true + ms = extract_module_summaries(compiled) + if (ms != null) { + compiled._module_summaries = ms + } + } + optimized = streamline_mod(compiled) if (optimized._verify) { delete optimized._verify delete optimized._verify_mod } - var mcode_json = json.encode(optimized) - var mach_blob = mach_compile_mcode_bin(name, mcode_json) + if (optimized._diagnostics != null && length(optimized._diagnostics) > 0) { + _di = 0 + _has_errors = false + while (_di < length(optimized._diagnostics)) { + _diag = optimized._diagnostics[_di] + print(`${_diag.file}:${text(_diag.line)}:${text(_diag.col)}: ${_diag.severity}: ${_diag.message}\n`) + if (_diag.severity == "error") _has_errors = true + _di = _di + 1 + } + if (_has_errors) disrupt + } + mcode_json = json.encode(optimized) + mach_blob = mach_compile_mcode_bin(name, mcode_json) return mach_load(mach_blob, env) } @@ -227,6 +335,9 @@ var _init = init if (_init != null && _init.native_mode) native_mode = true +// Inherit warn mode from init (set by C for --no-warn) +var _no_warn = (_init != null && _init.no_warn) ? true : false + // CLI path: convert args to init record if (args != null && (_init == null || !_init.program)) { _program = args[0] @@ -424,6 +535,19 @@ core_extras.native_mode = native_mode // NOW load shop -- it receives all of the above via env var shop = use_core('internal/shop') use_core('build') + +// Wire up module summary resolver now that shop is available +_summary_resolver = function(path) { + var resolved = shop.resolve_use_path(path, null) + if (resolved == null) return null + var summary_fn = function() { + return shop.summary_file(resolved) + } disruption { + return null + } + return summary_fn() +} + var time = use_core('time') var toml = use_core('toml') diff --git a/internal/shop.cm b/internal/shop.cm index 2d549f85..d66c9732 100644 --- a/internal/shop.cm +++ b/internal/shop.cm @@ -519,6 +519,7 @@ var _ast_cache = {} var _analyze_cache = {} var _compile_cache = {} var _index_cache = {} +var _summary_cache = {} var get_tokenize = function() { if (!_tokenize_mod) _tokenize_mod = use_cache['core/tokenize'] || use_cache['tokenize'] @@ -547,6 +548,14 @@ var get_index = function() { } return _index_mod } +var _analyze_mod = null +var get_analyze = function() { + if (!_analyze_mod) { + _analyze_mod = use_cache['core/analyze'] || use_cache['analyze'] + if (!_analyze_mod) _analyze_mod = Shop.use('analyze', 'core') + } + return _analyze_mod +} Shop.tokenize_file = function(path) { var src = text(fd.slurp(path)) @@ -611,6 +620,16 @@ Shop.index_file = function(path) { return idx } +Shop.summary_file = function(path) { + var src = text(fd.slurp(path)) + var key = content_hash(stone(blob(src))) + if (_summary_cache[key]) return _summary_cache[key] + var idx = Shop.index_file(path) + var summary = get_analyze().module_summary(idx) + _summary_cache[key] = summary + return summary +} + Shop.mcode_file = function(path) { var folded = Shop.analyze_file(path) return get_mcode()(folded) diff --git a/parse.cm b/parse.cm index 07c7f185..e5fc3d40 100644 --- a/parse.cm +++ b/parse.cm @@ -1727,6 +1727,13 @@ var parse = function(tokens, src, filename, tokenizer) { return null } + if (kind == "this") { + if (scope.function_nr == 0) { + sem_error(expr, "'this' cannot be used at the top level of a program") + } + return null + } + if (kind == "[") { sem_check_expr(scope, expr.left) sem_check_expr(scope, expr.right) diff --git a/query.ce b/query.ce index 490f93d0..b8d9fe98 100644 --- a/query.ce +++ b/query.ce @@ -1,29 +1,39 @@ // cell query — Semantic queries across packages. // // Usage: -// cell query --this [--top|--fn] [] this references -// cell query --intrinsic [] Find built-in intrinsic usage -// cell query --decl [] Variable declarations by name -// cell query --help Show usage +// cell query Find all references to +// cell query --top Top-level references only +// cell query --fn Inside-function references only +// cell query --channels Show . usage summary +// cell query --decl Find declarations of +// cell query --decl --fn Only function declarations +// cell query --intrinsic Find intrinsic usage +// cell query --excess-args Find call sites with >4 args +// cell query --help var shop = use('internal/shop') -var query_mod = use('query') +var analyze_mod = use('analyze') var fd = use('fd') var mode = null var name = null -var this_scope = null +var scope_filter = null +var kind_filter = null var pkg_filter = null var show_help = false var i = 0 for (i = 0; i < length(args); i++) { - if (args[i] == '--this') { - mode = "this" - } else if (args[i] == '--top') { - this_scope = "top" + if (args[i] == '--top') { + scope_filter = "top" } else if (args[i] == '--fn') { - this_scope = "fn" + if (mode == "decl") { + kind_filter = "fn" + } else { + scope_filter = "fn" + } + } else if (args[i] == '--channels') { + mode = "channels" } else if (args[i] == '--intrinsic') { mode = "intrinsic" if (i + 1 < length(args) && !starts_with(args[i + 1], '-')) { @@ -42,13 +52,26 @@ for (i = 0; i < length(args); i++) { log.error('--decl requires a name') mode = "error" } + } else if (args[i] == '--excess-args') { + mode = "excess_args" } else if (args[i] == '--help' || args[i] == '-h') { show_help = true } else if (!starts_with(args[i], '-')) { - pkg_filter = args[i] + if (name == null && mode == null) { + name = args[i] + mode = "refs" + } else { + pkg_filter = args[i] + } } } +// --channels requires a name from positional arg +if (mode == "channels" && name == null) { + log.error('--channels requires a name (e.g., cell query log --channels)') + mode = "error" +} + var all_files = null var files = [] var j = 0 @@ -56,6 +79,10 @@ var idx = null var hits = null var hit = null var k = 0 +var ch_result = null +var props = null +var prop = null +var parts = null // Use return pattern to avoid closure-over-object issue with disruption. var safe_index = function(path) { @@ -65,21 +92,24 @@ var safe_index = function(path) { } if (show_help) { - log.console("Usage: cell query [options] []") + log.console("Usage: cell query [options] [] []") log.console("") log.console("Semantic queries across packages.") log.console("") - log.console("Options:") - log.console(" --this All this references") - log.console(" --this --top Top-level this only (not inside functions)") - log.console(" --this --fn this inside functions only") - log.console(" --intrinsic Find built-in intrinsic usage (e.g., print)") - log.console(" --decl Variable declarations by name") + log.console("Commands:") + log.console(" Find all references to ") + log.console(" --top Top-level references only") + log.console(" --fn Inside-function references only") + log.console(" --channels Show . usage summary") + log.console(" --decl Find declarations of ") + log.console(" --decl --fn Only function declarations") + log.console(" --intrinsic Find intrinsic usage") + log.console(" --excess-args Find call sites with >4 args") log.console("") log.console("Without a package argument, searches all installed packages.") } else if (mode == null || mode == "error") { if (mode != "error") { - log.error('Specify --this, --intrinsic, or --decl. Use --help for usage.') + log.error('Specify a name or --decl/--intrinsic/--excess-args. Use --help for usage.') } } else { all_files = shop.all_script_paths() @@ -98,22 +128,61 @@ if (show_help) { idx = safe_index(files[j].full_path) if (idx == null) continue - hits = null - if (mode == "this") { - hits = query_mod.find_this(idx, this_scope) + if (mode == "refs") { + hits = analyze_mod.find_refs(idx, name, scope_filter) + if (hits != null && length(hits) > 0) { + for (k = 0; k < length(hits); k++) { + hit = hits[k] + if (hit.span != null) { + if (hit.enclosing != null) { + log.console(`${files[j].package}:${files[j].rel_path}:${text(hit.span.from_row)}:${text(hit.span.from_col)}: ${hit.name} (in: ${hit.enclosing})`) + } else { + log.console(`${files[j].package}:${files[j].rel_path}:${text(hit.span.from_row)}:${text(hit.span.from_col)}: ${hit.name} (top-level)`) + } + } + } + } + } else if (mode == "channels") { + ch_result = analyze_mod.channels(idx, name) + if (ch_result != null && ch_result.summary != null) { + props = array(ch_result.summary) + if (length(props) > 0) { + parts = [] + for (k = 0; k < length(props); k++) { + prop = props[k] + parts[] = `${prop}(${text(ch_result.summary[prop])})` + } + log.console(`${files[j].package}:${files[j].rel_path}: ${text(parts, " ")}`) + } + } } else if (mode == "intrinsic") { - hits = query_mod.intrinsic(idx, name) + hits = analyze_mod.find_intrinsic(idx, name) + if (hits != null && length(hits) > 0) { + for (k = 0; k < length(hits); k++) { + hit = hits[k] + if (hit.span != null) { + log.console(`${files[j].package}:${files[j].rel_path}:${text(hit.span.from_row)}:${text(hit.span.from_col)}: ${hit.name}`) + } + } + } } else if (mode == "decl") { - hits = query_mod.find_decl(idx, name, null) - } - - if (hits != null && length(hits) > 0) { - for (k = 0; k < length(hits); k++) { - hit = hits[k] - if (hit.span != null) { - log.console(`${files[j].package}:${files[j].rel_path}:${text(hit.span.from_row)}:${text(hit.span.from_col)}: ${hit.name}`) - } else if (hit.decl_span != null) { - log.console(`${files[j].package}:${files[j].rel_path}:${text(hit.decl_span.from_row)}:${text(hit.decl_span.from_col)}: ${hit.kind} ${hit.name}`) + hits = analyze_mod.find_decls(idx, name, kind_filter) + if (hits != null && length(hits) > 0) { + for (k = 0; k < length(hits); k++) { + hit = hits[k] + if (hit.decl_span != null) { + log.console(`${files[j].package}:${files[j].rel_path}:${text(hit.decl_span.from_row)}:${text(hit.decl_span.from_col)}: ${hit.kind} ${hit.name}`) + } + } + } + } else if (mode == "excess_args") { + hits = analyze_mod.excess_args(idx) + if (hits != null && length(hits) > 0) { + for (k = 0; k < length(hits); k++) { + hit = hits[k] + if (hit.span != null) { + log.console(`${files[j].package}:${files[j].rel_path}:${text(hit.span.from_row)}:${text(hit.span.from_col)}: ${hit.callee}() called with ${text(hit.args_count)} args (max 4)`) + } } } } diff --git a/query.cm b/query.cm index 96d30be1..b955e1e1 100644 --- a/query.cm +++ b/query.cm @@ -1,62 +1,19 @@ -// query.cm — Semantic queries over index data. -// -// All functions take an index object (from index.cm) and return arrays of hits. +// query.cm — Backward-compatible wrapper delegating to analyze.cm. + +var analyze = use('analyze') var query = {} -// Find this references. scope: "top" (top-level only), "fn" (in functions), null (all). query.find_this = function(idx, scope) { - var hits = [] - var i = 0 - var ref = null - while (i < length(idx.references)) { - ref = idx.references[i] - if (ref.name == "this") { - if (scope == null) { - hits[] = ref - } else if (scope == "top" && ref.enclosing == null) { - hits[] = ref - } else if (scope == "fn" && ref.enclosing != null) { - hits[] = ref - } - } - i = i + 1 - } - return hits + return analyze.find_refs(idx, "this", scope) } -// Intrinsic usage: find refs to a built-in name (e.g., print). query.intrinsic = function(idx, name) { - var hits = [] - var i = 0 - var ref = null - if (idx.intrinsic_refs == null) return hits - while (i < length(idx.intrinsic_refs)) { - ref = idx.intrinsic_refs[i] - if (ref.name == name) { - hits[] = ref - } - i = i + 1 - } - return hits + return analyze.find_intrinsic(idx, name) } -// Variable declarations matching a name and optional kind filter. -// kind is one of "var", "def", "fn", "param", or null (any). query.find_decl = function(idx, name, kind) { - var hits = [] - var i = 0 - var sym = null - while (i < length(idx.symbols)) { - sym = idx.symbols[i] - if (sym.name == name) { - if (kind == null || sym.kind == kind) { - hits[] = sym - } - } - i = i + 1 - } - return hits + return analyze.find_decls(idx, name, kind) } return query diff --git a/source/cell.c b/source/cell.c index 0bb7d4c2..95a8174e 100644 --- a/source/cell.c +++ b/source/cell.c @@ -30,6 +30,7 @@ cell_rt *root_cell = NULL; static char *shop_path = NULL; static char *core_path = NULL; static int native_mode = 0; +static int warn_mode = 1; static JSRuntime *g_runtime = NULL; // Compute blake2b hash of data and return hex string (caller must free) @@ -523,6 +524,9 @@ int cell_init(int argc, char **argv) } else if (strcmp(argv[arg_start], "--native") == 0) { native_mode = 1; arg_start++; + } else if (strcmp(argv[arg_start], "--no-warn") == 0) { + warn_mode = 0; + arg_start++; } else { break; } @@ -661,11 +665,14 @@ int cell_init(int argc, char **argv) JS_SetPropertyStr(ctx, env_ref.val, "actorsym", JS_DupValue(ctx, cli_rt->actor_sym_ref.val)); tmp = js_core_json_use(ctx); JS_SetPropertyStr(ctx, env_ref.val, "json", tmp); - if (native_mode) { + if (native_mode || !warn_mode) { JSGCRef init_ref; JS_AddGCRef(ctx, &init_ref); init_ref.val = JS_NewObject(ctx); - JS_SetPropertyStr(ctx, init_ref.val, "native_mode", JS_NewBool(ctx, 1)); + if (native_mode) + JS_SetPropertyStr(ctx, init_ref.val, "native_mode", JS_NewBool(ctx, 1)); + if (!warn_mode) + JS_SetPropertyStr(ctx, init_ref.val, "no_warn", JS_NewBool(ctx, 1)); JS_SetPropertyStr(ctx, env_ref.val, "init", init_ref.val); JS_DeleteGCRef(ctx, &init_ref); } else { diff --git a/streamline.ce b/streamline.ce index 1d82f97a..3e6294c5 100644 --- a/streamline.ce +++ b/streamline.ce @@ -6,6 +6,7 @@ // cell streamline --ir Human-readable IR // cell streamline --check Warnings only (e.g. high slot count) // cell streamline --types Optimized IR with type annotations +// cell streamline --diagnose Run diagnostics (type errors/warnings) var fd = use("fd") var json = use("json") @@ -15,8 +16,11 @@ var show_stats = false var show_ir = false var show_check = false var show_types = false +var show_diagnose = false var filename = null var i = 0 +var di = 0 +var diag = null for (i = 0; i < length(args); i++) { if (args[i] == '--stats') { @@ -27,8 +31,10 @@ for (i = 0; i < length(args); i++) { show_check = true } else if (args[i] == '--types') { show_types = true + } else if (args[i] == '--diagnose') { + show_diagnose = true } else if (args[i] == '--help' || args[i] == '-h') { - log.console("Usage: cell streamline [--stats] [--ir] [--check] [--types] ") + log.console("Usage: cell streamline [--stats] [--ir] [--check] [--types] [--diagnose] ") $stop() } else if (!starts_with(args[i], '-')) { filename = args[i] @@ -36,7 +42,7 @@ for (i = 0; i < length(args); i++) { } if (!filename) { - print("usage: cell streamline [--stats] [--ir] [--check] [--types] ") + print("usage: cell streamline [--stats] [--ir] [--check] [--types] [--diagnose] ") $stop() } @@ -46,10 +52,19 @@ if (show_stats) { before = json.decode(json.encode(shop.mcode_file(filename))) } -var optimized = shop.compile_file(filename) +// For --diagnose, compile with _warn enabled to get diagnostics +var optimized = null +var compiled = null +if (show_diagnose) { + compiled = shop.mcode_file(filename) + compiled._warn = true + optimized = use('streamline')(compiled) +} else { + optimized = shop.compile_file(filename) +} // If no flags, default to full JSON output -if (!show_stats && !show_ir && !show_check && !show_types) { +if (!show_stats && !show_ir && !show_check && !show_types && !show_diagnose) { print(json.encode(optimized, true)) $stop() } @@ -343,4 +358,18 @@ if (show_stats) { print('---') } +if (show_diagnose) { + if (optimized._diagnostics != null && length(optimized._diagnostics) > 0) { + di = 0 + while (di < length(optimized._diagnostics)) { + diag = optimized._diagnostics[di] + print(`${diag.file}:${text(diag.line)}:${text(diag.col)}: ${diag.severity}: ${diag.message}`) + di = di + 1 + } + print(`\n${text(length(optimized._diagnostics))} diagnostic(s)`) + } else { + print("No diagnostics.") + } +} + $stop() diff --git a/streamline.cm b/streamline.cm index 9fe9adb5..0d0c2064 100644 --- a/streamline.cm +++ b/streamline.cm @@ -1829,6 +1829,227 @@ var streamline = function(ir, log) { return null } + // ========================================================= + // Pass: diagnose_function — emit diagnostics from type info + // Runs after dead code elimination on surviving instructions. + // ========================================================= + var diagnose_function = function(func, ctx, ir) { + var param_types = ctx.param_types + var write_types = ctx.write_types + var instructions = func.instructions + var nr_args = func.nr_args != null ? func.nr_args : 0 + var num_instr = 0 + var base_types = null + var cur_types = null + var i = 0 + var j = 0 + var instr = null + var op = null + var n = 0 + var line = 0 + var col = 0 + var known = null + var filename = ir.filename != null ? ir.filename : "" + var frame_callee = {} + var frame_argc = {} + var callee_slot = null + var obj_type = null + var key_type = null + var module_slots = {} + var slot_arity = {} + var ms_i = 0 + var ms = null + var exp_info = null + var f_slot_key = null + var cs = null + var argc = null + var known_arity = null + + // Build module_slots map from ir._module_summaries + if (ir._module_summaries != null) { + ms_i = 0 + while (ms_i < length(ir._module_summaries)) { + ms = ir._module_summaries[ms_i] + module_slots[text(ms.slot)] = ms.summary + ms_i = ms_i + 1 + } + } + + if (instructions == null || length(instructions) == 0) return null + + num_instr = length(instructions) + + // Pre-compute base types from params + write-invariant types + base_types = array(func.nr_slots) + j = 1 + while (j <= nr_args) { + if (param_types != null && param_types[j] != null) { + base_types[j] = param_types[j] + } + j = j + 1 + } + if (write_types != null) { + j = 0 + while (j < length(write_types)) { + if (write_types[j] != null) { + base_types[j] = write_types[j] + } + j = j + 1 + } + } + + cur_types = array(base_types) + + var emit = function(severity, line, col, message) { + ir._diagnostics[] = { + severity: severity, + file: filename, + line: line, + col: col, + message: message + } + } + + i = 0 + while (i < num_instr) { + instr = instructions[i] + + if (is_text(instr)) { + // Label — reset types to base + if (!starts_with(instr, "_nop_")) { + cur_types = array(base_types) + } + i = i + 1 + continue + } + + if (!is_array(instr)) { + i = i + 1 + continue + } + + op = instr[0] + n = length(instr) + line = instr[n - 2] + col = instr[n - 1] + + // Track frame/invoke correlation + if (op == "frame" || op == "goframe") { + frame_callee[text(instr[1])] = instr[2] + if (n > 4) { + frame_argc[text(instr[1])] = instr[3] + } + } + + // --- Error checks (proven to always disrupt) --- + + if (op == "frame" || op == "goframe") { + callee_slot = instr[2] + known = cur_types[callee_slot] + if (known == T_NULL) { + emit("error", line, col, "invoking null — will always disrupt") + } else if (known != null && known != T_UNKNOWN && known != T_FUNCTION) { + emit("error", line, col, `invoking ${known} — will always disrupt`) + } + } + + if (op == "invoke" || op == "tail_invoke") { + f_slot_key = text(instr[1]) + cs = frame_callee[f_slot_key] + argc = frame_argc[f_slot_key] + if (cs != null && argc != null) { + known_arity = slot_arity[text(cs)] + if (known_arity != null) { + if (argc > known_arity) { + emit("error", line, col, `function expects ${text(known_arity)} args, called with ${text(argc)}`) + } else if (argc < known_arity) { + emit("warning", line, col, `function expects ${text(known_arity)} args, called with ${text(argc)}`) + } + } + } + } + + if (op == "store_field") { + obj_type = cur_types[instr[1]] + if (obj_type == T_TEXT) { + emit("error", line, col, "storing property on text — text is immutable") + } else if (obj_type == T_ARRAY) { + emit("error", line, col, "storing named property on array — use index or push") + } + } + + if (op == "store_index") { + obj_type = cur_types[instr[1]] + if (obj_type == T_TEXT) { + emit("error", line, col, "storing index on text — text is immutable") + } else if (obj_type == T_RECORD) { + emit("error", line, col, "storing numeric index on record — use text key") + } + } + + if (op == "store_dynamic") { + obj_type = cur_types[instr[1]] + if (obj_type == T_TEXT) { + emit("error", line, col, "storing on text — text is immutable") + } + } + + if (op == "push") { + obj_type = cur_types[instr[1]] + if (obj_type != null && obj_type != T_UNKNOWN && obj_type != T_ARRAY) { + emit("error", line, col, `push on ${obj_type} — only arrays support push`) + } + } + + // Note: arithmetic (add/subtract/etc), bitwise, and concat ops are NOT + // checked here because the mcode generator emits type-dispatch guards + // before these instructions. The guards ensure correct types at runtime. + + // --- Warning checks (likely bug) --- + + if (op == "load_field") { + obj_type = cur_types[instr[2]] + if (obj_type == T_ARRAY) { + emit("warning", line, col, "named property access on array — always returns null") + } else if (obj_type == T_TEXT) { + emit("warning", line, col, "named property access on text — always returns null") + } + // Cross-module: check if obj is a module with known exports + ms = module_slots[text(instr[2])] + if (ms != null && ms.exports != null && is_text(instr[3])) { + exp_info = ms.exports[instr[3]] + if (exp_info == null) { + emit("warning", line, col, `module does not export '${instr[3]}'`) + } else if (exp_info.type == "function") { + cur_types[instr[1]] = T_FUNCTION + slot_arity[text(instr[1])] = exp_info.arity + } + } + } + + if (op == "load_dynamic") { + obj_type = cur_types[instr[2]] + key_type = cur_types[instr[3]] + if (obj_type == T_ARRAY && key_type == T_TEXT) { + emit("warning", line, col, "text key on array — always returns null") + } + if (obj_type == T_TEXT && key_type != null && key_type != T_UNKNOWN && key_type != T_INT) { + emit("warning", line, col, `${key_type} key on text — requires integer index`) + } + if (obj_type == T_RECORD && key_type != null && key_type != T_UNKNOWN && key_type != T_TEXT) { + emit("warning", line, col, `${key_type} key on record — requires text key`) + } + } + + // Update types for this instruction + track_types(cur_types, instr) + + i = i + 1 + } + + return null + } + // ========================================================= // Compose all passes // ========================================================= @@ -1905,6 +2126,9 @@ var streamline = function(ir, log) { } run_cycle("") + if (ir._warn) { + diagnose_function(func, {param_types: param_types, write_types: write_types}, ir) + } return null } @@ -1914,6 +2138,10 @@ var streamline = function(ir, log) { // eliminator to mis-optimize comparisons on closure-written variables. mark_closure_writes(ir) + if (ir._warn) { + ir._diagnostics = [] + } + // Process main function if (ir.main != null) { optimize_function(ir.main, log) From e6d05abd0353f1fcd5dcac5f0efe3a3221929668 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 20 Feb 2026 12:52:40 -0600 Subject: [PATCH 2/4] harsher compile error --- internal/engine.cm | 68 ++++++++++++++++++++++++++++++++++++++++++++-- streamline.cm | 9 ++++++ 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/internal/engine.cm b/internal/engine.cm index 4a035cfa..7ff2b6ae 100644 --- a/internal/engine.cm +++ b/internal/engine.cm @@ -191,6 +191,7 @@ function extract_module_summaries(compiled) { if (_summary_resolver == null) return null var instrs = null var summaries = [] + var unresolved = [] var i = 0 var j = 0 var n = 0 @@ -205,6 +206,7 @@ function extract_module_summaries(compiled) { var path = null var result_slot = 0 var summary = null + var inv_n = 0 if (compiled.main == null) return null instrs = compiled.main.instructions @@ -254,6 +256,9 @@ function extract_module_summaries(compiled) { summary = _summary_resolver(path) if (summary != null) { summaries[] = {slot: result_slot, summary: summary} + } else { + inv_n = length(instr) + unresolved[] = {path: path, line: instr[inv_n - 2], col: instr[inv_n - 1]} } } } @@ -261,7 +266,9 @@ function extract_module_summaries(compiled) { i = i + 1 } - if (length(summaries) > 0) return summaries + if (length(summaries) > 0 || length(unresolved) > 0) { + return {summaries: summaries, unresolved: unresolved} + } return null } @@ -269,6 +276,8 @@ function extract_module_summaries(compiled) { function run_ast_fn(name, ast, env) { var compiled = mcode_mod(ast) var ms = null + var _ui = 0 + var _ur = null var optimized = null var _di = 0 var _diag = null @@ -286,9 +295,23 @@ function run_ast_fn(name, ast, env) { compiled._warn = true ms = extract_module_summaries(compiled) if (ms != null) { - compiled._module_summaries = ms + if (length(ms.summaries) > 0) { + compiled._module_summaries = ms.summaries + } + if (length(ms.unresolved) > 0) { + compiled._unresolved_imports = ms.unresolved + } } } + if (compiled._unresolved_imports != null) { + _ui = 0 + while (_ui < length(compiled._unresolved_imports)) { + _ur = compiled._unresolved_imports[_ui] + print(`${name}:${text(_ur.line)}:${text(_ur.col)}: error: cannot resolve module '${_ur.path}'\n`) + _ui = _ui + 1 + } + disrupt + } optimized = streamline_mod(compiled) if (optimized._verify) { delete optimized._verify @@ -321,7 +344,46 @@ function run_ast_noopt_fn(name, ast, env) { // Compile AST to blob without loading (for caching) function compile_to_blob(name, ast) { var compiled = mcode_mod(ast) - var optimized = streamline_mod(compiled) + var ms = null + var _ui = 0 + var _ur = null + var optimized = null + var _di = 0 + var _diag = null + var _has_errors = false + if (!_no_warn) { + compiled._warn = true + ms = extract_module_summaries(compiled) + if (ms != null) { + if (length(ms.summaries) > 0) { + compiled._module_summaries = ms.summaries + } + if (length(ms.unresolved) > 0) { + compiled._unresolved_imports = ms.unresolved + } + } + } + if (compiled._unresolved_imports != null) { + _ui = 0 + while (_ui < length(compiled._unresolved_imports)) { + _ur = compiled._unresolved_imports[_ui] + print(`${name}:${text(_ur.line)}:${text(_ur.col)}: error: cannot resolve module '${_ur.path}'\n`) + _ui = _ui + 1 + } + disrupt + } + optimized = streamline_mod(compiled) + if (optimized._diagnostics != null && length(optimized._diagnostics) > 0) { + _di = 0 + _has_errors = false + while (_di < length(optimized._diagnostics)) { + _diag = optimized._diagnostics[_di] + print(`${_diag.file}:${text(_diag.line)}:${text(_diag.col)}: ${_diag.severity}: ${_diag.message}\n`) + if (_diag.severity == "error") _has_errors = true + _di = _di + 1 + } + if (_has_errors) disrupt + } return mach_compile_mcode_bin(name, json.encode(optimized)) } diff --git a/streamline.cm b/streamline.cm index 0d0c2064..a53f1106 100644 --- a/streamline.cm +++ b/streamline.cm @@ -1864,6 +1864,7 @@ var streamline = function(ir, log) { var cs = null var argc = null var known_arity = null + var load_field_null = false // Build module_slots map from ir._module_summaries if (ir._module_summaries != null) { @@ -2007,12 +2008,15 @@ var streamline = function(ir, log) { // --- Warning checks (likely bug) --- + load_field_null = false if (op == "load_field") { obj_type = cur_types[instr[2]] if (obj_type == T_ARRAY) { emit("warning", line, col, "named property access on array — always returns null") + load_field_null = true } else if (obj_type == T_TEXT) { emit("warning", line, col, "named property access on text — always returns null") + load_field_null = true } // Cross-module: check if obj is a module with known exports ms = module_slots[text(instr[2])] @@ -2044,6 +2048,11 @@ var streamline = function(ir, log) { // Update types for this instruction track_types(cur_types, instr) + // Override: load_field on array/text always returns null + if (load_field_null) { + cur_types[instr[1]] = T_NULL + } + i = i + 1 } From 285395807b000148206a63eafe79e3b8bcb21e2f Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 20 Feb 2026 14:14:07 -0600 Subject: [PATCH 3/4] core packages now split out --- bench.ce | 2 +- bench_native.ce | 2 +- benches/encoders.cm | 4 +- benchmarks/nota.ce | 4 +- benchmarks/wota.ce | 4 +- benchmarks/wota_nota_json.ce | 6 +- boot/bootstrap.cm.mcode | 2 +- build.cm | 8 +- cellfs.cm | 4 +- compare_aot.ce | 2 +- diff.ce | 6 +- fd.cm | 2 +- fuzz.ce | 2 +- internal/bootstrap.cm | 2 +- crypto.c => internal/crypto.c | 2 +- internal/engine.cm | 26 +- fd_playdate.c => internal/fd_playdate.c | 0 internal/kim.c | 2 +- internal/nota.c | 431 +++++++ internal/os.c | 2 +- qop.c => internal/qop.c | 2 +- internal/shop.cm | 4 +- time_playdate.c => internal/time_playdate.c | 0 wildstar.c => internal/wildstar.c | 2 +- internal/wota.c | 461 +++++++ json.c | 67 + math/cycles.c | 66 + math/degrees.c | 67 + math/radians.c | 64 + meson.build | 16 +- qopconv.ce | 2 +- random.cm | 2 +- run_native.ce | 2 +- run_native_seed.ce | 2 +- seed.ce | 2 +- source/cell.c | 10 +- source/quickjs-internal.h | 2 + source/runtime.c | 1218 ------------------- src/cell_math.c | 50 + src/cell_math.h | 14 + fash.c => src/fash.c | 0 qbe_rt.c => src/qbe_rt.c | 0 streamline.cm | 2 +- test.ce | 2 +- tests/blob.cm | 2 +- tests/kim.cm | 2 +- tests/nota.cm | 4 +- tests/wota.cm | 4 +- update.ce | 2 +- 49 files changed, 1303 insertions(+), 1281 deletions(-) rename crypto.c => internal/crypto.c (99%) rename fd_playdate.c => internal/fd_playdate.c (100%) create mode 100644 internal/nota.c rename qop.c => internal/qop.c (99%) rename time_playdate.c => internal/time_playdate.c (100%) rename wildstar.c => internal/wildstar.c (95%) create mode 100644 internal/wota.c create mode 100644 json.c create mode 100644 math/cycles.c create mode 100644 math/degrees.c create mode 100644 math/radians.c create mode 100644 src/cell_math.c create mode 100644 src/cell_math.h rename fash.c => src/fash.c (100%) rename qbe_rt.c => src/qbe_rt.c (100%) diff --git a/bench.ce b/bench.ce index 9638ad3e..0435cbf3 100644 --- a/bench.ce +++ b/bench.ce @@ -6,7 +6,7 @@ var fd = use('fd') var time = use('time') var json = use('json') var blob = use('blob') -var os = use('os') +var os = use('internal/os') var testlib = use('internal/testlib') var math = use('math/radians') diff --git a/bench_native.ce b/bench_native.ce index de5c8b5c..ce27654e 100644 --- a/bench_native.ce +++ b/bench_native.ce @@ -6,7 +6,7 @@ // Compiles (if needed) and benchmarks a module via both VM and native dylib. // Reports median/mean timing per benchmark + speedup ratio. -var os = use('os') +var os = use('internal/os') var fd = use('fd') if (length(args) < 1) { diff --git a/benches/encoders.cm b/benches/encoders.cm index 4c53ad48..1364720c 100644 --- a/benches/encoders.cm +++ b/benches/encoders.cm @@ -1,8 +1,8 @@ // encoders.cm — nota/wota/json encode+decode benchmark // Isolates per-type bottlenecks across all three serializers. -var nota = use('nota') -var wota = use('wota') +var nota = use('internal/nota') +var wota = use('internal/wota') var json = use('json') // --- Test data shapes --- diff --git a/benchmarks/nota.ce b/benchmarks/nota.ce index eb212cfc..05cf8ae9 100644 --- a/benchmarks/nota.ce +++ b/benchmarks/nota.ce @@ -1,5 +1,5 @@ -var nota = use('nota') -var os = use('os') +var nota = use('internal/nota') +var os = use('internal/os') var io = use('fd') var json = use('json') diff --git a/benchmarks/wota.ce b/benchmarks/wota.ce index b4a654a2..bf94b94e 100644 --- a/benchmarks/wota.ce +++ b/benchmarks/wota.ce @@ -1,5 +1,5 @@ -var wota = use('wota'); -var os = use('os'); +var wota = use('internal/wota'); +var os = use('internal/os'); var i = 0 diff --git a/benchmarks/wota_nota_json.ce b/benchmarks/wota_nota_json.ce index 68045b2e..6d1a71dd 100644 --- a/benchmarks/wota_nota_json.ce +++ b/benchmarks/wota_nota_json.ce @@ -1,8 +1,8 @@ -var wota = use('wota'); -var nota = use('nota'); +var wota = use('internal/wota'); +var nota = use('internal/nota'); var json = use('json'); var jswota = use('jswota') -var os = use('os'); +var os = use('internal/os'); if (length(arg) != 2) { log.console('Usage: cell benchmark_wota_nota_json.ce '); diff --git a/boot/bootstrap.cm.mcode b/boot/bootstrap.cm.mcode index a8102e96..24baff15 100644 --- a/boot/bootstrap.cm.mcode +++ b/boot/bootstrap.cm.mcode @@ -1486,7 +1486,7 @@ ["setarg", 11, 1, 9, 10, 16], ["invoke", 11, 9, 10, 16], ["move", 11, 9, 10, 16], - ["access", 9, "crypto", 11, 24], + ["access", 9, "internal_crypto", 11, 24], ["frame", 12, 2, 1, 11, 14], ["null", 13, 11, 14], ["setarg", 12, 0, 13, 11, 14], diff --git a/build.cm b/build.cm index 5767fa53..e8b5cc88 100644 --- a/build.cm +++ b/build.cm @@ -7,9 +7,9 @@ // Build.build_static(packages, target, output) - Build static binary var fd = use('fd') -var crypto = use('crypto') +var crypto = use('internal/crypto') var blob = use('blob') -var os = use('os') +var os = use('internal/os') var toolchains = use('toolchains') var shop = use('internal/shop') var pkg_tools = use('package') @@ -718,7 +718,7 @@ Build.compile_native = function(src_path, target, buildtype, pkg) { // Compile QBE runtime stubs if needed var rc = null if (!fd.is_file(rt_o_path)) { - qbe_rt_path = shop.get_package_dir('core') + '/qbe_rt.c' + qbe_rt_path = shop.get_package_dir('core') + '/src/qbe_rt.c' rc = os.system(cc + san_flags + ' -c ' + qbe_rt_path + ' -o ' + rt_o_path + ' -fPIC') if (rc != 0) { print('QBE runtime stubs compilation failed'); disrupt @@ -790,7 +790,7 @@ Build.compile_native_ir = function(optimized, src_path, opts) { // Compile QBE runtime stubs if needed var rc = null if (!fd.is_file(rt_o_path)) { - qbe_rt_path = shop.get_package_dir('core') + '/qbe_rt.c' + qbe_rt_path = shop.get_package_dir('core') + '/src/qbe_rt.c' rc = os.system(cc + san_flags + ' -c ' + qbe_rt_path + ' -o ' + rt_o_path + ' -fPIC') if (rc != 0) { print('QBE runtime stubs compilation failed'); disrupt diff --git a/cellfs.cm b/cellfs.cm index d6a009bc..3d4ed959 100644 --- a/cellfs.cm +++ b/cellfs.cm @@ -2,8 +2,8 @@ var cellfs = {} var fd = use('fd') var miniz = use('miniz') -var qop = use('qop') -var wildstar = use('wildstar') +var qop = use('internal/qop') +var wildstar = use('internal/wildstar') var mounts = [] diff --git a/compare_aot.ce b/compare_aot.ce index 12a83b99..24b855a3 100644 --- a/compare_aot.ce +++ b/compare_aot.ce @@ -5,7 +5,7 @@ var build = use('build') var fd_mod = use('fd') -var os = use('os') +var os = use('internal/os') var json = use('json') var time = use('time') diff --git a/diff.ce b/diff.ce index d5e75e8b..4fe58b2f 100644 --- a/diff.ce +++ b/diff.ce @@ -11,9 +11,9 @@ var time = use('time') var _args = args == null ? [] : args -var analyze = use('os').analyze -var run_ast_fn = use('os').run_ast_fn -var run_ast_noopt_fn = use('os').run_ast_noopt_fn +var analyze = use('internal/os').analyze +var run_ast_fn = use('internal/os').run_ast_fn +var run_ast_noopt_fn = use('internal/os').run_ast_noopt_fn if (!run_ast_noopt_fn) { log.console("error: run_ast_noopt_fn not available (rebuild bootstrap)") diff --git a/fd.cm b/fd.cm index d9287eb5..ea5fbfd6 100644 --- a/fd.cm +++ b/fd.cm @@ -1,5 +1,5 @@ var fd = use('internal/fd') -var wildstar = use('wildstar') +var wildstar = use('internal/wildstar') function last_pos(str, sep) { var last = null diff --git a/fuzz.ce b/fuzz.ce index fa131090..832410a2 100644 --- a/fuzz.ce +++ b/fuzz.ce @@ -13,7 +13,7 @@ var fd = use('fd') var time = use('time') var json = use('json') -var os_ref = use('os') +var os_ref = use('internal/os') var analyze = os_ref.analyze var run_ast_fn = os_ref.run_ast_fn var run_ast_noopt_fn = os_ref.run_ast_noopt_fn diff --git a/internal/bootstrap.cm b/internal/bootstrap.cm index d2fb6590..25a036ac 100644 --- a/internal/bootstrap.cm +++ b/internal/bootstrap.cm @@ -8,7 +8,7 @@ function use_embed(name) { var fd = use_embed('internal_fd') var json_mod = use_embed('json') -var crypto = use_embed('crypto') +var crypto = use_embed('internal_crypto') function content_hash(content) { var data = content diff --git a/crypto.c b/internal/crypto.c similarity index 99% rename from crypto.c rename to internal/crypto.c index f10f7afc..24a8f02c 100644 --- a/crypto.c +++ b/internal/crypto.c @@ -238,7 +238,7 @@ static const JSCFunctionListEntry js_crypto_funcs[] = { JS_CFUNC_DEF("unlock", 3, js_crypto_unlock), }; -JSValue js_core_crypto_use(JSContext *js) +JSValue js_core_internal_crypto_use(JSContext *js) { JS_FRAME(js); JS_ROOT(mod, JS_NewObject(js)); diff --git a/internal/engine.cm b/internal/engine.cm index 7ff2b6ae..eaf62fb7 100644 --- a/internal/engine.cm +++ b/internal/engine.cm @@ -25,7 +25,7 @@ function use_embed(name) { var fd = use_embed('internal_fd') var js = use_embed('js') -var crypto = use_embed('crypto') +var crypto = use_embed('internal_crypto') // core_path and shop_path come from env (C runtime passes them through) // shop_path may be null if --core was used without --shop @@ -255,7 +255,9 @@ function extract_module_summaries(compiled) { result_slot = instr[2] summary = _summary_resolver(path) if (summary != null) { - summaries[] = {slot: result_slot, summary: summary} + if (summary._native != true) { + summaries[] = {slot: result_slot, summary: summary} + } } else { inv_n = length(instr) unresolved[] = {path: path, line: instr[inv_n - 2], col: instr[inv_n - 1]} @@ -343,6 +345,13 @@ function run_ast_noopt_fn(name, ast, env) { // Compile AST to blob without loading (for caching) function compile_to_blob(name, ast) { + var compiled = mcode_mod(ast) + var optimized = streamline_mod(compiled) + return mach_compile_mcode_bin(name, json.encode(optimized)) +} + +// Compile user program AST to blob with diagnostics +function compile_user_blob(name, ast) { var compiled = mcode_mod(ast) var ms = null var _ui = 0 @@ -415,7 +424,7 @@ if (args != null && (_init == null || !_init.program)) { } } -use_cache['core/os'] = os +use_cache['core/internal/os'] = os // Extra env properties added as engine initializes (log, runtime fns, etc.) var core_extras = {} @@ -487,8 +496,8 @@ function actor() { } var actor_mod = use_core('actor') -var wota = use_core('wota') -var nota = use_core('nota') +var wota = use_core('internal/wota') +var nota = use_core('internal/nota') var ENETSERVICE = 0.1 @@ -600,7 +609,10 @@ use_core('build') // Wire up module summary resolver now that shop is available _summary_resolver = function(path) { - var resolved = shop.resolve_use_path(path, null) + var info = shop.resolve_import_info(path, null) + if (info == null) return null + if (info.type == 'native') return {_native: true} + var resolved = info.resolved_path if (resolved == null) return null var summary_fn = function() { return shop.summary_file(resolved) @@ -1492,7 +1504,7 @@ $_.clock(_ => { } else { script = text(source_blob) ast = analyze(script, prog_path) - mach_blob = compile_to_blob(prog, ast) + mach_blob = compile_user_blob(prog, ast) if (cached_path) { ensure_build_dir() fd.slurpwrite(cached_path, mach_blob) diff --git a/fd_playdate.c b/internal/fd_playdate.c similarity index 100% rename from fd_playdate.c rename to internal/fd_playdate.c diff --git a/internal/kim.c b/internal/kim.c index 94139373..121b4689 100644 --- a/internal/kim.c +++ b/internal/kim.c @@ -73,7 +73,7 @@ static const JSCFunctionListEntry js_kim_funcs[] = { MIST_FUNC_DEF(kim, decode, 1), }; -JSValue js_core_kim_use(JSContext *js) +JSValue js_core_internal_kim_use(JSContext *js) { JS_FRAME(js); JS_ROOT(mod, JS_NewObject(js)); diff --git a/internal/nota.c b/internal/nota.c new file mode 100644 index 00000000..fe4999f2 --- /dev/null +++ b/internal/nota.c @@ -0,0 +1,431 @@ +#define NOTA_IMPLEMENTATION +#include "quickjs-internal.h" +#include "cell.h" + +static int nota_get_arr_len (JSContext *ctx, JSValue arr) { + int64_t len; + JS_GetLength (ctx, arr, &len); + return (int)len; +} + +typedef struct NotaVisitedNode { + JSGCRef ref; + struct NotaVisitedNode *next; +} NotaVisitedNode; + +typedef struct NotaEncodeContext { + JSContext *ctx; + NotaVisitedNode *visited_list; + NotaBuffer nb; + int cycle; + JSGCRef *replacer_ref; /* pointer to GC-rooted ref */ +} NotaEncodeContext; + +static void nota_stack_push (NotaEncodeContext *enc, JSValueConst val) { + NotaVisitedNode *node = (NotaVisitedNode *)sys_malloc (sizeof (NotaVisitedNode)); + JS_PushGCRef (enc->ctx, &node->ref); + node->ref.val = JS_DupValue (enc->ctx, val); + node->next = enc->visited_list; + enc->visited_list = node; +} + +static void nota_stack_pop (NotaEncodeContext *enc) { + NotaVisitedNode *node = enc->visited_list; + enc->visited_list = node->next; + JS_FreeValue (enc->ctx, node->ref.val); + JS_PopGCRef (enc->ctx, &node->ref); + sys_free (node); +} + +static int nota_stack_has (NotaEncodeContext *enc, JSValueConst val) { + NotaVisitedNode *node = enc->visited_list; + while (node) { + if (JS_StrictEq (enc->ctx, node->ref.val, val)) + return 1; + node = node->next; + } + return 0; +} + +static JSValue nota_apply_replacer (NotaEncodeContext *enc, JSValueConst holder, JSValueConst key, JSValueConst val) { + if (!enc->replacer_ref || JS_IsNull (enc->replacer_ref->val)) return JS_DupValue (enc->ctx, val); + + JSValue args[2] = { JS_DupValue (enc->ctx, key), JS_DupValue (enc->ctx, val) }; + JSValue result = JS_Call (enc->ctx, enc->replacer_ref->val, holder, 2, args); + JS_FreeValue (enc->ctx, args[0]); + JS_FreeValue (enc->ctx, args[1]); + + if (JS_IsException (result)) return JS_DupValue (enc->ctx, val); + return result; +} + +static 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); + sys_free (blob); + break; + case NOTA_TEXT: + nota = nota_read_text (&str, nota); + *tmp = JS_NewString (js, str); + sys_free (str); + break; + case NOTA_ARR: + nota = nota_read_array (&n, nota); + *tmp = JS_NewArrayLen (js, n); + for (int i = 0; i < n; i++) { + nota = js_do_nota_decode (js, &ret2, nota, *tmp, JS_NewInt32 (js, i), reviver); + JS_SetPropertyNumber (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++) { + JSGCRef prop_key_ref, sub_val_ref; + JS_PushGCRef (js, &prop_key_ref); + JS_PushGCRef (js, &sub_val_ref); + nota = nota_read_text (&str, nota); + prop_key_ref.val = JS_NewString (js, str); + sub_val_ref.val = JS_NULL; + nota = js_do_nota_decode (js, &sub_val_ref.val, nota, *tmp, prop_key_ref.val, reviver); + JS_SetPropertyStr (js, *tmp, str, sub_val_ref.val); + JS_PopGCRef (js, &sub_val_ref); + JS_PopGCRef (js, &prop_key_ref); + sys_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) { + JSGCRef inner_ref, obj_ref2; + JS_PushGCRef (js, &inner_ref); + JS_PushGCRef (js, &obj_ref2); + inner_ref.val = JS_NULL; + nota = js_do_nota_decode (js, &inner_ref.val, nota, holder, JS_NULL, reviver); + obj_ref2.val = JS_NewObject (js); + if (!JS_IsNull (js->actor_sym)) + JS_SetPropertyKey (js, obj_ref2.val, js->actor_sym, inner_ref.val); + JS_CellStone (js, obj_ref2.val); + *tmp = obj_ref2.val; + JS_PopGCRef (js, &obj_ref2); + JS_PopGCRef (js, &inner_ref); + } 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; + JSGCRef replaced_ref, keys_ref, elem_ref, prop_ref; + JS_PushGCRef (ctx, &replaced_ref); + replaced_ref.val = nota_apply_replacer (enc, holder, key, val); + int tag = JS_VALUE_GET_TAG (replaced_ref.val); + + switch (tag) { + case JS_TAG_INT: + case JS_TAG_FLOAT64: { + double d; + JS_ToFloat64 (ctx, &d, replaced_ref.val); + nota_write_number (&enc->nb, d); + break; + } + case JS_TAG_STRING: { + const char *str = JS_ToCString (ctx, replaced_ref.val); + nota_write_text (&enc->nb, str); + JS_FreeCString (ctx, str); + break; + } + case JS_TAG_BOOL: + if (JS_VALUE_GET_BOOL (replaced_ref.val)) 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_PTR: { + if (JS_IsText (replaced_ref.val)) { + const char *str = JS_ToCString (ctx, replaced_ref.val); + nota_write_text (&enc->nb, str); + JS_FreeCString (ctx, str); + break; + } + + if (js_is_blob (ctx, replaced_ref.val)) { + size_t buf_len; + void *buf_data = js_get_blob_data (ctx, &buf_len, replaced_ref.val); + if (buf_data == (void *)-1) { + JS_PopGCRef (ctx, &replaced_ref); + return; + } + nota_write_blob (&enc->nb, (unsigned long long)buf_len * 8, (const char *)buf_data); + break; + } + + if (JS_IsArray (replaced_ref.val)) { + if (nota_stack_has (enc, replaced_ref.val)) { + enc->cycle = 1; + break; + } + nota_stack_push (enc, replaced_ref.val); + int arr_len = nota_get_arr_len (ctx, replaced_ref.val); + nota_write_array (&enc->nb, arr_len); + JS_PushGCRef (ctx, &elem_ref); + for (int i = 0; i < arr_len; i++) { + elem_ref.val = JS_GetPropertyNumber (ctx, replaced_ref.val, i); + JSValue elem_key = JS_NewInt32 (ctx, i); + nota_encode_value (enc, elem_ref.val, replaced_ref.val, elem_key); + } + JS_PopGCRef (ctx, &elem_ref); + nota_stack_pop (enc); + break; + } + + JSValue adata = JS_NULL; + if (!JS_IsNull (ctx->actor_sym)) { + int has = JS_HasPropertyKey (ctx, replaced_ref.val, ctx->actor_sym); + if (has > 0) adata = JS_GetPropertyKey (ctx, replaced_ref.val, ctx->actor_sym); + } + if (!JS_IsNull (adata)) { + nota_write_sym (&enc->nb, NOTA_PRIVATE); + nota_encode_value (enc, adata, replaced_ref.val, JS_NULL); + JS_FreeValue (ctx, adata); + break; + } + JS_FreeValue (ctx, adata); + if (nota_stack_has (enc, replaced_ref.val)) { + enc->cycle = 1; + break; + } + nota_stack_push (enc, replaced_ref.val); + + JSValue to_json = JS_GetPropertyStr (ctx, replaced_ref.val, "toJSON"); + if (JS_IsFunction (to_json)) { + JSValue result = JS_Call (ctx, to_json, replaced_ref.val, 0, NULL); + if (!JS_IsException (result)) { + nota_encode_value (enc, result, holder, key); + } else { + nota_write_sym (&enc->nb, NOTA_NULL); + } + nota_stack_pop (enc); + break; + } + + JS_PushGCRef (ctx, &keys_ref); + keys_ref.val = JS_GetOwnPropertyNames (ctx, replaced_ref.val); + if (JS_IsException (keys_ref.val)) { + nota_write_sym (&enc->nb, NOTA_NULL); + nota_stack_pop (enc); + JS_PopGCRef (ctx, &keys_ref); + break; + } + int64_t plen64; + if (JS_GetLength (ctx, keys_ref.val, &plen64) < 0) { + nota_write_sym (&enc->nb, NOTA_NULL); + nota_stack_pop (enc); + JS_PopGCRef (ctx, &keys_ref); + break; + } + uint32_t plen = (uint32_t)plen64; + + JS_PushGCRef (ctx, &prop_ref); + JS_PushGCRef (ctx, &elem_ref); + uint32_t non_function_count = 0; + for (uint32_t i = 0; i < plen; i++) { + elem_ref.val = JS_GetPropertyNumber (ctx, keys_ref.val, i); + prop_ref.val = JS_GetProperty (ctx, replaced_ref.val, elem_ref.val); + if (!JS_IsFunction (prop_ref.val)) non_function_count++; + } + + nota_write_record (&enc->nb, non_function_count); + for (uint32_t i = 0; i < plen; i++) { + elem_ref.val = JS_GetPropertyNumber (ctx, keys_ref.val, i); + prop_ref.val = JS_GetProperty (ctx, replaced_ref.val, elem_ref.val); + if (!JS_IsFunction (prop_ref.val)) { + const char *prop_name = JS_ToCString (ctx, elem_ref.val); + nota_write_text (&enc->nb, prop_name ? prop_name : ""); + nota_encode_value (enc, prop_ref.val, replaced_ref.val, elem_ref.val); + JS_FreeCString (ctx, prop_name); + } + } + JS_PopGCRef (ctx, &elem_ref); + JS_PopGCRef (ctx, &prop_ref); + JS_PopGCRef (ctx, &keys_ref); + nota_stack_pop (enc); + break; + } + default: + nota_write_sym (&enc->nb, NOTA_NULL); + break; + } + JS_PopGCRef (ctx, &replaced_ref); +} + +void *value2nota (JSContext *ctx, JSValue v) { + JSGCRef val_ref, key_ref; + JS_PushGCRef (ctx, &val_ref); + JS_PushGCRef (ctx, &key_ref); + val_ref.val = v; + + NotaEncodeContext enc_s, *enc = &enc_s; + enc->ctx = ctx; + enc->visited_list = NULL; + enc->cycle = 0; + enc->replacer_ref = NULL; + + nota_buffer_init (&enc->nb, 128); + key_ref.val = JS_NewString (ctx, ""); + nota_encode_value (enc, val_ref.val, JS_NULL, key_ref.val); + + if (enc->cycle) { + JS_PopGCRef (ctx, &key_ref); + JS_PopGCRef (ctx, &val_ref); + nota_buffer_free (&enc->nb); + return NULL; + } + + JS_PopGCRef (ctx, &key_ref); + JS_PopGCRef (ctx, &val_ref); + 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; + JSGCRef holder_ref, key_ref, ret_ref; + JS_PushGCRef (js, &holder_ref); + JS_PushGCRef (js, &key_ref); + JS_PushGCRef (js, &ret_ref); + holder_ref.val = JS_NewObject (js); + key_ref.val = JS_NewString (js, ""); + ret_ref.val = JS_NULL; + js_do_nota_decode (js, &ret_ref.val, nota, holder_ref.val, key_ref.val, JS_NULL); + JSValue result = ret_ref.val; + JS_PopGCRef (js, &ret_ref); + JS_PopGCRef (js, &key_ref); + JS_PopGCRef (js, &holder_ref); + return result; +} + +static JSValue js_nota_encode (JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + if (argc < 1) return JS_RaiseDisrupt (ctx, "nota.encode requires at least 1 argument"); + + JSGCRef val_ref, replacer_ref, key_ref; + JS_PushGCRef (ctx, &val_ref); + JS_PushGCRef (ctx, &replacer_ref); + JS_PushGCRef (ctx, &key_ref); + val_ref.val = argv[0]; + replacer_ref.val = (argc > 1 && JS_IsFunction (argv[1])) ? argv[1] : JS_NULL; + + NotaEncodeContext enc_s, *enc = &enc_s; + enc->ctx = ctx; + enc->visited_list = NULL; + enc->cycle = 0; + enc->replacer_ref = &replacer_ref; + + nota_buffer_init (&enc->nb, 128); + key_ref.val = JS_NewString (ctx, ""); + nota_encode_value (enc, val_ref.val, JS_NULL, key_ref.val); + + JSValue ret; + if (enc->cycle) { + nota_buffer_free (&enc->nb); + ret = JS_RaiseDisrupt (ctx, "Tried to encode something to nota with a cycle."); + } else { + size_t total_len = enc->nb.size; + void *data_ptr = enc->nb.data; + ret = js_new_blob_stoned_copy (ctx, (uint8_t *)data_ptr, total_len); + nota_buffer_free (&enc->nb); + } + + JS_PopGCRef (ctx, &key_ref); + JS_PopGCRef (ctx, &replacer_ref); + JS_PopGCRef (ctx, &val_ref); + 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 == (unsigned char *)-1) return JS_EXCEPTION; + if (!nota) return JS_NULL; + + JSGCRef holder_ref, key_ref, ret_ref, reviver_ref; + JS_PushGCRef (js, &holder_ref); + JS_PushGCRef (js, &key_ref); + JS_PushGCRef (js, &ret_ref); + JS_PushGCRef (js, &reviver_ref); + + reviver_ref.val = (argc > 1 && JS_IsFunction (argv[1])) ? argv[1] : JS_NULL; + holder_ref.val = JS_NewObject (js); + key_ref.val = JS_NewString (js, ""); + ret_ref.val = JS_NULL; + + js_do_nota_decode (js, &ret_ref.val, (char *)nota, holder_ref.val, key_ref.val, reviver_ref.val); + + JSValue result = ret_ref.val; + JS_PopGCRef (js, &reviver_ref); + JS_PopGCRef (js, &ret_ref); + JS_PopGCRef (js, &key_ref); + JS_PopGCRef (js, &holder_ref); + return result; +} + +static const JSCFunctionListEntry js_nota_funcs[] = { + JS_CFUNC_DEF ("encode", 1, js_nota_encode), + JS_CFUNC_DEF ("decode", 1, js_nota_decode), +}; + +JSValue js_core_internal_nota_use (JSContext *js) { + JSGCRef export_ref; + JS_PushGCRef (js, &export_ref); + export_ref.val = JS_NewObject (js); + JS_SetPropertyFunctionList (js, export_ref.val, js_nota_funcs, sizeof (js_nota_funcs) / sizeof (JSCFunctionListEntry)); + JSValue result = export_ref.val; + JS_PopGCRef (js, &export_ref); + return result; +} diff --git a/internal/os.c b/internal/os.c index 3638c059..0e48db47 100644 --- a/internal/os.c +++ b/internal/os.c @@ -733,7 +733,7 @@ static const JSCFunctionListEntry js_os_funcs[] = { MIST_FUNC_DEF(os, stack, 1), }; -JSValue js_core_os_use(JSContext *js) { +JSValue js_core_internal_os_use(JSContext *js) { JS_NewClassID(&js_dylib_class_id); JS_NewClass(js, js_dylib_class_id, &js_dylib_class); diff --git a/qop.c b/internal/qop.c similarity index 99% rename from qop.c rename to internal/qop.c index 9d443f8a..f16f99d6 100644 --- a/qop.c +++ b/internal/qop.c @@ -455,7 +455,7 @@ static const JSCFunctionListEntry js_qop_funcs[] = { JS_PROP_INT32_DEF("FLAG_ENCRYPTED", QOP_FLAG_ENCRYPTED, 0), }; -JSValue js_core_qop_use(JSContext *js) { +JSValue js_core_internal_qop_use(JSContext *js) { JS_FRAME(js); JS_NewClassID(&js_qop_archive_class_id); JS_NewClass(js, js_qop_archive_class_id, &js_qop_archive_class); diff --git a/internal/shop.cm b/internal/shop.cm index d66c9732..aa8a539b 100644 --- a/internal/shop.cm +++ b/internal/shop.cm @@ -5,11 +5,11 @@ var fd = use('fd') var http = use('http') var miniz = use('miniz') var time = use('time') -var crypto = use('crypto') +var crypto = use('internal/crypto') var blob = use('blob') var pkg_tools = use('package') -var os = use('os') +var os = use('internal/os') var link = use('link') // These come from env (via core_extras in engine.cm): diff --git a/time_playdate.c b/internal/time_playdate.c similarity index 100% rename from time_playdate.c rename to internal/time_playdate.c diff --git a/wildstar.c b/internal/wildstar.c similarity index 95% rename from wildstar.c rename to internal/wildstar.c index 7bd8e8be..94d8aef3 100644 --- a/wildstar.c +++ b/internal/wildstar.c @@ -28,7 +28,7 @@ static const JSCFunctionListEntry js_wildstar_funcs[] = { JS_PROP_INT32_DEF("WM_WILDSTAR", WM_WILDSTAR, 0), }; -JSValue js_core_wildstar_use(JSContext *js) { +JSValue js_core_internal_wildstar_use(JSContext *js) { JS_FRAME(js); JS_ROOT(mod, JS_NewObject(js)); JS_SetPropertyFunctionList(js, mod.val, js_wildstar_funcs, countof(js_wildstar_funcs)); diff --git a/internal/wota.c b/internal/wota.c new file mode 100644 index 00000000..ce32c800 --- /dev/null +++ b/internal/wota.c @@ -0,0 +1,461 @@ +#define WOTA_IMPLEMENTATION +#include "quickjs-internal.h" +#include "cell.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) { + (void)enc; (void)val; + /* Cycle detection disabled for performance */ +} + +static void wota_stack_pop (WotaEncodeContext *enc) { + if (!enc->visited_stack) return; + + ObjectRef *top = enc->visited_stack; + enc->visited_stack = top->next; + sys_free (top); +} + +static int wota_stack_has (WotaEncodeContext *enc, JSValueConst val) { + (void)enc; (void)val; + return 0; + /* Cycle detection disabled for performance */ +} + +static void wota_stack_free (WotaEncodeContext *enc) { + while (enc->visited_stack) { + wota_stack_pop (enc); + } +} + +static JSValue wota_apply_replacer (WotaEncodeContext *enc, JSValueConst holder, JSValue key, JSValueConst val) { + if (JS_IsNull (enc->replacer)) return JS_DupValue (enc->ctx, val); + JSValue key_val = JS_IsNull (key) ? JS_NULL : JS_DupValue (enc->ctx, key); + JSValue args[2] = { key_val, JS_DupValue (enc->ctx, val) }; + 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, JSValue key); + +static void encode_object_properties (WotaEncodeContext *enc, JSValueConst val, JSValueConst holder) { + JSContext *ctx = enc->ctx; + + /* Root the input value to protect it during property enumeration */ + JSGCRef val_ref, keys_ref; + JS_PushGCRef (ctx, &val_ref); + JS_PushGCRef (ctx, &keys_ref); + val_ref.val = JS_DupValue (ctx, val); + + keys_ref.val = JS_GetOwnPropertyNames (ctx, val_ref.val); + if (JS_IsException (keys_ref.val)) { + wota_write_sym (&enc->wb, WOTA_NULL); + JS_FreeValue (ctx, val_ref.val); + JS_PopGCRef (ctx, &keys_ref); + JS_PopGCRef (ctx, &val_ref); + return; + } + int64_t plen64; + if (JS_GetLength (ctx, keys_ref.val, &plen64) < 0) { + JS_FreeValue (ctx, keys_ref.val); + JS_FreeValue (ctx, val_ref.val); + wota_write_sym (&enc->wb, WOTA_NULL); + JS_PopGCRef (ctx, &keys_ref); + JS_PopGCRef (ctx, &val_ref); + return; + } + uint32_t plen = (uint32_t)plen64; + uint32_t non_function_count = 0; + + /* Allocate GC-rooted arrays for props and keys */ + JSGCRef *prop_refs = sys_malloc (sizeof (JSGCRef) * plen); + JSGCRef *key_refs = sys_malloc (sizeof (JSGCRef) * plen); + for (uint32_t i = 0; i < plen; i++) { + JS_PushGCRef (ctx, &prop_refs[i]); + JS_PushGCRef (ctx, &key_refs[i]); + prop_refs[i].val = JS_NULL; + key_refs[i].val = JS_NULL; + } + + for (uint32_t i = 0; i < plen; i++) { + /* Store key into its GCRef slot immediately so it's rooted before + JS_GetProperty can trigger GC and relocate the string. */ + key_refs[i].val = JS_GetPropertyNumber (ctx, keys_ref.val, i); + JSValue prop_val = JS_GetProperty (ctx, val_ref.val, key_refs[i].val); + if (!JS_IsFunction (prop_val)) { + if (i != non_function_count) { + key_refs[non_function_count].val = key_refs[i].val; + key_refs[i].val = JS_NULL; + } + prop_refs[non_function_count].val = prop_val; + non_function_count++; + } else { + JS_FreeValue (ctx, prop_val); + JS_FreeValue (ctx, key_refs[i].val); + key_refs[i].val = JS_NULL; + } + } + JS_FreeValue (ctx, keys_ref.val); + wota_write_record (&enc->wb, non_function_count); + for (uint32_t i = 0; i < non_function_count; i++) { + size_t klen; + const char *prop_name = JS_ToCStringLen (ctx, &klen, key_refs[i].val); + wota_write_text_len (&enc->wb, prop_name ? prop_name : "", prop_name ? klen : 0); + wota_encode_value (enc, prop_refs[i].val, val_ref.val, key_refs[i].val); + JS_FreeCString (ctx, prop_name); + JS_FreeValue (ctx, prop_refs[i].val); + JS_FreeValue (ctx, key_refs[i].val); + } + /* Pop all GC refs in reverse order */ + for (int i = plen - 1; i >= 0; i--) { + JS_PopGCRef (ctx, &key_refs[i]); + JS_PopGCRef (ctx, &prop_refs[i]); + } + sys_free (prop_refs); + sys_free (key_refs); + JS_FreeValue (ctx, val_ref.val); + JS_PopGCRef (ctx, &keys_ref); + JS_PopGCRef (ctx, &val_ref); +} + +static void wota_encode_value (WotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValue key) { + JSContext *ctx = enc->ctx; + JSValue replaced; + if (!JS_IsNull (enc->replacer) && !JS_IsNull (key)) + replaced = wota_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: { + 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_PTR: { + if (JS_IsText (replaced)) { + 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; + } + 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; + } + 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 (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_GetPropertyNumber (ctx, replaced, i); + wota_encode_value (enc, elem_val, replaced, JS_NewInt32 (ctx, (int32_t)i)); + JS_FreeValue (ctx, elem_val); + } + wota_stack_pop (enc); + break; + } + JSValue adata = JS_NULL; + if (!JS_IsNull (ctx->actor_sym)) { + int has = JS_HasPropertyKey (ctx, replaced, ctx->actor_sym); + if (has > 0) adata = JS_GetPropertyKey (ctx, replaced, ctx->actor_sym); + } + if (!JS_IsNull (adata)) { + wota_write_sym (&enc->wb, WOTA_PRIVATE); + wota_encode_value (enc, adata, replaced, JS_NULL); + 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 (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, JSValue 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) { + JSGCRef inner_ref, obj_ref2; + JS_PushGCRef (ctx, &inner_ref); + JS_PushGCRef (ctx, &obj_ref2); + inner_ref.val = JS_NULL; + data_ptr = decode_wota_value (ctx, data_ptr, &inner_ref.val, holder, JS_NULL, reviver); + obj_ref2.val = JS_NewObject (ctx); + if (!JS_IsNull (ctx->actor_sym)) + JS_SetPropertyKey (ctx, obj_ref2.val, ctx->actor_sym, inner_ref.val); + JS_CellStone (ctx, obj_ref2.val); + *out_val = obj_ref2.val; + JS_PopGCRef (ctx, &obj_ref2); + JS_PopGCRef (ctx, &inner_ref); + } 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) sys_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) sys_free (utf8); + break; + } + case WOTA_ARR: { + long long c; + data_ptr = wota_read_array (&c, data_ptr); + JSGCRef arr_ref; + JS_PushGCRef (ctx, &arr_ref); + arr_ref.val = JS_NewArrayLen (ctx, c); + for (long long i = 0; i < c; i++) { + JSGCRef elem_ref; + JS_PushGCRef (ctx, &elem_ref); + elem_ref.val = JS_NULL; + JSValue idx_key = JS_NewInt32 (ctx, (int32_t)i); + data_ptr = decode_wota_value (ctx, data_ptr, &elem_ref.val, arr_ref.val, idx_key, reviver); + JS_SetPropertyNumber (ctx, arr_ref.val, i, elem_ref.val); + JS_PopGCRef (ctx, &elem_ref); + } + *out_val = arr_ref.val; + JS_PopGCRef (ctx, &arr_ref); + break; + } + case WOTA_REC: { + long long c; + data_ptr = wota_read_record (&c, data_ptr); + JSGCRef obj_ref; + JS_PushGCRef (ctx, &obj_ref); + obj_ref.val = 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; + JSGCRef prop_key_ref, sub_val_ref; + JS_PushGCRef (ctx, &prop_key_ref); + JS_PushGCRef (ctx, &sub_val_ref); + prop_key_ref.val = JS_NewStringLen (ctx, tkey, key_len); + sub_val_ref.val = JS_NULL; + data_ptr = decode_wota_value (ctx, data_ptr, &sub_val_ref.val, obj_ref.val, prop_key_ref.val, reviver); + JS_SetPropertyStr (ctx, obj_ref.val, tkey, sub_val_ref.val); + JS_PopGCRef (ctx, &sub_val_ref); + JS_PopGCRef (ctx, &prop_key_ref); + sys_free (tkey); + } + *out_val = obj_ref.val; + JS_PopGCRef (ctx, &obj_ref); + break; + } + default: + data_ptr += 8; + *out_val = JS_NULL; + break; + } + if (!JS_IsNull (reviver)) { + JSValue key_val = JS_IsNull (key) ? JS_NULL : JS_DupValue (ctx, key); + JSValue args[2] = { key_val, JS_DupValue (ctx, *out_val) }; + JSValue 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) { + JSGCRef val_ref, rep_ref; + JS_PushGCRef (ctx, &val_ref); + JS_PushGCRef (ctx, &rep_ref); + val_ref.val = v; + rep_ref.val = replacer; + + WotaEncodeContext enc_s, *enc = &enc_s; + + enc->ctx = ctx; + enc->visited_stack = NULL; + enc->cycle = 0; + enc->replacer = rep_ref.val; + wota_buffer_init (&enc->wb, 16); + wota_encode_value (enc, val_ref.val, JS_NULL, JS_NULL); + if (enc->cycle) { + wota_stack_free (enc); + wota_buffer_free (&enc->wb); + JS_PopGCRef (ctx, &rep_ref); + JS_PopGCRef (ctx, &val_ref); + return NULL; + } + wota_stack_free (enc); + size_t total_bytes = enc->wb.size * sizeof (uint64_t); + void *wota = sys_realloc (enc->wb.data, total_bytes); + if (bytes) *bytes = total_bytes; + JS_PopGCRef (ctx, &rep_ref); + JS_PopGCRef (ctx, &val_ref); + return wota; +} + +JSValue wota2value (JSContext *ctx, void *wota) { + JSGCRef holder_ref, result_ref; + JS_PushGCRef (ctx, &holder_ref); + JS_PushGCRef (ctx, &result_ref); + result_ref.val = JS_NULL; + holder_ref.val = JS_NewObject (ctx); + decode_wota_value (ctx, wota, &result_ref.val, holder_ref.val, JS_NULL, JS_NULL); + JSValue result = result_ref.val; + JS_PopGCRef (ctx, &result_ref); + JS_PopGCRef (ctx, &holder_ref); + return result; +} + +static JSValue js_wota_encode (JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + if (argc < 1) return JS_RaiseDisrupt (ctx, "wota.encode requires at least 1 argument"); + size_t total_bytes; + void *wota = value2wota (ctx, argv[0], JS_IsFunction (argv[1]) ? argv[1] : JS_NULL, &total_bytes); + JSValue ret = js_new_blob_stoned_copy (ctx, wota, total_bytes); + sys_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_RaiseDisrupt (ctx, "No blob data present"); + JSValue reviver = (argc > 1 && JS_IsFunction (argv[1])) ? argv[1] : JS_NULL; + char *data_ptr = (char *)buf; + JSGCRef result_ref, holder_ref, empty_key_ref; + JS_PushGCRef (ctx, &result_ref); + JS_PushGCRef (ctx, &holder_ref); + JS_PushGCRef (ctx, &empty_key_ref); + result_ref.val = JS_NULL; + holder_ref.val = JS_NewObject (ctx); + empty_key_ref.val = JS_NewString (ctx, ""); + decode_wota_value (ctx, data_ptr, &result_ref.val, holder_ref.val, empty_key_ref.val, reviver); + JSValue result = result_ref.val; + JS_PopGCRef (ctx, &empty_key_ref); + JS_PopGCRef (ctx, &holder_ref); + JS_PopGCRef (ctx, &result_ref); + 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_core_internal_wota_use (JSContext *ctx) { + JSGCRef exports_ref; + JS_PushGCRef (ctx, &exports_ref); + exports_ref.val = JS_NewObject (ctx); + JS_SetPropertyFunctionList (ctx, exports_ref.val, js_wota_funcs, sizeof (js_wota_funcs) / sizeof (js_wota_funcs[0])); + JSValue result = exports_ref.val; + JS_PopGCRef (ctx, &exports_ref); + return result; +} diff --git a/json.c b/json.c new file mode 100644 index 00000000..c479d941 --- /dev/null +++ b/json.c @@ -0,0 +1,67 @@ +#include "cell.h" +#include + +/* JSON uses JS_KEY_empty for reviver — define locally from public constants */ +#ifndef JS_KEY_empty +#define JS_KEY_empty ((JSValue)JS_TAG_STRING_IMM) +#endif + +static JSValue js_cell_json_encode (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + if (argc < 1) + return JS_RaiseDisrupt (ctx, "json.encode requires at least 1 argument"); + + int pretty = argc <= 1 || JS_ToBool (ctx, argv[1]); + JSValue space = pretty ? JS_NewInt32 (ctx, 2) : JS_NULL; + JSValue replacer = JS_NULL; + if (argc > 2 && JS_IsFunction (argv[2])) + replacer = argv[2]; + else if (argc > 3 && JS_IsArray (argv[3])) + replacer = argv[3]; + JSValue result = JS_JSONStringify (ctx, argv[0], replacer, space, pretty); + return result; +} + +static JSValue js_cell_json_decode (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + if (argc < 1) + return JS_RaiseDisrupt (ctx, "json.decode requires at least 1 argument"); + + if (!JS_IsText (argv[0])) + return JS_RaiseDisrupt (ctx, "couldn't parse text: not a string"); + + const char *str = JS_ToCString (ctx, argv[0]); + if (!str) return JS_EXCEPTION; + + size_t len = strlen (str); + JSValue result = JS_ParseJSON (ctx, str, len, ""); + JS_FreeCString (ctx, str); + + /* Apply reviver if provided */ + if (argc > 1 && JS_IsFunction (argv[1]) && !JS_IsException (result)) { + /* Create wrapper object to pass to reviver */ + JSValue wrapper = JS_NewObject (ctx); + JS_SetPropertyStr (ctx, wrapper, "", result); + + JSValue holder = wrapper; + JSValue key = JS_KEY_empty; + JSValue args[2] = { key, JS_GetProperty (ctx, holder, key) }; + JSValue final = JS_Call (ctx, argv[1], holder, 2, args); + result = final; + } + + return result; +} + +static const JSCFunctionListEntry js_cell_json_funcs[] = { + JS_CFUNC_DEF ("encode", 1, js_cell_json_encode), + JS_CFUNC_DEF ("decode", 1, js_cell_json_decode), +}; + +JSValue js_core_json_use (JSContext *ctx) { + JSGCRef obj_ref; + JS_PushGCRef (ctx, &obj_ref); + obj_ref.val = JS_NewObject (ctx); + JS_SetPropertyFunctionList (ctx, obj_ref.val, js_cell_json_funcs, countof (js_cell_json_funcs)); + JSValue result = obj_ref.val; + JS_PopGCRef (ctx, &obj_ref); + return result; +} diff --git a/math/cycles.c b/math/cycles.c new file mode 100644 index 00000000..fccdd791 --- /dev/null +++ b/math/cycles.c @@ -0,0 +1,66 @@ +#include "cell.h" +#include "cell_math.h" +#include + +#define TWOPI (2.0 * 3.14159265358979323846264338327950288419716939937510) + +static JSValue js_math_cyc_arc_cosine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, acos (x) / TWOPI); +} + +static JSValue js_math_cyc_arc_sine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, asin (x) / TWOPI); +} + +static JSValue js_math_cyc_arc_tangent (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, atan (x) / TWOPI); +} + +static JSValue js_math_cyc_cosine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, cos (x * TWOPI)); +} + +static JSValue js_math_cyc_sine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, sin (x * TWOPI)); +} + +static JSValue js_math_cyc_tangent (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, tan (x * TWOPI)); +} + +static const JSCFunctionListEntry js_math_cycles_funcs[] + = { JS_CFUNC_DEF ("arc_cosine", 1, js_math_cyc_arc_cosine), + JS_CFUNC_DEF ("arc_sine", 1, js_math_cyc_arc_sine), + JS_CFUNC_DEF ("arc_tangent", 1, js_math_cyc_arc_tangent), + JS_CFUNC_DEF ("cosine", 1, js_math_cyc_cosine), + JS_CFUNC_DEF ("sine", 1, js_math_cyc_sine), + JS_CFUNC_DEF ("tangent", 1, js_math_cyc_tangent), + JS_CFUNC_DEF ("ln", 1, js_math_ln), + JS_CFUNC_DEF ("log", 1, js_math_log10), + JS_CFUNC_DEF ("log2", 1, js_math_log2), + JS_CFUNC_DEF ("power", 2, js_math_power), + JS_CFUNC_DEF ("root", 2, js_math_root), + JS_CFUNC_DEF ("sqrt", 1, js_math_sqrt), + JS_CFUNC_DEF ("e", 1, js_math_e) }; + +JSValue js_core_math_cycles_use (JSContext *ctx) { + JSGCRef obj_ref; + JS_PushGCRef (ctx, &obj_ref); + obj_ref.val = JS_NewObject (ctx); + JS_SetPropertyFunctionList (ctx, obj_ref.val, js_math_cycles_funcs, countof (js_math_cycles_funcs)); + JSValue result = obj_ref.val; + JS_PopGCRef (ctx, &obj_ref); + return result; +} diff --git a/math/degrees.c b/math/degrees.c new file mode 100644 index 00000000..84585759 --- /dev/null +++ b/math/degrees.c @@ -0,0 +1,67 @@ +#include "cell.h" +#include "cell_math.h" +#include + +#define DEG2RAD (3.14159265358979323846264338327950288419716939937510 / 180.0) +#define RAD2DEG (180.0 / 3.14159265358979323846264338327950288419716939937510) + +static JSValue js_math_deg_arc_cosine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, acos (x) * RAD2DEG); +} + +static JSValue js_math_deg_arc_sine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, asin (x) * RAD2DEG); +} + +static JSValue js_math_deg_arc_tangent (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, atan (x) * RAD2DEG); +} + +static JSValue js_math_deg_cosine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, cos (x * DEG2RAD)); +} + +static JSValue js_math_deg_sine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, sin (x * DEG2RAD)); +} + +static JSValue js_math_deg_tangent (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, tan (x * DEG2RAD)); +} + +static const JSCFunctionListEntry js_math_degrees_funcs[] + = { JS_CFUNC_DEF ("arc_cosine", 1, js_math_deg_arc_cosine), + JS_CFUNC_DEF ("arc_sine", 1, js_math_deg_arc_sine), + JS_CFUNC_DEF ("arc_tangent", 1, js_math_deg_arc_tangent), + JS_CFUNC_DEF ("cosine", 1, js_math_deg_cosine), + JS_CFUNC_DEF ("sine", 1, js_math_deg_sine), + JS_CFUNC_DEF ("tangent", 1, js_math_deg_tangent), + JS_CFUNC_DEF ("ln", 1, js_math_ln), + JS_CFUNC_DEF ("log", 1, js_math_log10), + JS_CFUNC_DEF ("log2", 1, js_math_log2), + JS_CFUNC_DEF ("power", 2, js_math_power), + JS_CFUNC_DEF ("root", 2, js_math_root), + JS_CFUNC_DEF ("sqrt", 1, js_math_sqrt), + JS_CFUNC_DEF ("e", 1, js_math_e) }; + +JSValue js_core_math_degrees_use (JSContext *ctx) { + JSGCRef obj_ref; + JS_PushGCRef (ctx, &obj_ref); + obj_ref.val = JS_NewObject (ctx); + JS_SetPropertyFunctionList (ctx, obj_ref.val, js_math_degrees_funcs, countof (js_math_degrees_funcs)); + JSValue result = obj_ref.val; + JS_PopGCRef (ctx, &obj_ref); + return result; +} diff --git a/math/radians.c b/math/radians.c new file mode 100644 index 00000000..fbbd43fc --- /dev/null +++ b/math/radians.c @@ -0,0 +1,64 @@ +#include "cell.h" +#include "cell_math.h" +#include + +static JSValue js_math_rad_arc_cosine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, acos (x)); +} + +static JSValue js_math_rad_arc_sine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, asin (x)); +} + +static JSValue js_math_rad_arc_tangent (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, atan (x)); +} + +static JSValue js_math_rad_cosine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, cos (x)); +} + +static JSValue js_math_rad_sine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, sin (x)); +} + +static JSValue js_math_rad_tangent (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, tan (x)); +} + +static const JSCFunctionListEntry js_math_radians_funcs[] + = { JS_CFUNC_DEF ("arc_cosine", 1, js_math_rad_arc_cosine), + JS_CFUNC_DEF ("arc_sine", 1, js_math_rad_arc_sine), + JS_CFUNC_DEF ("arc_tangent", 1, js_math_rad_arc_tangent), + JS_CFUNC_DEF ("cosine", 1, js_math_rad_cosine), + JS_CFUNC_DEF ("sine", 1, js_math_rad_sine), + JS_CFUNC_DEF ("tangent", 1, js_math_rad_tangent), + JS_CFUNC_DEF ("ln", 1, js_math_ln), + JS_CFUNC_DEF ("log", 1, js_math_log10), + JS_CFUNC_DEF ("log2", 1, js_math_log2), + JS_CFUNC_DEF ("power", 2, js_math_power), + JS_CFUNC_DEF ("root", 2, js_math_root), + JS_CFUNC_DEF ("sqrt", 1, js_math_sqrt), + JS_CFUNC_DEF ("e", 1, js_math_e) }; + +JSValue js_core_math_radians_use (JSContext *ctx) { + JSGCRef obj_ref; + JS_PushGCRef (ctx, &obj_ref); + obj_ref.val = JS_NewObject (ctx); + JS_SetPropertyFunctionList (ctx, obj_ref.val, js_math_radians_funcs, countof (js_math_radians_funcs)); + JSValue result = obj_ref.val; + JS_PopGCRef (ctx, &obj_ref); + return result; +} diff --git a/meson.build b/meson.build index 28ea6cc9..21d08001 100644 --- a/meson.build +++ b/meson.build @@ -64,11 +64,18 @@ src += ['qbe_helpers.c'] src += ['qbe_backend.c'] scripts = [ + 'json.c', + 'internal/nota.c', + 'internal/wota.c', + 'math/radians.c', + 'math/degrees.c', + 'math/cycles.c', + 'src/cell_math.c', 'debug/js.c', - 'qop.c', - 'wildstar.c', + 'internal/qop.c', + 'internal/wildstar.c', 'fit.c', - 'crypto.c', + 'internal/crypto.c', 'internal/kim.c', 'internal/time.c', 'debug/debug.c', @@ -76,7 +83,6 @@ scripts = [ 'internal/fd.c', 'net/http.c', 'net/enet.c', - 'wildstar.c', 'archive/miniz.c', 'source/cJSON.c' ] @@ -86,7 +92,7 @@ foreach file: scripts endforeach srceng = 'source' -includes = [srceng, 'internal', 'debug', 'net', 'archive', 'src/qbe'] +includes = [srceng, 'internal', 'debug', 'net', 'archive', 'src/qbe', 'src'] foreach file : src full_path = join_paths(srceng, file) diff --git a/qopconv.ce b/qopconv.ce index 151fcf18..2e2d56e2 100644 --- a/qopconv.ce +++ b/qopconv.ce @@ -1,7 +1,7 @@ // cell qopconv - Convert QOP archive formats var fd = use('fd') -var qop = use('qop') +var qop = use('internal/qop') function print_usage() { log.console("Usage: qopconv [OPTION...] FILE...") diff --git a/random.cm b/random.cm index 1b7b5e7a..8731ace4 100644 --- a/random.cm +++ b/random.cm @@ -1,6 +1,6 @@ var rnd = {} -var os = use('os') +var os = use('internal/os') rnd.random = function() { diff --git a/run_native.ce b/run_native.ce index 0d803521..432ff60e 100644 --- a/run_native.ce +++ b/run_native.ce @@ -6,7 +6,7 @@ // Loads .cm via use() (interpreted) and via shop.use_native() (native), // runs both and compares results and timing. -var os = use('os') +var os = use('internal/os') var fd = use('fd') var shop = use('internal/shop') diff --git a/run_native_seed.ce b/run_native_seed.ce index ae837557..5de27347 100644 --- a/run_native_seed.ce +++ b/run_native_seed.ce @@ -2,7 +2,7 @@ // Usage: ./cell --dev --seed run_native_seed benches/fibonacci var fd = use("fd") -var os = use("os") +var os = use("internal/os") if (length(args) < 1) { print("usage: cell --dev --seed run_native_seed ") diff --git a/seed.ce b/seed.ce index d9aa2cd1..eeb0bce9 100644 --- a/seed.ce +++ b/seed.ce @@ -17,7 +17,7 @@ var fd = use("fd") var json = use("json") -var os = use("os") +var os = use("internal/os") var shop = use("internal/shop") var tokenize = use("tokenize") var parse = use("parse") diff --git a/source/cell.c b/source/cell.c index 95a8174e..79f8e5e3 100644 --- a/source/cell.c +++ b/source/cell.c @@ -261,7 +261,7 @@ void actor_disrupt(cell_rt *crt) actor_free(crt); } -JSValue js_core_os_use(JSContext *js); +JSValue js_core_internal_os_use(JSContext *js); JSValue js_core_json_use(JSContext *js); void script_startup(cell_rt *prt) @@ -317,7 +317,7 @@ void script_startup(cell_rt *prt) JS_AddGCRef(js, &boot_env_ref); boot_env_ref.val = JS_NewObject(js); JSValue btmp; - btmp = js_core_os_use(js); + btmp = js_core_internal_os_use(js); JS_SetPropertyStr(js, boot_env_ref.val, "os", btmp); if (core_path) { btmp = JS_NewString(js, core_path); @@ -347,7 +347,7 @@ void script_startup(cell_rt *prt) JS_AddGCRef(js, &env_ref); env_ref.val = JS_NewObject(js); JSValue tmp; - tmp = js_core_os_use(js); + tmp = js_core_internal_os_use(js); JS_SetPropertyStr(js, env_ref.val, "os", tmp); tmp = js_core_json_use(js); JS_SetPropertyStr(js, env_ref.val, "json", tmp); @@ -612,7 +612,7 @@ int cell_init(int argc, char **argv) JS_AddGCRef(ctx, &boot_env_ref); boot_env_ref.val = JS_NewObject(ctx); JSValue btmp; - btmp = js_core_os_use(ctx); + btmp = js_core_internal_os_use(ctx); JS_SetPropertyStr(ctx, boot_env_ref.val, "os", btmp); btmp = JS_NewString(ctx, core_path); JS_SetPropertyStr(ctx, boot_env_ref.val, "core_path", btmp); @@ -656,7 +656,7 @@ int cell_init(int argc, char **argv) JS_AddGCRef(ctx, &env_ref); env_ref.val = JS_NewObject(ctx); JSValue tmp; - tmp = js_core_os_use(ctx); + tmp = js_core_internal_os_use(ctx); JS_SetPropertyStr(ctx, env_ref.val, "os", tmp); tmp = JS_NewString(ctx, core_path); JS_SetPropertyStr(ctx, env_ref.val, "core_path", tmp); diff --git a/source/quickjs-internal.h b/source/quickjs-internal.h index db5546d6..fad78be9 100644 --- a/source/quickjs-internal.h +++ b/source/quickjs-internal.h @@ -1594,6 +1594,8 @@ int js_string_compare_value_nocase (JSContext *ctx, JSValue op1, JSValue op2); JSValue js_regexp_constructor (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); int JS_HasProperty (JSContext *ctx, JSValue obj, JSValue prop); int JS_HasPropertyKey (JSContext *ctx, JSValue obj, JSValue key); +JSValue JS_GetPropertyKey (JSContext *ctx, JSValue this_obj, JSValue key); +int JS_SetPropertyKey (JSContext *ctx, JSValue this_obj, JSValue key, JSValue val); void *js_realloc_rt (void *ptr, size_t size); char *js_strdup_rt (const char *str); JSValue JS_ConcatString (JSContext *ctx, JSValue op1, JSValue op2); diff --git a/source/runtime.c b/source/runtime.c index 920d0754..1b47971b 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -24,8 +24,6 @@ */ #define BLOB_IMPLEMENTATION -#define NOTA_IMPLEMENTATION -#define WOTA_IMPLEMENTATION #include "quickjs-internal.h" // #define DUMP_BUDDY @@ -11545,1223 +11543,7 @@ void js_debug_sethook (JSContext *ctx, js_hook hook, int type, void *user) { ctx->trace_data = user; } -/* ============================================================================ - * Cell Script Module: json - * Provides json.encode() and json.decode() using pure C implementation - * ============================================================================ - */ -static JSValue js_cell_json_encode (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - if (argc < 1) - return JS_RaiseDisrupt (ctx, "json.encode requires at least 1 argument"); - - BOOL pretty = argc <= 1 || JS_ToBool (ctx, argv[1]); - JSValue space = pretty ? JS_NewInt32 (ctx, 2) : JS_NULL; - JSValue replacer = JS_NULL; - if (argc > 2 && JS_IsFunction (argv[2])) - replacer = argv[2]; - else if (argc > 3 && JS_IsArray (argv[3])) - replacer = argv[3]; - JSValue result = JS_JSONStringify (ctx, argv[0], replacer, space, pretty); - return result; -} - -static JSValue js_cell_json_decode (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - if (argc < 1) - return JS_RaiseDisrupt (ctx, "json.decode requires at least 1 argument"); - - if (!JS_IsText (argv[0])) - return JS_RaiseDisrupt (ctx, "couldn't parse text: not a string"); - - const char *str = JS_ToCString (ctx, argv[0]); - if (!str) return JS_EXCEPTION; - - size_t len = strlen (str); - JSValue result = JS_ParseJSON (ctx, str, len, ""); - JS_FreeCString (ctx, str); - - /* Apply reviver if provided */ - if (argc > 1 && JS_IsFunction (argv[1]) && !JS_IsException (result)) { - /* Create wrapper object to pass to reviver */ - JSValue wrapper = JS_NewObject (ctx); - JS_SetPropertyStr (ctx, wrapper, "", result); - - JSValue holder = wrapper; - JSValue key = JS_KEY_empty; - JSValue args[2] = { key, JS_GetProperty (ctx, holder, key) }; - JSValue final = JS_Call (ctx, argv[1], holder, 2, args); - result = final; - } - - return result; -} - -static const JSCFunctionListEntry js_cell_json_funcs[] = { - JS_CFUNC_DEF ("encode", 1, js_cell_json_encode), - JS_CFUNC_DEF ("decode", 1, js_cell_json_decode), -}; - -JSValue js_core_json_use (JSContext *ctx) { - JSGCRef obj_ref; - JS_PushGCRef (ctx, &obj_ref); - obj_ref.val = JS_NewObject (ctx); - JS_SetPropertyFunctionList (ctx, obj_ref.val, js_cell_json_funcs, countof (js_cell_json_funcs)); - JSValue result = obj_ref.val; - JS_PopGCRef (ctx, &obj_ref); - return result; -} - -/* ============================================================================ - * Cell Script Module: nota - * Provides nota.encode() and nota.decode() for NOTA binary serialization - * ============================================================================ - */ - -static int nota_get_arr_len (JSContext *ctx, JSValue arr) { - int64_t len; - JS_GetLength (ctx, arr, &len); - return (int)len; -} - -typedef struct NotaVisitedNode { - JSGCRef ref; - struct NotaVisitedNode *next; -} NotaVisitedNode; - -typedef struct NotaEncodeContext { - JSContext *ctx; - NotaVisitedNode *visited_list; - NotaBuffer nb; - int cycle; - JSGCRef *replacer_ref; /* pointer to GC-rooted ref */ -} NotaEncodeContext; - -static void nota_stack_push (NotaEncodeContext *enc, JSValueConst val) { - NotaVisitedNode *node = (NotaVisitedNode *)sys_malloc (sizeof (NotaVisitedNode)); - JS_PushGCRef (enc->ctx, &node->ref); - node->ref.val = JS_DupValue (enc->ctx, val); - node->next = enc->visited_list; - enc->visited_list = node; -} - -static void nota_stack_pop (NotaEncodeContext *enc) { - NotaVisitedNode *node = enc->visited_list; - enc->visited_list = node->next; - JS_FreeValue (enc->ctx, node->ref.val); - JS_PopGCRef (enc->ctx, &node->ref); - sys_free (node); -} - -static int nota_stack_has (NotaEncodeContext *enc, JSValueConst val) { - NotaVisitedNode *node = enc->visited_list; - while (node) { - if (JS_StrictEq (enc->ctx, node->ref.val, val)) - return 1; - node = node->next; - } - return 0; -} - -static JSValue nota_apply_replacer (NotaEncodeContext *enc, JSValueConst holder, JSValueConst key, JSValueConst val) { - if (!enc->replacer_ref || JS_IsNull (enc->replacer_ref->val)) return JS_DupValue (enc->ctx, val); - - JSValue args[2] = { JS_DupValue (enc->ctx, key), JS_DupValue (enc->ctx, val) }; - JSValue result = JS_Call (enc->ctx, enc->replacer_ref->val, holder, 2, args); - JS_FreeValue (enc->ctx, args[0]); - JS_FreeValue (enc->ctx, args[1]); - - if (JS_IsException (result)) return JS_DupValue (enc->ctx, val); - return result; -} - -static 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); - sys_free (blob); - break; - case NOTA_TEXT: - nota = nota_read_text (&str, nota); - *tmp = JS_NewString (js, str); - sys_free (str); - break; - case NOTA_ARR: - nota = nota_read_array (&n, nota); - *tmp = JS_NewArrayLen (js, n); - for (int i = 0; i < n; i++) { - nota = js_do_nota_decode (js, &ret2, nota, *tmp, JS_NewInt32 (js, i), reviver); - JS_SetPropertyNumber (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++) { - JSGCRef prop_key_ref, sub_val_ref; - JS_PushGCRef (js, &prop_key_ref); - JS_PushGCRef (js, &sub_val_ref); - nota = nota_read_text (&str, nota); - prop_key_ref.val = JS_NewString (js, str); - sub_val_ref.val = JS_NULL; - nota = js_do_nota_decode (js, &sub_val_ref.val, nota, *tmp, prop_key_ref.val, reviver); - JS_SetPropertyStr (js, *tmp, str, sub_val_ref.val); - JS_PopGCRef (js, &sub_val_ref); - JS_PopGCRef (js, &prop_key_ref); - sys_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) { - JSGCRef inner_ref, obj_ref2; - JS_PushGCRef (js, &inner_ref); - JS_PushGCRef (js, &obj_ref2); - inner_ref.val = JS_NULL; - nota = js_do_nota_decode (js, &inner_ref.val, nota, holder, JS_NULL, reviver); - obj_ref2.val = JS_NewObject (js); - if (!JS_IsNull (js->actor_sym)) - JS_SetPropertyKey (js, obj_ref2.val, js->actor_sym, inner_ref.val); - JS_CellStone (js, obj_ref2.val); - *tmp = obj_ref2.val; - JS_PopGCRef (js, &obj_ref2); - JS_PopGCRef (js, &inner_ref); - } 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; - JSGCRef replaced_ref, keys_ref, elem_ref, prop_ref; - JS_PushGCRef (ctx, &replaced_ref); - replaced_ref.val = nota_apply_replacer (enc, holder, key, val); - int tag = JS_VALUE_GET_TAG (replaced_ref.val); - - switch (tag) { - case JS_TAG_INT: - case JS_TAG_FLOAT64: { - double d; - JS_ToFloat64 (ctx, &d, replaced_ref.val); - nota_write_number (&enc->nb, d); - break; - } - case JS_TAG_STRING: { - const char *str = JS_ToCString (ctx, replaced_ref.val); - nota_write_text (&enc->nb, str); - JS_FreeCString (ctx, str); - break; - } - case JS_TAG_BOOL: - if (JS_VALUE_GET_BOOL (replaced_ref.val)) 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_PTR: { - if (JS_IsText (replaced_ref.val)) { - const char *str = JS_ToCString (ctx, replaced_ref.val); - nota_write_text (&enc->nb, str); - JS_FreeCString (ctx, str); - break; - } - - if (js_is_blob (ctx, replaced_ref.val)) { - size_t buf_len; - void *buf_data = js_get_blob_data (ctx, &buf_len, replaced_ref.val); - if (buf_data == (void *)-1) { - JS_PopGCRef (ctx, &replaced_ref); - return; - } - nota_write_blob (&enc->nb, (unsigned long long)buf_len * 8, (const char *)buf_data); - break; - } - - if (JS_IsArray (replaced_ref.val)) { - if (nota_stack_has (enc, replaced_ref.val)) { - enc->cycle = 1; - break; - } - nota_stack_push (enc, replaced_ref.val); - int arr_len = nota_get_arr_len (ctx, replaced_ref.val); - nota_write_array (&enc->nb, arr_len); - JS_PushGCRef (ctx, &elem_ref); - for (int i = 0; i < arr_len; i++) { - elem_ref.val = JS_GetPropertyNumber (ctx, replaced_ref.val, i); - JSValue elem_key = JS_NewInt32 (ctx, i); - nota_encode_value (enc, elem_ref.val, replaced_ref.val, elem_key); - } - JS_PopGCRef (ctx, &elem_ref); - nota_stack_pop (enc); - break; - } - - JSValue adata = JS_NULL; - if (!JS_IsNull (ctx->actor_sym)) { - int has = JS_HasPropertyKey (ctx, replaced_ref.val, ctx->actor_sym); - if (has > 0) adata = JS_GetPropertyKey (ctx, replaced_ref.val, ctx->actor_sym); - } - if (!JS_IsNull (adata)) { - nota_write_sym (&enc->nb, NOTA_PRIVATE); - nota_encode_value (enc, adata, replaced_ref.val, JS_NULL); - JS_FreeValue (ctx, adata); - break; - } - JS_FreeValue (ctx, adata); - if (nota_stack_has (enc, replaced_ref.val)) { - enc->cycle = 1; - break; - } - nota_stack_push (enc, replaced_ref.val); - - JSValue to_json = JS_GetPropertyStr (ctx, replaced_ref.val, "toJSON"); - if (JS_IsFunction (to_json)) { - JSValue result = JS_Call (ctx, to_json, replaced_ref.val, 0, NULL); - if (!JS_IsException (result)) { - nota_encode_value (enc, result, holder, key); - } else { - nota_write_sym (&enc->nb, NOTA_NULL); - } - nota_stack_pop (enc); - break; - } - - JS_PushGCRef (ctx, &keys_ref); - keys_ref.val = JS_GetOwnPropertyNames (ctx, replaced_ref.val); - if (JS_IsException (keys_ref.val)) { - nota_write_sym (&enc->nb, NOTA_NULL); - nota_stack_pop (enc); - JS_PopGCRef (ctx, &keys_ref); - break; - } - int64_t plen64; - if (JS_GetLength (ctx, keys_ref.val, &plen64) < 0) { - nota_write_sym (&enc->nb, NOTA_NULL); - nota_stack_pop (enc); - JS_PopGCRef (ctx, &keys_ref); - break; - } - uint32_t plen = (uint32_t)plen64; - - JS_PushGCRef (ctx, &prop_ref); - JS_PushGCRef (ctx, &elem_ref); - uint32_t non_function_count = 0; - for (uint32_t i = 0; i < plen; i++) { - elem_ref.val = JS_GetPropertyNumber (ctx, keys_ref.val, i); - prop_ref.val = JS_GetProperty (ctx, replaced_ref.val, elem_ref.val); - if (!JS_IsFunction (prop_ref.val)) non_function_count++; - } - - nota_write_record (&enc->nb, non_function_count); - for (uint32_t i = 0; i < plen; i++) { - elem_ref.val = JS_GetPropertyNumber (ctx, keys_ref.val, i); - prop_ref.val = JS_GetProperty (ctx, replaced_ref.val, elem_ref.val); - if (!JS_IsFunction (prop_ref.val)) { - const char *prop_name = JS_ToCString (ctx, elem_ref.val); - nota_write_text (&enc->nb, prop_name ? prop_name : ""); - nota_encode_value (enc, prop_ref.val, replaced_ref.val, elem_ref.val); - JS_FreeCString (ctx, prop_name); - } - } - JS_PopGCRef (ctx, &elem_ref); - JS_PopGCRef (ctx, &prop_ref); - JS_PopGCRef (ctx, &keys_ref); - nota_stack_pop (enc); - break; - } - default: - nota_write_sym (&enc->nb, NOTA_NULL); - break; - } - JS_PopGCRef (ctx, &replaced_ref); -} - -void *value2nota (JSContext *ctx, JSValue v) { - JSGCRef val_ref, key_ref; - JS_PushGCRef (ctx, &val_ref); - JS_PushGCRef (ctx, &key_ref); - val_ref.val = v; - - NotaEncodeContext enc_s, *enc = &enc_s; - enc->ctx = ctx; - enc->visited_list = NULL; - enc->cycle = 0; - enc->replacer_ref = NULL; - - nota_buffer_init (&enc->nb, 128); - key_ref.val = JS_NewString (ctx, ""); - nota_encode_value (enc, val_ref.val, JS_NULL, key_ref.val); - - if (enc->cycle) { - JS_PopGCRef (ctx, &key_ref); - JS_PopGCRef (ctx, &val_ref); - nota_buffer_free (&enc->nb); - return NULL; - } - - JS_PopGCRef (ctx, &key_ref); - JS_PopGCRef (ctx, &val_ref); - 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; - JSGCRef holder_ref, key_ref, ret_ref; - JS_PushGCRef (js, &holder_ref); - JS_PushGCRef (js, &key_ref); - JS_PushGCRef (js, &ret_ref); - holder_ref.val = JS_NewObject (js); - key_ref.val = JS_NewString (js, ""); - ret_ref.val = JS_NULL; - js_do_nota_decode (js, &ret_ref.val, nota, holder_ref.val, key_ref.val, JS_NULL); - JSValue result = ret_ref.val; - JS_PopGCRef (js, &ret_ref); - JS_PopGCRef (js, &key_ref); - JS_PopGCRef (js, &holder_ref); - return result; -} - -static JSValue js_nota_encode (JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { - if (argc < 1) return JS_RaiseDisrupt (ctx, "nota.encode requires at least 1 argument"); - - JSGCRef val_ref, replacer_ref, key_ref; - JS_PushGCRef (ctx, &val_ref); - JS_PushGCRef (ctx, &replacer_ref); - JS_PushGCRef (ctx, &key_ref); - val_ref.val = argv[0]; - replacer_ref.val = (argc > 1 && JS_IsFunction (argv[1])) ? argv[1] : JS_NULL; - - NotaEncodeContext enc_s, *enc = &enc_s; - enc->ctx = ctx; - enc->visited_list = NULL; - enc->cycle = 0; - enc->replacer_ref = &replacer_ref; - - nota_buffer_init (&enc->nb, 128); - key_ref.val = JS_NewString (ctx, ""); - nota_encode_value (enc, val_ref.val, JS_NULL, key_ref.val); - - JSValue ret; - if (enc->cycle) { - nota_buffer_free (&enc->nb); - ret = JS_RaiseDisrupt (ctx, "Tried to encode something to nota with a cycle."); - } else { - size_t total_len = enc->nb.size; - void *data_ptr = enc->nb.data; - ret = js_new_blob_stoned_copy (ctx, (uint8_t *)data_ptr, total_len); - nota_buffer_free (&enc->nb); - } - - JS_PopGCRef (ctx, &key_ref); - JS_PopGCRef (ctx, &replacer_ref); - JS_PopGCRef (ctx, &val_ref); - 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 == (unsigned char *)-1) return JS_EXCEPTION; - if (!nota) return JS_NULL; - - JSGCRef holder_ref, key_ref, ret_ref, reviver_ref; - JS_PushGCRef (js, &holder_ref); - JS_PushGCRef (js, &key_ref); - JS_PushGCRef (js, &ret_ref); - JS_PushGCRef (js, &reviver_ref); - - reviver_ref.val = (argc > 1 && JS_IsFunction (argv[1])) ? argv[1] : JS_NULL; - holder_ref.val = JS_NewObject (js); - key_ref.val = JS_NewString (js, ""); - ret_ref.val = JS_NULL; - - js_do_nota_decode (js, &ret_ref.val, (char *)nota, holder_ref.val, key_ref.val, reviver_ref.val); - - JSValue result = ret_ref.val; - JS_PopGCRef (js, &reviver_ref); - JS_PopGCRef (js, &ret_ref); - JS_PopGCRef (js, &key_ref); - JS_PopGCRef (js, &holder_ref); - return result; -} - -static const JSCFunctionListEntry js_nota_funcs[] = { - JS_CFUNC_DEF ("encode", 1, js_nota_encode), - JS_CFUNC_DEF ("decode", 1, js_nota_decode), -}; - -JSValue js_core_nota_use (JSContext *js) { - JSGCRef export_ref; - JS_PushGCRef (js, &export_ref); - export_ref.val = JS_NewObject (js); - JS_SetPropertyFunctionList (js, export_ref.val, js_nota_funcs, sizeof (js_nota_funcs) / sizeof (JSCFunctionListEntry)); - JSValue result = export_ref.val; - JS_PopGCRef (js, &export_ref); - return result; -} - -/* ============================================================================ - * Cell Script Module: wota - * Provides wota.encode() and wota.decode() for WOTA binary serialization - * ============================================================================ - */ - -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) { - (void)enc; (void)val; - /* Cycle detection disabled for performance */ -} - -static void wota_stack_pop (WotaEncodeContext *enc) { - if (!enc->visited_stack) return; - - ObjectRef *top = enc->visited_stack; - enc->visited_stack = top->next; - sys_free (top); -} - -static int wota_stack_has (WotaEncodeContext *enc, JSValueConst val) { - (void)enc; (void)val; - return 0; - /* Cycle detection disabled for performance */ -} - -static void wota_stack_free (WotaEncodeContext *enc) { - while (enc->visited_stack) { - wota_stack_pop (enc); - } -} - -static JSValue wota_apply_replacer (WotaEncodeContext *enc, JSValueConst holder, JSValue key, JSValueConst val) { - if (JS_IsNull (enc->replacer)) return JS_DupValue (enc->ctx, val); - JSValue key_val = JS_IsNull (key) ? JS_NULL : JS_DupValue (enc->ctx, key); - JSValue args[2] = { key_val, JS_DupValue (enc->ctx, val) }; - 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, JSValue key); - -static void encode_object_properties (WotaEncodeContext *enc, JSValueConst val, JSValueConst holder) { - JSContext *ctx = enc->ctx; - - /* Root the input value to protect it during property enumeration */ - JSGCRef val_ref, keys_ref; - JS_PushGCRef (ctx, &val_ref); - JS_PushGCRef (ctx, &keys_ref); - val_ref.val = JS_DupValue (ctx, val); - - keys_ref.val = JS_GetOwnPropertyNames (ctx, val_ref.val); - if (JS_IsException (keys_ref.val)) { - wota_write_sym (&enc->wb, WOTA_NULL); - JS_FreeValue (ctx, val_ref.val); - JS_PopGCRef (ctx, &keys_ref); - JS_PopGCRef (ctx, &val_ref); - return; - } - int64_t plen64; - if (JS_GetLength (ctx, keys_ref.val, &plen64) < 0) { - JS_FreeValue (ctx, keys_ref.val); - JS_FreeValue (ctx, val_ref.val); - wota_write_sym (&enc->wb, WOTA_NULL); - JS_PopGCRef (ctx, &keys_ref); - JS_PopGCRef (ctx, &val_ref); - return; - } - uint32_t plen = (uint32_t)plen64; - uint32_t non_function_count = 0; - - /* Allocate GC-rooted arrays for props and keys */ - JSGCRef *prop_refs = sys_malloc (sizeof (JSGCRef) * plen); - JSGCRef *key_refs = sys_malloc (sizeof (JSGCRef) * plen); - for (uint32_t i = 0; i < plen; i++) { - JS_PushGCRef (ctx, &prop_refs[i]); - JS_PushGCRef (ctx, &key_refs[i]); - prop_refs[i].val = JS_NULL; - key_refs[i].val = JS_NULL; - } - - for (uint32_t i = 0; i < plen; i++) { - /* Store key into its GCRef slot immediately so it's rooted before - JS_GetProperty can trigger GC and relocate the string. */ - key_refs[i].val = JS_GetPropertyNumber (ctx, keys_ref.val, i); - JSValue prop_val = JS_GetProperty (ctx, val_ref.val, key_refs[i].val); - if (!JS_IsFunction (prop_val)) { - if (i != non_function_count) { - key_refs[non_function_count].val = key_refs[i].val; - key_refs[i].val = JS_NULL; - } - prop_refs[non_function_count].val = prop_val; - non_function_count++; - } else { - JS_FreeValue (ctx, prop_val); - JS_FreeValue (ctx, key_refs[i].val); - key_refs[i].val = JS_NULL; - } - } - JS_FreeValue (ctx, keys_ref.val); - wota_write_record (&enc->wb, non_function_count); - for (uint32_t i = 0; i < non_function_count; i++) { - size_t klen; - const char *prop_name = JS_ToCStringLen (ctx, &klen, key_refs[i].val); - wota_write_text_len (&enc->wb, prop_name ? prop_name : "", prop_name ? klen : 0); - wota_encode_value (enc, prop_refs[i].val, val_ref.val, key_refs[i].val); - JS_FreeCString (ctx, prop_name); - JS_FreeValue (ctx, prop_refs[i].val); - JS_FreeValue (ctx, key_refs[i].val); - } - /* Pop all GC refs in reverse order */ - for (int i = plen - 1; i >= 0; i--) { - JS_PopGCRef (ctx, &key_refs[i]); - JS_PopGCRef (ctx, &prop_refs[i]); - } - sys_free (prop_refs); - sys_free (key_refs); - JS_FreeValue (ctx, val_ref.val); - JS_PopGCRef (ctx, &keys_ref); - JS_PopGCRef (ctx, &val_ref); -} - -static void wota_encode_value (WotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValue key) { - JSContext *ctx = enc->ctx; - JSValue replaced; - if (!JS_IsNull (enc->replacer) && !JS_IsNull (key)) - replaced = wota_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: { - 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_PTR: { - if (JS_IsText (replaced)) { - 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; - } - 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; - } - 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 (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_GetPropertyNumber (ctx, replaced, i); - wota_encode_value (enc, elem_val, replaced, JS_NewInt32 (ctx, (int32_t)i)); - JS_FreeValue (ctx, elem_val); - } - wota_stack_pop (enc); - break; - } - JSValue adata = JS_NULL; - if (!JS_IsNull (ctx->actor_sym)) { - int has = JS_HasPropertyKey (ctx, replaced, ctx->actor_sym); - if (has > 0) adata = JS_GetPropertyKey (ctx, replaced, ctx->actor_sym); - } - if (!JS_IsNull (adata)) { - wota_write_sym (&enc->wb, WOTA_PRIVATE); - wota_encode_value (enc, adata, replaced, JS_NULL); - 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 (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, JSValue 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) { - JSGCRef inner_ref, obj_ref2; - JS_PushGCRef (ctx, &inner_ref); - JS_PushGCRef (ctx, &obj_ref2); - inner_ref.val = JS_NULL; - data_ptr = decode_wota_value (ctx, data_ptr, &inner_ref.val, holder, JS_NULL, reviver); - obj_ref2.val = JS_NewObject (ctx); - if (!JS_IsNull (ctx->actor_sym)) - JS_SetPropertyKey (ctx, obj_ref2.val, ctx->actor_sym, inner_ref.val); - JS_CellStone (ctx, obj_ref2.val); - *out_val = obj_ref2.val; - JS_PopGCRef (ctx, &obj_ref2); - JS_PopGCRef (ctx, &inner_ref); - } 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) sys_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) sys_free (utf8); - break; - } - case WOTA_ARR: { - long long c; - data_ptr = wota_read_array (&c, data_ptr); - JSGCRef arr_ref; - JS_PushGCRef (ctx, &arr_ref); - arr_ref.val = JS_NewArrayLen (ctx, c); - for (long long i = 0; i < c; i++) { - JSGCRef elem_ref; - JS_PushGCRef (ctx, &elem_ref); - elem_ref.val = JS_NULL; - JSValue idx_key = JS_NewInt32 (ctx, (int32_t)i); - data_ptr = decode_wota_value (ctx, data_ptr, &elem_ref.val, arr_ref.val, idx_key, reviver); - JS_SetPropertyNumber (ctx, arr_ref.val, i, elem_ref.val); - JS_PopGCRef (ctx, &elem_ref); - } - *out_val = arr_ref.val; - JS_PopGCRef (ctx, &arr_ref); - break; - } - case WOTA_REC: { - long long c; - data_ptr = wota_read_record (&c, data_ptr); - JSGCRef obj_ref; - JS_PushGCRef (ctx, &obj_ref); - obj_ref.val = 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; - JSGCRef prop_key_ref, sub_val_ref; - JS_PushGCRef (ctx, &prop_key_ref); - JS_PushGCRef (ctx, &sub_val_ref); - prop_key_ref.val = JS_NewStringLen (ctx, tkey, key_len); - sub_val_ref.val = JS_NULL; - data_ptr = decode_wota_value (ctx, data_ptr, &sub_val_ref.val, obj_ref.val, prop_key_ref.val, reviver); - JS_SetPropertyStr (ctx, obj_ref.val, tkey, sub_val_ref.val); - JS_PopGCRef (ctx, &sub_val_ref); - JS_PopGCRef (ctx, &prop_key_ref); - sys_free (tkey); - } - *out_val = obj_ref.val; - JS_PopGCRef (ctx, &obj_ref); - break; - } - default: - data_ptr += 8; - *out_val = JS_NULL; - break; - } - if (!JS_IsNull (reviver)) { - JSValue key_val = JS_IsNull (key) ? JS_NULL : JS_DupValue (ctx, key); - JSValue args[2] = { key_val, JS_DupValue (ctx, *out_val) }; - JSValue 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) { - JSGCRef val_ref, rep_ref; - JS_PushGCRef (ctx, &val_ref); - JS_PushGCRef (ctx, &rep_ref); - val_ref.val = v; - rep_ref.val = replacer; - - WotaEncodeContext enc_s, *enc = &enc_s; - - enc->ctx = ctx; - enc->visited_stack = NULL; - enc->cycle = 0; - enc->replacer = rep_ref.val; - wota_buffer_init (&enc->wb, 16); - wota_encode_value (enc, val_ref.val, JS_NULL, JS_NULL); - if (enc->cycle) { - wota_stack_free (enc); - wota_buffer_free (&enc->wb); - JS_PopGCRef (ctx, &rep_ref); - JS_PopGCRef (ctx, &val_ref); - return NULL; - } - wota_stack_free (enc); - size_t total_bytes = enc->wb.size * sizeof (uint64_t); - void *wota = sys_realloc (enc->wb.data, total_bytes); - if (bytes) *bytes = total_bytes; - JS_PopGCRef (ctx, &rep_ref); - JS_PopGCRef (ctx, &val_ref); - return wota; -} - -JSValue wota2value (JSContext *ctx, void *wota) { - JSGCRef holder_ref, result_ref; - JS_PushGCRef (ctx, &holder_ref); - JS_PushGCRef (ctx, &result_ref); - result_ref.val = JS_NULL; - holder_ref.val = JS_NewObject (ctx); - decode_wota_value (ctx, wota, &result_ref.val, holder_ref.val, JS_NULL, JS_NULL); - JSValue result = result_ref.val; - JS_PopGCRef (ctx, &result_ref); - JS_PopGCRef (ctx, &holder_ref); - return result; -} - -static JSValue js_wota_encode (JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { - if (argc < 1) return JS_RaiseDisrupt (ctx, "wota.encode requires at least 1 argument"); - size_t total_bytes; - void *wota = value2wota (ctx, argv[0], JS_IsFunction (argv[1]) ? argv[1] : JS_NULL, &total_bytes); - JSValue ret = js_new_blob_stoned_copy (ctx, wota, total_bytes); - sys_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_RaiseDisrupt (ctx, "No blob data present"); - JSValue reviver = (argc > 1 && JS_IsFunction (argv[1])) ? argv[1] : JS_NULL; - char *data_ptr = (char *)buf; - JSGCRef result_ref, holder_ref, empty_key_ref; - JS_PushGCRef (ctx, &result_ref); - JS_PushGCRef (ctx, &holder_ref); - JS_PushGCRef (ctx, &empty_key_ref); - result_ref.val = JS_NULL; - holder_ref.val = JS_NewObject (ctx); - empty_key_ref.val = JS_NewString (ctx, ""); - decode_wota_value (ctx, data_ptr, &result_ref.val, holder_ref.val, empty_key_ref.val, reviver); - JSValue result = result_ref.val; - JS_PopGCRef (ctx, &empty_key_ref); - JS_PopGCRef (ctx, &holder_ref); - JS_PopGCRef (ctx, &result_ref); - 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_core_wota_use (JSContext *ctx) { - JSGCRef exports_ref; - JS_PushGCRef (ctx, &exports_ref); - exports_ref.val = JS_NewObject (ctx); - JS_SetPropertyFunctionList (ctx, exports_ref.val, js_wota_funcs, sizeof (js_wota_funcs) / sizeof (js_wota_funcs[0])); - JSValue result = exports_ref.val; - JS_PopGCRef (ctx, &exports_ref); - return result; -} - -static JSValue js_math_e (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double power = 1.0; - if (argc > 0 && !JS_IsNull (argv[0])) { - if (JS_ToFloat64 (ctx, &power, argv[0]) < 0) return JS_EXCEPTION; - } - return JS_NewFloat64 (ctx, exp (power)); -} - -/* ============================================================================ - * Cell Script Module: math/radians - * Provides trigonometric and math functions using radians - * ============================================================================ - */ - -static JSValue js_math_rad_arc_cosine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, acos (x)); -} - -static JSValue js_math_rad_arc_sine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, asin (x)); -} - -static JSValue js_math_rad_arc_tangent (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, atan (x)); -} - -static JSValue js_math_rad_cosine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, cos (x)); -} - -static JSValue js_math_rad_sine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, sin (x)); -} - -static JSValue js_math_rad_tangent (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, tan (x)); -} - -static JSValue js_math_ln (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, log (x)); -} - -static JSValue js_math_log10 (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, log10 (x)); -} - -static JSValue js_math_log2 (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, log2 (x)); -} - -static JSValue js_math_power (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x, y; - if (argc < 2) return JS_NULL; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - if (JS_ToFloat64 (ctx, &y, argv[1]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, pow (x, y)); -} - -static JSValue js_math_root (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x, n; - if (argc < 2) return JS_NULL; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - if (JS_ToFloat64 (ctx, &n, argv[1]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, pow (x, 1.0 / n)); -} - -static JSValue js_math_sqrt (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, sqrt (x)); -} - -static const JSCFunctionListEntry js_math_radians_funcs[] - = { JS_CFUNC_DEF ("arc_cosine", 1, js_math_rad_arc_cosine), - JS_CFUNC_DEF ("arc_sine", 1, js_math_rad_arc_sine), - JS_CFUNC_DEF ("arc_tangent", 1, js_math_rad_arc_tangent), - JS_CFUNC_DEF ("cosine", 1, js_math_rad_cosine), - JS_CFUNC_DEF ("sine", 1, js_math_rad_sine), - JS_CFUNC_DEF ("tangent", 1, js_math_rad_tangent), - JS_CFUNC_DEF ("ln", 1, js_math_ln), - JS_CFUNC_DEF ("log", 1, js_math_log10), - JS_CFUNC_DEF ("log2", 1, js_math_log2), - JS_CFUNC_DEF ("power", 2, js_math_power), - JS_CFUNC_DEF ("root", 2, js_math_root), - JS_CFUNC_DEF ("sqrt", 1, js_math_sqrt), - JS_CFUNC_DEF ("e", 1, js_math_e) }; - -JSValue js_core_math_radians_use (JSContext *ctx) { - JSGCRef obj_ref; - JS_PushGCRef (ctx, &obj_ref); - obj_ref.val = JS_NewObject (ctx); - JS_SetPropertyFunctionList (ctx, obj_ref.val, js_math_radians_funcs, countof (js_math_radians_funcs)); - JSValue result = obj_ref.val; - JS_PopGCRef (ctx, &obj_ref); - return result; -} - -/* ============================================================================ - * Cell Script Module: math/degrees - * Provides trigonometric and math functions using degrees - * ============================================================================ - */ - -#define DEG2RAD (3.14159265358979323846264338327950288419716939937510 / 180.0) -#define RAD2DEG (180.0 / 3.14159265358979323846264338327950288419716939937510) - -static JSValue js_math_deg_arc_cosine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, acos (x) * RAD2DEG); -} - -static JSValue js_math_deg_arc_sine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, asin (x) * RAD2DEG); -} - -static JSValue js_math_deg_arc_tangent (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, atan (x) * RAD2DEG); -} - -static JSValue js_math_deg_cosine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, cos (x * DEG2RAD)); -} - -static JSValue js_math_deg_sine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, sin (x * DEG2RAD)); -} - -static JSValue js_math_deg_tangent (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, tan (x * DEG2RAD)); -} - -static const JSCFunctionListEntry js_math_degrees_funcs[] - = { JS_CFUNC_DEF ("arc_cosine", 1, js_math_deg_arc_cosine), - JS_CFUNC_DEF ("arc_sine", 1, js_math_deg_arc_sine), - JS_CFUNC_DEF ("arc_tangent", 1, js_math_deg_arc_tangent), - JS_CFUNC_DEF ("cosine", 1, js_math_deg_cosine), - JS_CFUNC_DEF ("sine", 1, js_math_deg_sine), - JS_CFUNC_DEF ("tangent", 1, js_math_deg_tangent), - JS_CFUNC_DEF ("ln", 1, js_math_ln), - JS_CFUNC_DEF ("log", 1, js_math_log10), - JS_CFUNC_DEF ("log2", 1, js_math_log2), - JS_CFUNC_DEF ("power", 2, js_math_power), - JS_CFUNC_DEF ("root", 2, js_math_root), - JS_CFUNC_DEF ("sqrt", 1, js_math_sqrt), - JS_CFUNC_DEF ("e", 1, js_math_e) }; - -JSValue js_core_math_degrees_use (JSContext *ctx) { - JSGCRef obj_ref; - JS_PushGCRef (ctx, &obj_ref); - obj_ref.val = JS_NewObject (ctx); - JS_SetPropertyFunctionList (ctx, obj_ref.val, js_math_degrees_funcs, countof (js_math_degrees_funcs)); - JSValue result = obj_ref.val; - JS_PopGCRef (ctx, &obj_ref); - return result; -} - -/* ============================================================================ - * Cell Script Module: math/cycles - * Provides trigonometric and math functions using cycles (0-1 = full rotation) - * ============================================================================ - */ - -#define TWOPI (2.0 * 3.14159265358979323846264338327950288419716939937510) - -static JSValue js_math_cyc_arc_cosine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, acos (x) / TWOPI); -} - -static JSValue js_math_cyc_arc_sine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, asin (x) / TWOPI); -} - -static JSValue js_math_cyc_arc_tangent (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, atan (x) / TWOPI); -} - -static JSValue js_math_cyc_cosine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, cos (x * TWOPI)); -} - -static JSValue js_math_cyc_sine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, sin (x * TWOPI)); -} - -static JSValue js_math_cyc_tangent (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { - double x; - if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; - return JS_NewFloat64 (ctx, tan (x * TWOPI)); -} - -static const JSCFunctionListEntry js_math_cycles_funcs[] - = { JS_CFUNC_DEF ("arc_cosine", 1, js_math_cyc_arc_cosine), - JS_CFUNC_DEF ("arc_sine", 1, js_math_cyc_arc_sine), - JS_CFUNC_DEF ("arc_tangent", 1, js_math_cyc_arc_tangent), - JS_CFUNC_DEF ("cosine", 1, js_math_cyc_cosine), - JS_CFUNC_DEF ("sine", 1, js_math_cyc_sine), - JS_CFUNC_DEF ("tangent", 1, js_math_cyc_tangent), - JS_CFUNC_DEF ("ln", 1, js_math_ln), - JS_CFUNC_DEF ("log", 1, js_math_log10), - JS_CFUNC_DEF ("log2", 1, js_math_log2), - JS_CFUNC_DEF ("power", 2, js_math_power), - JS_CFUNC_DEF ("root", 2, js_math_root), - JS_CFUNC_DEF ("sqrt", 1, js_math_sqrt), - JS_CFUNC_DEF ("e", 1, js_math_e) }; - -JSValue js_core_math_cycles_use (JSContext *ctx) { - JSGCRef obj_ref; - JS_PushGCRef (ctx, &obj_ref); - obj_ref.val = JS_NewObject (ctx); - JS_SetPropertyFunctionList (ctx, obj_ref.val, js_math_cycles_funcs, countof (js_math_cycles_funcs)); - JSValue result = obj_ref.val; - JS_PopGCRef (ctx, &obj_ref); - return result; -} /* Public API: get stack trace as JS array of {fn, file, line, col} objects. Does NOT clear reg_current_frame — caller is responsible if needed. Two-phase approach for GC safety: Phase 1 collects raw C data on the C stack diff --git a/src/cell_math.c b/src/cell_math.c new file mode 100644 index 00000000..59c79ef8 --- /dev/null +++ b/src/cell_math.c @@ -0,0 +1,50 @@ +#include "cell_math.h" +#include + +JSValue js_math_e (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double power = 1.0; + if (argc > 0 && !JS_IsNull (argv[0])) { + if (JS_ToFloat64 (ctx, &power, argv[0]) < 0) return JS_EXCEPTION; + } + return JS_NewFloat64 (ctx, exp (power)); +} + +JSValue js_math_ln (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, log (x)); +} + +JSValue js_math_log10 (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, log10 (x)); +} + +JSValue js_math_log2 (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, log2 (x)); +} + +JSValue js_math_power (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x, y; + if (argc < 2) return JS_NULL; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + if (JS_ToFloat64 (ctx, &y, argv[1]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, pow (x, y)); +} + +JSValue js_math_root (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x, n; + if (argc < 2) return JS_NULL; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + if (JS_ToFloat64 (ctx, &n, argv[1]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, pow (x, 1.0 / n)); +} + +JSValue js_math_sqrt (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + double x; + if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION; + return JS_NewFloat64 (ctx, sqrt (x)); +} diff --git a/src/cell_math.h b/src/cell_math.h new file mode 100644 index 00000000..acd6970d --- /dev/null +++ b/src/cell_math.h @@ -0,0 +1,14 @@ +#ifndef CELL_MATH_H +#define CELL_MATH_H + +#include "cell.h" + +JSValue js_math_e (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); +JSValue js_math_ln (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); +JSValue js_math_log10 (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); +JSValue js_math_log2 (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); +JSValue js_math_power (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); +JSValue js_math_root (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); +JSValue js_math_sqrt (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); + +#endif diff --git a/fash.c b/src/fash.c similarity index 100% rename from fash.c rename to src/fash.c diff --git a/qbe_rt.c b/src/qbe_rt.c similarity index 100% rename from qbe_rt.c rename to src/qbe_rt.c diff --git a/streamline.cm b/streamline.cm index a53f1106..97d28baa 100644 --- a/streamline.cm +++ b/streamline.cm @@ -1949,7 +1949,7 @@ var streamline = function(ir, log) { known = cur_types[callee_slot] if (known == T_NULL) { emit("error", line, col, "invoking null — will always disrupt") - } else if (known != null && known != T_UNKNOWN && known != T_FUNCTION) { + } else if (known != null && known != T_UNKNOWN && known != T_FUNCTION && known != T_RECORD) { emit("error", line, col, `invoking ${known} — will always disrupt`) } } diff --git a/test.ce b/test.ce index 16112456..6cfe1a84 100644 --- a/test.ce +++ b/test.ce @@ -19,7 +19,7 @@ var gc_after_each_test = false var verify_ir = false var diff_mode = false -var os_ref = use('os') +var os_ref = use('internal/os') var analyze = os_ref.analyze var run_ast_fn = os_ref.run_ast_fn var run_ast_noopt_fn = os_ref.run_ast_noopt_fn diff --git a/tests/blob.cm b/tests/blob.cm index b8671b12..65a83cf1 100644 --- a/tests/blob.cm +++ b/tests/blob.cm @@ -1,5 +1,5 @@ var Blob = use('blob'); -var os = use('os'); +var os = use('internal/os'); function assert(condition, message) { if (!condition) disrupt diff --git a/tests/kim.cm b/tests/kim.cm index d5d4ca1b..7743aac8 100644 --- a/tests/kim.cm +++ b/tests/kim.cm @@ -1,4 +1,4 @@ -var kim = use("kim"); +var kim = use("internal/kim"); var blob = use('blob') return { diff --git a/tests/nota.cm b/tests/nota.cm index 75681dc2..d9d5c68a 100644 --- a/tests/nota.cm +++ b/tests/nota.cm @@ -1,5 +1,5 @@ -var nota = use('nota'); -var os = use('os'); +var nota = use('internal/nota'); +var os = use('internal/os'); var blob = use('blob') var EPSILON = 1e-12 diff --git a/tests/wota.cm b/tests/wota.cm index 5e40bf19..ae01b10d 100644 --- a/tests/wota.cm +++ b/tests/wota.cm @@ -1,5 +1,5 @@ -var wota = use('wota') -var os = use('os') +var wota = use('internal/wota') +var os = use('internal/os') var blob = use('blob') var math = use('math/radians') diff --git a/update.ce b/update.ce index 3c40e2f7..ceb395c5 100644 --- a/update.ce +++ b/update.ce @@ -14,7 +14,7 @@ var shop = use('internal/shop') var build = use('build') var fd = use('fd') -var os = use('os') +var os = use('internal/os') var target_pkg = null var run_build = false From 2f41f58521526be1f9038df6bac3e09592eb1445 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 20 Feb 2026 14:35:48 -0600 Subject: [PATCH 4/4] update docs for compile chain --- docs/c-modules.md | 3 + docs/compiler-tools.md | 2 + docs/nota.md | 2 +- docs/shop.md | 2 +- docs/spec/pipeline.md | 3 + docs/spec/streamline.md | 31 ++++++++++ docs/testing.md | 30 ++++++++++ docs/wota.md | 2 +- tests/compile.cm | 129 ++++++++++++++++++++++++++++++++++++++++ vm_suite.ce | 6 +- 10 files changed, 205 insertions(+), 5 deletions(-) create mode 100644 tests/compile.cm diff --git a/docs/c-modules.md b/docs/c-modules.md index e2aa7027..006cf18f 100644 --- a/docs/c-modules.md +++ b/docs/c-modules.md @@ -52,10 +52,13 @@ Where: Examples: - `mypackage/math.c` -> `js_mypackage_math_use` - `gitea.pockle.world/john/lib/render.c` -> `js_gitea_pockle_world_john_lib_render_use` +- `mypackage/internal/helpers.c` -> `js_mypackage_internal_helpers_use` - `mypackage/game.ce` (AOT actor) -> `js_mypackage_game_program` Actor files (`.ce`) use the `_program` suffix instead of `_use`. +Internal modules (in `internal/` subdirectories) follow the same convention — the `internal` directory name becomes part of the symbol. For example, `internal/os.c` in the core package has the symbol `js_core_internal_os_use`. + **Note:** Having both a `.cm` and `.c` file with the same stem at the same scope is a build error. ## Required Headers diff --git a/docs/compiler-tools.md b/docs/compiler-tools.md index e91eb605..774df046 100644 --- a/docs/compiler-tools.md +++ b/docs/compiler-tools.md @@ -72,6 +72,7 @@ cell streamline --stats # summary stats per function cell streamline --ir # human-readable IR cell streamline --check # warnings only cell streamline --types # IR with type annotations +cell streamline --diagnose # compile-time diagnostics ``` | Flag | Description | @@ -81,6 +82,7 @@ cell streamline --types # IR with type annotations | `--ir` | Human-readable canonical IR (same format as `ir_report.ce`) | | `--check` | Warnings only (e.g. `nr_slots > 200` approaching 255 limit) | | `--types` | Optimized IR with inferred type annotations per slot | +| `--diagnose` | Run compile-time diagnostics (type errors and warnings) | Flags can be combined. diff --git a/docs/nota.md b/docs/nota.md index c04047c8..af96de58 100644 --- a/docs/nota.md +++ b/docs/nota.md @@ -5,7 +5,7 @@ weight: 85 type: "docs" --- -Nota is a binary message format developed for use in the Procession Protocol. It provides a compact, JSON-like encoding that supports blobs, text, arrays, records, numbers, and symbols. +Nota is a binary message format developed for use in the Procession Protocol. It provides a compact, JSON-like encoding that supports blobs, text, arrays, records, numbers, and symbols. Nota is an internal module: `use('internal/nota')`. Nota stands for Network Object Transfer Arrangement. diff --git a/docs/shop.md b/docs/shop.md index 73963074..8727f76b 100644 --- a/docs/shop.md +++ b/docs/shop.md @@ -230,4 +230,4 @@ If `shop.toml` is missing or has no `[policy]` section, all methods are enabled | `internal/os.c` | OS intrinsics: dylib ops, internal symbol lookup, embedded modules | | `package.cm` | Package directory detection, alias resolution, file listing | | `link.cm` | Development link management (link.toml read/write) | -| `boot/*.cm.mcode` | Pre-compiled pipeline seeds (tokenize, parse, fold, mcode, bootstrap) | +| `boot/*.cm.mcode` | Pre-compiled pipeline seeds (tokenize, parse, fold, mcode, streamline, bootstrap) | diff --git a/docs/spec/pipeline.md b/docs/spec/pipeline.md index 15d251cb..802ed9e9 100644 --- a/docs/spec/pipeline.md +++ b/docs/spec/pipeline.md @@ -69,6 +69,7 @@ Optimizes the Mcode IR through a series of independent passes. Operates per-func 6. **Move elimination**: Removes self-moves (`move a, a`). 7. **Unreachable elimination**: Nops dead code after `return` until the next label. 8. **Dead jump elimination**: Removes jumps to the immediately following label. +9. **Compile-time diagnostics** (optional): When `_warn` is set on the mcode input, emits errors for provably wrong operations (storing named property on array, invoking null, etc.) and warnings for suspicious patterns (named property access on array/text). The engine aborts compilation if any error-severity diagnostics are emitted. See [Streamline Optimizer](streamline.md) for detailed pass descriptions. @@ -134,6 +135,7 @@ Seeds are used during cold start (empty cache) to compile the pipeline modules f | `mcode.ce --pretty` | Print raw Mcode IR before streamlining | | `streamline.ce --types` | Print streamlined IR with type annotations | | `streamline.ce --stats` | Print IR after streamlining with before/after stats | +| `streamline.ce --diagnose` | Print compile-time diagnostics (type errors and warnings) | ## Test Files @@ -146,3 +148,4 @@ Seeds are used during cold start (empty cache) to compile the pipeline modules f | `qbe_test.ce` | End-to-end QBE IL generation | | `test_intrinsics.cm` | Inlined intrinsic opcodes (is_array, length, push, etc.) | | `test_backward.cm` | Backward type propagation for parameters | +| `tests/compile.cm` | Compile-time diagnostics (type errors and warnings) | diff --git a/docs/spec/streamline.md b/docs/spec/streamline.md index 33e5f0a5..0167dc49 100644 --- a/docs/spec/streamline.md +++ b/docs/spec/streamline.md @@ -178,6 +178,36 @@ Removes `jump L` instructions where `L` is the immediately following label (skip **Nop prefix:** `_nop_dj_` +### 9. diagnose_function (compile-time diagnostics) + +Optional pass that runs when `_warn` is set on the mcode input. Performs a forward type-tracking scan and emits diagnostics for provably wrong operations. Diagnostics are collected in `ir._diagnostics` as `{severity, file, line, col, message}` records. + +This pass does not modify instructions — it only emits diagnostics. + +**Errors** (compilation is aborted): + +| Pattern | Message | +|---------|---------| +| `store_field` on T_ARRAY | storing named property on array | +| `store_index` on T_RECORD | storing numeric index on record | +| `store_field` / `store_index` on T_TEXT | storing property/index on text | +| `push` on T_TEXT / T_RECORD | push on text/record | +| `invoke` on T_NULL / T_INT / T_FLOAT / T_NUM / T_TEXT / T_BOOL / T_ARRAY | invoking null/number/text/bool/array | +| arity mismatch (module imports only) | function expects N arguments, got M | + +**Warnings** (compilation continues): + +| Pattern | Message | +|---------|---------| +| `load_field` on T_ARRAY | named property access on array | +| `load_field` on T_TEXT | named property access on text | +| `load_dynamic` with T_TEXT key on T_RECORD | text key on record | +| `load_dynamic` with T_RECORD / T_ARRAY / T_BOOL / T_NULL key on T_RECORD | record/array/bool/null key on record | + +The engine (`internal/engine.cm`) prints all diagnostics and aborts compilation if any have severity `"error"`. Warnings are printed but do not block compilation. + +**Nop prefix:** none (diagnostics only, does not modify instructions) + ## Pass Composition All passes run in sequence in `optimize_function`: @@ -191,6 +221,7 @@ simplify_booleans eliminate_moves eliminate_unreachable eliminate_dead_jumps +diagnose_function → optional, when _warn is set ``` Each pass is independent and can be commented out for testing or benchmarking. diff --git a/docs/testing.md b/docs/testing.md index 38e98e48..6e5c7dcf 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -192,6 +192,36 @@ Failures saved to tests/fuzz_failures/ Saved failure files are valid `.cm` modules that can be run directly or added to the test suite. +## Compile-Time Diagnostics Tests + +The `tests/compile.cm` test suite verifies that the type checker catches provably wrong operations at compile time. It works by compiling source snippets through the pipeline with `_warn` enabled and checking that the expected diagnostics are emitted. + +```javascript +var shop = use('internal/shop') +var streamline = use('streamline') + +function get_diagnostics(src) { + fd.slurpwrite(tmpfile, stone(blob(src))) + var compiled = shop.mcode_file(tmpfile) + compiled._warn = true + var optimized = streamline(compiled) + if (optimized._diagnostics == null) return [] + return optimized._diagnostics +} +``` + +The suite covers: +- **Store errors**: storing named property on array, numeric index on record, property/index on text, push on text/record +- **Invoke errors**: invoking null, number, text +- **Warnings**: named property access on array/text, record key on record +- **Clean code**: valid operations produce no diagnostics + +Run the compile diagnostics tests with: + +```bash +pit test compile +``` + ## Test File Organization Tests live in the `tests/` directory of a package: diff --git a/docs/wota.md b/docs/wota.md index 3beff594..ba3c19e7 100644 --- a/docs/wota.md +++ b/docs/wota.md @@ -5,7 +5,7 @@ weight: 86 type: "docs" --- -Wota is a binary message format for local inter-process communication. It is similar to Nota but works at word granularity (64-bit words) rather than byte granularity. Wota arrangements are less compact than Nota but faster to arrange and consume. +Wota is a binary message format for local inter-process communication. It is similar to Nota but works at word granularity (64-bit words) rather than byte granularity. Wota arrangements are less compact than Nota but faster to arrange and consume. Wota is an internal module: `use('internal/wota')`. Wota stands for Word Object Transfer Arrangement. diff --git a/tests/compile.cm b/tests/compile.cm new file mode 100644 index 00000000..16f32cab --- /dev/null +++ b/tests/compile.cm @@ -0,0 +1,129 @@ +// Compile-time diagnostics tests — verify the type checker catches errors +var fd = use('fd') +var shop = use('internal/shop') +var streamline = use('streamline') + +var tmpfile = "/tmp/_cell_compile_test.cm" + +function write_source(src) { + fd.slurpwrite(tmpfile, stone(blob(src))) +} + +function get_diagnostics(src) { + write_source(src) + var compiled = shop.mcode_file(tmpfile) + compiled._warn = true + var optimized = streamline(compiled) + if (optimized._diagnostics == null) return [] + return optimized._diagnostics +} + +function has_diagnostic(diags, severity, pattern) { + var i = 0 + while (i < length(diags)) { + if (diags[i].severity == severity && search(diags[i].message, pattern) != null) { + return true + } + i = i + 1 + } + return false +} + +function expect(diags, severity, pattern) { + if (!has_diagnostic(diags, severity, pattern)) { + return `expected ${severity} matching '${pattern}', got ${text(length(diags))} diagnostic(s)` + } + return null +} + +function expect_clean(diags) { + if (length(diags) > 0) { + return `expected no diagnostics, got ${text(length(diags))}` + } + return null +} + +return { + // === Store errors === + + test_store_field_on_array: function() { + var d = get_diagnostics("var a = []\na[\"x\"] = 1") + return expect(d, "error", "storing named property on array") + }, + + test_store_index_on_record: function() { + var d = get_diagnostics("var a = {}\na[1] = 1") + return expect(d, "error", "storing numeric index on record") + }, + + test_store_field_on_text: function() { + var d = get_diagnostics("var s = \"hello\"\ns.x = 1") + return expect(d, "error", "storing property on text") + }, + + test_store_index_on_text: function() { + var d = get_diagnostics("var s = \"hello\"\ns[0] = 1") + return expect(d, "error", "storing index on text") + }, + + test_push_on_text: function() { + var d = get_diagnostics("var s = \"hello\"\ns[] = 1") + return expect(d, "error", "push on text") + }, + + test_push_on_record: function() { + var d = get_diagnostics("var r = {}\nr[] = 1") + return expect(d, "error", "push on record") + }, + + // === Invoke errors === + + test_invoke_null: function() { + var d = get_diagnostics("var x = null\nx()") + return expect(d, "error", "invoking null") + }, + + test_invoke_number: function() { + var d = get_diagnostics("var x = 42\nx()") + return expect(d, "error", "invoking") + }, + + test_invoke_text: function() { + var d = get_diagnostics("var x = \"hello\"\nx()") + return expect(d, "error", "invoking") + }, + + // === Warnings === + + test_field_on_array_warns: function() { + var d = get_diagnostics("var a = [1, 2]\nvar x = a.name") + return expect(d, "warning", "named property access on array") + }, + + test_field_on_text_warns: function() { + var d = get_diagnostics("var s = \"hello\"\nvar x = s.name") + return expect(d, "warning", "named property access on text") + }, + + test_record_key_on_record_warns: function() { + var d = get_diagnostics("var r = {a: 1}\nvar k = {}\nvar x = r[k]") + return expect(d, "warning", "record key on record") + }, + + // === Clean code produces no diagnostics === + + test_clean_array_ops: function() { + var d = get_diagnostics("var a = [1, 2, 3]\nvar x = a[0]\na[1] = 5\na[] = 4") + return expect_clean(d) + }, + + test_clean_record_ops: function() { + var d = get_diagnostics("var r = {a: 1, b: 2}\nvar x = r.a\nr.c = 3") + return expect_clean(d) + }, + + test_clean_function_call: function() { + var d = get_diagnostics("function f(a, b) { return a + b }\nvar x = f(1, 2)") + return expect_clean(d) + } +} diff --git a/vm_suite.ce b/vm_suite.ce index 553456b4..5eaf66de 100644 --- a/vm_suite.ce +++ b/vm_suite.ce @@ -1927,7 +1927,8 @@ run("array for", function() { // ============================================================================ run("array string key disrupts", function() { - if (!should_disrupt(function() { var a = []; a["a"] = 1 })) fail("array should not use string as key") + var f = function(a) { a["a"] = 1 } + if (!should_disrupt(function() { f([]) })) fail("array should not use string as key") }) run("array object key disrupts", function() { @@ -1947,7 +1948,8 @@ run("array array key disrupts", function() { }) run("obj number key disrupts", function() { - if (!should_disrupt(function() { var a = {}; a[1] = 1 })) fail("object should not use number as key") + var f = function(a) { a[1] = 1 } + if (!should_disrupt(function() { f({}) })) fail("object should not use number as key") }) run("obj array key disrupts", function() {