Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dbf2c659d9 | |||
| 2b8062c55f | |||
| 2ed6b26dde | |||
| d8e9fd12f4 | |||
| 8fa9c4ba20 | |||
| 9c7bde47dc | |||
| c0553459e1 | |||
| fd208583fe | |||
| 3e29fc43ab | |||
| 979a5677d5 | |||
| 17b1aa0736 | |||
| f0c731d2db | |||
| e7e0f7d3e5 |
@@ -3135,23 +3135,49 @@ static void jb_puts(JsonBuf* b, const char* s) {
|
||||
|
||||
static void jb_emit_escaped(JsonBuf* b, const char* s) {
|
||||
jb_putc(b, '"');
|
||||
for (; *s; s++) {
|
||||
unsigned char c = (unsigned char)*s;
|
||||
const unsigned char* p = (const unsigned char*)s;
|
||||
while (*p) {
|
||||
unsigned char c = *p;
|
||||
switch (c) {
|
||||
case '"': jb_puts(b, "\\\""); break;
|
||||
case '\\': jb_puts(b, "\\\\"); break;
|
||||
case '\b': jb_puts(b, "\\b"); break;
|
||||
case '\f': jb_puts(b, "\\f"); break;
|
||||
case '\n': jb_puts(b, "\\n"); break;
|
||||
case '\r': jb_puts(b, "\\r"); break;
|
||||
case '\t': jb_puts(b, "\\t"); break;
|
||||
case '"': jb_puts(b, "\\\""); p++; break;
|
||||
case '\\': jb_puts(b, "\\\\"); p++; break;
|
||||
case '\b': jb_puts(b, "\\b"); p++; break;
|
||||
case '\f': jb_puts(b, "\\f"); p++; break;
|
||||
case '\n': jb_puts(b, "\\n"); p++; break;
|
||||
case '\r': jb_puts(b, "\\r"); p++; break;
|
||||
case '\t': jb_puts(b, "\\t"); p++; break;
|
||||
default:
|
||||
if (c < 0x20) {
|
||||
char tmp[8];
|
||||
snprintf(tmp, sizeof(tmp), "\\u%04x", c);
|
||||
jb_puts(b, tmp);
|
||||
} else {
|
||||
p++;
|
||||
} else if (c < 0x80) {
|
||||
jb_putc(b, (char)c);
|
||||
p++;
|
||||
} else {
|
||||
/* Multi-byte UTF-8: validate sequence, pass through if valid,
|
||||
* escape as \u00xx if the start byte is invalid/orphaned. */
|
||||
int seq_len = 0;
|
||||
if ((c & 0xE0) == 0xC0) seq_len = 2;
|
||||
else if ((c & 0xF0) == 0xE0) seq_len = 3;
|
||||
else if ((c & 0xF8) == 0xF0) seq_len = 4;
|
||||
if (seq_len >= 2) {
|
||||
int valid = 1;
|
||||
for (int i = 1; i < seq_len; i++) {
|
||||
if ((p[i] & 0xC0) != 0x80) { valid = 0; break; }
|
||||
}
|
||||
if (valid) {
|
||||
for (int i = 0; i < seq_len; i++) jb_putc(b, (char)p[i]);
|
||||
p += seq_len;
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* Invalid start byte or truncated sequence — escape it */
|
||||
char tmp[8];
|
||||
snprintf(tmp, sizeof(tmp), "\\u%04x", c);
|
||||
jb_puts(b, tmp);
|
||||
p++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -8447,7 +8473,7 @@ static el_val_t llm_provider_request(const char* url, const char* key,
|
||||
}
|
||||
}
|
||||
|
||||
static el_val_t llm_chain_call(const char* system_str, const char* user_str) {
|
||||
static el_val_t llm_chain_call(const char* model_pref, const char* system_str, const char* user_str) {
|
||||
char url_key[64], key_key[64], fmt_key[64], model_key[64];
|
||||
for (int i = 0; i < LLM_MAX_PROVIDERS; i++) {
|
||||
snprintf(url_key, sizeof(url_key), "NEURON_LLM_%d_URL", i);
|
||||
@@ -8460,6 +8486,7 @@ static el_val_t llm_chain_call(const char* system_str, const char* user_str) {
|
||||
const char* fmt_s = getenv(fmt_key);
|
||||
int fmt = (fmt_s && strcmp(fmt_s, "anthropic") == 0) ? 1 : 0;
|
||||
const char* model = getenv(model_key);
|
||||
if (!model || !*model) model = model_pref; /* fall back to the caller-requested model */
|
||||
fprintf(stderr, "[llm] trying provider %d (%s)\n", i, url);
|
||||
el_val_t result = llm_provider_request(url, key, fmt, model, system_str, user_str);
|
||||
const char* t = EL_CSTR(result);
|
||||
@@ -8470,7 +8497,7 @@ static el_val_t llm_chain_call(const char* system_str, const char* user_str) {
|
||||
const char* api_key = getenv("ANTHROPIC_API_KEY");
|
||||
if (!api_key || !*api_key) return http_error_json("no LLM providers configured");
|
||||
fprintf(stderr, "[llm] using legacy ANTHROPIC_API_KEY fallback\n");
|
||||
return llm_provider_request(LLM_API_URL, api_key, 1, NULL, system_str, user_str);
|
||||
return llm_provider_request(LLM_API_URL, api_key, 1, model_pref, system_str, user_str);
|
||||
}
|
||||
|
||||
/* Legacy llm_request — kept for backward compat with agentic loop internals */
|
||||
@@ -8534,14 +8561,16 @@ static el_val_t llm_extract_text(el_val_t resp_val) {
|
||||
}
|
||||
|
||||
el_val_t llm_call(el_val_t model, el_val_t prompt) {
|
||||
const char* m = EL_CSTR(model);
|
||||
const char* u = EL_CSTR(prompt); if (!u) u = "";
|
||||
return llm_chain_call(NULL, u);
|
||||
return llm_chain_call(m, NULL, u);
|
||||
}
|
||||
|
||||
el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt) {
|
||||
const char* m = EL_CSTR(model);
|
||||
const char* s = EL_CSTR(system_prompt); if (!s) s = "";
|
||||
const char* u = EL_CSTR(user_prompt); if (!u) u = "";
|
||||
return llm_chain_call(s, u);
|
||||
return llm_chain_call(m, s, u);
|
||||
}
|
||||
|
||||
/* ── Tool registry for llm_call_agentic ─────────────────────────────────── */
|
||||
|
||||
@@ -324,10 +324,6 @@ fn cg_html_parts(children: [Map<String, Any>], acc_var: String) -> String {
|
||||
let each_c: String = cg_html_each(child, acc_var)
|
||||
let parts = native_list_append(parts, each_c)
|
||||
}
|
||||
if str_eq(html_kind, "HtmlIf") {
|
||||
let if_c: String = cg_html_if(child, acc_var)
|
||||
let parts = native_list_append(parts, if_c)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
str_join(parts, "")
|
||||
@@ -417,17 +413,6 @@ fn cg_html_each(node: Map<String, Any>, acc_var: String) -> String {
|
||||
"{ el_val_t " + list_var + " = (" + list_c + "); el_val_t " + len_var + " = el_list_len(" + list_var + "); for (el_val_t " + idx_var + " = 0; " + idx_var + " < " + len_var + "; " + idx_var + "++) { el_val_t " + item_name + " = el_list_get(" + list_var + ", " + idx_var + "); " + inner_c + "} } "
|
||||
}
|
||||
|
||||
// Generate code for {#if cond} ... {/if} (with optional {#else}).
|
||||
fn cg_html_if(node: Map<String, Any>, acc_var: String) -> String {
|
||||
let cond_expr = node["cond"]
|
||||
let then_children: [Map<String, Any>] = node["then"]
|
||||
let else_children: [Map<String, Any>] = node["else"]
|
||||
let cond_c: String = cg_expr(cond_expr)
|
||||
let then_c: String = cg_html_parts(then_children, acc_var)
|
||||
let else_c: String = cg_html_parts(else_children, acc_var)
|
||||
"if (" + cond_c + ") { " + then_c + " } else { " + else_c + " } "
|
||||
}
|
||||
|
||||
// Top-level HTML template codegen — returns a C statement-expression string.
|
||||
fn cg_html_template(expr: Map<String, Any>) -> String {
|
||||
let root = expr["root"]
|
||||
|
||||
+94
-284
@@ -293,48 +293,6 @@ fn is_void_element(name: String) -> Bool {
|
||||
false
|
||||
}
|
||||
|
||||
// Collect all tokens as raw text until </tag_name> is encountered.
|
||||
// Used for <style> and <script> elements to avoid parsing CSS/JS as El.
|
||||
// Returns { "text": "...", "pos": p_after_closing_tag }
|
||||
fn parse_raw_text_content(tokens: [Any], pos: Int, tag_name: String) -> Map<String, Any> {
|
||||
let parts: [String] = native_list_empty()
|
||||
let p = pos
|
||||
let running = true
|
||||
while running {
|
||||
let k = tok_kind(tokens, p)
|
||||
if str_eq(k, "Eof") {
|
||||
let running = false
|
||||
} else {
|
||||
if str_eq(k, "Lt") {
|
||||
let k2 = tok_kind(tokens, p + 1)
|
||||
if str_eq(k2, "Slash") {
|
||||
// Check if this is </tag_name>
|
||||
let close_name = tok_value(tokens, p + 2)
|
||||
if str_eq(close_name, tag_name) {
|
||||
// consume </tag_name>
|
||||
let p = p + 3
|
||||
let p = expect(tokens, p, "Gt")
|
||||
let running = false
|
||||
} else {
|
||||
let v = tok_value(tokens, p)
|
||||
let parts = native_list_append(parts, v)
|
||||
let p = p + 1
|
||||
}
|
||||
} else {
|
||||
let v = tok_value(tokens, p)
|
||||
let parts = native_list_append(parts, v)
|
||||
let p = p + 1
|
||||
}
|
||||
} else {
|
||||
let v = tok_value(tokens, p)
|
||||
let parts = native_list_append(parts, v)
|
||||
let p = p + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "text": str_join(parts, ""), "pos": p }
|
||||
}
|
||||
|
||||
// Collect tokens as text content until we hit Lt, LBrace, Eof, or a
|
||||
// closing-tag marker (Lt Slash). Returns { "text": "...", "pos": p }
|
||||
fn parse_html_text_tokens(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||
@@ -362,7 +320,7 @@ fn parse_html_text_tokens(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "text": str_join(parts, ""), "pos": p }
|
||||
{ "text": str_join(parts, " "), "pos": p }
|
||||
}
|
||||
|
||||
// Parse an attribute list: (attrname | attrname="val" | attrname={expr})*
|
||||
@@ -477,125 +435,77 @@ fn parse_html_children(tokens: [Any], pos: Int, parent_tag: String) -> Map<Strin
|
||||
}
|
||||
} else {
|
||||
if str_eq(k, "LBrace") {
|
||||
// Interpolation: {expr}, {#each ...}, {#if ...}, {#else}, {/each}, {/if}
|
||||
// Note: '#' (ASCII 35) is skipped by the lexer, so {#each} lexes as
|
||||
// LBrace Ident:"each" ... and {#if} lexes as LBrace If ... and
|
||||
// {#else} lexes as LBrace Else RBrace.
|
||||
// Interpolation: {expr} or {#each ...} or {/each}
|
||||
let k2 = tok_kind(tokens, p + 1)
|
||||
if str_eq(k2, "Slash") {
|
||||
// {/each} or {/if} — end of block, stop
|
||||
// skip { /
|
||||
let p = p + 2
|
||||
// skip the close-tag name token (each, if, etc.)
|
||||
let p = p + 1
|
||||
// skip }
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
let running = false
|
||||
} else {
|
||||
if str_eq(k2, "If") {
|
||||
// {#if condition} ... {/if}
|
||||
// Skip { if (2 tokens; '#' was silently skipped by lexer)
|
||||
let p = p + 2
|
||||
// Parse condition expression (no block expr)
|
||||
if str_eq(k2, "Hash") {
|
||||
// {#each list as item}
|
||||
let k3_v = tok_value(tokens, p + 2)
|
||||
if str_eq(k3_v, "each") {
|
||||
let p = p + 3
|
||||
// parse list expr up to "as" keyword
|
||||
let prev_no_block: String = state_get("__no_block_expr")
|
||||
state_set("__no_block_expr", "1")
|
||||
let r_cond = parse_expr(tokens, p)
|
||||
let r_list = parse_expr(tokens, p)
|
||||
state_set("__no_block_expr", prev_no_block)
|
||||
let cond_expr = r_cond["node"]
|
||||
let p = r_cond["pos"]
|
||||
el_release(r_cond)
|
||||
let list_expr = r_list["node"]
|
||||
let p = r_list["pos"]
|
||||
// r_list result map fully consumed — release to free peak heap.
|
||||
el_release(r_list)
|
||||
// expect "as"
|
||||
let p = expect(tokens, p, "As")
|
||||
// item variable name
|
||||
let item_name = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
// consume closing }
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
// parse then-children until {#else} or {/if}
|
||||
let r_then = parse_html_children(tokens, p, "__if_then__")
|
||||
let then_children = r_then["children"]
|
||||
let p = r_then["pos"]
|
||||
el_release(r_then)
|
||||
// check for {#else} — lexed as LBrace Else RBrace
|
||||
let else_children: [Map<String, Any>] = native_list_empty()
|
||||
let ck = tok_kind(tokens, p)
|
||||
if str_eq(ck, "LBrace") {
|
||||
let ck2 = tok_kind(tokens, p + 1)
|
||||
if str_eq(ck2, "Else") {
|
||||
// consume { else }
|
||||
let p = p + 2
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
// parse else-children until {/if}
|
||||
let r_else = parse_html_children(tokens, p, "__if_else__")
|
||||
let else_children = r_else["children"]
|
||||
let p = r_else["pos"]
|
||||
el_release(r_else)
|
||||
// parse body until {/each}
|
||||
let r_body = parse_html_each_body(tokens, p)
|
||||
let body_children = r_body["children"]
|
||||
let p = r_body["pos"]
|
||||
// r_body result map fully consumed — release to free peak heap.
|
||||
el_release(r_body)
|
||||
let each_node: Map<String, Any> = { "html": "Each", "list": list_expr, "item": item_name, "body": body_children }
|
||||
let children = native_list_append(children, each_node)
|
||||
} else {
|
||||
let p = p + 1
|
||||
}
|
||||
} else {
|
||||
if str_eq(k2, "Slash") {
|
||||
// {/each} — end of each block, stop
|
||||
// skip {/each}
|
||||
let p = p + 2
|
||||
// skip "each"
|
||||
let p = p + 1
|
||||
// skip }
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
let running = false
|
||||
} else {
|
||||
// regular {expr}
|
||||
let r = parse_expr(tokens, p + 1)
|
||||
let interp_val = r["node"]
|
||||
let p = r["pos"]
|
||||
// r result map fully consumed — release to free peak heap.
|
||||
el_release(r)
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
// Check if the expr is a call to raw()
|
||||
let is_raw_call = false
|
||||
let interp_kind: String = interp_val["expr"]
|
||||
if str_eq(interp_kind, "Call") {
|
||||
let fn_node = interp_val["func"]
|
||||
let fn_kind: String = fn_node["expr"]
|
||||
if str_eq(fn_kind, "Ident") {
|
||||
let fn_name_v: String = fn_node["name"]
|
||||
if str_eq(fn_name_v, "raw") {
|
||||
let is_raw_call = true
|
||||
}
|
||||
}
|
||||
}
|
||||
let if_node: Map<String, Any> = { "html": "HtmlIf", "cond": cond_expr, "then": then_children, "else": else_children }
|
||||
let children = native_list_append(children, if_node)
|
||||
} else {
|
||||
if str_eq(k2, "Else") {
|
||||
// {#else} sentinel — lexed as LBrace Else RBrace
|
||||
// Do NOT consume — leave position for caller ({#if} handler checks for it)
|
||||
let running = false
|
||||
if is_raw_call {
|
||||
let raw_args = interp_val["args"]
|
||||
let raw_inner = native_list_get(raw_args, 0)
|
||||
let children = native_list_append(children, { "html": "Raw", "value": raw_inner })
|
||||
} else {
|
||||
// Check for {#each list as item} — lexed as LBrace Ident:"each" ...
|
||||
let k2_v = tok_value(tokens, p + 1)
|
||||
if str_eq(k2_v, "each") {
|
||||
let p = p + 2
|
||||
// parse list expr up to "as" keyword
|
||||
let prev_no_block: String = state_get("__no_block_expr")
|
||||
state_set("__no_block_expr", "1")
|
||||
let r_list = parse_expr(tokens, p)
|
||||
state_set("__no_block_expr", prev_no_block)
|
||||
let list_expr = r_list["node"]
|
||||
let p = r_list["pos"]
|
||||
// r_list result map fully consumed — release to free peak heap.
|
||||
el_release(r_list)
|
||||
// expect "as"
|
||||
let p = expect(tokens, p, "As")
|
||||
// item variable name
|
||||
let item_name = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
// consume closing }
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
// parse body until {/each}
|
||||
let r_body = parse_html_each_body(tokens, p)
|
||||
let body_children = r_body["children"]
|
||||
let p = r_body["pos"]
|
||||
// r_body result map fully consumed — release to free peak heap.
|
||||
el_release(r_body)
|
||||
let each_node: Map<String, Any> = { "html": "Each", "list": list_expr, "item": item_name, "body": body_children }
|
||||
let children = native_list_append(children, each_node)
|
||||
} else {
|
||||
// regular {expr} — disable map-literal parsing so {fn(a,b)}
|
||||
// does not trigger the LBrace→map path inside parse_primary
|
||||
let prev_no_block: String = state_get("__no_block_expr")
|
||||
state_set("__no_block_expr", "1")
|
||||
let r = parse_expr(tokens, p + 1)
|
||||
state_set("__no_block_expr", prev_no_block)
|
||||
let interp_val = r["node"]
|
||||
let p = r["pos"]
|
||||
// r result map fully consumed — release to free peak heap.
|
||||
el_release(r)
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
// Check if the expr is a call to raw()
|
||||
let is_raw_call = false
|
||||
let interp_kind: String = interp_val["expr"]
|
||||
if str_eq(interp_kind, "Call") {
|
||||
let fn_node = interp_val["func"]
|
||||
let fn_kind: String = fn_node["expr"]
|
||||
if str_eq(fn_kind, "Ident") {
|
||||
let fn_name_v: String = fn_node["name"]
|
||||
if str_eq(fn_name_v, "raw") {
|
||||
let is_raw_call = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if is_raw_call {
|
||||
let raw_args = interp_val["args"]
|
||||
let raw_inner = native_list_get(raw_args, 0)
|
||||
let children = native_list_append(children, { "html": "Raw", "value": raw_inner })
|
||||
} else {
|
||||
let children = native_list_append(children, { "html": "Interp", "value": interp_val })
|
||||
}
|
||||
}
|
||||
let children = native_list_append(children, { "html": "Interp", "value": interp_val })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -655,27 +565,6 @@ fn parse_html_element(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||
if is_void_element(tag_name) {
|
||||
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p)
|
||||
}
|
||||
// raw-text mode for style/script — collect content as plain text without parsing CSS/JS as El
|
||||
if str_eq(tag_name, "style") {
|
||||
let r_raw = parse_raw_text_content(tokens, p, "style")
|
||||
let raw_text: String = r_raw["text"]
|
||||
let p = r_raw["pos"]
|
||||
el_release(r_raw)
|
||||
let raw_child: Map<String, Any> = { "html": "Text", "text": raw_text }
|
||||
let raw_children: [Map<String, Any>] = native_list_empty()
|
||||
let raw_children = native_list_append(raw_children, raw_child)
|
||||
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": raw_children, "self_closing": false }, p)
|
||||
}
|
||||
if str_eq(tag_name, "script") {
|
||||
let r_raw = parse_raw_text_content(tokens, p, "script")
|
||||
let raw_text: String = r_raw["text"]
|
||||
let p = r_raw["pos"]
|
||||
el_release(r_raw)
|
||||
let raw_child: Map<String, Any> = { "html": "Text", "text": raw_text }
|
||||
let raw_children: [Map<String, Any>] = native_list_empty()
|
||||
let raw_children = native_list_append(raw_children, raw_child)
|
||||
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": raw_children, "self_closing": false }, p)
|
||||
}
|
||||
// parse children
|
||||
let r_children = parse_html_children(tokens, p, tag_name)
|
||||
let children = r_children["children"]
|
||||
@@ -829,123 +718,44 @@ fn parse_primary(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||
// as the start of the block they're expecting.
|
||||
return make_result({ "expr": "Nil" }, pos)
|
||||
}
|
||||
// Distinguish map literal from interpolation chain.
|
||||
// A map literal requires { key: value } — the second token inside { must be Colon.
|
||||
// An empty {} is a map literal. Everything else is an interpolation chain.
|
||||
let first_k: String = tok_kind(tokens, pos + 1)
|
||||
let second_k: String = tok_kind(tokens, pos + 2)
|
||||
if str_eq(first_k, "RBrace") {
|
||||
// Empty map literal {}
|
||||
return make_result({ "expr": "Map", "pairs": native_list_empty() }, pos + 2)
|
||||
}
|
||||
if str_eq(second_k, "Colon") {
|
||||
// MAP LITERAL: { key: value, ... }
|
||||
let p = pos + 1
|
||||
let pairs: [Map<String, Any>] = native_list_empty()
|
||||
let running = true
|
||||
while running {
|
||||
let k2 = tok_kind(tokens, p)
|
||||
if k2 == "RBrace" {
|
||||
let p = pos + 1
|
||||
let pairs: [Map<String, Any>] = native_list_empty()
|
||||
let running = true
|
||||
while running {
|
||||
let k2 = tok_kind(tokens, p)
|
||||
if k2 == "RBrace" {
|
||||
let running = false
|
||||
} else {
|
||||
if k2 == "Eof" {
|
||||
let running = false
|
||||
} else {
|
||||
if k2 == "Eof" {
|
||||
let running = false
|
||||
// key: Str token
|
||||
let key = tok_value(tokens, p)
|
||||
let new_p: Int = p + 1
|
||||
let new_p = expect(tokens, new_p, "Colon")
|
||||
let r = parse_expr(tokens, new_p)
|
||||
let val_node = r["node"]
|
||||
let new_p = r["pos"]
|
||||
// r result map fully consumed — release to free peak heap.
|
||||
el_release(r)
|
||||
let pair = { "key": key, "value": val_node }
|
||||
let pairs = native_list_append(pairs, pair)
|
||||
let k3 = tok_kind(tokens, new_p)
|
||||
if k3 == "Comma" {
|
||||
let new_p = new_p + 1
|
||||
}
|
||||
// Non-progress guard: malformed map content can leave
|
||||
// parse_expr returning the same pos. Force advance.
|
||||
if new_p <= p {
|
||||
let p = p + 1
|
||||
} else {
|
||||
// key: Str or Ident token
|
||||
let key = tok_value(tokens, p)
|
||||
let new_p: Int = p + 1
|
||||
let new_p = expect(tokens, new_p, "Colon")
|
||||
let r = parse_expr(tokens, new_p)
|
||||
let val_node = r["node"]
|
||||
let new_p = r["pos"]
|
||||
// r result map fully consumed — release to free peak heap.
|
||||
el_release(r)
|
||||
let pair = { "key": key, "value": val_node }
|
||||
let pairs = native_list_append(pairs, pair)
|
||||
let k3 = tok_kind(tokens, new_p)
|
||||
if k3 == "Comma" {
|
||||
let new_p = new_p + 1
|
||||
}
|
||||
// Non-progress guard: malformed map content can leave
|
||||
// parse_expr returning the same pos. Force advance.
|
||||
if new_p <= p {
|
||||
let p = p + 1
|
||||
} else {
|
||||
let p = new_p
|
||||
}
|
||||
let p = new_p
|
||||
}
|
||||
}
|
||||
}
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
return make_result({ "expr": "Map", "pairs": pairs }, p)
|
||||
}
|
||||
// INTERPOLATION CHAIN: {expr}, {expr}{expr}, {expr}<html>, etc.
|
||||
// Build a BinOp(Plus, ...) concatenation chain.
|
||||
let p = pos
|
||||
let chain_node: Map<String, Any> = { "expr": "Nil" }
|
||||
let chain_started = false
|
||||
let chain_running = true
|
||||
while chain_running {
|
||||
let ck: String = tok_kind(tokens, p)
|
||||
if str_eq(ck, "LBrace") {
|
||||
let prev_no_block: String = state_get("__no_block_expr")
|
||||
state_set("__no_block_expr", "1")
|
||||
let r = parse_expr(tokens, p + 1)
|
||||
state_set("__no_block_expr", prev_no_block)
|
||||
let part = r["node"]
|
||||
let p = r["pos"]
|
||||
// r result map fully consumed — release to free peak heap.
|
||||
el_release(r)
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
if !chain_started {
|
||||
let chain_node = part
|
||||
let chain_started = true
|
||||
} else {
|
||||
let chain_node: Map<String, Any> = { "expr": "BinOp", "op": "Plus", "left": chain_node, "right": part }
|
||||
}
|
||||
} else {
|
||||
if str_eq(ck, "Lt") {
|
||||
let ck2: String = tok_kind(tokens, p + 1)
|
||||
if str_eq(ck2, "Not") {
|
||||
let r = parse_html_template(tokens, p)
|
||||
let part = r["node"]
|
||||
let p = r["pos"]
|
||||
// r result map fully consumed — release to free peak heap.
|
||||
el_release(r)
|
||||
if !chain_started {
|
||||
let chain_node = part
|
||||
let chain_started = true
|
||||
} else {
|
||||
let chain_node: Map<String, Any> = { "expr": "BinOp", "op": "Plus", "left": chain_node, "right": part }
|
||||
}
|
||||
} else {
|
||||
if str_eq(ck2, "Ident") {
|
||||
let tag_candidate: String = tok_value(tokens, p + 1)
|
||||
if is_html_tag_name(tag_candidate) {
|
||||
let r = parse_html_template(tokens, p)
|
||||
let part = r["node"]
|
||||
let p = r["pos"]
|
||||
// r result map fully consumed — release to free peak heap.
|
||||
el_release(r)
|
||||
if !chain_started {
|
||||
let chain_node = part
|
||||
let chain_started = true
|
||||
} else {
|
||||
let chain_node: Map<String, Any> = { "expr": "BinOp", "op": "Plus", "left": chain_node, "right": part }
|
||||
}
|
||||
} else {
|
||||
let chain_running = false
|
||||
}
|
||||
} else {
|
||||
let chain_running = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let chain_running = false
|
||||
}
|
||||
}
|
||||
}
|
||||
return make_result(chain_node, p)
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
return make_result({ "expr": "Map", "pairs": pairs }, p)
|
||||
}
|
||||
|
||||
// if expression
|
||||
|
||||
Reference in New Issue
Block a user