/* * QuickJS Javascript Engine * * Copyright (c) 2017-2025 Fabrice Bellard * Copyright (c) 2017-2025 Charlie Gordon * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ #include "quickjs-internal.h" static cJSON *ast_parse_primary (ASTParseState *s) { const uint8_t *start = s->token_ptr; cJSON *node = NULL; switch (s->token_val) { case TOK_NUMBER: { node = ast_node (s, "number", start); double d = s->token_u.num.val; /* Store original text representation */ size_t len = s->buf_ptr - start; char *text = sys_malloc (len + 1); memcpy (text, start, len); text[len] = '\0'; cJSON_AddStringToObject (node, "value", text); cJSON_AddNumberToObject (node, "number", d); sys_free (text); ast_node_end (s, node, s->buf_ptr); ast_next_token (s); } break; case TOK_STRING: { node = ast_node (s, "text", start); cjson_add_strn (node, "value", s->token_u.str.str, s->token_u.str.len); ast_node_end (s, node, s->buf_ptr); ast_next_token (s); } break; case TOK_TEMPLATE: { const uint8_t *tmpl_start = start + 1; const uint8_t *tmpl_end = s->buf_ptr - 1; const uint8_t *saved_end = s->buf_ptr; /* Quick scan for ${ */ BOOL has_expr = FALSE; for (const uint8_t *sc = tmpl_start; sc < tmpl_end; sc++) { if (*sc == '\\' && sc + 1 < tmpl_end) { sc++; continue; } if (*sc == '$' && sc + 1 < tmpl_end && sc[1] == '{') { has_expr = TRUE; break; } } if (!has_expr) { /* Simple template — unchanged behavior */ node = ast_node (s, "text", start); cjson_add_strn (node, "value", s->token_u.str.str, s->token_u.str.len); ast_node_end (s, node, s->buf_ptr); ast_next_token (s); } else { node = ast_node (s, "text literal", start); cJSON *list = cJSON_AddArrayToObject (node, "list"); /* Build format string with {N} placeholders */ int cap = 256; char *fmt = sys_malloc (cap); int len = 0; int idx = 0; const uint8_t *p = tmpl_start; while (p < tmpl_end) { if (*p == '\\' && p + 1 < tmpl_end) { p++; /* skip backslash */ if (len + 8 >= cap) { cap *= 2; fmt = sys_realloc (fmt, cap); } switch (*p) { case 'n': fmt[len++] = '\n'; p++; break; case 't': fmt[len++] = '\t'; p++; break; case 'r': fmt[len++] = '\r'; p++; break; case '\\': fmt[len++] = '\\'; p++; break; case '`': fmt[len++] = '`'; p++; break; case '$': fmt[len++] = '$'; p++; break; case '0': fmt[len++] = '\0'; p++; break; case 'u': { p++; unsigned int cp = 0; for (int i = 0; i < 4 && p < tmpl_end; i++, p++) { cp <<= 4; if (*p >= '0' && *p <= '9') cp |= *p - '0'; else if (*p >= 'a' && *p <= 'f') cp |= *p - 'a' + 10; else if (*p >= 'A' && *p <= 'F') cp |= *p - 'A' + 10; else break; } len += unicode_to_utf8 ((uint8_t *)fmt + len, cp); } break; default: fmt[len++] = *p++; break; } continue; } if (*p == '$' && p + 1 < tmpl_end && p[1] == '{') { /* Add {N} placeholder */ if (len + 12 >= cap) { cap *= 2; fmt = sys_realloc (fmt, cap); } len += snprintf (fmt + len, cap - len, "{%d}", idx++); p += 2; /* skip ${ */ /* Parse expression: redirect buf_ptr, tokenize, parse */ s->buf_ptr = p; ast_next_token (s); cJSON *expr = ast_parse_assign_expr (s); if (expr) cJSON_AddItemToArray (list, expr); /* After expression, token should be '}' */ if (s->token_val == '}') { p = s->buf_ptr; } else { ast_error (s, p, "expected '}' after template expression"); p = s->buf_ptr; } continue; } if (len + 1 >= cap) { cap *= 2; fmt = sys_realloc (fmt, cap); } fmt[len++] = *p++; } fmt[len] = '\0'; cJSON_AddStringToObject (node, "value", fmt); sys_free (fmt); s->buf_ptr = saved_end; ast_node_end (s, node, saved_end); ast_next_token (s); } } break; case TOK_IDENT: { /* Check for single-param arrow function: x => ... */ const uint8_t *p = s->buf_ptr; while (p < s->buf_end && (*p == ' ' || *p == '\t')) p++; if (p + 1 < s->buf_end && p[0] == '=' && p[1] == '>') { node = ast_parse_arrow_function (s); } else { node = ast_node (s, "name", start); cjson_add_strn (node, "name", s->token_u.ident.str, s->token_u.ident.len); ast_node_end (s, node, s->buf_ptr); ast_next_token (s); } } break; case TOK_NULL: node = ast_node (s, "null", start); ast_node_end (s, node, s->buf_ptr); ast_next_token (s); break; case TOK_TRUE: node = ast_node (s, "true", start); ast_node_end (s, node, s->buf_ptr); ast_next_token (s); break; case TOK_FALSE: node = ast_node (s, "false", start); ast_node_end (s, node, s->buf_ptr); ast_next_token (s); break; case TOK_THIS: node = ast_node (s, "this", start); ast_node_end (s, node, s->buf_ptr); ast_next_token (s); break; case '[': { node = ast_node (s, "array", start); cJSON *list = cJSON_AddArrayToObject (node, "list"); ast_next_token (s); while (s->token_val != ']' && s->token_val != TOK_EOF) { cJSON *elem = ast_parse_assign_expr (s); if (elem) cJSON_AddItemToArray (list, elem); if (s->token_val == ',') ast_next_token (s); else break; } ast_node_end (s, node, s->buf_ptr); if (s->token_val == ']') { ast_next_token (s); } else if (s->token_val == TOK_EOF) { ast_error (s, s->token_ptr, "unterminated array literal, expected ']'"); } } break; case '{': { node = ast_node (s, "record", start); cJSON *list = cJSON_AddArrayToObject (node, "list"); ast_next_token (s); while (s->token_val != '}' && s->token_val != TOK_EOF) { cJSON *pair = cJSON_CreateObject (); /* property name */ int is_ident = (s->token_val == TOK_IDENT); int is_keyword = (s->token_val >= TOK_FIRST_KEYWORD && s->token_val <= TOK_LAST_KEYWORD); if (is_ident || is_keyword || s->token_val == TOK_STRING || s->token_val == TOK_NUMBER) { cJSON *left; if (is_keyword) { left = ast_node (s, "name", s->token_ptr); cjson_add_strn (left, "name", s->token_u.ident.str, s->token_u.ident.len); ast_node_end (s, left, s->buf_ptr); ast_next_token (s); } else { left = ast_parse_primary (s); } cJSON_AddItemToObject (pair, "left", left); } else if (s->token_val == '[') { /* computed property */ ast_next_token (s); cJSON *left = ast_parse_assign_expr (s); cJSON_AddItemToObject (pair, "left", left); if (s->token_val == ']') { ast_next_token (s); } else { ast_error (s, s->token_ptr, "expected ']' after computed property"); } } else { cJSON_Delete (pair); ast_error (s, s->token_ptr, "expected property name in object literal"); break; } /* colon and value */ if (s->token_val == ':') { ast_next_token (s); cJSON *right = ast_parse_assign_expr (s); cJSON_AddItemToObject (pair, "right", right); } else if (s->token_val == '(') { /* Method shorthand: init() {} => init: function init() {} */ const uint8_t *fn_start = s->token_ptr; cJSON *fn = ast_node (s, "function", fn_start); /* Set method name from property key */ cJSON *left = cJSON_GetObjectItemCaseSensitive (pair, "left"); cJSON *name_item = cJSON_GetObjectItemCaseSensitive (left, "name"); if (name_item) cJSON_AddStringToObject (fn, "name", name_item->valuestring); /* Parse parameters */ cJSON *params = cJSON_AddArrayToObject (fn, "list"); ast_next_token (s); /* skip '(' */ while (s->token_val != ')' && s->token_val != TOK_EOF) { if (s->token_val == TOK_IDENT) { cJSON *param = ast_node (s, "name", s->token_ptr); cjson_add_strn (param, "name", s->token_u.ident.str, s->token_u.ident.len); ast_node_end (s, param, s->buf_ptr); ast_next_token (s); if (s->token_val == '=' || s->token_val == '|') { ast_next_token (s); cJSON *default_val = ast_parse_expr (s); cJSON_AddItemToObject (param, "expression", default_val); } cJSON_AddItemToArray (params, param); } else { ast_error (s, s->token_ptr, "expected parameter name"); break; } if (s->token_val == ',') ast_next_token (s); else break; } if (s->token_val == ')') ast_next_token (s); else if (s->token_val == TOK_EOF) ast_error (s, s->token_ptr, "unterminated method parameter list"); if (cJSON_GetArraySize (params) > 4) ast_error (s, s->token_ptr, "functions cannot have more than 4 parameters"); /* Parse body */ if (s->token_val == '{') { ast_next_token (s); cJSON *stmts = ast_parse_block_statements (s); cJSON_AddItemToObject (fn, "statements", stmts); if (s->token_val == '}') ast_next_token (s); else if (s->token_val == TOK_EOF) ast_error (s, s->token_ptr, "unterminated method body"); } else { ast_error (s, s->token_ptr, "expected '{' for method body"); } cJSON_AddNumberToObject (fn, "function_nr", s->function_nr++); ast_node_end (s, fn, s->buf_ptr); cJSON_AddItemToObject (pair, "right", fn); } else if (!(is_ident && (s->token_val == ',' || s->token_val == '}'))) { ast_error (s, s->token_ptr, "expected ':' after property name"); } cJSON_AddItemToArray (list, pair); if (s->token_val == ',') ast_next_token (s); else break; } ast_node_end (s, node, s->buf_ptr); if (s->token_val == '}') { ast_next_token (s); } else if (s->token_val == TOK_EOF) { ast_error (s, s->token_ptr, "unterminated object literal, expected '}'"); } } break; case '(': { /* Check for arrow function: () => ..., (a, b) => ... */ if (ast_is_arrow_function (s)) { node = ast_parse_arrow_function (s); } else { ast_next_token (s); node = ast_parse_expr (s); if (s->token_val == ')') { ast_next_token (s); } else if (s->token_val == TOK_EOF) { ast_error (s, s->token_ptr, "unterminated parenthesized expression, expected ')'"); } else { ast_error (s, s->token_ptr, "expected ')' after expression"); } } } break; case TOK_FUNCTION: { node = ast_parse_function_inner (s, TRUE); } break; case '/': { /* Regex literal - when / appears in primary position, it's a regex */ node = ast_node (s, "regexp", start); const uint8_t *p = s->token_ptr + 1; /* skip opening / */ const uint8_t *pattern_start = p; /* Parse pattern - find closing / (not escaped) */ while (p < s->buf_end && *p != '/') { if (*p == '\\' && p + 1 < s->buf_end) { p += 2; /* skip escape sequence */ } else if (*p == '\n' || *p == '\r') { ast_error (s, p, "unterminated regex literal"); break; } else { p++; } } size_t pattern_len = p - pattern_start; if (p < s->buf_end) p++; /* skip closing / */ /* Parse flags */ const uint8_t *flags_start = p; while (p < s->buf_end && ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z'))) { p++; } size_t flags_len = p - flags_start; char *pattern = sys_malloc (pattern_len + 1); memcpy (pattern, pattern_start, pattern_len); pattern[pattern_len] = '\0'; cJSON_AddStringToObject (node, "pattern", pattern); sys_free (pattern); if (flags_len > 0) { char *flags = sys_malloc (flags_len + 1); memcpy (flags, flags_start, flags_len); flags[flags_len] = '\0'; cJSON_AddStringToObject (node, "flags", flags); sys_free (flags); } s->buf_ptr = p; ast_node_end (s, node, s->buf_ptr); ast_next_token (s); } break; default: /* Report syntax error with token info */ if (s->token_val >= 32 && s->token_val < 127) { ast_error (s, start, "unexpected '%c' in expression", s->token_val); } else if (s->token_val == TOK_EOF) { ast_error (s, start, "unexpected end of input"); } else { ast_error (s, start, "unexpected token (keyword or operator) where expression expected"); } ast_next_token (s); return NULL; } return node; } static cJSON *ast_parse_postfix (ASTParseState *s) { cJSON *node = ast_parse_primary (s); if (!node) return NULL; for (;;) { const uint8_t *start = s->token_ptr; if (s->token_val == '.') { ast_next_token (s); cJSON *new_node = ast_node (s, ".", start); cJSON_AddItemToObject (new_node, "left", node); if (s->token_val == TOK_IDENT || (s->token_val >= TOK_FIRST_KEYWORD && s->token_val <= TOK_LAST_KEYWORD)) { cjson_add_strn (new_node, "right", s->token_u.ident.str, s->token_u.ident.len); ast_next_token (s); } else { ast_error (s, s->token_ptr, "expected property name after '.'"); } ast_node_end (s, new_node, s->buf_ptr); node = new_node; } else if (s->token_val == '[') { ast_next_token (s); cJSON *new_node = ast_node (s, "[", start); cJSON_AddItemToObject (new_node, "left", node); if (s->token_val == ']') { ast_next_token (s); } else { cJSON *index = ast_parse_assign_expr (s); cJSON_AddItemToObject (new_node, "right", index); if (s->token_val == ']') ast_next_token (s); else ast_error (s, s->token_ptr, "expected ']'"); } ast_node_end (s, new_node, s->buf_ptr); node = new_node; } else if (s->token_val == '(') { ast_next_token (s); cJSON *new_node = ast_node (s, "(", start); cJSON_AddItemToObject (new_node, "expression", node); cJSON *list = cJSON_AddArrayToObject (new_node, "list"); while (s->token_val != ')' && s->token_val != TOK_EOF) { cJSON *arg = ast_parse_assign_expr (s); if (arg) cJSON_AddItemToArray (list, arg); if (s->token_val == ',') ast_next_token (s); else break; } if (s->token_val == ')') ast_next_token (s); else ast_error (s, s->token_ptr, "unterminated argument list, expected ')'"); ast_node_end (s, new_node, s->buf_ptr); node = new_node; } else if (s->token_val == TOK_INC) { cJSON *new_node = ast_node (s, "++", start); cJSON_AddItemToObject (new_node, "expression", node); cJSON_AddBoolToObject (new_node, "postfix", 1); ast_next_token (s); ast_node_end (s, new_node, s->buf_ptr); node = new_node; } else if (s->token_val == TOK_DEC) { cJSON *new_node = ast_node (s, "--", start); cJSON_AddItemToObject (new_node, "expression", node); cJSON_AddBoolToObject (new_node, "postfix", 1); ast_next_token (s); ast_node_end (s, new_node, s->buf_ptr); node = new_node; } else { break; } } return node; } static cJSON *ast_parse_unary (ASTParseState *s) { const uint8_t *start = s->token_ptr; switch (s->token_val) { case '!': { ast_next_token (s); cJSON *node = ast_node (s, "!", start); cJSON *expr = ast_parse_unary (s); cJSON_AddItemToObject (node, "expression", expr); ast_node_end (s, node, s->buf_ptr); return node; } case '~': { ast_next_token (s); cJSON *node = ast_node (s, "~", start); cJSON *expr = ast_parse_unary (s); cJSON_AddItemToObject (node, "expression", expr); ast_node_end (s, node, s->buf_ptr); return node; } case '+': { ast_next_token (s); cJSON *node = ast_node (s, "+unary", start); cJSON *expr = ast_parse_unary (s); cJSON_AddItemToObject (node, "expression", expr); ast_node_end (s, node, s->buf_ptr); return node; } case '-': { ast_next_token (s); cJSON *node = ast_node (s, "-unary", start); cJSON *expr = ast_parse_unary (s); cJSON_AddItemToObject (node, "expression", expr); ast_node_end (s, node, s->buf_ptr); return node; } case TOK_INC: { ast_next_token (s); cJSON *node = ast_node (s, "++", start); cJSON *expr = ast_parse_unary (s); cJSON_AddItemToObject (node, "expression", expr); cJSON_AddBoolToObject (node, "postfix", 0); ast_node_end (s, node, s->buf_ptr); return node; } case TOK_DEC: { ast_next_token (s); cJSON *node = ast_node (s, "--", start); cJSON *expr = ast_parse_unary (s); cJSON_AddItemToObject (node, "expression", expr); cJSON_AddBoolToObject (node, "postfix", 0); ast_node_end (s, node, s->buf_ptr); return node; } case TOK_DELETE: { ast_next_token (s); cJSON *node = ast_node (s, "delete", start); cJSON *expr = ast_parse_unary (s); cJSON_AddItemToObject (node, "expression", expr); ast_node_end (s, node, s->buf_ptr); return node; } default: return ast_parse_postfix (s); } } /* Binary operator precedence levels */ typedef struct { int token; const char *kind; int prec; } ASTBinOp; static const ASTBinOp ast_binops[] = { { TOK_POW, "**", 14 }, { '*', "*", 13 }, { '/', "/", 13 }, { '%', "%", 13 }, { '+', "+", 12 }, { '-', "-", 12 }, { TOK_SHL, "<<", 11 }, { TOK_SAR, ">>", 11 }, { TOK_SHR, ">>>", 11 }, { '<', "<", 10 }, { '>', ">", 10 }, { TOK_LTE, "<=", 10 }, { TOK_GTE, ">=", 10 }, { TOK_IN, "in", 10 }, { TOK_EQ, "==", 9 }, { TOK_NEQ, "!=", 9 }, { TOK_STRICT_EQ, "===", 9 }, { TOK_STRICT_NEQ, "!==", 9 }, { '&', "&", 8 }, { '^', "^", 7 }, { '|', "|", 6 }, { TOK_LAND, "&&", 5 }, { TOK_LOR, "||", 4 }, { 0, NULL, 0 } }; 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]; } return NULL; } static cJSON *ast_parse_binary (ASTParseState *s, int min_prec) { cJSON *left = ast_parse_unary (s); if (!left) return NULL; for (;;) { const uint8_t *start = s->token_ptr; const ASTBinOp *op = ast_get_binop (s->token_val); if (!op || op->prec < min_prec) break; ast_next_token (s); /* Right associativity for ** */ int next_prec = (op->prec == 14) ? op->prec : op->prec + 1; cJSON *right = ast_parse_binary (s, next_prec); 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); left = node; } return left; } static cJSON *ast_parse_ternary (ASTParseState *s) { cJSON *cond = ast_parse_binary (s, 1); if (!cond) return NULL; if (s->token_val == '?') { const uint8_t *start = s->token_ptr; ast_next_token (s); cJSON *then_expr = ast_parse_expr (s); if (s->token_val == ':') ast_next_token (s); else ast_error (s, s->token_ptr, "expected ':' in ternary expression"); cJSON *else_expr = ast_parse_expr (s); cJSON *node = ast_node (s, "then", start); cJSON_AddItemToObject (node, "expression", cond); cJSON_AddItemToObject (node, "then", then_expr); cJSON_AddItemToObject (node, "else", else_expr); ast_node_end (s, node, s->buf_ptr); return node; } return cond; } static cJSON *ast_parse_assign (ASTParseState *s) { cJSON *left = ast_parse_ternary (s); if (!left) return NULL; const uint8_t *start = s->token_ptr; const char *kind = NULL; switch (s->token_val) { case '=': kind = "assign"; break; case TOK_PLUS_ASSIGN: kind = "+="; break; case TOK_MINUS_ASSIGN: kind = "-="; break; case TOK_MUL_ASSIGN: kind = "*="; break; case TOK_DIV_ASSIGN: kind = "/="; break; case TOK_MOD_ASSIGN: kind = "%="; break; case TOK_SHL_ASSIGN: kind = "<<="; break; case TOK_SAR_ASSIGN: kind = ">>="; break; case TOK_SHR_ASSIGN: kind = ">>>="; break; case TOK_AND_ASSIGN: kind = "&="; break; case TOK_XOR_ASSIGN: kind = "^="; break; case TOK_OR_ASSIGN: kind = "|="; break; case TOK_POW_ASSIGN: kind = "**="; break; case TOK_LAND_ASSIGN: kind = "&&="; break; case TOK_LOR_ASSIGN: kind = "||="; break; default: return left; } /* Validate assignment target */ { const char *left_kind = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (left, "kind")); if (left_kind && strcmp (left_kind, "name") != 0 && strcmp (left_kind, ".") != 0 && strcmp (left_kind, "[") != 0 && strcmp (left_kind, "?.") != 0 && strcmp (left_kind, "?.[") != 0) { ast_error (s, start, "invalid assignment left-hand side"); } } ast_next_token (s); cJSON *right = ast_parse_assign (s); cJSON *node = ast_node (s, kind, start); cJSON_AddItemToObject (node, "left", left); cJSON_AddItemToObject (node, "right", right); /* Check for push/pop bracket syntax */ const char *left_kind = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (left, "kind")); if (left_kind && strcmp (left_kind, "[") == 0 && !cJSON_GetObjectItemCaseSensitive (left, "right")) cJSON_AddBoolToObject (node, "push", 1); const char *right_kind = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (right, "kind")); if (right_kind && strcmp (right_kind, "[") == 0 && !cJSON_GetObjectItemCaseSensitive (right, "right")) cJSON_AddBoolToObject (node, "pop", 1); ast_node_end (s, node, s->buf_ptr); return node; } /* Parse assignment expression (excludes comma operator) */ cJSON *ast_parse_assign_expr (ASTParseState *s) { return ast_parse_assign (s); } /* Parse full expression including comma operator */ cJSON *ast_parse_expr (ASTParseState *s) { cJSON *left = ast_parse_assign (s); if (!left) return NULL; /* Handle comma operator: (1, 2, 3) => 3 */ while (s->token_val == ',') { const uint8_t *start = s->token_ptr; ast_next_token (s); cJSON *right = ast_parse_assign (s); cJSON *node = ast_node (s, ",", start); cJSON_AddItemToObject (node, "left", left); cJSON_AddItemToObject (node, "right", right); ast_node_end (s, node, s->buf_ptr); left = node; } return left; } cJSON *ast_parse_block_statements (ASTParseState *s) { cJSON *stmts = cJSON_CreateArray (); while (s->token_val != '}' && s->token_val != TOK_EOF) { const uint8_t *before = s->token_ptr; cJSON *stmt = ast_parse_statement (s); if (stmt) { cJSON_AddItemToArray (stmts, stmt); } else if (s->token_ptr == before) { ast_sync_to_statement (s); } } return stmts; } cJSON *ast_parse_function_inner (ASTParseState *s, BOOL is_expr) { const uint8_t *start = s->token_ptr; cJSON *node = ast_node (s, "function", start); if (s->in_disruption) { ast_error (s, s->token_ptr, "cannot define function inside disruption clause"); } ast_next_token (s); /* skip 'function' */ /* Optional function name */ if (s->token_val == TOK_IDENT) { cjson_add_strn (node, "name", s->token_u.ident.str, s->token_u.ident.len); ast_next_token (s); } /* Parameters */ cJSON *params = cJSON_AddArrayToObject (node, "list"); if (s->token_val == '(') { ast_next_token (s); while (s->token_val != ')' && s->token_val != TOK_EOF) { if (s->token_val == TOK_IDENT) { const uint8_t *param_ptr = s->token_ptr; cJSON *param = ast_node (s, "name", param_ptr); cjson_add_strn (param, "name", s->token_u.ident.str, s->token_u.ident.len); /* Check for duplicate parameter name */ { char *tmp_name = sys_malloc (s->token_u.ident.len + 1); memcpy (tmp_name, s->token_u.ident.str, s->token_u.ident.len); tmp_name[s->token_u.ident.len] = '\0'; cJSON *prev; cJSON_ArrayForEach (prev, params) { const char *prev_name = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (prev, "name")); if (prev_name && strcmp (prev_name, tmp_name) == 0) { ast_error (s, param_ptr, "duplicate parameter name '%s'", tmp_name); break; } } sys_free (tmp_name); } ast_node_end (s, param, s->buf_ptr); ast_next_token (s); if (s->token_val == '=' || s->token_val == '|') { ast_next_token (s); cJSON *default_val = ast_parse_assign_expr (s); cJSON_AddItemToObject (param, "expression", default_val); } cJSON_AddItemToArray (params, param); } else { ast_error (s, s->token_ptr, "expected parameter name"); break; } if (s->token_val == ',') ast_next_token (s); else break; } if (s->token_val == ')') { ast_next_token (s); } else if (s->token_val == TOK_EOF) { ast_error (s, s->token_ptr, "unterminated function parameter list, expected ')'"); } } else { ast_error (s, s->token_ptr, "expected '(' after function name"); } if (cJSON_GetArraySize (params) > 4) { ast_error (s, s->token_ptr, "functions cannot have more than 4 parameters"); } /* Body */ if (s->token_val == '{') { ast_next_token (s); cJSON *stmts = ast_parse_block_statements (s); cJSON_AddItemToObject (node, "statements", stmts); if (s->token_val == '}') { ast_next_token (s); } else if (s->token_val == TOK_EOF) { ast_error (s, s->token_ptr, "unterminated function body, expected '}'"); } } else { ast_error (s, s->token_ptr, "expected '{' for function body"); } /* Optional disruption clause */ if (s->token_val == TOK_DISRUPTION) { ast_next_token (s); if (s->token_val == '{') { ast_next_token (s); int old_in_disruption = s->in_disruption; s->in_disruption = 1; cJSON *disruption_stmts = ast_parse_block_statements (s); s->in_disruption = old_in_disruption; cJSON_AddItemToObject (node, "disruption", disruption_stmts); if (s->token_val == '}') { ast_next_token (s); } else if (s->token_val == TOK_EOF) { ast_error (s, s->token_ptr, "unterminated disruption clause, expected '}'"); } } else { ast_error (s, s->token_ptr, "expected '{' after disruption"); } } cJSON_AddNumberToObject (node, "function_nr", s->function_nr++); ast_node_end (s, node, s->buf_ptr); return node; } /* Parse arrow function: x => expr, (a, b) => expr, (x = 10) => expr, () => expr */ cJSON *ast_parse_arrow_function (ASTParseState *s) { const uint8_t *start = s->token_ptr; cJSON *node = ast_node (s, "function", start); cJSON_AddBoolToObject (node, "arrow", 1); if (s->in_disruption) { ast_error (s, s->token_ptr, "cannot define function inside disruption clause"); } /* Parameters */ cJSON *params = cJSON_AddArrayToObject (node, "list"); if (s->token_val == TOK_IDENT) { /* Single parameter without parens: x => ... */ cJSON *param = ast_node (s, "name", s->token_ptr); cjson_add_strn (param, "name", s->token_u.ident.str, s->token_u.ident.len); ast_node_end (s, param, s->buf_ptr); cJSON_AddItemToArray (params, param); ast_next_token (s); } else if (s->token_val == '(') { /* Parenthesized parameters: () => ..., (a, b) => ..., (x = 10) => ... */ ast_next_token (s); while (s->token_val != ')' && s->token_val != TOK_EOF) { if (s->token_val == TOK_IDENT) { const uint8_t *param_ptr = s->token_ptr; cJSON *param = ast_node (s, "name", param_ptr); cjson_add_strn (param, "name", s->token_u.ident.str, s->token_u.ident.len); /* Check for duplicate parameter name */ { char *tmp_name = sys_malloc (s->token_u.ident.len + 1); memcpy (tmp_name, s->token_u.ident.str, s->token_u.ident.len); tmp_name[s->token_u.ident.len] = '\0'; cJSON *prev; cJSON_ArrayForEach (prev, params) { const char *prev_name = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (prev, "name")); if (prev_name && strcmp (prev_name, tmp_name) == 0) { ast_error (s, param_ptr, "duplicate parameter name '%s'", tmp_name); break; } } sys_free (tmp_name); } ast_node_end (s, param, s->buf_ptr); ast_next_token (s); /* Check for default value */ if (s->token_val == '=' || s->token_val == '|') { ast_next_token (s); cJSON *default_val = ast_parse_assign_expr (s); cJSON_AddItemToObject (param, "expression", default_val); } cJSON_AddItemToArray (params, param); } else { ast_error (s, s->token_ptr, "expected parameter name"); break; } if (s->token_val == ',') ast_next_token (s); else break; } if (s->token_val == ')') ast_next_token (s); } if (cJSON_GetArraySize (params) > 4) { ast_error (s, s->token_ptr, "functions cannot have more than 4 parameters"); } /* Arrow token */ if (s->token_val != TOK_ARROW) { ast_error (s, s->token_ptr, "expected '=>' in arrow function"); } else { ast_next_token (s); } /* Body: either block or expression */ if (s->token_val == '{') { ast_next_token (s); cJSON *stmts = ast_parse_block_statements (s); cJSON_AddItemToObject (node, "statements", stmts); if (s->token_val == '}') ast_next_token (s); } else { /* Expression body - wrap in implicit return. Use assign_expr (not full expr) so commas after the body are NOT consumed — matches JS spec (AssignmentExpression). */ cJSON *stmts = cJSON_CreateArray (); cJSON *ret = ast_node (s, "return", s->token_ptr); cJSON *expr = ast_parse_assign_expr (s); cJSON_AddItemToObject (ret, "expression", expr); ast_node_end (s, ret, s->buf_ptr); cJSON_AddItemToArray (stmts, ret); cJSON_AddItemToObject (node, "statements", stmts); } cJSON_AddNumberToObject (node, "function_nr", s->function_nr++); ast_node_end (s, node, s->buf_ptr); return node; } static void ast_expect_semi (ASTParseState *s) { if (s->token_val == ';') { ast_next_token (s); return; } if (s->token_val == TOK_EOF || s->token_val == '}' || s->got_lf || s->token_val == TOK_ELSE) return; ast_error (s, s->token_ptr, "expecting ';'"); } /* Skip tokens until a statement sync point to recover from errors */ void ast_sync_to_statement (ASTParseState *s) { while (s->token_val != TOK_EOF) { switch (s->token_val) { case ';': ast_next_token (s); /* consume semicolon */ return; case '}': return; /* don't consume - let caller handle */ case TOK_VAR: case TOK_DEF: case TOK_IF: case TOK_WHILE: case TOK_FOR: case TOK_RETURN: case TOK_DISRUPT: case TOK_FUNCTION: case TOK_BREAK: case TOK_CONTINUE: case TOK_DO: return; /* statement-starting keyword found */ default: ast_next_token (s); break; } } } cJSON *ast_parse_statement (ASTParseState *s) { const uint8_t *start = s->token_ptr; cJSON *node = NULL; switch (s->token_val) { case '{': { node = ast_node (s, "block", start); ast_next_token (s); cJSON *stmts = ast_parse_block_statements (s); cJSON_AddItemToObject (node, "statements", stmts); if (s->token_val == '}') ast_next_token (s); ast_node_end (s, node, s->buf_ptr); } break; case TOK_VAR: case TOK_DEF: { const char *kind_name = (s->token_val == TOK_VAR) ? "var" : "def"; ast_next_token (s); /* Expect an identifier */ if (s->token_val != TOK_IDENT) { ast_error (s, s->token_ptr, "expected identifier after '%s'", kind_name); return NULL; } /* Can have multiple declarations: var x = 1, y = 2 */ cJSON *decls = cJSON_CreateArray (); int decl_count = 0; while (s->token_val == TOK_IDENT) { const uint8_t *var_ptr = s->token_ptr; node = ast_node (s, kind_name, start); cJSON *left = ast_node (s, "name", s->token_ptr); cjson_add_strn (left, "name", s->token_u.ident.str, s->token_u.ident.len); /* Save name for potential error message */ char *var_name = sys_malloc (s->token_u.ident.len + 1); memcpy (var_name, s->token_u.ident.str, s->token_u.ident.len); var_name[s->token_u.ident.len] = '\0'; ast_node_end (s, left, s->buf_ptr); cJSON_AddItemToObject (node, "left", left); ast_next_token (s); if (s->token_val == '=') { ast_next_token (s); cJSON *right = ast_parse_assign_expr (s); cJSON_AddItemToObject (node, "right", right); const char *right_kind = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (right, "kind")); if (right_kind && strcmp (right_kind, "[") == 0 && !cJSON_GetObjectItemCaseSensitive (right, "right")) cJSON_AddBoolToObject (node, "pop", 1); } else { /* var and def both require initializer */ ast_error (s, var_ptr, "missing initializer for '%s' '%s'", kind_name, var_name); } sys_free (var_name); ast_node_end (s, node, s->buf_ptr); cJSON_AddItemToArray (decls, node); decl_count++; if (s->token_val == ',') { ast_next_token (s); } else { break; } } ast_expect_semi (s); if (decl_count == 1) { node = cJSON_DetachItemFromArray (decls, 0); cJSON_Delete (decls); } else { node = ast_node (s, "var_list", start); cJSON_AddItemToObject (node, "list", decls); ast_node_end (s, node, s->buf_ptr); } } break; case TOK_IF: { node = ast_node (s, "if", start); ast_next_token (s); if (s->token_val == '(') ast_next_token (s); else ast_error (s, s->token_ptr, "expected '(' before condition"); cJSON *cond = ast_parse_expr (s); cJSON_AddItemToObject (node, "expression", cond); if (s->token_val == ')') ast_next_token (s); else ast_error (s, s->token_ptr, "expected ')' after if condition"); cJSON *then_stmts = cJSON_AddArrayToObject (node, "then"); cJSON *then_stmt = ast_parse_statement (s); if (then_stmt) cJSON_AddItemToArray (then_stmts, then_stmt); cJSON *else_ifs = cJSON_AddArrayToObject (node, "list"); if (s->token_val == TOK_ELSE) { ast_next_token (s); if (s->token_val == TOK_IF) { /* else if - add to list */ cJSON *elif = ast_parse_statement (s); if (elif) cJSON_AddItemToArray (else_ifs, elif); } else { cJSON *else_stmts = cJSON_AddArrayToObject (node, "else"); cJSON *else_stmt = ast_parse_statement (s); if (else_stmt) cJSON_AddItemToArray (else_stmts, else_stmt); } } ast_node_end (s, node, s->buf_ptr); } break; case TOK_WHILE: { node = ast_node (s, "while", start); ast_next_token (s); if (s->token_val == '(') ast_next_token (s); else ast_error (s, s->token_ptr, "expected '(' before condition"); cJSON *cond = ast_parse_expr (s); cJSON_AddItemToObject (node, "expression", cond); if (s->token_val == ')') ast_next_token (s); else ast_error (s, s->token_ptr, "expected ')' after while condition"); cJSON *stmts = cJSON_AddArrayToObject (node, "statements"); cJSON *body = ast_parse_statement (s); if (body) cJSON_AddItemToArray (stmts, body); ast_node_end (s, node, s->buf_ptr); } break; case TOK_DO: { node = ast_node (s, "do", start); ast_next_token (s); cJSON *stmts = cJSON_AddArrayToObject (node, "statements"); cJSON *body = ast_parse_statement (s); if (body) cJSON_AddItemToArray (stmts, body); if (s->token_val == TOK_WHILE) ast_next_token (s); else ast_error (s, s->token_ptr, "expected 'while' after do body"); if (s->token_val == '(') ast_next_token (s); else ast_error (s, s->token_ptr, "expected '(' before condition"); cJSON *cond = ast_parse_expr (s); cJSON_AddItemToObject (node, "expression", cond); if (s->token_val == ')') ast_next_token (s); else ast_error (s, s->token_ptr, "expected ')' after do-while condition"); ast_expect_semi (s); ast_node_end (s, node, s->buf_ptr); } break; case TOK_FOR: { node = ast_node (s, "for", start); ast_next_token (s); if (s->token_val == '(') ast_next_token (s); else ast_error (s, s->token_ptr, "expected '(' after for"); /* Init — only expressions allowed (no var/def) */ if (s->token_val != ';') { cJSON *init = ast_parse_expr (s); cJSON_AddItemToObject (node, "init", init); if (s->token_val == ';') ast_next_token (s); } else { ast_next_token (s); } /* Test */ if (s->token_val != ';') { cJSON *test = ast_parse_expr (s); cJSON_AddItemToObject (node, "test", test); } if (s->token_val == ';') ast_next_token (s); /* Update */ if (s->token_val != ')') { cJSON *update = ast_parse_expr (s); cJSON_AddItemToObject (node, "update", update); } if (s->token_val == ')') ast_next_token (s); else ast_error (s, s->token_ptr, "expected ')' after for clauses"); cJSON *stmts = cJSON_AddArrayToObject (node, "statements"); cJSON *body = ast_parse_statement (s); if (body) cJSON_AddItemToArray (stmts, body); ast_node_end (s, node, s->buf_ptr); } break; case TOK_RETURN: { node = ast_node (s, "return", start); ast_next_token (s); if (s->token_val != ';' && s->token_val != '}' && !s->got_lf) { cJSON *expr = ast_parse_expr (s); cJSON_AddItemToObject (node, "expression", expr); } ast_expect_semi (s); ast_node_end (s, node, s->buf_ptr); } break; case TOK_GO: { node = ast_node (s, "go", start); ast_next_token (s); if (s->token_val != ';' && s->token_val != '}' && !s->got_lf) { cJSON *expr = ast_parse_expr (s); cJSON_AddItemToObject (node, "expression", expr); } ast_expect_semi (s); ast_node_end (s, node, s->buf_ptr); } break; case TOK_DISRUPT: { node = ast_node (s, "disrupt", start); ast_next_token (s); ast_expect_semi (s); ast_node_end (s, node, s->buf_ptr); } break; case TOK_BREAK: { node = ast_node (s, "break", start); ast_next_token (s); if (s->token_val == TOK_IDENT && !s->got_lf) { cjson_add_strn (node, "name", s->token_u.ident.str, s->token_u.ident.len); ast_next_token (s); } ast_expect_semi (s); ast_node_end (s, node, s->buf_ptr); } break; case TOK_CONTINUE: { node = ast_node (s, "continue", start); ast_next_token (s); if (s->token_val == TOK_IDENT && !s->got_lf) { cjson_add_strn (node, "name", s->token_u.ident.str, s->token_u.ident.len); ast_next_token (s); } ast_expect_semi (s); ast_node_end (s, node, s->buf_ptr); } break; case TOK_FUNCTION: { node = ast_parse_function_inner (s, FALSE); } break; case ';': /* Empty statement */ ast_next_token (s); return NULL; case TOK_IDENT: { /* Check if this is a labeled statement: identifier: statement */ const uint8_t *p = s->buf_ptr; while (p < s->buf_end && (*p == ' ' || *p == '\t')) p++; if (p < s->buf_end && *p == ':') { /* Labeled statement */ node = ast_node (s, "label", start); cjson_add_strn (node, "name", s->token_u.ident.str, s->token_u.ident.len); ast_next_token (s); /* skip identifier */ ast_next_token (s); /* skip colon */ cJSON *stmt = ast_parse_statement (s); cJSON_AddItemToObject (node, "statement", stmt); ast_node_end (s, node, s->buf_ptr); } else { /* Expression statement */ cJSON *expr = ast_parse_expr (s); if (expr) { node = ast_node (s, "call", start); cJSON_AddItemToObject (node, "expression", expr); ast_node_end (s, node, s->buf_ptr); } ast_expect_semi (s); } } break; default: { /* Expression statement */ cJSON *expr = ast_parse_expr (s); if (expr) { node = ast_node (s, "call", start); cJSON_AddItemToObject (node, "expression", expr); ast_node_end (s, node, s->buf_ptr); } else { ast_error (s, start, "unexpected token at start of statement"); return NULL; /* caller syncs */ } ast_expect_semi (s); } break; } return node; } static cJSON *ast_parse_program (ASTParseState *s) { cJSON *root = cJSON_CreateObject (); cJSON_AddStringToObject (root, "kind", "program"); cJSON_AddStringToObject (root, "filename", s->filename); cJSON *functions = cJSON_AddArrayToObject (root, "functions"); cJSON *statements = cJSON_AddArrayToObject (root, "statements"); while (s->token_val != TOK_EOF) { const uint8_t *before = s->token_ptr; cJSON *stmt = ast_parse_statement (s); if (stmt) { const char *kind = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (stmt, "kind")); if (kind && strcmp (kind, "function") == 0) { cJSON_AddItemToArray (functions, stmt); } else { cJSON_AddItemToArray (statements, stmt); } } else if (s->token_ptr == before) { /* Statement returned NULL and didn't advance - sync to avoid infinite loop */ ast_sync_to_statement (s); } } return root; } /* ============================================================ AST Semantic Pass ============================================================ */ #define AST_SEM_MAX_VARS 256 typedef struct ASTSemVar { const char *name; int is_const; const char *make; /* "def", "var", "function", "input" */ int function_nr; /* which function this var belongs to */ int nr_uses; /* reference count */ int closure; /* 1 if used by inner function */ } ASTSemVar; typedef struct ASTSemScope { struct ASTSemScope *parent; ASTSemVar vars[AST_SEM_MAX_VARS]; int var_count; int in_loop; int function_nr; /* function_nr of enclosing function */ int is_function_scope; /* 1 if this is a function's top-level scope */ } ASTSemScope; typedef struct ASTSemState { cJSON *errors; int has_error; cJSON *scopes_array; const char *intrinsics[256]; int intrinsic_count; } ASTSemState; static void ast_sem_error (ASTSemState *st, cJSON *node, const char *fmt, ...) { va_list ap; char buf[256]; va_start (ap, fmt); vsnprintf (buf, sizeof (buf), fmt, ap); va_end (ap); cJSON *err = cJSON_CreateObject (); cJSON_AddStringToObject (err, "message", buf); cJSON *line_obj = cJSON_GetObjectItemCaseSensitive (node, "from_row"); cJSON *col_obj = cJSON_GetObjectItemCaseSensitive (node, "from_column"); if (line_obj) cJSON_AddNumberToObject (err, "line", cJSON_GetNumberValue (line_obj) + 1); if (col_obj) cJSON_AddNumberToObject (err, "column", cJSON_GetNumberValue (col_obj) + 1); if (!st->errors) st->errors = cJSON_CreateArray (); cJSON_AddItemToArray (st->errors, err); st->has_error = 1; } static void ast_sem_add_var (ASTSemScope *scope, const char *name, int is_const, const char *make, int function_nr) { if (scope->var_count < AST_SEM_MAX_VARS) { ASTSemVar *v = &scope->vars[scope->var_count]; v->name = name; v->is_const = is_const; v->make = make; v->function_nr = function_nr; v->nr_uses = 0; v->closure = 0; scope->var_count++; } } typedef struct { ASTSemVar *var; int level; int def_function_nr; } ASTSemLookup; static ASTSemLookup ast_sem_lookup_var (ASTSemScope *scope, const char *name) { ASTSemLookup result = {NULL, 0, -1}; int cur_fn = scope->function_nr; for (ASTSemScope *s = scope; s; s = s->parent) { for (int i = 0; i < s->var_count; i++) { if (strcmp (s->vars[i].name, name) == 0) { result.var = &s->vars[i]; result.def_function_nr = s->vars[i].function_nr; return result; } } /* When crossing into a parent with a different function_nr, increment level */ if (s->parent && s->parent->function_nr != cur_fn) { result.level++; cur_fn = s->parent->function_nr; } } return result; } static ASTSemVar *ast_sem_find_var (ASTSemScope *scope, const char *name) { ASTSemLookup r = ast_sem_lookup_var (scope, name); return r.var; } static void ast_sem_add_intrinsic (ASTSemState *st, const char *name) { for (int i = 0; i < st->intrinsic_count; i++) { if (strcmp (st->intrinsics[i], name) == 0) return; } if (st->intrinsic_count < 256) { st->intrinsics[st->intrinsic_count++] = name; } } static cJSON *ast_sem_build_scope_record (ASTSemScope *scope, int *nr_slots, int *nr_close) { cJSON *rec = cJSON_CreateObject (); cJSON_AddNumberToObject (rec, "function_nr", scope->function_nr); int slots = 0, close_slots = 0; for (int i = 0; i < scope->var_count; i++) { ASTSemVar *v = &scope->vars[i]; cJSON *entry = cJSON_CreateObject (); cJSON_AddStringToObject (entry, "make", v->make); cJSON_AddNumberToObject (entry, "function_nr", v->function_nr); cJSON_AddNumberToObject (entry, "nr_uses", v->nr_uses); cJSON_AddBoolToObject (entry, "closure", v->closure); cJSON_AddNumberToObject (entry, "level", 0); cJSON_AddItemToObject (rec, v->name, entry); slots++; if (v->closure) close_slots++; } *nr_slots = slots; *nr_close = close_slots; return rec; } static int ast_sem_in_loop (ASTSemScope *scope) { for (ASTSemScope *s = scope; s; s = s->parent) { if (s->in_loop) return 1; } return 0; } static BOOL is_functino_name(const char *name) { static const char *functinos[] = { "+!", "-!", "*!", "/!", "%!", "**!", "!", "<=!", ">=!", "=!", "!=!", "&!", "|!", "^!", "<>!", ">>>!", "&&!", "||!", "~!", "[]!", NULL }; for (int i = 0; functinos[i]; i++) if (strcmp(name, functinos[i]) == 0) return TRUE; return FALSE; } static void ast_sem_check_expr (ASTSemState *st, ASTSemScope *scope, cJSON *expr); static void ast_sem_check_stmt (ASTSemState *st, ASTSemScope *scope, cJSON *stmt); static void ast_sem_predeclare_vars (ASTSemState *st, ASTSemScope *scope, cJSON *stmts) { cJSON *stmt; cJSON_ArrayForEach (stmt, stmts) { const char *kind = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (stmt, "kind")); if (!kind) continue; if (strcmp (kind, "function") == 0) { const char *name = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (stmt, "name")); if (name && !ast_sem_find_var (scope, name)) ast_sem_add_var (scope, name, 0, "function", scope->function_nr); } else if (strcmp (kind, "var") == 0) { cJSON *left = cJSON_GetObjectItemCaseSensitive (stmt, "left"); const char *name = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (left, "name")); if (name && !ast_sem_find_var (scope, name)) ast_sem_add_var (scope, name, 0, kind, scope->function_nr); } else if (strcmp (kind, "var_list") == 0) { cJSON *item; cJSON_ArrayForEach (item, cJSON_GetObjectItemCaseSensitive (stmt, "list")) { const char *ik = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (item, "kind")); if (ik && strcmp (ik, "var") == 0) { cJSON *left = cJSON_GetObjectItemCaseSensitive (item, "left"); const char *name = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (left, "name")); if (name && !ast_sem_find_var (scope, name)) ast_sem_add_var (scope, name, 0, ik, scope->function_nr); } } } } } /* Check whether an expression is being assigned to (=, +=, etc.) */ static void ast_sem_check_assign_target (ASTSemState *st, ASTSemScope *scope, cJSON *left) { if (!left) return; const char *kind = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (left, "kind")); if (!kind) return; if (strcmp (kind, "name") == 0) { const char *name = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (left, "name")); if (!name) return; ASTSemVar *v = ast_sem_find_var (scope, name); if (!v) { ast_sem_error (st, left, "cannot assign to unbound variable '%s'", name); } else if (v->is_const) { ast_sem_error (st, left, "cannot assign to constant '%s'", name); } /* Annotate with level/function_nr so compilers can emit correct set instructions */ ASTSemLookup r = ast_sem_lookup_var (scope, name); if (r.var) { cJSON_AddNumberToObject (left, "level", r.level); cJSON_AddNumberToObject (left, "function_nr", r.def_function_nr); } else { cJSON_AddNumberToObject (left, "level", -1); } } else if (strcmp (kind, ".") == 0 || strcmp (kind, "[") == 0 || strcmp (kind, "?.") == 0 || strcmp (kind, "?.[") == 0) { /* Property access as assignment target: resolve the object expression */ cJSON *obj_expr = cJSON_GetObjectItemCaseSensitive (left, "expression"); if (!obj_expr) obj_expr = cJSON_GetObjectItemCaseSensitive (left, "left"); ast_sem_check_expr (st, scope, obj_expr); /* Also resolve the index expression for computed access */ cJSON *idx_expr = cJSON_GetObjectItemCaseSensitive (left, "index"); if (!idx_expr && strcmp (kind, "[") == 0) idx_expr = cJSON_GetObjectItemCaseSensitive (left, "right"); if (idx_expr && cJSON_IsObject (idx_expr)) ast_sem_check_expr (st, scope, idx_expr); } } /* Recursively check an expression for semantic errors */ static void ast_sem_check_expr (ASTSemState *st, ASTSemScope *scope, cJSON *expr) { if (!expr) return; const char *kind = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (expr, "kind")); if (!kind) return; /* Assignment operators */ if (strcmp (kind, "assign") == 0 || strcmp (kind, "+=") == 0 || strcmp (kind, "-=") == 0 || strcmp (kind, "*=") == 0 || strcmp (kind, "/=") == 0 || strcmp (kind, "%=") == 0 || strcmp (kind, "<<=") == 0 || strcmp (kind, ">>=") == 0 || strcmp (kind, ">>>=") == 0 || strcmp (kind, "&=") == 0 || strcmp (kind, "^=") == 0 || strcmp (kind, "|=") == 0 || strcmp (kind, "**=") == 0 || strcmp (kind, "&&=") == 0 || strcmp (kind, "||=") == 0 || strcmp (kind, "??=") == 0) { ast_sem_check_assign_target (st, scope, cJSON_GetObjectItemCaseSensitive (expr, "left")); ast_sem_check_expr (st, scope, cJSON_GetObjectItemCaseSensitive (expr, "right")); return; } /* Increment/decrement */ if (strcmp (kind, "++") == 0 || strcmp (kind, "--") == 0) { cJSON *operand = cJSON_GetObjectItemCaseSensitive (expr, "expression"); if (operand) { const char *op_kind = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (operand, "kind")); if (op_kind && strcmp (op_kind, "name") == 0) { const char *name = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (operand, "name")); if (name) { ASTSemVar *v = ast_sem_find_var (scope, name); if (!v) { ast_sem_error (st, expr, "cannot assign to unbound variable '%s'", name); } else if (v->is_const) { ast_sem_error (st, expr, "cannot assign to constant '%s'", name); } /* Annotate with level/function_nr so compilers can emit correct set instructions */ ASTSemLookup r = ast_sem_lookup_var (scope, name); if (r.var) { cJSON_AddNumberToObject (operand, "level", r.level); cJSON_AddNumberToObject (operand, "function_nr", r.def_function_nr); } else { cJSON_AddNumberToObject (operand, "level", -1); } } } } return; } /* Binary ops, ternary, comma — recurse into children */ if (strcmp (kind, ",") == 0 || strcmp (kind, "+") == 0 || strcmp (kind, "-") == 0 || strcmp (kind, "*") == 0 || strcmp (kind, "/") == 0 || strcmp (kind, "%") == 0 || strcmp (kind, "==") == 0 || strcmp (kind, "!=") == 0 || strcmp (kind, "<") == 0 || strcmp (kind, ">") == 0 || strcmp (kind, "<=") == 0 || strcmp (kind, ">=") == 0 || strcmp (kind, "&&") == 0 || strcmp (kind, "||") == 0 || strcmp (kind, "??") == 0 || strcmp (kind, "&") == 0 || strcmp (kind, "|") == 0 || strcmp (kind, "^") == 0 || strcmp (kind, "<<") == 0 || strcmp (kind, ">>") == 0 || strcmp (kind, ">>>") == 0 || strcmp (kind, "**") == 0 || strcmp (kind, "in") == 0 || strcmp (kind, "of") == 0 || strcmp (kind, ".") == 0 || strcmp (kind, "[") == 0 || strcmp (kind, "?.") == 0 || strcmp (kind, "?.[") == 0) { ast_sem_check_expr (st, scope, cJSON_GetObjectItemCaseSensitive (expr, "left")); ast_sem_check_expr (st, scope, cJSON_GetObjectItemCaseSensitive (expr, "right")); return; } /* Ternary */ if (strcmp (kind, "then") == 0) { ast_sem_check_expr (st, scope, cJSON_GetObjectItemCaseSensitive (expr, "expression")); ast_sem_check_expr (st, scope, cJSON_GetObjectItemCaseSensitive (expr, "then")); ast_sem_check_expr (st, scope, cJSON_GetObjectItemCaseSensitive (expr, "else")); return; } /* Call and optional call */ if (strcmp (kind, "(") == 0 || strcmp (kind, "?.(") == 0) { ast_sem_check_expr (st, scope, cJSON_GetObjectItemCaseSensitive (expr, "expression")); cJSON *arg; cJSON_ArrayForEach (arg, cJSON_GetObjectItemCaseSensitive (expr, "list")) { ast_sem_check_expr (st, scope, arg); } return; } /* Unary ops */ if (strcmp (kind, "!") == 0 || strcmp (kind, "~") == 0 || strcmp (kind, "delete") == 0 || strcmp (kind, "neg") == 0 || strcmp (kind, "pos") == 0 || strcmp (kind, "spread") == 0 || strcmp (kind, "-unary") == 0 || strcmp (kind, "+unary") == 0 || strcmp (kind, "unary_-") == 0) { ast_sem_check_expr (st, scope, cJSON_GetObjectItemCaseSensitive (expr, "expression")); return; } /* Array literal */ if (strcmp (kind, "array") == 0) { cJSON *el; cJSON_ArrayForEach (el, cJSON_GetObjectItemCaseSensitive (expr, "list")) { ast_sem_check_expr (st, scope, el); } return; } /* Object literal */ if (strcmp (kind, "object") == 0 || strcmp (kind, "record") == 0) { cJSON *prop; cJSON_ArrayForEach (prop, cJSON_GetObjectItemCaseSensitive (expr, "list")) { cJSON *val = cJSON_GetObjectItemCaseSensitive (prop, "value"); if (!val) val = cJSON_GetObjectItemCaseSensitive (prop, "right"); ast_sem_check_expr (st, scope, val); } return; } /* Function expression / arrow function — create new scope */ if (strcmp (kind, "function") == 0) { cJSON *fn_nr_node = cJSON_GetObjectItemCaseSensitive (expr, "function_nr"); int fn_nr = fn_nr_node ? (int)cJSON_GetNumberValue (fn_nr_node) : scope->function_nr; ASTSemScope fn_scope = {0}; fn_scope.parent = scope; fn_scope.function_nr = fn_nr; fn_scope.is_function_scope = 1; cJSON_AddNumberToObject (expr, "outer", scope->function_nr); /* Add parameters as input */ cJSON *param; cJSON_ArrayForEach (param, cJSON_GetObjectItemCaseSensitive (expr, "list")) { const char *pname = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (param, "name")); if (pname) ast_sem_add_var (&fn_scope, pname, 1, "input", fn_nr); /* Check default value expressions */ cJSON *def_val = cJSON_GetObjectItemCaseSensitive (param, "expression"); if (def_val) ast_sem_check_expr (st, &fn_scope, def_val); } /* Pre-register all declarations for mutual recursion / forward references */ cJSON *fn_stmts = cJSON_GetObjectItemCaseSensitive (expr, "statements"); ast_sem_predeclare_vars (st, &fn_scope, fn_stmts); /* Check function body */ cJSON *stmt; cJSON_ArrayForEach (stmt, fn_stmts) { ast_sem_check_stmt (st, &fn_scope, stmt); } /* Check disruption clause */ cJSON *disruption = cJSON_GetObjectItemCaseSensitive (expr, "disruption"); if (disruption) { cJSON_ArrayForEach (stmt, disruption) { ast_sem_check_stmt (st, &fn_scope, stmt); } } /* Build scope record and attach to scopes array */ int nr_slots, nr_close; cJSON *rec = ast_sem_build_scope_record (&fn_scope, &nr_slots, &nr_close); cJSON_AddItemToArray (st->scopes_array, rec); cJSON_AddNumberToObject (expr, "nr_slots", nr_slots); cJSON_AddNumberToObject (expr, "nr_close_slots", nr_close); return; } /* Template literal */ if (strcmp (kind, "template") == 0 || strcmp (kind, "text literal") == 0) { cJSON *el; cJSON_ArrayForEach (el, cJSON_GetObjectItemCaseSensitive (expr, "list")) { ast_sem_check_expr (st, scope, el); } return; } /* Name token — annotate with level and function_nr */ if (strcmp (kind, "name") == 0) { const char *name = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (expr, "name")); if (name) { if (is_functino_name(name)) { cJSON_AddStringToObject (expr, "make", "functino"); cJSON_AddNumberToObject (expr, "level", -1); return; } ASTSemLookup r = ast_sem_lookup_var (scope, name); if (r.var) { cJSON_AddNumberToObject (expr, "level", r.level); cJSON_AddNumberToObject (expr, "function_nr", r.def_function_nr); r.var->nr_uses++; if (r.level > 0) r.var->closure = 1; } else { cJSON_AddNumberToObject (expr, "level", -1); ast_sem_add_intrinsic (st, name); } } return; } /* number, string, regexp, null, true, false, this — leaf nodes, no check needed */ } /* Check a statement for semantic errors */ static void ast_sem_check_stmt (ASTSemState *st, ASTSemScope *scope, cJSON *stmt) { if (!stmt) return; const char *kind = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (stmt, "kind")); if (!kind) return; if (strcmp (kind, "var_list") == 0) { if (!scope->is_function_scope) { ast_sem_error (st, stmt, "'var' declaration must be at function body level"); return; } cJSON *item; cJSON_ArrayForEach (item, cJSON_GetObjectItemCaseSensitive (stmt, "list")) { ast_sem_check_stmt (st, scope, item); } return; } if (strcmp (kind, "var") == 0) { if (!scope->is_function_scope) { ast_sem_error (st, stmt, "'var' declaration must be at function body level"); return; } /* Register variable */ cJSON *left = cJSON_GetObjectItemCaseSensitive (stmt, "left"); const char *name = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (left, "name")); if (name) { ASTSemVar *existing = ast_sem_find_var (scope, name); if (existing && existing->is_const) { ast_sem_error (st, left, "cannot redeclare constant '%s'", name); } if (!existing || existing->function_nr != scope->function_nr) ast_sem_add_var (scope, name, 0, "var", scope->function_nr); } ast_sem_check_expr (st, scope, cJSON_GetObjectItemCaseSensitive (stmt, "right")); return; } if (strcmp (kind, "def") == 0) { if (!scope->is_function_scope) { ast_sem_error (st, stmt, "'def' declaration must be at function body level"); return; } /* Register constant */ cJSON *left = cJSON_GetObjectItemCaseSensitive (stmt, "left"); const char *name = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (left, "name")); if (name) { ASTSemVar *existing = ast_sem_find_var (scope, name); if (existing && existing->is_const) { ast_sem_error (st, left, "cannot redeclare constant '%s'", name); } else if (existing && !existing->is_const && existing->function_nr == scope->function_nr) { /* Pre-scanned as var, now upgrading to const */ existing->is_const = 1; existing->make = "def"; } else { ast_sem_add_var (scope, name, 1, "def", scope->function_nr); } } ast_sem_check_expr (st, scope, cJSON_GetObjectItemCaseSensitive (stmt, "right")); return; } if (strcmp (kind, "call") == 0) { ast_sem_check_expr (st, scope, cJSON_GetObjectItemCaseSensitive (stmt, "expression")); return; } if (strcmp (kind, "if") == 0) { ast_sem_check_expr (st, scope, cJSON_GetObjectItemCaseSensitive (stmt, "expression")); cJSON *s2; { ASTSemScope then_scope = {0}; then_scope.parent = scope; then_scope.function_nr = scope->function_nr; cJSON_ArrayForEach (s2, cJSON_GetObjectItemCaseSensitive (stmt, "then")) { ast_sem_check_stmt (st, &then_scope, s2); } } { ASTSemScope list_scope = {0}; list_scope.parent = scope; list_scope.function_nr = scope->function_nr; cJSON_ArrayForEach (s2, cJSON_GetObjectItemCaseSensitive (stmt, "list")) { ast_sem_check_stmt (st, &list_scope, s2); } } { ASTSemScope else_scope = {0}; else_scope.parent = scope; else_scope.function_nr = scope->function_nr; cJSON_ArrayForEach (s2, cJSON_GetObjectItemCaseSensitive (stmt, "else")) { ast_sem_check_stmt (st, &else_scope, s2); } } return; } if (strcmp (kind, "while") == 0) { ast_sem_check_expr (st, scope, cJSON_GetObjectItemCaseSensitive (stmt, "expression")); ASTSemScope loop_scope = {0}; loop_scope.parent = scope; loop_scope.in_loop = 1; loop_scope.function_nr = scope->function_nr; cJSON *s2; cJSON_ArrayForEach (s2, cJSON_GetObjectItemCaseSensitive (stmt, "statements")) { ast_sem_check_stmt (st, &loop_scope, s2); } return; } if (strcmp (kind, "do") == 0) { ASTSemScope loop_scope = {0}; loop_scope.parent = scope; loop_scope.in_loop = 1; loop_scope.function_nr = scope->function_nr; cJSON *s2; cJSON_ArrayForEach (s2, cJSON_GetObjectItemCaseSensitive (stmt, "statements")) { ast_sem_check_stmt (st, &loop_scope, s2); } ast_sem_check_expr (st, scope, cJSON_GetObjectItemCaseSensitive (stmt, "expression")); return; } if (strcmp (kind, "for") == 0) { ASTSemScope loop_scope = {0}; loop_scope.parent = scope; loop_scope.in_loop = 1; loop_scope.function_nr = scope->function_nr; /* init is expression only (no var/def in for init) */ cJSON *init = cJSON_GetObjectItemCaseSensitive (stmt, "init"); if (init) ast_sem_check_expr (st, &loop_scope, init); ast_sem_check_expr (st, &loop_scope, cJSON_GetObjectItemCaseSensitive (stmt, "test")); ast_sem_check_expr (st, &loop_scope, cJSON_GetObjectItemCaseSensitive (stmt, "update")); cJSON *s2; cJSON_ArrayForEach (s2, cJSON_GetObjectItemCaseSensitive (stmt, "statements")) { ast_sem_check_stmt (st, &loop_scope, s2); } return; } if (strcmp (kind, "return") == 0 || strcmp (kind, "go") == 0) { ast_sem_check_expr (st, scope, cJSON_GetObjectItemCaseSensitive (stmt, "expression")); return; } if (strcmp (kind, "disrupt") == 0) { return; } if (strcmp (kind, "break") == 0) { if (!ast_sem_in_loop (scope)) { ast_sem_error (st, stmt, "'break' used outside of loop"); } return; } if (strcmp (kind, "continue") == 0) { if (!ast_sem_in_loop (scope)) { ast_sem_error (st, stmt, "'continue' used outside of loop"); } return; } if (strcmp (kind, "block") == 0) { ASTSemScope block_scope = {0}; block_scope.parent = scope; block_scope.function_nr = scope->function_nr; cJSON *s2; cJSON_ArrayForEach (s2, cJSON_GetObjectItemCaseSensitive (stmt, "statements")) { ast_sem_check_stmt (st, &block_scope, s2); } return; } if (strcmp (kind, "label") == 0) { ast_sem_check_stmt (st, scope, cJSON_GetObjectItemCaseSensitive (stmt, "statement")); return; } if (strcmp (kind, "function") == 0) { /* Function declaration — register name, then check body in new scope */ const char *name = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (stmt, "name")); if (name) ast_sem_add_var (scope, name, 0, "function", scope->function_nr); cJSON *fn_nr_node = cJSON_GetObjectItemCaseSensitive (stmt, "function_nr"); int fn_nr = fn_nr_node ? (int)cJSON_GetNumberValue (fn_nr_node) : scope->function_nr; ASTSemScope fn_scope = {0}; fn_scope.parent = scope; fn_scope.function_nr = fn_nr; fn_scope.is_function_scope = 1; cJSON_AddNumberToObject (stmt, "outer", scope->function_nr); cJSON *param; cJSON_ArrayForEach (param, cJSON_GetObjectItemCaseSensitive (stmt, "list")) { const char *pname = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (param, "name")); if (pname) ast_sem_add_var (&fn_scope, pname, 1, "input", fn_nr); /* Check default value expressions */ cJSON *def_val = cJSON_GetObjectItemCaseSensitive (param, "expression"); if (def_val) ast_sem_check_expr (st, &fn_scope, def_val); } /* Pre-register all var/def/function declarations for mutual recursion */ ast_sem_predeclare_vars (st, &fn_scope, cJSON_GetObjectItemCaseSensitive (stmt, "statements")); cJSON *s2; cJSON_ArrayForEach (s2, cJSON_GetObjectItemCaseSensitive (stmt, "statements")) { ast_sem_check_stmt (st, &fn_scope, s2); } /* Check disruption clause */ cJSON *disruption = cJSON_GetObjectItemCaseSensitive (stmt, "disruption"); if (disruption) { cJSON_ArrayForEach (s2, disruption) { ast_sem_check_stmt (st, &fn_scope, s2); } } /* Build scope record and attach to scopes array */ int nr_slots, nr_close; cJSON *rec = ast_sem_build_scope_record (&fn_scope, &nr_slots, &nr_close); cJSON_AddItemToArray (st->scopes_array, rec); cJSON_AddNumberToObject (stmt, "nr_slots", nr_slots); cJSON_AddNumberToObject (stmt, "nr_close_slots", nr_close); return; } } /* Run the semantic pass on a parsed AST, adding errors to the AST */ static void ast_semantic_check (cJSON *ast, cJSON **errors_out, cJSON **scopes_out, cJSON **intrinsics_out) { ASTSemState st = {0}; st.scopes_array = cJSON_CreateArray (); ASTSemScope global_scope = {0}; global_scope.function_nr = 0; global_scope.is_function_scope = 1; /* Process top-level function declarations first (they are hoisted) */ cJSON *stmt; cJSON_ArrayForEach (stmt, cJSON_GetObjectItemCaseSensitive (ast, "functions")) { const char *name = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (stmt, "name")); if (name) ast_sem_add_var (&global_scope, name, 0, "function", 0); } /* Check all statements (var/def are registered as they are encountered) */ cJSON_ArrayForEach (stmt, cJSON_GetObjectItemCaseSensitive (ast, "statements")) { ast_sem_check_stmt (&st, &global_scope, stmt); } /* Check function bodies */ cJSON_ArrayForEach (stmt, cJSON_GetObjectItemCaseSensitive (ast, "functions")) { ast_sem_check_stmt (&st, &global_scope, stmt); } /* Build program scope record (function_nr 0) and prepend to scopes array */ int nr_slots, nr_close; cJSON *prog_rec = ast_sem_build_scope_record (&global_scope, &nr_slots, &nr_close); /* Prepend: detach all children, add prog_rec, re-add children */ cJSON *existing = st.scopes_array->child; st.scopes_array->child = NULL; cJSON_AddItemToArray (st.scopes_array, prog_rec); if (existing) { cJSON *last = prog_rec; last->next = existing; existing->prev = last; } /* Build intrinsics array */ cJSON *intr_arr = cJSON_CreateArray (); for (int i = 0; i < st.intrinsic_count; i++) { cJSON_AddItemToArray (intr_arr, cJSON_CreateString (st.intrinsics[i])); } *errors_out = st.errors; *scopes_out = st.scopes_array; *intrinsics_out = intr_arr; } cJSON *JS_ASTTree (const char *source, size_t len, const char *filename) { ASTParseState s; memset (&s, 0, sizeof (s)); s.filename = filename; s.buf_start = (const uint8_t *)source; s.buf_ptr = (const uint8_t *)source; s.buf_end = (const uint8_t *)source + len; 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); /* Parse program */ cJSON *ast = ast_parse_program (&s); if (!ast) { if (s.errors) cJSON_Delete (s.errors); return NULL; } /* Run semantic pass - only if parsing succeeded without errors */ if (!s.has_error) { cJSON *sem_errors = NULL; cJSON *scopes = NULL; cJSON *intrinsics = NULL; ast_semantic_check (ast, &sem_errors, &scopes, &intrinsics); /* Attach scopes and intrinsics to AST */ if (scopes) cJSON_AddItemToObject (ast, "scopes", scopes); if (intrinsics) cJSON_AddItemToObject (ast, "intrinsics", intrinsics); if (sem_errors) { cJSON_AddItemToObject (ast, "errors", sem_errors); } } if (s.errors) { cJSON *existing = cJSON_GetObjectItemCaseSensitive (ast, "errors"); if (existing) { /* Append parse errors to existing semantic errors */ cJSON *err; cJSON *next; for (err = s.errors->child; err; err = next) { next = err->next; cJSON_DetachItemViaPointer (s.errors, err); cJSON_AddItemToArray (existing, err); } cJSON_Delete (s.errors); } else { cJSON_AddItemToObject (ast, "errors", s.errors); } } 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; } /* Build a token object for the tokenizer output */