diff --git a/lang/el-compiler/src/codegen.el b/lang/el-compiler/src/codegen.el index 917fb31..25e6511 100644 --- a/lang/el-compiler/src/codegen.el +++ b/lang/el-compiler/src/codegen.el @@ -2916,6 +2916,24 @@ fn build_int_names_for_params(params: [Map]) -> Bool { return true } +// fn_has_decorator — does this FnDef carry a decorator named `name`? +// Reads the `decorators` list [{name, args}] attached by the parser. Absent +// key -> native_list_len returns 0 -> false. This is the multi-decorator-aware +// replacement for the old single `decorator` string check, so a fn may stack +// roles with other decorators (e.g. `@route(...) @manager fn ...`). +fn fn_has_decorator(stmt: Map, name: String) -> Bool { + let dl = stmt["decorators"] + let n: Int = native_list_len(dl) + let i = 0 + while i < n { + let d = native_list_get(dl, i) + let dn: String = d["name"] + if str_eq(dn, name) { return true } + let i = i + 1 + } + false +} + fn cg_fn(stmt: Map) -> Void { let fn_name: String = stmt["name"] // Skip El's `fn main()` - C provides its own main() for top-level stmts @@ -2927,10 +2945,10 @@ fn cg_fn(stmt: Map) -> Void { let params_c: String = params_to_c(params) // VBD role enforcement: dharma_emit / dharma_field may only be called // from @manager-decorated functions. Surface violations to the C compiler - // via #error directives emitted before the function definition. - let decorator: String = stmt["decorator"] + // via #error directives emitted before the function definition. Read the + // decorator LIST so the role may be stacked with other decorators. if vbd_has_restricted_call(body) { - if !str_eq(decorator, "manager") { + if !fn_has_decorator(stmt, "manager") { emit_line("#error \"VBD violation: dharma_emit/dharma_field called from non-@manager fn '" + fn_name + "'\"") } } @@ -3479,6 +3497,259 @@ fn cg_decl_streaming(stmt: Map) -> Void { } } +// ── @route dispatcher generation ────────────────────────────────────────────── +// +// Scan the token stream for @route-decorated fns and synthesize a generic HTTP +// dispatcher `el_route_dispatch(method, clean, path, body)`. A decorated handler +// must have the uniform signature (method, path, body) -> String. The dispatcher +// matches `clean` (the query-stripped path, supplied by the caller) against each +// route and calls the handler with the ORIGINAL `path` so query strings survive. +// Returns the sentinel "__EL_NO_ROUTE__" when nothing matches, so the caller may +// fall through to any remaining hand-written branches (mixed mode). +// +// Decorator grammar: @route(path, method, kind, suffix) +// path — the match string (or the prefix, for compound) +// method — "GET" | "POST" | ... ; a '|'-list like "GET|POST"; "ANY"/"" = no guard +// kind — "exact" (default) | "prefix" | "suffix" | "compound" +// suffix — for "compound": the required str_ends_with suffix +// +// The dispatch table is emitted SPECIFICITY-SORTED (most-specific first), NOT in +// source order, so overlapping prefixes (e.g. /api/x/search vs /api/x) never +// shadow each other regardless of how the handlers are written. + +// split_pipe — split "GET|POST" on '|' into ["GET","POST"]. Self-contained +// (no dependency on str_split runtime semantics). +fn split_pipe(s: String) -> [String] { + let out: [String] = native_list_empty() + let cur: String = "" + let n: Int = str_len(s) + let i: Int = 0 + while i < n { + let ch: String = str_slice(s, i, i + 1) + if str_eq(ch, "|") { + let out = native_list_append(out, cur) + let cur = "" + } else { + let cur = cur + ch + } + let i = i + 1 + } + let out = native_list_append(out, cur) + out +} + +// route_make_record — build a route record map from the @route decorator args. +fn route_make_record(fn_name: String, args: [String]) -> Map { + let na: Int = native_list_len(args) + let rpath: String = "" + if na >= 1 { let rpath = native_list_get(args, 0) } + let rmethod: String = "GET" + if na >= 2 { let rmethod = native_list_get(args, 1) } + let rkind: String = "exact" + if na >= 3 { let rkind = native_list_get(args, 2) } + let rsuffix: String = "" + if na >= 4 { let rsuffix = native_list_get(args, 3) } + { "name": fn_name, "path": rpath, "method": rmethod, "kind": rkind, "suffix": rsuffix } +} + +// route_spec_score — higher = more specific = emitted earlier. Ordering: +// exact > compound > suffix > prefix; within a class, a longer path/suffix +// wins (so /api/x/search sorts before /api/x). Guarantees correct dispatch +// independent of source order. +fn route_spec_score(rec: Map) -> Int { + let kind: String = rec["kind"] + let path: String = rec["path"] + let suffix: String = rec["suffix"] + let plen: Int = str_len(path) + let slen: Int = str_len(suffix) + if str_eq(kind, "exact") { return 4000000 + plen } + if str_eq(kind, "compound") { return 3000000 + plen * 100 + slen } + if str_eq(kind, "suffix") { return 2000000 + slen } + return 1000000 + plen +} + +// route_sort_desc — selection sort of route records by descending specificity. +// N is small (routes per module), so O(n^2) is fine and keeps codegen simple. +fn route_sort_desc(recs: [Map]) -> [Map] { + let n: Int = native_list_len(recs) + let out: [Map] = native_list_empty() + let used: [Bool] = native_list_empty() + let u: Int = 0 + while u < n { + let used = native_list_append(used, false) + let u = u + 1 + } + let picked: Int = 0 + while picked < n { + let best_i: Int = 0 - 1 + let best_score: Int = 0 - 1 + let i: Int = 0 + while i < n { + let is_used: Bool = native_list_get(used, i) + if !is_used { + let sc: Int = route_spec_score(native_list_get(recs, i)) + if sc > best_score { + let best_score = sc + let best_i = i + } + } + let i = i + 1 + } + let out = native_list_append(out, native_list_get(recs, best_i)) + // Rebuild `used` with best_i marked (runtime has no native_list_set). + let new_used: [Bool] = native_list_empty() + let j: Int = 0 + while j < n { + if j == best_i { + let new_used = native_list_append(new_used, true) + } else { + let new_used = native_list_append(new_used, native_list_get(used, j)) + } + let j = j + 1 + } + let used = new_used + let picked = picked + 1 + } + out +} + +// scan_routes — token-level scan collecting every @route-decorated fn as a +// route record. Runs once per module (like scan_fn_sigs) so the dispatcher can +// be synthesized in the streaming backend, which discards per-fn ASTs. Handles +// decorator STACKING: `@route(...) @manager fn` still records the route. +fn scan_routes(tokens: [Any]) -> [Map] { + let total: Int = native_list_len(tokens) / 2 + let recs: [Map] = native_list_empty() + let has_pending: Bool = false + let pending_args: [String] = native_list_empty() + let pos: Int = 0 + let going: Bool = true + while going { + if pos >= total { + let going = false + } else { + let k: String = tok_kind(tokens, pos) + if str_eq(k, "Eof") { + let going = false + } else { + if str_eq(k, "At") { + let dname: String = tok_value(tokens, pos + 1) + let p: Int = pos + 2 + let args: [String] = native_list_empty() + let ka: String = tok_kind(tokens, p) + if str_eq(ka, "LParen") { + let p = p + 1 + let running: Bool = true + while running { + let kd: String = tok_kind(tokens, p) + if str_eq(kd, "RParen") { + let running = false + } else { + if str_eq(kd, "Eof") { + let running = false + } else { + if str_eq(kd, "Str") { + let args = native_list_append(args, tok_value(tokens, p)) + } + let p = p + 1 + } + } + } + if str_eq(tok_kind(tokens, p), "RParen") { let p = p + 1 } + } + if str_eq(dname, "route") { + let has_pending = true + let pending_args = args + } + let pos = p + } else { + if str_eq(k, "Fn") { + let fname: String = tok_value(tokens, pos + 1) + if has_pending { + let recs = native_list_append(recs, route_make_record(fname, pending_args)) + let has_pending = false + } + let pos = pos + 2 + } else { + let pos = pos + 1 + } + } + } + } + } + recs +} + +// program_has_routes — did scan_routes find any @route fn? +fn program_has_routes(recs: [Map]) -> Bool { + native_list_len(recs) > 0 +} + +// route_method_guard — C boolean prefix guarding on HTTP method, or "" for none. +fn route_method_guard(method: String) -> String { + if str_eq(method, "") { return "" } + if str_eq(method, "ANY") { return "" } + if str_contains(method, "|") { + let parts: [String] = split_pipe(method) + let np: Int = native_list_len(parts) + let expr: String = "" + let i: Int = 0 + while i < np { + let m: String = native_list_get(parts, i) + if str_eq(m, "") { + let i = i + 1 + } else { + let piece: String = "str_eq(method, EL_STR(" + c_str_lit(m) + "))" + if str_eq(expr, "") { + let expr = piece + } else { + let expr = expr + " || " + piece + } + let i = i + 1 + } + } + if str_eq(expr, "") { return "" } + return "(" + expr + ") && " + } + "str_eq(method, EL_STR(" + c_str_lit(method) + ")) && " +} + +// route_match_expr — C boolean matching `clean` against the route path/kind. +fn route_match_expr(kind: String, path: String, suffix: String) -> String { + if str_eq(kind, "prefix") { + return "str_starts_with(clean, EL_STR(" + c_str_lit(path) + "))" + } + if str_eq(kind, "suffix") { + return "str_ends_with(clean, EL_STR(" + c_str_lit(path) + "))" + } + if str_eq(kind, "compound") { + return "str_starts_with(clean, EL_STR(" + c_str_lit(path) + ")) && str_ends_with(clean, EL_STR(" + c_str_lit(suffix) + "))" + } + "str_eq(clean, EL_STR(" + c_str_lit(path) + "))" +} + +// emit_route_dispatch — emit the generated el_route_dispatch definition from the +// specificity-sorted route records. No-op if there are no routes. +fn emit_route_dispatch(recs: [Map]) -> Void { + if !program_has_routes(recs) { return } + let sorted: [Map] = route_sort_desc(recs) + emit_line("// ── generated @route dispatcher (specificity-sorted) ──") + emit_line("el_val_t el_route_dispatch(el_val_t method, el_val_t clean, el_val_t path, el_val_t body) {") + let n: Int = native_list_len(sorted) + let i: Int = 0 + while i < n { + let rec = native_list_get(sorted, i) + let guard: String = route_method_guard(rec["method"]) + let match_e: String = route_match_expr(rec["kind"], rec["path"], rec["suffix"]) + let fn_name: String = rec["name"] + emit_line(" if (" + guard + match_e + ") { return " + fn_name + "(method, path, body); }") + let i = i + 1 + } + emit_line(" return EL_STR(\"__EL_NO_ROUTE__\");") + emit_line("}") + emit_blank() +} + // emit_streaming_preamble — emit #includes, forward decls, and file-scope lets // using the pre-scanned signature data (no full AST). fn emit_streaming_preamble(sigs: [Map], source: String) -> Void { @@ -3571,6 +3842,17 @@ fn codegen_streaming(tokens: [Any], sigs: [Map], source: String) -> emit_streaming_preamble(sigs, source) el_arena_pop(preamble_mark) + // @route: scan the token stream once for @route-decorated fns. Kept in + // codegen_streaming scope (survives the per-fn arena pops and el_release of + // tokens below via refcount, like `sigs`). If any exist, forward-declare the + // generated dispatcher NOW so hand-written fns (e.g. handle_request) may call + // it before its definition is emitted after the fn-emit loop. + let route_records: [Map] = scan_routes(tokens) + if program_has_routes(route_records) { + emit_line("el_val_t el_route_dispatch(el_val_t method, el_val_t clean, el_val_t path, el_val_t body);") + emit_blank() + } + // Detect whether there is a fn main() and whether there are top-level // executable stmts (for library detection) from sigs. let has_el_main: Bool = false @@ -3758,6 +4040,15 @@ fn codegen_streaming(tokens: [Any], sigs: [Map], source: String) -> } } + // @route: emit the generated dispatcher definition now — after every handler + // fn has been emitted, but before `tokens` is released (route_records holds + // its own refs to the extracted strings). No-op unless the module declared + // at least one @route fn. Emitted before the test/library early-returns so it + // is present in library modules (e.g. neuron's routes.el) too. + let route_arena_mark: Any = el_arena_push() + emit_route_dispatch(route_records) + el_arena_pop(route_arena_mark) + // Tokens fully consumed by the streaming loop — release now to free peak heap. el_release(tokens) diff --git a/lang/el-compiler/src/parser.el b/lang/el-compiler/src/parser.el index d93fcba..8cdca47 100644 --- a/lang/el-compiler/src/parser.el +++ b/lang/el-compiler/src/parser.el @@ -1758,23 +1758,68 @@ fn parse_stmt(tokens: [Any], pos: Int) -> Map { return make_result({ "stmt": "TryCatch", "try_body": try_body, "catch_name": catch_name, "catch_body": native_list_empty() }, p) } - // @decorator - capture decorator name and attach to following stmt + // @decorator - capture decorator name (and optional string args) and + // attach to the following stmt. Backward-compatible: bare @manager / + // @engine / @accessor still parse (no parens -> empty args). Decorators + // STACK: `@route("/p","GET") @manager fn f()` attaches BOTH to f via a + // `decorators` list [{name, args}]. The legacy `decorator` string is kept + // populated (topmost decorator) so the JS backend keeps working unchanged. if k == "At" { let p = pos + 1 let dec_name = tok_value(tokens, p) let p = p + 1 + // Optional decorator argument list: @name("a", "b", ...) + let dec_args = native_list_empty() + let ka = tok_kind(tokens, p) + if str_eq(ka, "LParen") { + let p = p + 1 + let running_da = true + while running_da { + let kd = tok_kind(tokens, p) + if str_eq(kd, "RParen") { + let running_da = false + } else { + if str_eq(kd, "Eof") { + let running_da = false + } else { + if str_eq(kd, "Str") { + let dec_args = native_list_append(dec_args, tok_value(tokens, p)) + } + let p = p + 1 + let kc = tok_kind(tokens, p) + if str_eq(kc, "Comma") { + let p = p + 1 + } + } + } + } + let p = expect(tokens, p, "RParen") + } let r = parse_stmt(tokens, p) let inner = r["node"] let p2 = r["pos"] let inner_kind: String = inner["stmt"] if str_eq(inner_kind, "FnDef") { + // Stack this decorator (topmost-first) onto any decorators the inner + // FnDef already carries from decorators written below this one. + let this_dec = { "name": dec_name, "args": dec_args } + let existing = inner["decorators"] + let dlist = native_list_empty() + let dlist = native_list_append(dlist, this_dec) + let ne: Int = native_list_len(existing) + let ei = 0 + while ei < ne { + let dlist = native_list_append(dlist, native_list_get(existing, ei)) + let ei = ei + 1 + } let with_dec = { "stmt": "FnDef", "name": inner["name"], "params": inner["params"], "body": inner["body"], "ret_type": inner["ret_type"], - "decorator": dec_name + "decorator": dec_name, + "decorators": dlist } // r result map fully consumed — release to free peak heap. el_release(r)