From 08d2bacb1fe00d6453c7d734dad914769798e23b Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Sat, 7 Feb 2026 15:22:18 -0600 Subject: [PATCH 1/3] improve ast parsing time --- source/quickjs.c | 17 +- syntax_suite.ce | 615 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 619 insertions(+), 13 deletions(-) create mode 100644 syntax_suite.ce diff --git a/source/quickjs.c b/source/quickjs.c index 764a6cff..574640f4 100644 --- a/source/quickjs.c +++ b/source/quickjs.c @@ -28316,6 +28316,7 @@ typedef struct ASTParseState { int has_error; int in_disruption; char *decoded_str; /* allocated buffer for decoded string escapes */ + GetLineColCache lc_cache; union { struct { const char *str; @@ -28395,19 +28396,7 @@ static void ast_free_token (ASTParseState *s) { } static void ast_get_line_col (ASTParseState *s, const uint8_t *ptr, int *line, int *col) { - int line_num = 0, col_num = 0; - const uint8_t *p = s->buf_start; - while (p < ptr) { - if (*p == '\n') { - line_num++; - col_num = 0; - } else if (*p < 0x80 || *p >= 0xc0) { - col_num++; - } - p++; - } - *line = line_num; - *col = col_num; + *line = get_line_col_cached (&s->lc_cache, col, ptr); } static cJSON *ast_node (ASTParseState *s, const char *kind, const uint8_t *start_ptr) { @@ -31182,6 +31171,8 @@ char *JS_AST (const char *source, size_t len, const char *filename) { s.function_nr = 1; s.errors = NULL; s.has_error = 0; + s.lc_cache.ptr = s.buf_start; + s.lc_cache.buf_start = s.buf_start; /* Get first token */ ast_next_token (&s); diff --git a/syntax_suite.ce b/syntax_suite.ce new file mode 100644 index 00000000..409779d8 --- /dev/null +++ b/syntax_suite.ce @@ -0,0 +1,615 @@ +// Syntax suite: covers every syntactic feature of cell script +// Run: ./cell --mach-run syntax_suite.ce + +var passed = 0 +var failed = 0 +var error_names = [] +var error_reasons = [] +var fail_msg = "" + +for (var _i = 0; _i < 100; _i++) { + error_names[] = null + error_reasons[] = null +} + +var fail = function(msg) { + fail_msg = msg + disrupt +} + +var assert_eq = function(actual, expected, msg) { + if (actual != expected) fail(msg + " (got=" + text(actual) + " expected=" + text(expected) + ")") +} + +var run = function(name, fn) { + fail_msg = "" + fn() + passed = passed + 1 +} disruption { + error_names[failed] = name + error_reasons[failed] = fail_msg == "" ? "disruption" : fail_msg + failed = failed + 1 +} + +var should_disrupt = function(fn) { + var caught = false + var wrapper = function() { + fn() + } disruption { + caught = true + } + wrapper() + return caught +} + +// === LITERALS === + +run("number literals", function() { + assert_eq(42, 42, "integer") + assert_eq(3.14 > 3, true, "float") + assert_eq(-5, -5, "negative") + assert_eq(0, 0, "zero") + assert_eq(1e3, 1000, "scientific") +}) + +run("string literals", function() { + assert_eq("hello", "hello", "double quote") + assert_eq("", "", "empty string") + assert_eq("line1\nline2" != "line1line2", true, "escape sequence") +}) + +run("template literals", function() { + var x = "world" + assert_eq(`hello ${x}`, "hello world", "interpolation") + assert_eq(`${1 + 2}`, "3", "expression interpolation") + assert_eq(`plain`, "plain", "no interpolation") +}) + +run("boolean literals", function() { + assert_eq(true, true, "true") + assert_eq(false, false, "false") +}) + +run("null literal", function() { + assert_eq(null, null, "null") +}) + +run("array literal", function() { + var a = [1, 2, 3] + assert_eq(length(a), 3, "array length") + assert_eq(a[0], 1, "first element") + var e = [] + assert_eq(length(e), 0, "empty array") +}) + +run("object literal", function() { + var o = {a: 1, b: "two"} + assert_eq(o.a, 1, "object prop") + var e = {} + assert_eq(e.x, null, "empty object missing prop") +}) + +run("regex literal", function() { + var r = /\d+/ + var result = extract("abc123", r) + assert_eq(result[0], "123", "regex match") + var ri = /hello/i + var result2 = extract("Hello", ri) + assert_eq(result2[0], "Hello", "regex flags") +}) + +// === DECLARATIONS === + +run("var declaration", function() { + var x = 5 + assert_eq(x, 5, "var init") +}) + +run("var uninitialized", function() { + var x + assert_eq(x, null, "var defaults to null") +}) + +run("var multiple declaration", function() { + var a = 1, b = 2, c = 3 + assert_eq(a + b + c, 6, "multi var") +}) + +run("def declaration", function() { + def x = 42 + assert_eq(x, 42, "def const") +}) + +// === ARITHMETIC OPERATORS === + +run("arithmetic operators", function() { + assert_eq(2 + 3, 5, "add") + assert_eq(5 - 3, 2, "sub") + assert_eq(3 * 4, 12, "mul") + assert_eq(12 / 4, 3, "div") + assert_eq(10 % 3, 1, "mod") + assert_eq(2 ** 3, 8, "exp") +}) + +// === COMPARISON OPERATORS === + +run("comparison operators", function() { + assert_eq(5 == 5, true, "eq") + assert_eq(5 != 6, true, "neq") + assert_eq(3 < 5, true, "lt") + assert_eq(5 > 3, true, "gt") + assert_eq(3 <= 3, true, "lte") + assert_eq(5 >= 5, true, "gte") +}) + +// === LOGICAL OPERATORS === + +run("logical operators", function() { + assert_eq(true && true, true, "and") + assert_eq(true && false, false, "and false") + assert_eq(false || true, true, "or") + assert_eq(false || false, false, "or false") + assert_eq(!true, false, "not") + assert_eq(!false, true, "not false") +}) + +run("short circuit", function() { + var called = false + var fn = function() { called = true; return true } + var r = false && fn() + assert_eq(called, false, "and short circuit") + r = true || fn() + assert_eq(called, false, "or short circuit") +}) + +// === BITWISE OPERATORS === + +run("bitwise operators", function() { + assert_eq(5 & 3, 1, "and") + assert_eq(5 | 3, 7, "or") + assert_eq(5 ^ 3, 6, "xor") + assert_eq(~0, -1, "not") + assert_eq(1 << 3, 8, "lshift") + assert_eq(8 >> 3, 1, "rshift") + assert_eq(-1 >>> 1, 2147483647, "unsigned rshift") +}) + +// === UNARY OPERATORS === + +run("unary operators", function() { + assert_eq(+5, 5, "unary plus") + assert_eq(-5, -5, "unary minus") + assert_eq(-(-5), 5, "double negate") +}) + +run("increment decrement", function() { + var x = 5 + assert_eq(x++, 5, "postfix inc returns old") + assert_eq(x, 6, "postfix inc side effect") + x = 5 + assert_eq(++x, 6, "prefix inc returns new") + x = 5 + assert_eq(x--, 5, "postfix dec returns old") + assert_eq(x, 4, "postfix dec side effect") + x = 5 + assert_eq(--x, 4, "prefix dec returns new") +}) + +// === COMPOUND ASSIGNMENT === + +run("compound assignment", function() { + var x = 10 + x += 3; assert_eq(x, 13, "+=") + x -= 3; assert_eq(x, 10, "-=") + x *= 2; assert_eq(x, 20, "*=") + x /= 4; assert_eq(x, 5, "/=") + x %= 3; assert_eq(x, 2, "%=") +}) + +// === TERNARY OPERATOR === + +run("ternary operator", function() { + var a = true ? 1 : 2 + assert_eq(a, 1, "ternary true") + var b = false ? 1 : 2 + assert_eq(b, 2, "ternary false") + var c = true ? (false ? 1 : 2) : 3 + assert_eq(c, 2, "ternary nested") +}) + +// === COMMA OPERATOR === + +run("comma operator", function() { + var x = (1, 2, 3) + assert_eq(x, 3, "comma returns last") +}) + +// === IN OPERATOR === + +run("in operator", function() { + var o = {a: 1} + assert_eq("a" in o, true, "key exists") + assert_eq("b" in o, false, "key missing") +}) + +// === DELETE OPERATOR === + +run("delete operator", function() { + var o = {a: 1, b: 2} + delete o.a + assert_eq("a" in o, false, "delete removes key") + assert_eq(o.b, 2, "delete leaves others") +}) + +// === PROPERTY ACCESS === + +run("dot access", function() { + var o = {x: 10} + assert_eq(o.x, 10, "dot read") + o.x = 20 + assert_eq(o.x, 20, "dot write") +}) + +run("bracket access", function() { + var o = {x: 10} + assert_eq(o["x"], 10, "bracket read") + var key = "x" + assert_eq(o[key], 10, "computed bracket") + o["y"] = 20 + assert_eq(o.y, 20, "bracket write") +}) + +run("object-as-key", function() { + var k = {} + var o = {} + o[k] = 42 + assert_eq(o[k], 42, "object key set/get") + assert_eq(o[{}], null, "new object is different key") + assert_eq(k in o, true, "object key in") + delete o[k] + assert_eq(k in o, false, "object key delete") +}) + +run("chained access", function() { + var d = {a: {b: [1, {c: 99}]}} + assert_eq(d.a.b[1].c, 99, "mixed chain") +}) + +// === ARRAY PUSH/POP SYNTAX === + +run("array push pop", function() { + var a = [1, 2] + a[] = 3 + assert_eq(length(a), 3, "push length") + assert_eq(a[2], 3, "push value") + var v = a[] + assert_eq(v, 3, "pop value") + assert_eq(length(a), 2, "pop length") +}) + +// === CONTROL FLOW: IF/ELSE === + +run("if else", function() { + var x = 0 + if (true) x = 1 + assert_eq(x, 1, "if true") + if (false) x = 2 else x = 3 + assert_eq(x, 3, "if else") + if (false) x = 4 + else if (true) x = 5 + else x = 6 + assert_eq(x, 5, "else if") +}) + +// === CONTROL FLOW: WHILE === + +run("while loop", function() { + var i = 0 + while (i < 5) i++ + assert_eq(i, 5, "while basic") +}) + +run("while break continue", function() { + var i = 0 + while (true) { + if (i >= 3) break + i++ + } + assert_eq(i, 3, "while break") + var sum = 0 + i = 0 + while (i < 5) { + i++ + if (i % 2 == 0) continue + sum += i + } + assert_eq(sum, 9, "while continue") +}) + +// === CONTROL FLOW: FOR === + +run("for loop", function() { + var sum = 0 + for (var i = 0; i < 5; i++) sum += i + assert_eq(sum, 10, "for basic") +}) + +run("for break continue", function() { + var sum = 0 + for (var i = 0; i < 10; i++) { + if (i == 5) break + sum += i + } + assert_eq(sum, 10, "for break") + sum = 0 + for (var i = 0; i < 5; i++) { + if (i % 2 == 0) continue + sum += i + } + assert_eq(sum, 4, "for continue") +}) + +run("nested for", function() { + var sum = 0 + for (var i = 0; i < 3; i++) + for (var j = 0; j < 3; j++) + sum++ + assert_eq(sum, 9, "nested for") +}) + +// === BLOCK SCOPING === + +run("block scoping", function() { + var x = 1 + { + var x = 2 + assert_eq(x, 2, "inner block") + } + assert_eq(x, 1, "outer preserved") +}) + +run("for iterator scope", function() { + for (var i = 0; i < 1; i++) {} + assert_eq(should_disrupt(function() { var y = i }), true, "for var does not leak") +}) + +// === FUNCTIONS === + +run("function expression", function() { + var fn = function(a, b) { return a + b } + assert_eq(fn(2, 3), 5, "basic call") +}) + +run("arrow function", function() { + var double = x => x * 2 + assert_eq(double(5), 10, "arrow single param") + var add = (a, b) => a + b + assert_eq(add(2, 3), 5, "arrow multi param") + var block = x => { + var y = x * 2 + return y + 1 + } + assert_eq(block(5), 11, "arrow block body") +}) + +run("function no return", function() { + var fn = function() { var x = 1 } + assert_eq(fn(), null, "no return gives null") +}) + +run("function early return", function() { + var fn = function() { return 1; return 2 } + assert_eq(fn(), 1, "early return") +}) + +run("extra and missing args", function() { + var fn = function(a, b) { return a + b } + assert_eq(fn(1, 2, 3), 3, "extra args ignored") + var fn2 = function(a, b) { return a } + assert_eq(fn2(1), 1, "missing args ok") +}) + +run("iife", function() { + var r = (function(x) { return x * 2 })(21) + assert_eq(r, 42, "immediately invoked") +}) + +// === CLOSURES === + +run("closure", function() { + var make = function(x) { + return function(y) { return x + y } + } + var add5 = make(5) + assert_eq(add5(3), 8, "closure captures") +}) + +run("closure mutation", function() { + var counter = function() { + var n = 0 + return function() { n = n + 1; return n } + } + var c = counter() + assert_eq(c(), 1, "first") + assert_eq(c(), 2, "second") +}) + +// === RECURSION === + +run("recursion", function() { + var fact = function(n) { + if (n <= 1) return 1 + return n * fact(n - 1) + } + assert_eq(fact(5), 120, "factorial") +}) + +// === THIS BINDING === + +run("this binding", function() { + var obj = { + val: 10, + get: function() { return this.val } + } + assert_eq(obj.get(), 10, "method this") +}) + +// === DISRUPTION === + +run("disrupt keyword", function() { + assert_eq(should_disrupt(function() { disrupt }), true, "bare disrupt") +}) + +run("disruption handler", function() { + var x = 0 + var fn = function() { x = 1 } disruption { x = 2 } + fn() + assert_eq(x, 1, "no disruption path") + var fn2 = function() { disrupt } disruption { x = 3 } + fn2() + assert_eq(x, 3, "disruption caught") +}) + +run("disruption re-raise", function() { + var outer_caught = false + var outer = function() { + var inner = function() { disrupt } disruption { disrupt } + inner() + } disruption { + outer_caught = true + } + outer() + assert_eq(outer_caught, true, "re-raise propagates") +}) + +// === PROTOTYPAL INHERITANCE === + +run("meme and proto", function() { + var parent = {x: 10} + var child = meme(parent) + assert_eq(child.x, 10, "inherited prop") + assert_eq(proto(child), parent, "proto returns parent") + child.x = 20 + assert_eq(parent.x, 10, "override does not mutate parent") +}) + +run("meme with mixins", function() { + var p = {a: 1} + var m1 = {b: 2} + var m2 = {c: 3} + var child = meme(p, [m1, m2]) + assert_eq(child.a, 1, "parent prop") + assert_eq(child.b, 2, "mixin1") + assert_eq(child.c, 3, "mixin2") +}) + +// === STONE (FREEZE) === + +run("stone", function() { + var o = {x: 1} + assert_eq(is_stone(o), false, "not frozen") + stone(o) + assert_eq(is_stone(o), true, "frozen") + assert_eq(should_disrupt(function() { o.x = 2 }), true, "write disrupts") +}) + +// === FUNCTION PROXY === + +run("function proxy", function() { + var proxy = function(name, args) { + return `${name}:${length(args)}` + } + assert_eq(proxy.hello(), "hello:0", "proxy dot call") + assert_eq(proxy.add(1, 2), "add:2", "proxy with args") + assert_eq(proxy["method"](), "method:0", "proxy bracket call") + var m = "dynamic" + assert_eq(proxy[m](), "dynamic:0", "proxy computed name") +}) + +run("non-proxy function prop access disrupts", function() { + var fn = function() { return 1 } + assert_eq(should_disrupt(function() { var x = fn.foo }), true, "prop read disrupts") + assert_eq(should_disrupt(function() { fn.foo = 1 }), true, "prop write disrupts") +}) + +// === TYPE CHECKING === + +run("is_* functions", function() { + assert_eq(is_number(42), true, "is_number") + assert_eq(is_text("hi"), true, "is_text") + assert_eq(is_logical(true), true, "is_logical") + assert_eq(is_object({}), true, "is_object") + assert_eq(is_array([]), true, "is_array") + assert_eq(is_function(function(){}), true, "is_function") + assert_eq(is_null(null), true, "is_null") + assert_eq(is_object([]), false, "array not object") + assert_eq(is_array({}), false, "object not array") +}) + +// === TRUTHINESS / FALSINESS === + +run("falsy values", function() { + if (false) fail("false") + if (0) fail("0") + if ("") fail("empty string") + if (null) fail("null") + assert_eq(true, true, "all falsy passed") +}) + +run("truthy values", function() { + if (!1) fail("1") + if (!"hi") fail("string") + if (!{}) fail("object") + if (![]) fail("array") + if (!true) fail("true") + assert_eq(true, true, "all truthy passed") +}) + +// === VARIABLE SHADOWING === + +run("variable shadowing", function() { + var x = 10 + var fn = function() { + var x = 20 + return x + } + assert_eq(fn(), 20, "inner shadows") + assert_eq(x, 10, "outer unchanged") +}) + +// === OPERATOR PRECEDENCE === + +run("precedence", function() { + assert_eq(2 + 3 * 4, 14, "mul before add") + assert_eq((2 + 3) * 4, 20, "parens override") + assert_eq(-2 * 3, -6, "unary before mul") +}) + +// === CURRYING / HIGHER-ORDER === + +run("curried function", function() { + var f = function(a) { + return function(b) { + return function(c) { return a + b + c } + } + } + assert_eq(f(1)(2)(3), 6, "triple curry") +}) + +// === SELF-REFERENCING STRUCTURES === + +run("self-referencing object", function() { + var o = {name: "root"} + o.self = o + assert_eq(o.self.self.name, "root", "cycle access") +}) + +// === SUMMARY === + +print(text(passed) + " passed, " + text(failed) + " failed out of " + text(passed + failed)) +if (failed > 0) { + print("") + for (var _j = 0; _j < failed; _j++) { + print(" FAIL " + error_names[_j] + ": " + error_reasons[_j]) + } +} From 1a925371d3a7e6180eb659f5cf27cd757350b2f7 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Sat, 7 Feb 2026 15:38:36 -0600 Subject: [PATCH 2/3] faster parsing --- source/cell.c | 24 +++++++++++++++++++++--- source/quickjs.c | 31 ++++++++++++------------------- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/source/cell.c b/source/cell.c index faa974fb..b58d513c 100644 --- a/source/cell.c +++ b/source/cell.c @@ -402,8 +402,14 @@ int cell_init(int argc, char **argv) char *json = JS_AST(script, strlen(script), filename); if (json) { int has_errors = print_json_errors(json); - printf("%s\n", json); + cJSON *root = cJSON_Parse(json); free(json); + if (root) { + char *pretty = cJSON_Print(root); + cJSON_Delete(root); + printf("%s\n", pretty); + free(pretty); + } free(allocated_script); return has_errors ? 1 : 0; } else { @@ -445,8 +451,14 @@ int cell_init(int argc, char **argv) char *json = JS_Tokenize(script, strlen(script), filename); if (json) { int has_errors = print_json_errors(json); - printf("%s\n", json); + cJSON *root = cJSON_Parse(json); free(json); + if (root) { + char *pretty = cJSON_Print(root); + cJSON_Delete(root); + printf("%s\n", pretty); + free(pretty); + } free(allocated_script); return has_errors ? 1 : 0; } else { @@ -507,8 +519,14 @@ int cell_init(int argc, char **argv) return 1; } - printf("%s\n", mcode_json); + cJSON *root = cJSON_Parse(mcode_json); free(mcode_json); + if (root) { + char *pretty = cJSON_Print(root); + cJSON_Delete(root); + printf("%s\n", pretty); + free(pretty); + } free(allocated_script); return 0; diff --git a/source/quickjs.c b/source/quickjs.c index 574640f4..87b3693b 100644 --- a/source/quickjs.c +++ b/source/quickjs.c @@ -28342,11 +28342,12 @@ typedef struct ASTParseState { /* Add a length-delimited string to a cJSON object (source pointers aren't null-terminated) */ static void cjson_add_strn (cJSON *obj, const char *key, const char *str, size_t len) { - char *tmp = sys_malloc (len + 1); + char buf[256]; + char *tmp = (len < sizeof (buf)) ? buf : sys_malloc (len + 1); memcpy (tmp, str, len); tmp[len] = '\0'; cJSON_AddStringToObject (obj, key, tmp); - sys_free (tmp); + if (tmp != buf) sys_free (tmp); } /* Compare a length-delimited token string against a null-terminated literal */ @@ -29724,16 +29725,9 @@ static const ASTBinOp ast_binops[] = { { 0, NULL, 0 } }; -static int ast_get_binop_prec (int token) { +static const ASTBinOp *ast_get_binop (int token) { for (int i = 0; ast_binops[i].kind; i++) { - if (ast_binops[i].token == token) return ast_binops[i].prec; - } - return 0; -} - -static const char *ast_get_binop_kind (int token) { - for (int i = 0; ast_binops[i].kind; i++) { - if (ast_binops[i].token == token) return ast_binops[i].kind; + if (ast_binops[i].token == token) return &ast_binops[i]; } return NULL; } @@ -29744,17 +29738,16 @@ static cJSON *ast_parse_binary (ASTParseState *s, int min_prec) { for (;;) { const uint8_t *start = s->token_ptr; - int prec = ast_get_binop_prec (s->token_val); - if (prec == 0 || prec < min_prec) break; + const ASTBinOp *op = ast_get_binop (s->token_val); + if (!op || op->prec < min_prec) break; - const char *kind = ast_get_binop_kind (s->token_val); ast_next_token (s); /* Right associativity for ** */ - int next_prec = (prec == 14) ? prec : prec + 1; + int next_prec = (op->prec == 14) ? op->prec : op->prec + 1; cJSON *right = ast_parse_binary (s, next_prec); - cJSON *node = ast_node (s, kind, start); + cJSON *node = ast_node (s, op->kind, start); cJSON_AddItemToObject (node, "left", left); cJSON_AddItemToObject (node, "right", right); ast_node_end (s, node, s->buf_ptr); @@ -31213,7 +31206,7 @@ char *JS_AST (const char *source, size_t len, const char *filename) { } /* Convert to JSON string */ - char *json = cJSON_Print (ast); + char *json = cJSON_PrintUnformatted (ast); cJSON_Delete (ast); return json; @@ -31316,7 +31309,7 @@ char *JS_Tokenize (const char *source, size_t len, const char *filename) { cJSON_AddItemToObject (root, "errors", s.errors); } - char *json = cJSON_Print (root); + char *json = cJSON_PrintUnformatted (root); cJSON_Delete (root); return json; } @@ -35643,7 +35636,7 @@ char *JS_Mcode (const char *ast_json) { if (s.errors) cJSON_AddItemToObject (mach, "errors", s.errors); - char *json = cJSON_Print (mach); + char *json = cJSON_PrintUnformatted (mach); cJSON_Delete (mach); return json; } From 13a6f6c79d19fbb6b92cfec81c93021d40f12f55 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Sat, 7 Feb 2026 15:49:09 -0600 Subject: [PATCH 3/3] faster mach bytecode generation --- Makefile | 2 +- source/cell.c | 68 ++++++++++++++++++++++++------------------------ source/quickjs.c | 52 ++++++++++++++++++++++++++++-------- source/quickjs.h | 32 ++++++++++++++--------- source/suite.c | 20 +++++++------- 5 files changed, 105 insertions(+), 69 deletions(-) diff --git a/Makefile b/Makefile index 287f1cdc..261bd62a 100755 --- a/Makefile +++ b/Makefile @@ -58,7 +58,7 @@ static: # Bootstrap: build cell from scratch using meson (only needed once) # Also installs core scripts to ~/.cell/core bootstrap: - meson setup build_bootstrap -Dbuildtype=debug -Db_sanitize=address + meson setup build_bootstrap -Dbuildtype=debugoptimized meson compile -C build_bootstrap cp build_bootstrap/cell . cp build_bootstrap/libcell_runtime.dylib . diff --git a/source/cell.c b/source/cell.c index b58d513c..58bde784 100644 --- a/source/cell.c +++ b/source/cell.c @@ -105,15 +105,11 @@ static char* load_core_file(const char *filename, size_t *out_size) { return data; } -static int print_json_errors(const char *json) { - if (!json) return 0; - cJSON *root = cJSON_Parse(json); +static int print_tree_errors(cJSON *root) { if (!root) return 0; cJSON *errors = cJSON_GetObjectItemCaseSensitive(root, "errors"); - if (!cJSON_IsArray(errors) || cJSON_GetArraySize(errors) == 0) { - cJSON_Delete(root); + if (!cJSON_IsArray(errors) || cJSON_GetArraySize(errors) == 0) return 0; - } const char *filename = ""; cJSON *fname = cJSON_GetObjectItemCaseSensitive(root, "filename"); if (cJSON_IsString(fname)) @@ -130,10 +126,18 @@ static int print_json_errors(const char *json) { else if (msg) fprintf(stderr, "%s: error: %s\n", filename, msg); } - cJSON_Delete(root); return 1; } +static int print_json_errors(const char *json) { + if (!json) return 0; + cJSON *root = cJSON_Parse(json); + if (!root) return 0; + int result = print_tree_errors(root); + cJSON_Delete(root); + return result; +} + // Get the core path for use by scripts const char* cell_get_core_path(void) { return core_path; @@ -399,17 +403,13 @@ int cell_init(int argc, char **argv) script = (char *)script_or_file; } - char *json = JS_AST(script, strlen(script), filename); - if (json) { - int has_errors = print_json_errors(json); - cJSON *root = cJSON_Parse(json); - free(json); - if (root) { - char *pretty = cJSON_Print(root); - cJSON_Delete(root); - printf("%s\n", pretty); - free(pretty); - } + cJSON *ast = JS_ASTTree(script, strlen(script), filename); + if (ast) { + int has_errors = print_tree_errors(ast); + char *pretty = cJSON_Print(ast); + cJSON_Delete(ast); + printf("%s\n", pretty); + free(pretty); free(allocated_script); return has_errors ? 1 : 0; } else { @@ -647,15 +647,15 @@ int cell_init(int argc, char **argv) script = (char *)script_or_file; } - char *ast_json = JS_AST(script, strlen(script), filename); - if (!ast_json) { + cJSON *ast = JS_ASTTree(script, strlen(script), filename); + if (!ast) { printf("Failed to parse AST\n"); free(allocated_script); return 1; } - if (print_json_errors(ast_json)) { - free(ast_json); + if (print_tree_errors(ast)) { + cJSON_Delete(ast); free(allocated_script); return 1; } @@ -663,18 +663,18 @@ int cell_init(int argc, char **argv) JSRuntime *rt = JS_NewRuntime(); if (!rt) { printf("Failed to create JS runtime\n"); - free(ast_json); free(allocated_script); + cJSON_Delete(ast); free(allocated_script); return 1; } JSContext *ctx = JS_NewContext(rt); if (!ctx) { printf("Failed to create JS context\n"); - free(ast_json); JS_FreeRuntime(rt); free(allocated_script); + cJSON_Delete(ast); JS_FreeRuntime(rt); free(allocated_script); return 1; } - JS_DumpMach(ctx, ast_json, JS_NULL); - free(ast_json); + JS_DumpMachTree(ctx, ast, JS_NULL); + cJSON_Delete(ast); JS_FreeContext(ctx); JS_FreeRuntime(rt); @@ -711,15 +711,15 @@ int cell_init(int argc, char **argv) script = (char *)script_or_file; } - char *ast_json = JS_AST(script, strlen(script), filename); - if (!ast_json) { + cJSON *ast = JS_ASTTree(script, strlen(script), filename); + if (!ast) { printf("Failed to parse AST\n"); free(allocated_script); return 1; } - if (print_json_errors(ast_json)) { - free(ast_json); + if (print_tree_errors(ast)) { + cJSON_Delete(ast); free(allocated_script); return 1; } @@ -727,18 +727,18 @@ int cell_init(int argc, char **argv) JSRuntime *rt = JS_NewRuntime(); if (!rt) { printf("Failed to create JS runtime\n"); - free(ast_json); free(allocated_script); + cJSON_Delete(ast); free(allocated_script); return 1; } JSContext *ctx = JS_NewContext(rt); if (!ctx) { printf("Failed to create JS context\n"); - free(ast_json); JS_FreeRuntime(rt); free(allocated_script); + cJSON_Delete(ast); JS_FreeRuntime(rt); free(allocated_script); return 1; } - JSValue result = JS_RunMach(ctx, ast_json, JS_NULL); - free(ast_json); + JSValue result = JS_RunMachTree(ctx, ast, JS_NULL); + cJSON_Delete(ast); int exit_code = 0; if (JS_IsException(result)) { diff --git a/source/quickjs.c b/source/quickjs.c index 87b3693b..ce2b4207 100644 --- a/source/quickjs.c +++ b/source/quickjs.c @@ -31153,7 +31153,7 @@ static void ast_semantic_check (cJSON *ast, cJSON **errors_out, *intrinsics_out = intr_arr; } -char *JS_AST (const char *source, size_t len, const char *filename) { +cJSON *JS_ASTTree (const char *source, size_t len, const char *filename) { ASTParseState s; memset (&s, 0, sizeof (s)); @@ -31205,10 +31205,14 @@ char *JS_AST (const char *source, size_t len, const char *filename) { cJSON_AddItemToObject (ast, "errors", sem_errors); } - /* Convert to JSON string */ + return ast; +} + +char *JS_AST (const char *source, size_t len, const char *filename) { + cJSON *ast = JS_ASTTree (source, len, filename); + if (!ast) return NULL; char *json = cJSON_PrintUnformatted (ast); cJSON_Delete (ast); - return json; } @@ -33021,8 +33025,7 @@ static MachCode *mach_compile_program(MachCompState *cs, cJSON *ast) { } /* Public API: compile AST JSON to MachCode (context-free) */ -MachCode *JS_CompileMach(const char *ast_json) { - cJSON *ast = cJSON_Parse(ast_json); +MachCode *JS_CompileMachTree(cJSON *ast) { if (!ast) return NULL; MachCompState cs = {0}; @@ -33036,6 +33039,13 @@ MachCode *JS_CompileMach(const char *ast_json) { sys_free(cs.vars[i].name); sys_free(cs.vars); + return code; +} + +MachCode *JS_CompileMach(const char *ast_json) { + cJSON *ast = cJSON_Parse(ast_json); + if (!ast) return NULL; + MachCode *code = JS_CompileMachTree(ast); cJSON_Delete(ast); return code; } @@ -37386,9 +37396,9 @@ static void dump_register_code(JSContext *ctx, JSCodeRegister *code, int indent) } } -/* Dump MACH bytecode to stdout for debugging. Takes AST JSON. */ -void JS_DumpMach(JSContext *ctx, const char *ast_json, JSValue env) { - MachCode *mc = JS_CompileMach(ast_json); +/* Dump MACH bytecode to stdout for debugging. Takes AST cJSON tree. */ +void JS_DumpMachTree(JSContext *ctx, cJSON *ast, JSValue env) { + MachCode *mc = JS_CompileMachTree(ast); if (!mc) { printf("=== MACH Bytecode ===\nFailed to compile\n=== End MACH Bytecode ===\n"); return; @@ -37400,9 +37410,19 @@ void JS_DumpMach(JSContext *ctx, const char *ast_json, JSValue env) { printf("=== End MACH Bytecode ===\n"); } -/* Compile and execute MACH bytecode. Takes AST JSON. */ -JSValue JS_RunMach(JSContext *ctx, const char *ast_json, JSValue env) { - MachCode *mc = JS_CompileMach(ast_json); +void JS_DumpMach(JSContext *ctx, const char *ast_json, JSValue env) { + cJSON *ast = cJSON_Parse(ast_json); + if (!ast) { + printf("=== MACH Bytecode ===\nFailed to parse\n=== End MACH Bytecode ===\n"); + return; + } + JS_DumpMachTree(ctx, ast, env); + cJSON_Delete(ast); +} + +/* Compile and execute MACH bytecode. Takes AST cJSON tree. */ +JSValue JS_RunMachTree(JSContext *ctx, cJSON *ast, JSValue env) { + MachCode *mc = JS_CompileMachTree(ast); if (!mc) { return JS_ThrowSyntaxError(ctx, "failed to compile AST to MACH bytecode"); } @@ -37411,3 +37431,13 @@ JSValue JS_RunMach(JSContext *ctx, const char *ast_json, JSValue env) { JSValue result = JS_CallRegisterVM(ctx, code, ctx->global_obj, 0, NULL, env, JS_NULL); return result; } + +JSValue JS_RunMach(JSContext *ctx, const char *ast_json, JSValue env) { + cJSON *ast = cJSON_Parse(ast_json); + if (!ast) { + return JS_ThrowSyntaxError(ctx, "failed to parse AST JSON"); + } + JSValue result = JS_RunMachTree(ctx, ast, env); + cJSON_Delete(ast); + return result; +} diff --git a/source/quickjs.h b/source/quickjs.h index b434b527..4f9b8d4c 100644 --- a/source/quickjs.h +++ b/source/quickjs.h @@ -1218,37 +1218,43 @@ CellModule *cell_module_from_bytecode (JSContext *ctx, JSFunctionBytecode *main_ Returns allocated CellModule (caller must free with cell_module_free), or NULL on error. */ CellModule *JS_CompileModule (JSContext *ctx, const char *input, size_t input_len, const char *filename); +/* Parse source code and return AST as cJSON tree. + Caller must call cJSON_Delete() on result. */ +struct cJSON *JS_ASTTree (const char *source, size_t len, const char *filename); + /* Parse source code and return AST as JSON string. - Returns malloc'd JSON string (caller must free), or NULL on error. - No JSContext needed — pure string transformation. */ + Returns malloc'd JSON string (caller must free), or NULL on error. */ char *JS_AST (const char *source, size_t len, const char *filename); /* Tokenize source code and return token array as JSON string. - Returns malloc'd JSON string (caller must free), or NULL on error. - No JSContext needed — pure string transformation. */ + Returns malloc'd JSON string (caller must free), or NULL on error. */ char *JS_Tokenize (const char *source, size_t len, const char *filename); /* Compiled bytecode (context-free, serializable) */ typedef struct MachCode MachCode; -/* Compile AST JSON to context-free MachCode. - Returns MachCode* (caller must free with JS_FreeMachCode), or NULL on error. */ +/* Compile AST cJSON tree to context-free MachCode. */ +MachCode *JS_CompileMachTree(struct cJSON *ast); + +/* Compile AST JSON string to context-free MachCode. */ MachCode *JS_CompileMach(const char *ast_json); /* Free a compiled MachCode tree. */ void JS_FreeMachCode(MachCode *mc); -/* Load compiled MachCode into a JSContext, materializing JSValues. - Returns JSCodeRegister* linked and ready for execution. */ +/* Load compiled MachCode into a JSContext, materializing JSValues. */ struct JSCodeRegister *JS_LoadMachCode(JSContext *ctx, MachCode *mc, JSValue env); -/* Dump MACH bytecode to stdout for debugging. Takes AST JSON. - Internally compiles, loads, and dumps binary bytecode. */ +/* Dump MACH bytecode to stdout. Takes AST cJSON tree. */ +void JS_DumpMachTree (JSContext *ctx, struct cJSON *ast, JSValue env); + +/* Dump MACH bytecode to stdout. Takes AST JSON string. */ void JS_DumpMach (JSContext *ctx, const char *ast_json, JSValue env); -/* Compile and execute MACH bytecode. Takes AST JSON. - Internally compiles, loads, and executes. - Returns result of execution, or JS_EXCEPTION on error. */ +/* Compile and execute MACH bytecode from AST cJSON tree. */ +JSValue JS_RunMachTree (JSContext *ctx, struct cJSON *ast, JSValue env); + +/* Compile and execute MACH bytecode from AST JSON string. */ JSValue JS_RunMach (JSContext *ctx, const char *ast_json, JSValue env); /* Compile AST JSON to MCODE JSON (string-based IR). diff --git a/source/suite.c b/source/suite.c index f2e3ffb0..cdf529e2 100644 --- a/source/suite.c +++ b/source/suite.c @@ -2393,22 +2393,22 @@ TEST(ast_sem_nested_function_scope) { TEST(mach_compile_basic) { const char *src = "var x = 1; x = x + 1"; - char *ast_json = JS_AST(src, strlen(src), ""); - ASSERT_MSG(ast_json != NULL, "JS_AST returned NULL"); - MachCode *mc = JS_CompileMach(ast_json); - free(ast_json); - ASSERT_MSG(mc != NULL, "JS_CompileMach returned NULL"); + cJSON *ast = JS_ASTTree(src, strlen(src), ""); + ASSERT_MSG(ast != NULL, "JS_ASTTree returned NULL"); + MachCode *mc = JS_CompileMachTree(ast); + cJSON_Delete(ast); + ASSERT_MSG(mc != NULL, "JS_CompileMachTree returned NULL"); JS_FreeMachCode(mc); return 1; } TEST(mach_compile_function) { const char *src = "function f(x) { return x + 1 }"; - char *ast_json = JS_AST(src, strlen(src), ""); - ASSERT_MSG(ast_json != NULL, "JS_AST returned NULL"); - MachCode *mc = JS_CompileMach(ast_json); - free(ast_json); - ASSERT_MSG(mc != NULL, "JS_CompileMach returned NULL"); + cJSON *ast = JS_ASTTree(src, strlen(src), ""); + ASSERT_MSG(ast != NULL, "JS_ASTTree returned NULL"); + MachCode *mc = JS_CompileMachTree(ast); + cJSON_Delete(ast); + ASSERT_MSG(mc != NULL, "JS_CompileMachTree returned NULL"); JS_FreeMachCode(mc); return 1; }