2088 lines
72 KiB
C
2088 lines
72 KiB
C
/*
|
|
* 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";
|
|
int is_def = (s->token_val == TOK_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 if (is_def) {
|
|
/* def (constant) requires initializer */
|
|
ast_error (s, var_ptr, "missing initializer for constant '%s'", 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 */
|
|
if (s->token_val != ';') {
|
|
if (s->token_val == TOK_VAR || s->token_val == TOK_DEF) {
|
|
cJSON *init = ast_parse_statement (s);
|
|
cJSON_AddItemToObject (node, "init", init);
|
|
} else {
|
|
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;
|
|
const char *scope_name; /* disambiguated name for block-scope vars (NULL = use 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 */
|
|
int block_depth; /* 0 = function scope, 1+ = block scope */
|
|
} ASTSemScope;
|
|
|
|
typedef struct ASTSemState {
|
|
cJSON *errors;
|
|
int has_error;
|
|
cJSON *scopes_array;
|
|
const char *intrinsics[256];
|
|
int intrinsic_count;
|
|
int block_var_counter; /* monotonically increasing counter for unique block var names */
|
|
} 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->scope_name = NULL;
|
|
v->is_const = is_const;
|
|
v->make = make;
|
|
v->function_nr = function_nr;
|
|
v->nr_uses = 0;
|
|
v->closure = 0;
|
|
scope->var_count++;
|
|
}
|
|
}
|
|
|
|
/* Propagate block-scope vars to the function scope (parent) with disambiguated names */
|
|
static void ast_sem_propagate_block_vars (ASTSemState *st, ASTSemScope *parent,
|
|
ASTSemScope *block) {
|
|
for (int i = 0; i < block->var_count; i++) {
|
|
ASTSemVar *v = &block->vars[i];
|
|
const char *sn = v->scope_name ? v->scope_name : v->name;
|
|
if (parent->var_count < AST_SEM_MAX_VARS) {
|
|
ASTSemVar *pv = &parent->vars[parent->var_count];
|
|
pv->name = sn;
|
|
pv->scope_name = NULL;
|
|
pv->is_const = v->is_const;
|
|
pv->make = v->make;
|
|
pv->function_nr = v->function_nr;
|
|
pv->nr_uses = v->nr_uses;
|
|
pv->closure = v->closure;
|
|
parent->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);
|
|
if (r.var->scope_name)
|
|
cJSON_AddStringToObject (left, "scope_name", r.var->scope_name);
|
|
} 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/scope_name 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);
|
|
if (r.var->scope_name)
|
|
cJSON_AddStringToObject (operand, "scope_name", r.var->scope_name);
|
|
} 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;
|
|
if (r.var->scope_name)
|
|
cJSON_AddStringToObject (expr, "scope_name", r.var->scope_name);
|
|
} 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) {
|
|
cJSON *item;
|
|
cJSON_ArrayForEach (item, cJSON_GetObjectItemCaseSensitive (stmt, "list")) {
|
|
ast_sem_check_stmt (st, scope, item);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (strcmp (kind, "var") == 0) {
|
|
/* 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
|
|
|| scope->block_depth > 0)
|
|
ast_sem_add_var (scope, name, 0, "var", scope->function_nr);
|
|
if (scope->block_depth > 0) {
|
|
char buf[128];
|
|
snprintf (buf, sizeof (buf), "_%s_%d", name, st->block_var_counter++);
|
|
char *sn = sys_malloc (strlen (buf) + 1);
|
|
strcpy (sn, buf);
|
|
scope->vars[scope->var_count - 1].scope_name = sn;
|
|
cJSON_AddStringToObject (left, "scope_name", sn);
|
|
}
|
|
}
|
|
ast_sem_check_expr (st, scope, cJSON_GetObjectItemCaseSensitive (stmt, "right"));
|
|
return;
|
|
}
|
|
|
|
if (strcmp (kind, "def") == 0) {
|
|
/* 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);
|
|
if (scope->block_depth > 0) {
|
|
char buf[128];
|
|
snprintf (buf, sizeof (buf), "_%s_%d", name, st->block_var_counter++);
|
|
char *sn = sys_malloc (strlen (buf) + 1);
|
|
strcpy (sn, buf);
|
|
scope->vars[scope->var_count - 1].scope_name = sn;
|
|
cJSON_AddStringToObject (left, "scope_name", sn);
|
|
}
|
|
}
|
|
}
|
|
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;
|
|
then_scope.block_depth = scope->block_depth + 1;
|
|
cJSON_ArrayForEach (s2, cJSON_GetObjectItemCaseSensitive (stmt, "then")) {
|
|
ast_sem_check_stmt (st, &then_scope, s2);
|
|
}
|
|
ast_sem_propagate_block_vars (st, scope, &then_scope);
|
|
}
|
|
{
|
|
ASTSemScope list_scope = {0};
|
|
list_scope.parent = scope;
|
|
list_scope.function_nr = scope->function_nr;
|
|
list_scope.block_depth = scope->block_depth + 1;
|
|
cJSON_ArrayForEach (s2, cJSON_GetObjectItemCaseSensitive (stmt, "list")) {
|
|
ast_sem_check_stmt (st, &list_scope, s2);
|
|
}
|
|
ast_sem_propagate_block_vars (st, scope, &list_scope);
|
|
}
|
|
{
|
|
ASTSemScope else_scope = {0};
|
|
else_scope.parent = scope;
|
|
else_scope.function_nr = scope->function_nr;
|
|
else_scope.block_depth = scope->block_depth + 1;
|
|
cJSON_ArrayForEach (s2, cJSON_GetObjectItemCaseSensitive (stmt, "else")) {
|
|
ast_sem_check_stmt (st, &else_scope, s2);
|
|
}
|
|
ast_sem_propagate_block_vars (st, scope, &else_scope);
|
|
}
|
|
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;
|
|
loop_scope.block_depth = scope->block_depth + 1;
|
|
cJSON *s2;
|
|
cJSON_ArrayForEach (s2, cJSON_GetObjectItemCaseSensitive (stmt, "statements")) {
|
|
ast_sem_check_stmt (st, &loop_scope, s2);
|
|
}
|
|
ast_sem_propagate_block_vars (st, scope, &loop_scope);
|
|
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;
|
|
loop_scope.block_depth = scope->block_depth + 1;
|
|
cJSON *s2;
|
|
cJSON_ArrayForEach (s2, cJSON_GetObjectItemCaseSensitive (stmt, "statements")) {
|
|
ast_sem_check_stmt (st, &loop_scope, s2);
|
|
}
|
|
ast_sem_propagate_block_vars (st, scope, &loop_scope);
|
|
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;
|
|
loop_scope.block_depth = scope->block_depth + 1;
|
|
/* init may be a var/def statement or expression */
|
|
cJSON *init = cJSON_GetObjectItemCaseSensitive (stmt, "init");
|
|
if (init) {
|
|
const char *init_kind = cJSON_GetStringValue (cJSON_GetObjectItemCaseSensitive (init, "kind"));
|
|
if (init_kind && (strcmp (init_kind, "var") == 0 || strcmp (init_kind, "def") == 0)) {
|
|
ast_sem_check_stmt (st, &loop_scope, init);
|
|
} else {
|
|
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);
|
|
}
|
|
ast_sem_propagate_block_vars (st, scope, &loop_scope);
|
|
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;
|
|
block_scope.block_depth = scope->block_depth + 1;
|
|
cJSON *s2;
|
|
cJSON_ArrayForEach (s2, cJSON_GetObjectItemCaseSensitive (stmt, "statements")) {
|
|
ast_sem_check_stmt (st, &block_scope, s2);
|
|
}
|
|
ast_sem_propagate_block_vars (st, scope, &block_scope);
|
|
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 */
|