diff --git a/arbor/vessels/arbor-cli/manifest.el b/arbor/vessels/arbor-cli/manifest.el new file mode 100644 index 0000000..907c947 --- /dev/null +++ b/arbor/vessels/arbor-cli/manifest.el @@ -0,0 +1,23 @@ +// arbor-cli — the `arbor` command-line tool. +// Inlines its own copies of the parse / layout / render pipeline so that the +// resulting binary is self-contained. (El's `import` form today concatenates +// source; once a real module loader lands this becomes a thin driver.) + +vessel "arbor-cli" { + version "0.1.0" + description "Command-line interface for the Arbor diagram language" + authors ["Neuron Technologies"] + edition "2026" +} + +dependencies { + arbor-core "0.1" + arbor-parse "0.1" + arbor-layout "0.1" + arbor-render "0.1" +} + +build { + entry "src/main.el" + output "dist/" +} diff --git a/arbor/vessels/arbor-cli/src/main.el b/arbor/vessels/arbor-cli/src/main.el new file mode 100644 index 0000000..5f46e08 --- /dev/null +++ b/arbor/vessels/arbor-cli/src/main.el @@ -0,0 +1,1020 @@ +// arbor-cli — the `arbor` command-line tool. +// +// Self-contained driver. Because El's `import` form is presently purely +// syntactic source-concatenation, this file inlines just enough of the +// parse / layout / render pipeline to let the binary stand alone. The +// inlined logic is the public surface of arbor-parse, arbor-layout, and +// arbor-render — same field names, same wire shapes — so the behaviour +// matches the standalone vessels. +// +// Subcommands: +// arbor render [-o out.svg] → write SVG to disk +// arbor check → parse and report counts +// arbor ls → list nodes / edges / groups +// arbor fmt [--in-place] → canonical .arbor formatting +// arbor version → "Arbor v0.1.0" +// +// PNG output is intentionally unsupported (the El runtime has no SVG +// rasterizer; see report). +// +// Smoke test: when the binary is invoked with no args we print usage and +// run a built-in self-test (parse → summarise → render-to-string) so the +// binary doubles as the vessel's verification harness. + +// ── Inlined parse helpers (subset of arbor-parse) ─────────────────────────── + +fn cli_is_alnum_underscore(ch: String) -> Bool { + let code: Int = str_char_code(ch, 0) + if code >= 48 { + if code <= 57 { return true } + } + if code >= 65 { + if code <= 90 { return true } + } + if code >= 97 { + if code <= 122 { return true } + } + if code == 95 { return true } + false +} + +fn cli_is_ascii_digit(ch: String) -> Bool { + let code: Int = str_char_code(ch, 0) + if code >= 48 { + if code <= 57 { return true } + } + false +} + +fn cli_sanitize_id(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { return "node" } + let out = "" + let prev_underscore = false + let i = 0 + while i < n { + let ch: String = str_char_at(s, i) + if cli_is_alnum_underscore(ch) { + let out = out + ch + let prev_underscore = false + } else { + if !prev_underscore { + let out = out + "_" + } + let prev_underscore = true + } + let i = i + 1 + } + let m: Int = str_len(out) + let end = m + let stripping = true + while stripping { + if end <= 0 { + let stripping = false + } else { + let last: String = str_char_at(out, end - 1) + if last == "_" { + let end = end - 1 + } else { + let stripping = false + } + } + } + let out = str_slice(out, 0, end) + if str_len(out) == 0 { return "node" } + let first: String = str_char_at(out, 0) + if cli_is_ascii_digit(first) { + let out = "n" + out + } + out +} + +fn cli_shape_token(tok: String) -> String { + let t: String = str_trim(tok) + if t == "rect" { return "rect" } + if t == "rounded" { return "rounded" } + if t == "cylinder" { return "cylinder" } + if t == "diamond" { return "diamond" } + if t == "stadium" { return "stadium" } + if t == "primary" { return "primary" } + "" +} + +fn cli_parse_quoted(s: String) -> Map { + let t: String = str_trim(s) + if str_len(t) < 2 { + return { "ok": false, "value": "", "rest": s } + } + let first: String = str_char_at(t, 0) + if first != "\"" { + return { "ok": false, "value": "", "rest": s } + } + let body: String = str_slice(t, 1, str_len(t)) + let close: Int = str_index_of(body, "\"") + if close < 0 { + return { "ok": false, "value": "", "rest": s } + } + let inner: String = str_slice(body, 0, close) + let rest: String = str_slice(body, close + 1, str_len(body)) + { "ok": true, "value": inner, "rest": rest } +} + +fn cli_split_id(s: String) -> Map { + let n: Int = str_len(s) + let i = 0 + while i < n { + let ch: String = str_char_at(s, i) + if !cli_is_alnum_underscore(ch) { + return { "id": str_slice(s, 0, i), "rest": str_slice(s, i, n) } + } + let i = i + 1 + } + { "id": s, "rest": "" } +} + +fn cli_extract_arrow(line: String) -> Map { + let f1: Int = str_index_of(line, "-/->") + if f1 >= 0 { + return { "ok": true, + "from_str": str_slice(line, 0, f1), + "kind": "forbidden", + "rest": str_slice(line, f1 + 4, str_len(line)) } + } + let f2: Int = str_index_of(line, "<->") + if f2 >= 0 { + return { "ok": true, + "from_str": str_slice(line, 0, f2), + "kind": "bidirectional", + "rest": str_slice(line, f2 + 3, str_len(line)) } + } + let f3: Int = str_index_of(line, "-->") + if f3 >= 0 { + return { "ok": true, + "from_str": str_slice(line, 0, f3), + "kind": "dashed", + "rest": str_slice(line, f3 + 3, str_len(line)) } + } + let f4: Int = str_index_of(line, "->") + if f4 >= 0 { + return { "ok": true, + "from_str": str_slice(line, 0, f4), + "kind": "solid", + "rest": str_slice(line, f4 + 2, str_len(line)) } + } + { "ok": false, "from_str": "", "kind": "", "rest": "" } +} + +fn cli_is_edge(line: String) -> Bool { + if str_contains(line, "->") { return true } + if str_contains(line, "<->") { return true } + false +} + +// State-keyed parser, same shape as arbor-parse. +fn cli_st_set_int(key: String, v: Int) -> Int { state_set(key, int_to_str(v)); 0 } +fn cli_st_get_int(key: String) -> Int { + let s: String = state_get(key) + if str_eq(s, "") { return 0 } + str_to_int(s) +} + +fn cli_reset() -> Int { + state_set("graph_title", "") + state_set("graph_direction", "top-down") + cli_st_set_int("node_count", 0) + cli_st_set_int("edge_count", 0) + cli_st_set_int("group_count", 0) + cli_st_set_int("group_stack_depth", 0) + state_set("parse_error", "") + cli_st_set_int("parse_error_line", 0) + state_set("parse_error_text", "") + 0 +} + +fn cli_set_error(msg: String, line_no: Int, text: String) -> Int { + state_set("parse_error", msg) + cli_st_set_int("parse_error_line", line_no) + state_set("parse_error_text", text) + 0 +} + +fn cli_has_error() -> Bool { + let m: String = state_get("parse_error") + if str_eq(m, "") { return false } + true +} + +fn cli_store_node(id: String, label: String, shape: String) -> Int { + let n: Int = cli_st_get_int("node_count") + state_set("node_id_" + int_to_str(n), id) + state_set("node_label_" + int_to_str(n), label) + state_set("node_shape_" + int_to_str(n), shape) + cli_st_set_int("node_count", n + 1) + 0 +} + +fn cli_store_edge(src: String, dst: String, label: String, kind: String) -> Int { + let n: Int = cli_st_get_int("edge_count") + state_set("edge_from_" + int_to_str(n), src) + state_set("edge_to_" + int_to_str(n), dst) + state_set("edge_label_" + int_to_str(n), label) + state_set("edge_kind_" + int_to_str(n), kind) + cli_st_set_int("edge_count", n + 1) + 0 +} + +fn cli_store_group(id: String, label: String, ids: String) -> Int { + let n: Int = cli_st_get_int("group_count") + state_set("group_id_" + int_to_str(n), id) + state_set("group_label_" + int_to_str(n), label) + state_set("group_ids_" + int_to_str(n), ids) + cli_st_set_int("group_count", n + 1) + 0 +} + +fn cli_group_frame_key(idx: Int, suffix: String) -> String { + "gs_" + int_to_str(idx) + "_" + suffix +} + +fn cli_open_group(id: String, label: String, line_no: Int) -> Int { + let depth: Int = cli_st_get_int("group_stack_depth") + state_set(cli_group_frame_key(depth, "id"), id) + state_set(cli_group_frame_key(depth, "label"), label) + state_set(cli_group_frame_key(depth, "line"), int_to_str(line_no)) + state_set(cli_group_frame_key(depth, "ids"), "") + cli_st_set_int("group_stack_depth", depth + 1) + 0 +} + +fn cli_close_group() -> Map { + let depth: Int = cli_st_get_int("group_stack_depth") + if depth <= 0 { + return { "ok": false, "id": "", "label": "", "ids": "" } + } + let idx: Int = depth - 1 + let id: String = state_get(cli_group_frame_key(idx, "id")) + let label: String = state_get(cli_group_frame_key(idx, "label")) + let ids: String = state_get(cli_group_frame_key(idx, "ids")) + cli_st_set_int("group_stack_depth", idx) + { "ok": true, "id": id, "label": label, "ids": ids } +} + +fn cli_register_in_group(node_id: String) -> Int { + let depth: Int = cli_st_get_int("group_stack_depth") + if depth <= 0 { return 0 } + let idx: Int = depth - 1 + let key: String = cli_group_frame_key(idx, "ids") + let prev: String = state_get(key) + if str_eq(prev, "") { + state_set(key, node_id) + } else { + state_set(key, prev + "" + node_id) + } + 0 +} + +fn cli_parse_node(line_no: Int, line: String) -> Int { + let s: Map = cli_split_id(line) + let raw_id: String = s["id"] + if str_eq(raw_id, "") { + cli_set_error("expected node id, edge, or keyword", line_no, line) + return 0 + } + let id: String = cli_sanitize_id(raw_id) + let rest: String = str_trim(s["rest"]) + let shape = "rect" + let after_shape = rest + if str_len(rest) > 0 { + let lead: String = str_char_at(rest, 0) + if lead == "[" { + let close: Int = str_index_of(rest, "]") + if close < 0 { + cli_set_error("unclosed `[` in shape token", line_no, line) + return 0 + } + let token: String = str_slice(rest, 1, close) + let parsed: String = cli_shape_token(token) + if str_eq(parsed, "") { + cli_set_error("unknown shape `" + token + "`", line_no, line) + return 0 + } + let shape = parsed + let after_shape = str_trim(str_slice(rest, close + 1, str_len(rest))) + } + } + let q: Map = cli_parse_quoted(after_shape) + let label = raw_id + let ok: Bool = q["ok"] + if ok { + let label = q["value"] + } + cli_store_node(id, label, shape) + cli_register_in_group(id) + 1 +} + +fn cli_parse_edge(line_no: Int, line: String) -> Int { + let p: Map = cli_extract_arrow(line) + let ok: Bool = p["ok"] + if !ok { + cli_set_error("malformed edge", line_no, line) + return 0 + } + let from_str: String = p["from_str"] + let rest_str: String = p["rest"] + let kind: String = p["kind"] + let src: String = cli_sanitize_id(str_trim(from_str)) + let rest_t: String = str_trim(rest_str) + let s: Map = cli_split_id(rest_t) + let to_raw: String = s["id"] + if str_eq(to_raw, "") { + cli_set_error("edge missing target id", line_no, line) + return 0 + } + let dst: String = cli_sanitize_id(to_raw) + let label_rest: String = str_trim(s["rest"]) + let q: Map = cli_parse_quoted(label_rest) + let label = "" + let qok: Bool = q["ok"] + if qok { + let label = q["value"] + } + cli_store_edge(src, dst, label, kind) + 1 +} + +fn cli_parse_group_open(line_no: Int, line: String, rest: String) -> Int { + let trimmed: String = str_trim(rest) + let n: Int = str_len(trimmed) + let body = trimmed + if n > 0 { + let last: String = str_char_at(trimmed, n - 1) + if last == "{" { + let body = str_trim(str_slice(trimmed, 0, n - 1)) + } + } + let s: Map = cli_split_id(body) + let raw_id: String = s["id"] + if str_eq(raw_id, "") { + cli_set_error("group declaration missing id", line_no, line) + return 0 + } + let label_rest: String = str_trim(s["rest"]) + let q: Map = cli_parse_quoted(label_rest) + let label = raw_id + let qok: Bool = q["ok"] + if qok { + let label = q["value"] + } + cli_open_group(raw_id, label, line_no) + 1 +} + +fn cli_dispatch(line_no: Int, line: String) -> Int { + if line == "}" { + let frame: Map = cli_close_group() + let frame_ok: Bool = frame["ok"] + if !frame_ok { + cli_set_error("unexpected `}`", line_no, "}") + return 0 + } + cli_store_group(frame["id"], frame["label"], frame["ids"]) + return 1 + } + if str_starts_with(line, "title:") { + let after: String = str_trim(str_slice(line, 6, str_len(line))) + let q: Map = cli_parse_quoted(after) + let qok: Bool = q["ok"] + if !qok { cli_set_error("expected quoted string after `title:`", line_no, line); return 0 } + state_set("graph_title", q["value"]) + return 1 + } + if str_starts_with(line, "direction:") { + let after: String = str_trim(str_slice(line, 10, str_len(line))) + if str_eq(after, "TD") { state_set("graph_direction", "top-down"); return 1 } + if str_eq(after, "LR") { state_set("graph_direction", "left-right"); return 1 } + if str_eq(after, "RL") { state_set("graph_direction", "right-left"); return 1 } + if str_eq(after, "BU") { state_set("graph_direction", "bottom-up"); return 1 } + if str_eq(after, "top-down") { state_set("graph_direction", "top-down"); return 1 } + if str_eq(after, "left-right") { state_set("graph_direction", "left-right"); return 1 } + if str_eq(after, "right-left") { state_set("graph_direction", "right-left"); return 1 } + if str_eq(after, "bottom-up") { state_set("graph_direction", "bottom-up"); return 1 } + cli_set_error("unknown direction", line_no, line) + return 0 + } + if str_starts_with(line, "group ") { + let after: String = str_slice(line, 6, str_len(line)) + return cli_parse_group_open(line_no, line, after) + } + if cli_is_edge(line) { return cli_parse_edge(line_no, line) } + cli_parse_node(line_no, line) +} + +fn cli_preprocess(source: String) -> [Map] { + let lines: [String] = str_split(source, "\n") + let n: Int = el_list_len(lines) + let out: [Map] = native_list_empty() + let i = 0 + while i < n { + let raw: String = get(lines, i) + let cidx: Int = str_index_of(raw, "//") + let stripped = raw + if cidx >= 0 { + let stripped = str_slice(raw, 0, cidx) + } + let trimmed: String = str_trim(stripped) + if str_len(trimmed) > 0 { + let row: Map = { "no": i + 1, "text": trimmed } + let out = native_list_append(out, row) + } + let i = i + 1 + } + out +} + +fn cli_parse(source: String) -> Map { + cli_reset() + let lines: [Map] = cli_preprocess(source) + let n: Int = el_list_len(lines) + let i = 0 + let abort = false + while i < n { + if abort { + // skip + } else { + let row: Map = get(lines, i) + let line_no: Int = row["no"] + let text: String = row["text"] + cli_dispatch(line_no, text) + if cli_has_error() { let abort = true } + } + let i = i + 1 + } + if !cli_has_error() { + let depth: Int = cli_st_get_int("group_stack_depth") + if depth > 0 { + cli_set_error("unclosed group", 0, "") + } + } + if cli_has_error() { + return { + "error": state_get("parse_error"), + "line": cli_st_get_int("parse_error_line"), + "text": state_get("parse_error_text") + } + } + // Build result map directly from state. + let n_nodes: Int = cli_st_get_int("node_count") + let n_edges: Int = cli_st_get_int("edge_count") + let n_groups: Int = cli_st_get_int("group_count") + { + "title": state_get("graph_title"), + "direction": state_get("graph_direction"), + "n_nodes": n_nodes, + "n_edges": n_edges, + "n_groups": n_groups + } +} + +// ── XML escape (subset of arbor-render) ───────────────────────────────────── + +fn cli_esc(s: String) -> String { + let r1: String = str_replace(s, "&", "&") + let r2: String = str_replace(r1, "<", "<") + let r3: String = str_replace(r2, ">", ">") + let r4: String = str_replace(r3, "\"", """) + r4 +} + +// ── Render a minimal SVG directly from parser state ───────────────────────── +// +// Layout is simplified: top-down hierarchy, each node sized 120x40 with a +// 60-px vertical gap. This is enough for `arbor render` to produce a usable +// SVG without wiring in the full layout vessel. The standalone arbor-render +// vessel does the real job. + +// Layout constants. The codegen treats `Ident + Call(...)` as string concat +// in the current heuristic, so all int arithmetic is staged through state +// (string-keyed K/V) where the operands are integer-valued strings. +fn cli_layout_int(a: Int, b: Int) -> Int { + state_set("cli_lyt_a", int_to_str(a)) + state_set("cli_lyt_b", int_to_str(b)) + let av: Int = str_to_int(state_get("cli_lyt_a")) + let bv: Int = str_to_int(state_get("cli_lyt_b")) + av + bv +} + +fn cli_layout_cw_v() -> Int { 200 } // 120 + 2*40 +fn cli_layout_ch_v(n_nodes: Int) -> Int { + if n_nodes <= 0 { return 120 } + // 2*40 + n_nodes*40 + (n_nodes-1)*60 + let part1: Int = 40 * n_nodes + let part2: Int = 60 * (n_nodes - 1) + let s1: Int = cli_layout_int(80, part1) + cli_layout_int(s1, part2) +} +fn cli_layout_cw_h(n_nodes: Int) -> Int { + if n_nodes <= 0 { return 200 } + let part1: Int = 120 * n_nodes + let part2: Int = 60 * (n_nodes - 1) + let s1: Int = cli_layout_int(80, part1) + cli_layout_int(s1, part2) +} +fn cli_layout_ch_h() -> Int { 120 } + +// Layout: every node 120x40, 60-px gap, 40-px margin. Constants are +// hard-coded throughout so the codegen never has to evaluate `Ident + +// Ident` arithmetic on integer values. The `cli_layout_int` helper +// stages adds through state for the cases where ident-arithmetic is +// unavoidable. +fn cli_centre_v(i: Int) -> Int { + // 40 + 20 + i*(40+60) = 60 + i*100 + let ii: Int = 100 * i + cli_layout_int(60, ii) +} + +fn cli_centre_h(i: Int) -> Int { + // 40 + 60 + i*(120+60) = 100 + i*180 + let ii: Int = 180 * i + cli_layout_int(100, ii) +} + +fn cli_render_svg() -> String { + let n_nodes: Int = cli_st_get_int("node_count") + let n_edges: Int = cli_st_get_int("edge_count") + let direction: String = state_get("graph_direction") + let is_vertical = true + if str_eq(direction, "left-right") { let is_vertical = false } + if str_eq(direction, "right-left") { let is_vertical = false } + + let cw = 0 + let ch = 0 + if is_vertical { + let cw = cli_layout_cw_v() + let ch = cli_layout_ch_v(n_nodes) + } + if !is_vertical { + let cw = cli_layout_cw_h(n_nodes) + let ch = cli_layout_ch_h() + } + + let buf = "\n" + let buf = buf + " \n" + let buf = buf + " \n" + let buf = buf + " \n" + let buf = buf + " \n" + let buf = buf + " \n" + + // Pre-compute centre coordinates per node into state. + let i = 0 + while i < n_nodes { + let cx = 100 + let cy = 60 + if is_vertical { + // cx = 100 (40 + 120/2), cy = 60 + i*100 + let cy = cli_centre_v(i) + } + if !is_vertical { + let cx = cli_centre_h(i) + // cy = 60 (40 + 40/2) + } + state_set("cli_cx_" + int_to_str(i), int_to_str(cx)) + state_set("cli_cy_" + int_to_str(i), int_to_str(cy)) + let i = i + 1 + } + + // Edges first (so nodes layer over them). + let i = 0 + while i < n_edges { + let s_idx = int_to_str(i) + let from_id: String = state_get("edge_from_" + s_idx) + let to_id: String = state_get("edge_to_" + s_idx) + let label: String = state_get("edge_label_" + s_idx) + let kind: String = state_get("edge_kind_" + s_idx) + let from_idx = 0 - 1 + let to_idx = 0 - 1 + let j = 0 + while j < n_nodes { + let nid: String = state_get("node_id_" + int_to_str(j)) + if str_eq(nid, from_id) { let from_idx = j } + if str_eq(nid, to_id) { let to_idx = j } + let j = j + 1 + } + if from_idx >= 0 { + if to_idx >= 0 { + let fx: Int = str_to_int(state_get("cli_cx_" + int_to_str(from_idx))) + let fy: Int = str_to_int(state_get("cli_cy_" + int_to_str(from_idx))) + let tx: Int = str_to_int(state_get("cli_cx_" + int_to_str(to_idx))) + let ty: Int = str_to_int(state_get("cli_cy_" + int_to_str(to_idx))) + let dash = "" + if str_eq(kind, "dashed") { let dash = " stroke-dasharray=\"5,3\"" } + let stroke = "#64748B" + if str_eq(kind, "forbidden") { let stroke = "#DC2626" } + let buf = buf + " \n" + if str_len(label) > 0 { + let sum_x: Int = cli_layout_int(fx, tx) + let sum_y: Int = cli_layout_int(fy, ty) + let mx: Int = sum_x / 2 + let my: Int = sum_y / 2 + let buf = buf + " " + let buf = buf + cli_esc(label) + "\n" + } + } + } + let i = i + 1 + } + + // Nodes. + let i = 0 + while i < n_nodes { + let s_idx = int_to_str(i) + let id: String = state_get("node_id_" + s_idx) + let label: String = state_get("node_label_" + s_idx) + let shape: String = state_get("node_shape_" + s_idx) + let cx: Int = str_to_int(state_get("cli_cx_" + s_idx)) + let cy: Int = str_to_int(state_get("cli_cy_" + s_idx)) + let x: Int = cli_layout_int(cx, 0 - 60) // cx - 120/2 + let y: Int = cli_layout_int(cy, 0 - 20) // cy - 40/2 + let fill = "#ffffff" + let stroke = "#334155" + let txt = "#0D0D14" + if str_eq(shape, "primary") { + let fill = "#0052A0" + let stroke = "#0052A0" + let txt = "#ffffff" + } + let rx = 4 + if str_eq(shape, "rounded") { let rx = 20 } + if str_eq(shape, "stadium") { let rx = 20 } + let buf = buf + " \n" + let buf = buf + " " + let buf = buf + cli_esc(label) + "\n" + let i = i + 1 + } + + // Title. + let title: String = state_get("graph_title") + if str_len(title) > 0 { + let tx: Int = cw / 2 + let buf = buf + " " + let buf = buf + cli_esc(title) + "\n" + } + let buf = buf + "\n" + buf +} + +// ── Subcommands ───────────────────────────────────────────────────────────── + +fn cmd_version() -> Int { + println("Arbor v0.1.0") + 0 +} + +fn cmd_usage() -> Int { + println("Arbor — brain-themed diagram language") + println("") + println("USAGE:") + println(" arbor render [-o ]") + println(" arbor check ") + println(" arbor ls ") + println(" arbor fmt [--in-place]") + println(" arbor version") + println("") + println("Note: PNG output is unavailable in the El build (use the Rust binary).") + 0 +} + +fn cmd_check(path: String) -> Int { + let src: String = fs_read(path) + let g: Map = cli_parse(src) + let err: String = g["error"] + if str_len(err) > 0 { + let line: Int = g["line"] + println("Parse error at line " + int_to_str(line) + ": " + err) + return 1 + } + let nn: Int = g["n_nodes"] + let ne: Int = g["n_edges"] + let ng: Int = g["n_groups"] + println("OK — " + int_to_str(nn) + " nodes, " + int_to_str(ne) + " edges, " + int_to_str(ng) + " groups") + 0 +} + +fn cmd_ls(path: String) -> Int { + let src: String = fs_read(path) + let g: Map = cli_parse(src) + let err: String = g["error"] + if str_len(err) > 0 { + println("Parse error: " + err) + return 1 + } + let nn: Int = g["n_nodes"] + println("Nodes (" + int_to_str(nn) + "):") + let i = 0 + while i < nn { + let s_idx = int_to_str(i) + let id: String = state_get("node_id_" + s_idx) + let label: String = state_get("node_label_" + s_idx) + let shape: String = state_get("node_shape_" + s_idx) + println(" " + id + " \"" + label + "\" [" + shape + "]") + let i = i + 1 + } + let ne: Int = g["n_edges"] + println("") + println("Edges (" + int_to_str(ne) + "):") + let i = 0 + while i < ne { + let s_idx = int_to_str(i) + let from_id: String = state_get("edge_from_" + s_idx) + let to_id: String = state_get("edge_to_" + s_idx) + let label: String = state_get("edge_label_" + s_idx) + let kind: String = state_get("edge_kind_" + s_idx) + let arrow = "->" + if str_eq(kind, "dashed") { let arrow = "-->" } + if str_eq(kind, "forbidden") { let arrow = "-/->" } + if str_eq(kind, "bidirectional") { let arrow = "<->" } + if str_len(label) > 0 { + println(" " + from_id + " " + arrow + " " + to_id + " \"" + label + "\"") + } else { + println(" " + from_id + " " + arrow + " " + to_id) + } + let i = i + 1 + } + let ng: Int = g["n_groups"] + println("") + println("Groups (" + int_to_str(ng) + "):") + let i = 0 + while i < ng { + let s_idx = int_to_str(i) + let id: String = state_get("group_id_" + s_idx) + let label: String = state_get("group_label_" + s_idx) + println(" " + id + " \"" + label + "\"") + let i = i + 1 + } + 0 +} + +fn cmd_render(path: String, out_path: String) -> Int { + let src: String = fs_read(path) + let g: Map = cli_parse(src) + let err: String = g["error"] + if str_len(err) > 0 { + println("Parse error: " + err) + return 1 + } + let svg: String = cli_render_svg() + let final_out = out_path + if str_eq(final_out, "") { + // Replace extension or append .svg. + let dot: Int = str_index_of(path, ".") + if dot < 0 { + let final_out = path + ".svg" + } else { + let final_out = str_slice(path, 0, dot) + ".svg" + } + } + fs_write(final_out, svg) + println("Wrote " + int_to_str(str_len(svg)) + " bytes -> " + final_out) + 0 +} + +fn cmd_fmt(path: String, in_place: Bool) -> Int { + let src: String = fs_read(path) + let g: Map = cli_parse(src) + let err: String = g["error"] + if str_len(err) > 0 { + println("Parse error: " + err) + return 1 + } + let title: String = state_get("graph_title") + let direction: String = state_get("graph_direction") + let buf = "" + if str_len(title) > 0 { + let buf = buf + "title: \"" + title + "\"\n" + } + let buf = buf + "direction: " + direction + "\n" + + // Standalone nodes (those not in any group). + let nn: Int = g["n_nodes"] + let ng: Int = g["n_groups"] + + // Build a "in-group" set as a single delimited string. + let grouped_set = "" + let i = 0 + while i < ng { + let s_idx = int_to_str(i) + let ids: String = state_get("group_ids_" + s_idx) + let grouped_set = grouped_set + ids + "" + let i = i + 1 + } + + // Groups + let i = 0 + while i < ng { + let s_idx = int_to_str(i) + let gid: String = state_get("group_id_" + s_idx) + let glabel: String = state_get("group_label_" + s_idx) + let gids: String = state_get("group_ids_" + s_idx) + let buf = buf + "\ngroup " + gid + " \"" + glabel + "\" {\n" + let id_list: [String] = native_list_empty() + if !str_eq(gids, "") { + let id_list = str_split(gids, "") + } + let m: Int = el_list_len(id_list) + let j = 0 + while j < m { + let want_id: String = get(id_list, j) + // Find that node and emit it indented. + let k = 0 + while k < nn { + let nid: String = state_get("node_id_" + int_to_str(k)) + if str_eq(nid, want_id) { + let lbl: String = state_get("node_label_" + int_to_str(k)) + let shp: String = state_get("node_shape_" + int_to_str(k)) + let shape_part = "" + if !str_eq(shp, "rect") { let shape_part = " [" + shp + "]" } + let label_part = "" + if !str_eq(lbl, nid) { let label_part = " \"" + lbl + "\"" } + let buf = buf + " " + nid + shape_part + label_part + "\n" + } + let k = k + 1 + } + let j = j + 1 + } + let buf = buf + "}\n" + let i = i + 1 + } + + // Standalones. + let standalones = "" + let i = 0 + while i < nn { + let s_idx = int_to_str(i) + let nid: String = state_get("node_id_" + s_idx) + let lookfor = "" + nid + "" + if !str_contains(grouped_set, lookfor) { + let lbl: String = state_get("node_label_" + s_idx) + let shp: String = state_get("node_shape_" + s_idx) + let shape_part = "" + if !str_eq(shp, "rect") { let shape_part = " [" + shp + "]" } + let label_part = "" + if !str_eq(lbl, nid) { let label_part = " \"" + lbl + "\"" } + let standalones = standalones + nid + shape_part + label_part + "\n" + } + let i = i + 1 + } + if str_len(standalones) > 0 { + let buf = buf + "\n" + standalones + } + + // Edges. + let ne: Int = g["n_edges"] + if ne > 0 { + let buf = buf + "\n" + let i = 0 + while i < ne { + let s_idx = int_to_str(i) + let from_id: String = state_get("edge_from_" + s_idx) + let to_id: String = state_get("edge_to_" + s_idx) + let label: String = state_get("edge_label_" + s_idx) + let kind: String = state_get("edge_kind_" + s_idx) + let arrow = "->" + if str_eq(kind, "dashed") { let arrow = "-->" } + if str_eq(kind, "forbidden") { let arrow = "-/->" } + if str_eq(kind, "bidirectional") { let arrow = "<->" } + let label_part = "" + if str_len(label) > 0 { let label_part = " \"" + label + "\"" } + let buf = buf + from_id + " " + arrow + " " + to_id + label_part + "\n" + let i = i + 1 + } + } + + if in_place { + fs_write(path, buf) + println("Formatted " + path) + } else { + println(buf) + } + 0 +} + +// ── Self-test ─────────────────────────────────────────────────────────────── + +fn smoke_fail(label: String, msg: String) -> Int { + println("FAIL " + label + ": " + msg) + state_set("smoke_failures", "1") + 0 +} + +fn run_smoke_tests() -> Int { + let src = "title: \"Test\"\ndirection: left-right\napi [rounded] \"REST API\"\ndb [cylinder] \"Postgres\"\napi -> db \"reads\"" + let g: Map = cli_parse(src) + let err: String = g["error"] + if str_len(err) > 0 { + smoke_fail("parse smoke", err) + } else { + let nn: Int = g["n_nodes"] + let ne: Int = g["n_edges"] + if nn < 2 { smoke_fail("smoke nn", "expected >=2 got " + int_to_str(nn)) } else { println("ok parse 2 nodes") } + if ne < 1 { smoke_fail("smoke ne", "expected >=1 got " + int_to_str(ne)) } else { println("ok parse 1 edge") } + let svg: String = cli_render_svg() + if !str_contains(svg, "") { smoke_fail("svg tail", "missing closing tag") } else { println("ok svg tail") } + } + let f: String = state_get("smoke_failures") + if str_eq(f, "1") { + println("arbor-cli: FAILED") + return 1 + } + println("arbor-cli: ok") + 0 +} + +// ── Entry ─────────────────────────────────────────────────────────────────── + +let cli_args: [String] = args() +let cli_argc: Int = el_list_len(cli_args) + +if cli_argc == 0 { + cmd_usage() + let rc: Int = run_smoke_tests() + exit_program(rc) +} else { + let cmd: String = get(cli_args, 0) + if str_eq(cmd, "version") { + cmd_version() + exit_program(0) + } + if str_eq(cmd, "--version") { + cmd_version() + exit_program(0) + } + if str_eq(cmd, "-V") { + cmd_version() + exit_program(0) + } + if str_eq(cmd, "help") { + cmd_usage() + exit_program(0) + } + if str_eq(cmd, "--help") { + cmd_usage() + exit_program(0) + } + if str_eq(cmd, "-h") { + cmd_usage() + exit_program(0) + } + if cli_argc < 2 { + println("arbor: missing input file") + cmd_usage() + exit_program(1) + } + let path: String = get(cli_args, 1) + if str_eq(cmd, "check") { exit_program(cmd_check(path)) } + if str_eq(cmd, "ls") { exit_program(cmd_ls(path)) } + if str_eq(cmd, "render") { + let out_path = "" + let i = 2 + while i < cli_argc { + let opt: String = get(cli_args, i) + if str_eq(opt, "-o") { + if i + 1 < cli_argc { + let out_path = get(cli_args, i + 1) + let i = i + 1 + } + } + if str_eq(opt, "--png") { + println("arbor: PNG not supported by El build — falling back to SVG") + } + let i = i + 1 + } + exit_program(cmd_render(path, out_path)) + } + if str_eq(cmd, "fmt") { + let in_place = false + let i = 2 + while i < cli_argc { + let opt: String = get(cli_args, i) + if str_eq(opt, "--in-place") { let in_place = true } + let i = i + 1 + } + exit_program(cmd_fmt(path, in_place)) + } + println("arbor: unknown command `" + cmd + "`") + cmd_usage() + exit_program(1) +} diff --git a/arbor/vessels/arbor-core/manifest.el b/arbor/vessels/arbor-core/manifest.el new file mode 100644 index 0000000..35893f9 --- /dev/null +++ b/arbor/vessels/arbor-core/manifest.el @@ -0,0 +1,18 @@ +// arbor-core — fundamental types for Arbor diagrams. +// Node IDs (sanitised), shape vocabulary, edge kinds, and the lightweight +// graph value used by every other vessel. + +vessel "arbor-core" { + version "0.1.0" + description "Core types for Arbor diagrams: NodeId, ArborShape, ArborEdgeKind, graphs" + authors ["Neuron Technologies"] + edition "2026" +} + +dependencies { +} + +build { + entry "src/main.el" + output "dist/" +} diff --git a/arbor/vessels/arbor-core/src/main.el b/arbor/vessels/arbor-core/src/main.el new file mode 100644 index 0000000..3a91e13 --- /dev/null +++ b/arbor/vessels/arbor-core/src/main.el @@ -0,0 +1,333 @@ +// arbor-core — core types for Arbor diagrams. +// +// Idiomatic El: everything is a Map. Functions take/return maps; helpers are +// pure and small. The downstream vessels (parse, layout, render) consume the +// shapes defined here. +// +// Shape vocabulary: +// ArborShape strings — "rect" "rounded" "cylinder" "diamond" "stadium" "primary" +// +// Edge-kind strings: +// "solid" "dashed" "forbidden" "bidirectional" +// +// Node value: { "id":Str, "label":Str, "shape":Str } +// Edge value: { "from":Str, "to":Str, "label":Str, "kind":Str } +// Group value: { "id":Str, "label":Str, "node_ids":[Str], "direction":Str } +// Graph value: { "title":Str, "direction":Str, "nodes":[Node], "edges":[Edge], "groups":[Group] } +// +// Diagram-form (lowered) is the same shape but with NodeStyle/EdgeLine/Arrow +// resolved into renderer-friendly fields: +// Node: + "sublabel":Str, "style_fill":Str, "style_stroke":Str, "style_color":Str +// Edge: + "line":Str ("solid"/"dashed"/"dotted"/"thick"), "arrow":Str ("forward"/"backward"/"both"/"none") +// +// This file is the canonical definition of those shapes. Other vessels rely on +// these field names. + +// ── NodeId sanitisation ────────────────────────────────────────────────────── +// +// Sanitise an arbitrary string into a Mermaid-safe identifier. +// - any char not in [a-zA-Z0-9_] becomes '_' +// - consecutive underscores collapse +// - trailing underscores stripped +// - if first char is a digit, prepend 'n' +// - if empty, return "node" + +fn is_alnum_underscore(ch: String) -> Bool { + let code: Int = str_char_code(ch, 0) + if code >= 48 { + if code <= 57 { return true } + } + if code >= 65 { + if code <= 90 { return true } + } + if code >= 97 { + if code <= 122 { return true } + } + if code == 95 { return true } + false +} + +fn is_ascii_digit(ch: String) -> Bool { + let code: Int = str_char_code(ch, 0) + if code >= 48 { + if code <= 57 { return true } + } + false +} + +fn sanitize_id(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { return "node" } + + // Pass 1: replace and collapse. + let out = "" + let prev_underscore = false + let i = 0 + while i < n { + let ch: String = str_char_at(s, i) + if is_alnum_underscore(ch) { + let out = out + ch + let prev_underscore = false + } else { + if !prev_underscore { + let out = out + "_" + } + let prev_underscore = true + } + let i = i + 1 + } + + // Pass 2: strip trailing underscores. + let m: Int = str_len(out) + let end = m + let stripping = true + while stripping { + if end <= 0 { + let stripping = false + } else { + let last: String = str_char_at(out, end - 1) + if last == "_" { + let end = end - 1 + } else { + let stripping = false + } + } + } + let out = str_slice(out, 0, end) + + if str_len(out) == 0 { return "node" } + + // Pass 3: leading-digit guard. + let first: String = str_char_at(out, 0) + if is_ascii_digit(first) { + let out = "n" + out + } + out +} + +// ── Constructors ────────────────────────────────────────────────────────────── + +fn make_node(id: String, label: String, shape: String) -> Map { + { "id": id, "label": label, "shape": shape } +} + +fn make_edge(src: String, dst: String, kind: String) -> Map { + { "from": src, "to": dst, "label": "", "kind": kind } +} + +fn make_edge_with_label(src: String, dst: String, kind: String, label: String) -> Map { + { "from": src, "to": dst, "label": label, "kind": kind } +} + +fn make_group(id: String, label: String) -> Map { + let empty_ids: [String] = el_list_empty() + { "id": id, "label": label, "node_ids": empty_ids, "direction": "" } +} + +fn make_graph() -> Map { + let empty_n: [Map] = el_list_empty() + let empty_e: [Map] = el_list_empty() + let empty_g: [Map] = el_list_empty() + { "title": "", "direction": "top-down", + "nodes": empty_n, "edges": empty_e, "groups": empty_g } +} + +// ── Shape vocabulary ────────────────────────────────────────────────────────── +// Returns the canonical shape string for a token, or "" if unknown. + +fn shape_from_token(tok: String) -> String { + let t: String = str_trim(tok) + if t == "rect" { return "rect" } + if t == "rounded" { return "rounded" } + if t == "cylinder" { return "cylinder" } + if t == "diamond" { return "diamond" } + if t == "stadium" { return "stadium" } + if t == "primary" { return "primary" } + "" +} + +// Lower an Arbor shape into the renderer's NodeShape vocabulary. +fn shape_to_node_shape(shape: String) -> String { + if shape == "rect" { return "rectangle" } + if shape == "primary" { return "rectangle" } + if shape == "rounded" { return "rounded_rect" } + if shape == "cylinder" { return "cylinder" } + if shape == "diamond" { return "diamond" } + if shape == "stadium" { return "stadium" } + "rectangle" +} + +// ── Lowering: ArborGraph → DiagramGraph ────────────────────────────────────── +// +// Replaces every node with a diagram-form node carrying explicit style fields, +// and every edge with a diagram-form edge carrying line/arrow strings. + +fn lower_node(n: Map) -> Map { + let shape: String = n["shape"] + let node_shape: String = shape_to_node_shape(shape) + let fill = "" + let stroke = "" + let color = "" + if shape == "primary" { + let fill = "#0052A0" + let stroke = "#0052A0" + let color = "#ffffff" + } + { "id": n["id"], "label": n["label"], "sublabel": "", + "shape": node_shape, + "style_fill": fill, "style_stroke": stroke, "style_color": color } +} + +fn lower_edge(e: Map) -> Map { + let kind: String = e["kind"] + let line = "solid" + let arrow = "forward" + if kind == "dashed" { + let line = "dashed" + } + if kind == "bidirectional" { + let arrow = "both" + } + // forbidden uses solid line + forward arrow; the renderer overlays the + // circle-X marker based on a forbidden-set the caller threads through. + { "from": e["from"], "to": e["to"], "label": e["label"], + "line": line, "arrow": arrow } +} + +fn lower_graph(g: Map) -> Map { + let nodes: [Map] = g["nodes"] + let edges: [Map] = g["edges"] + let lowered_nodes: [Map] = el_list_empty() + let i = 0 + let n: Int = el_list_len(nodes) + while i < n { + let lowered_nodes = native_list_append(lowered_nodes, lower_node(get(nodes, i))) + let i = i + 1 + } + let lowered_edges: [Map] = el_list_empty() + let i = 0 + let m: Int = el_list_len(edges) + while i < m { + let lowered_edges = native_list_append(lowered_edges, lower_edge(get(edges, i))) + let i = i + 1 + } + { "title": g["title"], "direction": g["direction"], + "nodes": lowered_nodes, "edges": lowered_edges, "groups": g["groups"] } +} + +// Find a node by id within a (lowered or raw) graph. Returns an empty map +// when not found — callers check map_get(result, "id") for presence. +fn graph_find_node(graph: Map, id: String) -> Map { + let nodes: [Map] = graph["nodes"] + let n: Int = el_list_len(nodes) + let i = 0 + while i < n { + let node: Map = get(nodes, i) + let nid: String = node["id"] + if nid == id { return node } + let i = i + 1 + } + let empty: Map = el_map_new(0) + empty +} + +// ── Forbidden-edge set helpers ──────────────────────────────────────────────── +// The lowered graph drops the "forbidden" kind (line/arrow have no slot for +// it). Callers preserve the set as a list of "from->to" strings. + +fn forbidden_key(from: String, to: String) -> String { + from + "->" + to +} + +fn collect_forbidden(graph: Map) -> [String] { + let edges: [Map] = graph["edges"] + let n: Int = el_list_len(edges) + let out: [String] = el_list_empty() + let i = 0 + while i < n { + let e: Map = get(edges, i) + let kind: String = e["kind"] + if kind == "forbidden" { + let f: String = e["from"] + let t: String = e["to"] + let out = native_list_append(out, forbidden_key(f, t)) + } + let i = i + 1 + } + out +} + +fn forbidden_contains(set: [String], src: String, dst: String) -> Bool { + let key: String = forbidden_key(src, dst) + let n: Int = el_list_len(set) + let i = 0 + while i < n { + let s: String = get(set, i) + if s == key { return true } + let i = i + 1 + } + false +} + +// ── Smoke test ──────────────────────────────────────────────────────────────── +// +// State is kept in process-local k/v storage so we never mix Int + Call or +// Int + Ident in `+` (which the codegen heuristic emits as string concat +// on tagged-pointer values, segfaulting on Int operands). + +fn fail(label: String, got: String, want: String) -> Int { + println("FAIL " + label + " got=[" + got + "] want=[" + want + "]") + state_set("failures", "1") + 0 +} + +fn check_eq(label: String, got: String, want: String) -> Int { + if got == want { + println("ok " + label + " = " + got) + return 1 + } + fail(label, got, want) +} + +check_eq("sanitize crates/nc-core", + sanitize_id("crates/nc-core"), "crates_nc_core") + +check_eq("sanitize package.json", + sanitize_id("package.json"), "package_json") + +check_eq("sanitize 42-module", + sanitize_id("42-module"), "n42_module") + +check_eq("sanitize empty", sanitize_id(""), "node") + +check_eq("sanitize !!--@@", sanitize_id("!!--@@"), "node") + +check_eq("shape_from_token rounded", + shape_from_token("rounded"), "rounded") + +check_eq("shape_to_node_shape primary", + shape_to_node_shape("primary"), "rectangle") + +// Lowering preserves a node id and adds style. +let n: Map = make_node("svc", "Service", "primary") +let ln: Map = lower_node(n) +check_eq("lower preserves id", ln["id"], "svc") +check_eq("lower applies primary fill", ln["style_fill"], "#0052A0") + +// Edge lowering +let e: Map = make_edge("a", "b", "dashed") +let le: Map = lower_edge(e) +check_eq("lower edge dashed line", le["line"], "dashed") + +let e2: Map = make_edge("a", "b", "bidirectional") +let le2: Map = lower_edge(e2) +check_eq("lower edge bidirectional arrow", le2["arrow"], "both") + +println("") +let failures: String = state_get("failures") +if str_eq(failures, "1") { + println("arbor-core: FAILED") + exit_program(1) +} else { + println("arbor-core: ok") +} diff --git a/arbor/vessels/arbor-diagram/manifest.el b/arbor/vessels/arbor-diagram/manifest.el new file mode 100644 index 0000000..d2561f5 --- /dev/null +++ b/arbor/vessels/arbor-diagram/manifest.el @@ -0,0 +1,19 @@ +// arbor-diagram — diagram intermediate representation + Mermaid serializer +// + dependency-graph builders. Consumes raw graph values built by arbor-core +// or arbor-parse and produces Mermaid markup or other serializations. + +vessel "arbor-diagram" { + version "0.1.0" + description "Diagram IR + Mermaid serializer + architecture diagram builders" + authors ["Neuron Technologies"] + edition "2026" +} + +dependencies { + arbor-core "0.1" +} + +build { + entry "src/main.el" + output "dist/" +} diff --git a/arbor/vessels/arbor-diagram/src/main.el b/arbor/vessels/arbor-diagram/src/main.el new file mode 100644 index 0000000..c3e03c9 --- /dev/null +++ b/arbor/vessels/arbor-diagram/src/main.el @@ -0,0 +1,433 @@ +// arbor-diagram — diagram intermediate representation (AST + IR). +// +// Where arbor-core supplies the *.arbor source-language model — Mermaid-safe +// IDs, ArborShape strings, ArborEdgeKind strings, and the lowered "diagram- +// form" map — arbor-diagram exposes the same lowered model as the canonical +// IR for downstream serializers (arbor-render and any future Mermaid-style +// emitter). The two vessels overlap by design: arbor-core is responsible for +// *naming* the schema; arbor-diagram is responsible for *building* values +// against it. +// +// The Rust crate ships small AST builder structs (`DiagramNode::new`, +// `DiagramEdge::with_label`, `DiagramGraph::add_node`). El has no method +// chaining, no Default::default(), no enum types. The El idiom is a stack +// of immutable maps with explicit constructor + with_* helpers that take +// the value and return a freshly-allocated map. +// +// Public surface: +// make_node(id, label) → DiagramNode +// with_shape(node, shape) → DiagramNode +// with_sublabel(node, sublabel) → DiagramNode +// with_style(node, fill, stroke, color) → DiagramNode +// +// make_edge(from, to) → DiagramEdge +// with_label(edge, label) +// with_line(edge, line) // "solid"/"dashed"/"dotted"/"thick" +// with_arrow(edge, arrow) // "forward"/"backward"/"both"/"none" +// +// make_group(id, label) → DiagramGroup +// with_node(group, node_id) +// with_nodes(group, [node_id]) +// with_direction(group, dir) +// +// make_graph(title) → DiagramGraph +// with_direction(graph, dir) +// graph_add_node(graph, node) → DiagramGraph +// graph_add_edge(graph, edge) → DiagramGraph +// graph_add_group(graph, group) → DiagramGraph +// graph_node(graph, id) → DiagramNode | empty map +// +// Shape vocabulary (lowered): see arbor-core. The local copy here mirrors +// the table in arbor-core/src/main.el so this vessel is hermetic. + +// ── NodeShape vocabulary ──────────────────────────────────────────────────── + +fn node_shape_rectangle() -> String { "rectangle" } +fn node_shape_rounded_rect() -> String { "rounded_rect" } +fn node_shape_stadium() -> String { "stadium" } +fn node_shape_cylinder() -> String { "cylinder" } +fn node_shape_diamond() -> String { "diamond" } +fn node_shape_parallelogram() -> String { "parallelogram" } +fn node_shape_database() -> String { "database" } +fn node_shape_subroutine() -> String { "subroutine" } + +fn node_shape_valid(s: String) -> Bool { + if str_eq(s, "rectangle") { return true } + if str_eq(s, "rounded_rect") { return true } + if str_eq(s, "stadium") { return true } + if str_eq(s, "cylinder") { return true } + if str_eq(s, "diamond") { return true } + if str_eq(s, "parallelogram") { return true } + if str_eq(s, "database") { return true } + if str_eq(s, "subroutine") { return true } + false +} + +// ── EdgeLine vocabulary ───────────────────────────────────────────────────── + +fn edge_line_solid() -> String { "solid" } +fn edge_line_dashed() -> String { "dashed" } +fn edge_line_dotted() -> String { "dotted" } +fn edge_line_thick() -> String { "thick" } + +fn edge_line_valid(s: String) -> Bool { + if str_eq(s, "solid") { return true } + if str_eq(s, "dashed") { return true } + if str_eq(s, "dotted") { return true } + if str_eq(s, "thick") { return true } + false +} + +// ── EdgeArrow vocabulary ──────────────────────────────────────────────────── + +fn edge_arrow_forward() -> String { "forward" } +fn edge_arrow_backward() -> String { "backward" } +fn edge_arrow_both() -> String { "both" } +fn edge_arrow_none() -> String { "none" } + +fn edge_arrow_valid(s: String) -> Bool { + if str_eq(s, "forward") { return true } + if str_eq(s, "backward") { return true } + if str_eq(s, "both") { return true } + if str_eq(s, "none") { return true } + false +} + +// ── Direction vocabulary ──────────────────────────────────────────────────── + +fn direction_top_down() -> String { "top-down" } +fn direction_left_right() -> String { "left-right" } +fn direction_right_left() -> String { "right-left" } +fn direction_bottom_up() -> String { "bottom-up" } + +fn direction_valid(s: String) -> Bool { + if str_eq(s, "top-down") { return true } + if str_eq(s, "left-right") { return true } + if str_eq(s, "right-left") { return true } + if str_eq(s, "bottom-up") { return true } + false +} + +// ── DiagramNode ───────────────────────────────────────────────────────────── + +fn make_node(id: String, label: String) -> Map { + { + "id": id, + "label": label, + "sublabel": "", + "shape": "rectangle", + "style_fill": "", + "style_stroke": "", + "style_color": "" + } +} + +fn with_shape(node: Map, shape: String) -> Map { + { + "id": node["id"], + "label": node["label"], + "sublabel": node["sublabel"], + "shape": shape, + "style_fill": node["style_fill"], + "style_stroke": node["style_stroke"], + "style_color": node["style_color"] + } +} + +fn with_sublabel(node: Map, sublabel: String) -> Map { + { + "id": node["id"], + "label": node["label"], + "sublabel": sublabel, + "shape": node["shape"], + "style_fill": node["style_fill"], + "style_stroke": node["style_stroke"], + "style_color": node["style_color"] + } +} + +fn with_style(node: Map, fill: String, stroke: String, color: String) -> Map { + { + "id": node["id"], + "label": node["label"], + "sublabel": node["sublabel"], + "shape": node["shape"], + "style_fill": fill, + "style_stroke": stroke, + "style_color": color + } +} + +// ── DiagramEdge ───────────────────────────────────────────────────────────── + +fn make_edge(from: String, to: String) -> Map { + { + "from": from, + "to": to, + "label": "", + "line": "solid", + "arrow": "forward" + } +} + +fn with_label(edge: Map, label: String) -> Map { + { + "from": edge["from"], + "to": edge["to"], + "label": label, + "line": edge["line"], + "arrow": edge["arrow"] + } +} + +fn with_line(edge: Map, line: String) -> Map { + { + "from": edge["from"], + "to": edge["to"], + "label": edge["label"], + "line": line, + "arrow": edge["arrow"] + } +} + +fn with_arrow(edge: Map, arrow: String) -> Map { + { + "from": edge["from"], + "to": edge["to"], + "label": edge["label"], + "line": edge["line"], + "arrow": arrow + } +} + +// ── DiagramGroup ──────────────────────────────────────────────────────────── + +fn make_group(id: String, label: String) -> Map { + let empty: [String] = native_list_empty() + { + "id": id, + "label": label, + "node_ids": empty, + "direction": "" + } +} + +fn with_node(group: Map, node_id: String) -> Map { + let cur: [String] = group["node_ids"] + let next: [String] = native_list_append(cur, node_id) + { + "id": group["id"], + "label": group["label"], + "node_ids": next, + "direction": group["direction"] + } +} + +fn with_nodes(group: Map, ids: [String]) -> Map { + let cur: [String] = group["node_ids"] + let n: Int = el_list_len(ids) + let i = 0 + while i < n { + let cur = native_list_append(cur, get(ids, i)) + let i = i + 1 + } + { + "id": group["id"], + "label": group["label"], + "node_ids": cur, + "direction": group["direction"] + } +} + +fn with_group_direction(group: Map, dir: String) -> Map { + { + "id": group["id"], + "label": group["label"], + "node_ids": group["node_ids"], + "direction": dir + } +} + +// ── DiagramGraph ──────────────────────────────────────────────────────────── + +fn make_graph(title: String) -> Map { + let empty_n: [Map] = native_list_empty() + let empty_e: [Map] = native_list_empty() + let empty_g: [Map] = native_list_empty() + { + "title": title, + "direction": "top-down", + "nodes": empty_n, + "edges": empty_e, + "groups": empty_g + } +} + +fn with_direction(graph: Map, dir: String) -> Map { + { + "title": graph["title"], + "direction": dir, + "nodes": graph["nodes"], + "edges": graph["edges"], + "groups": graph["groups"] + } +} + +fn graph_add_node(graph: Map, node: Map) -> Map { + let cur: [Map] = graph["nodes"] + let next: [Map] = native_list_append(cur, node) + { + "title": graph["title"], + "direction": graph["direction"], + "nodes": next, + "edges": graph["edges"], + "groups": graph["groups"] + } +} + +fn graph_add_edge(graph: Map, edge: Map) -> Map { + let cur: [Map] = graph["edges"] + let next: [Map] = native_list_append(cur, edge) + { + "title": graph["title"], + "direction": graph["direction"], + "nodes": graph["nodes"], + "edges": next, + "groups": graph["groups"] + } +} + +fn graph_add_group(graph: Map, group: Map) -> Map { + let cur: [Map] = graph["groups"] + let next: [Map] = native_list_append(cur, group) + { + "title": graph["title"], + "direction": graph["direction"], + "nodes": graph["nodes"], + "edges": graph["edges"], + "groups": next + } +} + +// Find a node by id. Returns an empty map (no "id" field) when not present. +fn graph_node(graph: Map, id: String) -> Map { + let nodes: [Map] = graph["nodes"] + let n: Int = el_list_len(nodes) + let i = 0 + while i < n { + let nd: Map = get(nodes, i) + let nid: String = nd["id"] + if str_eq(nid, id) { return nd } + let i = i + 1 + } + let empty: Map = el_map_new(0) + empty +} + +// ── Smoke test ────────────────────────────────────────────────────────────── + +fn fail(label: String, got: String, want: String) -> Int { + println("FAIL " + label + " got=[" + got + "] want=[" + want + "]") + state_set("smoke_failures", "1") + 0 +} + +fn check_eq(label: String, got: String, want: String) -> Int { + if got == want { + println("ok " + label + " = " + got) + return 1 + } + fail(label, got, want) +} + +// Vocabulary self-checks +check_eq("shape rectangle valid", + bool_to_str(node_shape_valid("rectangle")), "true") +check_eq("shape hexagon invalid", + bool_to_str(node_shape_valid("hexagon")), "false") +check_eq("line dashed valid", + bool_to_str(edge_line_valid("dashed")), "true") +check_eq("arrow both valid", + bool_to_str(edge_arrow_valid("both")), "true") +check_eq("dir top-down valid", + bool_to_str(direction_valid("top-down")), "true") + +// Node builder +let n0: Map = make_node("svc", "Service") +check_eq("node default shape", n0["shape"], "rectangle") +check_eq("node default sublabel empty", n0["sublabel"], "") + +let n1: Map = with_shape(n0, "cylinder") +check_eq("node with_shape", n1["shape"], "cylinder") +check_eq("node id preserved", n1["id"], "svc") + +let n2: Map = with_sublabel(n1, "v0.1.0") +check_eq("node with_sublabel", n2["sublabel"], "v0.1.0") + +let n3: Map = with_style(n2, "#0052A0", "#0052A0", "#ffffff") +check_eq("node style fill", n3["style_fill"], "#0052A0") +check_eq("node style color", n3["style_color"], "#ffffff") + +// Edge builder +let e0: Map = make_edge("a", "b") +check_eq("edge default line", e0["line"], "solid") +check_eq("edge default arrow", e0["arrow"], "forward") +let e1: Map = with_line(e0, "dashed") +let e2: Map = with_arrow(e1, "both") +let e3: Map = with_label(e2, "calls") +check_eq("edge line", e3["line"], "dashed") +check_eq("edge arrow", e3["arrow"], "both") +check_eq("edge label", e3["label"], "calls") + +// Group builder +let g0: Map = make_group("core", "Application Core") +let g1: Map = with_node(g0, "api") +let g2: Map = with_node(g1, "svc") +let ids2: [String] = g2["node_ids"] +check_eq("group with two nodes", int_to_str(el_list_len(ids2)), "2") + +let g3: Map = make_group("infra", "Infrastructure") +let extras: [String] = native_list_empty() +let extras = native_list_append(extras, "db") +let extras = native_list_append(extras, "cache") +let g4: Map = with_nodes(g3, extras) +let ids4: [String] = g4["node_ids"] +check_eq("group with_nodes appends", int_to_str(el_list_len(ids4)), "2") + +// Graph builder + lookup +let G0: Map = make_graph("System") +let G1: Map = with_direction(G0, "left-right") +let G2: Map = graph_add_node(G1, n3) +let nb: Map = make_node("b", "Backend") +let G3: Map = graph_add_node(G2, nb) +let G4: Map = graph_add_edge(G3, e3) +let G5: Map = graph_add_group(G4, g4) + +check_eq("graph title", G5["title"], "System") +check_eq("graph direction", G5["direction"], "left-right") +let gn: [Map] = G5["nodes"] +let ge: [Map] = G5["edges"] +let gg: [Map] = G5["groups"] +check_eq("graph nodes count", int_to_str(el_list_len(gn)), "2") +check_eq("graph edges count", int_to_str(el_list_len(ge)), "1") +check_eq("graph groups count", int_to_str(el_list_len(gg)), "1") + +let found: Map = graph_node(G5, "svc") +check_eq("graph_node found", found["id"], "svc") +let missing: Map = graph_node(G5, "nonexistent") +let missing_id: String = missing["id"] +if str_len(missing_id) == 0 { + println("ok graph_node missing returns empty") +} else { + println("FAIL graph_node missing returned: " + missing_id) + state_set("smoke_failures", "1") +} + +println("") +let failures: String = state_get("smoke_failures") +if str_eq(failures, "1") { + println("arbor-diagram: FAILED") + exit_program(1) +} else { + println("arbor-diagram: ok") +} diff --git a/arbor/vessels/arbor-layout/manifest.el b/arbor/vessels/arbor-layout/manifest.el new file mode 100644 index 0000000..a115374 --- /dev/null +++ b/arbor/vessels/arbor-layout/manifest.el @@ -0,0 +1,19 @@ +// arbor-layout — hierarchical layout engine. Assigns (x, y) positions to +// every node, computes group bounding boxes, and the canvas size. Consumes +// a diagram graph; produces a layout-result value. + +vessel "arbor-layout" { + version "0.1.0" + description "Hierarchical layout engine — rank assignment, positioning, group bounds" + authors ["Neuron Technologies"] + edition "2026" +} + +dependencies { + arbor-core "0.1" +} + +build { + entry "src/main.el" + output "dist/" +} diff --git a/arbor/vessels/arbor-layout/src/main.el b/arbor/vessels/arbor-layout/src/main.el new file mode 100644 index 0000000..4fa2a73 --- /dev/null +++ b/arbor/vessels/arbor-layout/src/main.el @@ -0,0 +1,591 @@ +// arbor-layout — hierarchical layout for diagram graphs. +// +// Public entry point: +// fn arbor_layout(graph: Map) -> Map +// +// The graph is the lowered (diagram-form) shape. The result map has: +// "node_pos_" → { "x":Float, "y":Float } centre point +// "node_size_" → { "w":Float, "h":Float } +// "group_bounds_" → { "x":Float, "y":Float, "w":Float, "h":Float } +// "node_ids" → [String] iteration order +// "group_ids" → [String] iteration order +// "canvas" → { "w":Float, "h":Float } +// +// Floats are El-encoded — store via the runtime's bit-cast convention. +// All arithmetic on positions/sizes is done in Float; integers (rank index) +// stay as Int. +// +// Algorithm (simplified Sugiyama): +// 1. Assign ranks via topological propagation (longest path from sources). +// 2. Group nodes by rank, preserving declaration order. +// 3. Position each rank as a row (top-down/bottom-up) or column (LR/RL). +// 4. Compute group bounding boxes from member positions. +// 5. Compute canvas size to enclose everything. +// +// The current implementation is the same simplified Sugiyama as the Rust +// version; perfectly identical numerical output is not promised but the +// relative ordering and bounding-box semantics match. + +// ── Spacing constants (declared as float-bit-cast helpers) ────────────────── + +fn k_node_base_w() -> el_val_t { int_to_float(120) } +fn k_node_base_h() -> el_val_t { int_to_float(40) } +fn k_node_char_extra() -> el_val_t { int_to_float(8) } +fn k_h_gap() -> el_val_t { int_to_float(60) } +fn k_v_gap() -> el_val_t { int_to_float(80) } +fn k_group_pad() -> el_val_t { int_to_float(20) } +fn k_margin() -> el_val_t { int_to_float(40) } + +// Float-aware max/min via int_to_float / float arithmetic — but el_max +// works in raw int comparison space, so we bit-cast carefully. +// For our purposes we only need monotonic comparisons on positive values, +// which IEEE 754 doubles + sign-magnitude bit patterns happen to preserve +// for non-negative floats — but it's safer to do the comparison via the +// math layer. We use a helper that decodes both, picks the bigger, and +// re-encodes. +// +// Implemented in C terms: math_max(a, b) — but el_runtime doesn't expose +// a float-aware max, so we synthesise one. + +fn fmax(a: el_val_t, b: el_val_t) -> el_val_t { + // Compare via float subtraction's sign: a - b. Float subtraction is the + // multiply chain implemented via the C code generator. But el's `-` on + // bit-cast doubles doesn't perform IEEE arithmetic — it's a 64-bit int + // subtract. Workaround: round-trip through format_float and str_to_float. + // For our layout numbers (small non-negative integers stored as floats) + // we can compare via the raw bits: a positive float's bit pattern is + // monotonically ordered, so `a > b` on the int reinterpretation gives + // the same result as on the actual double for non-negative values. + if a > b { return a } + b +} + +fn fadd(a: el_val_t, b: el_val_t) -> el_val_t { + // a, b are bit-cast doubles. Safe addition: int-to-float, format, parse. + // For the small positive integers we work with, we reconstruct the + // numeric value via format_float → str_to_float, perform addition by + // pulling them through str representations. Costly but correct on the + // current runtime. Fast path: if both are exact ints stored as floats + // we can also keep an Int "shadow" — but the simpler approach is to + // route through the printf-based formatter once per layout pass. + let as: String = format_float(a, 6) + let bs: String = format_float(b, 6) + // Parse back to numeric. + let af: el_val_t = str_to_float(as) + let bf: el_val_t = str_to_float(bs) + // No real-add primitive; build the sum from int parts where possible. + // Convert to int at full resolution: float_to_int truncates towards zero, + // which for our values (always integer-valued) is exact. + let ai: Int = float_to_int(af) + let bi: Int = float_to_int(bf) + int_to_float(ai + bi) +} + +fn fsub(a: el_val_t, b: el_val_t) -> el_val_t { + let ai: Int = float_to_int(a) + let bi: Int = float_to_int(b) + int_to_float(ai - bi) +} + +fn fmul(a: el_val_t, b: el_val_t) -> el_val_t { + let ai: Int = float_to_int(a) + let bi: Int = float_to_int(b) + int_to_float(ai * bi) +} + +fn fdiv2(a: el_val_t) -> el_val_t { + let ai: Int = float_to_int(a) + int_to_float(ai / 2) +} + +// ── Node size based on label width ────────────────────────────────────────── + +fn node_size_for(label: String) -> Map { + let len: Int = str_len(label) + let extra: Int = 0 + if len > 10 { + let extra = len - 10 + } + let w_int: Int = 120 + 8 * extra + let w: el_val_t = int_to_float(w_int) + let h: el_val_t = int_to_float(40) + { "w": w, "h": h } +} + +// ── Adjacency-list construction ───────────────────────────────────────────── +// +// Builds successor and in-degree maps keyed by node id. + +fn build_succ_indeg(graph: Map) -> Map { + let nodes: [Map] = graph["nodes"] + let edges: [Map] = graph["edges"] + let n: Int = el_list_len(nodes) + let m: Int = el_list_len(edges) + + let succ: Map = el_map_new(0) + let indeg: Map = el_map_new(0) + + let i = 0 + while i < n { + let nd: Map = get(nodes, i) + let nid: String = nd["id"] + let empty: [String] = el_list_empty() + let succ = el_map_set(succ, nid, empty) + let indeg = el_map_set(indeg, nid, 0) + let i = i + 1 + } + + let i = 0 + while i < m { + let e: Map = get(edges, i) + let src: String = e["from"] + let dst: String = e["to"] + let cur_succ: [String] = el_map_get(succ, src) + let new_succ: [String] = native_list_append(cur_succ, dst) + let succ = el_map_set(succ, src, new_succ) + let prev: Int = el_map_get(indeg, dst) + let indeg = el_map_set(indeg, dst, prev + 1) + let i = i + 1 + } + + { "succ": succ, "indeg": indeg } +} + +// ── Topological rank assignment ───────────────────────────────────────────── +// +// Returns a map: node_id → rank. + +fn assign_ranks(graph: Map) -> Map { + let nodes: [Map] = graph["nodes"] + let n: Int = el_list_len(nodes) + let adj: Map = build_succ_indeg(graph) + let succ: Map = adj["succ"] + let indeg: Map = adj["indeg"] + + let ranks: Map = el_map_new(0) + let i = 0 + while i < n { + let nd: Map = get(nodes, i) + let nid: String = nd["id"] + let ranks = el_map_set(ranks, nid, 0) + let i = i + 1 + } + + // Initialise queue with all nodes whose in-degree is 0 (in declaration + // order, mirroring the Rust implementation's ordering guarantee). + let queue: [String] = el_list_empty() + let i = 0 + while i < n { + let nd: Map = get(nodes, i) + let nid: String = nd["id"] + let d: Int = el_map_get(indeg, nid) + if d == 0 { + let queue = native_list_append(queue, nid) + } + let i = i + 1 + } + + let head = 0 + let running = true + while running { + if head >= el_list_len(queue) { + let running = false + } else { + let cur: String = get(queue, head) + let head = head + 1 + let cur_rank: Int = el_map_get(ranks, cur) + let neighbours: [String] = el_map_get(succ, cur) + let nn: Int = el_list_len(neighbours) + let j = 0 + while j < nn { + let nb: String = get(neighbours, j) + let nb_rank: Int = el_map_get(ranks, nb) + let cand: Int = cur_rank + 1 + if cand > nb_rank { + let ranks = el_map_set(ranks, nb, cand) + } + let cur_d: Int = el_map_get(indeg, nb) + let new_d: Int = cur_d - 1 + let indeg = el_map_set(indeg, nb, new_d) + if new_d <= 0 { + let queue = native_list_append(queue, nb) + } + let j = j + 1 + } + } + } + ranks +} + +// ── Layout pass ───────────────────────────────────────────────────────────── + +fn arbor_layout(graph: Map) -> Map { + let nodes: [Map] = graph["nodes"] + let n: Int = el_list_len(nodes) + let direction: String = graph["direction"] + + let result: Map = el_map_new(0) + let result = el_map_set(result, "node_ids", el_list_empty()) + let result = el_map_set(result, "group_ids", el_list_empty()) + + if n == 0 { + let canvas: Map = { "w": int_to_float(200), "h": int_to_float(100) } + let result = el_map_set(result, "canvas", canvas) + return result + } + + let ranks: Map = assign_ranks(graph) + let max_rank = 0 + let i = 0 + while i < n { + let nd: Map = get(nodes, i) + let nid: String = nd["id"] + let r: Int = el_map_get(ranks, nid) + if r > max_rank { let max_rank = r } + let i = i + 1 + } + + // Group nodes by rank, preserving declaration order. Buckets are stored + // in process state so we can iterate without nested-list mutation. + let i = 0 + while i <= max_rank { + state_set("rank_bucket_" + int_to_str(i), "") + let i = i + 1 + } + let i = 0 + while i < n { + let nd: Map = get(nodes, i) + let nid: String = nd["id"] + let r: Int = el_map_get(ranks, nid) + let key = "rank_bucket_" + int_to_str(r) + let prev: String = state_get(key) + if str_eq(prev, "") { + state_set(key, nid) + } else { + state_set(key, prev + "" + nid) + } + let i = i + 1 + } + + // Pre-compute sizes and stash a label-keyed cache. + let id_list: [String] = el_list_empty() + let i = 0 + while i < n { + let nd: Map = get(nodes, i) + let nid: String = nd["id"] + let lbl: String = nd["label"] + let sz: Map = node_size_for(lbl) + let result = el_map_set(result, "node_size_" + nid, sz) + let id_list = native_list_append(id_list, nid) + let i = i + 1 + } + let result = el_map_set(result, "node_ids", id_list) + + // Position pass. + let is_vertical = true + if str_eq(direction, "left-right") { let is_vertical = false } + if str_eq(direction, "right-left") { let is_vertical = false } + + let cursor: el_val_t = k_margin() + + let r = 0 + while r <= max_rank { + let bucket_str: String = state_get("rank_bucket_" + int_to_str(r)) + if !str_eq(bucket_str, "") { + let ids: [String] = str_split(bucket_str, "") + let ids_n: Int = el_list_len(ids) + + // Track row height (for vertical) or column width (for horizontal). + let cross_max: el_val_t = int_to_float(40) + let j = 0 + while j < ids_n { + let nid: String = get(ids, j) + let sz: Map = el_map_get(result, "node_size_" + nid) + if is_vertical { + let h: el_val_t = sz["h"] + let cross_max = fmax(cross_max, h) + } else { + let w: el_val_t = sz["w"] + let cross_max = fmax(cross_max, w) + } + let j = j + 1 + } + + if is_vertical { + let row_h: el_val_t = cross_max + let y_center: el_val_t = fadd(cursor, fdiv2(row_h)) + let x_cursor: el_val_t = k_margin() + let j = 0 + while j < ids_n { + let nid: String = get(ids, j) + let sz: Map = el_map_get(result, "node_size_" + nid) + let w: el_val_t = sz["w"] + let cx: el_val_t = fadd(x_cursor, fdiv2(w)) + let pos: Map = { "x": cx, "y": y_center } + let result = el_map_set(result, "node_pos_" + nid, pos) + let x_cursor = fadd(fadd(x_cursor, w), k_h_gap()) + let j = j + 1 + } + let cursor = fadd(fadd(cursor, row_h), k_v_gap()) + } else { + let col_w: el_val_t = cross_max + let x_center: el_val_t = fadd(cursor, fdiv2(col_w)) + let y_cursor: el_val_t = k_margin() + let j = 0 + while j < ids_n { + let nid: String = get(ids, j) + let sz: Map = el_map_get(result, "node_size_" + nid) + let h: el_val_t = sz["h"] + let cy: el_val_t = fadd(y_cursor, fdiv2(h)) + let pos: Map = { "x": x_center, "y": cy } + let result = el_map_set(result, "node_pos_" + nid, pos) + let y_cursor = fadd(fadd(y_cursor, h), k_v_gap()) + let j = j + 1 + } + let cursor = fadd(fadd(cursor, col_w), k_h_gap()) + } + } else { + // Empty bucket — advance cursor by a default node size. + if is_vertical { + let cursor = fadd(cursor, fadd(int_to_float(40), k_v_gap())) + } else { + let cursor = fadd(cursor, fadd(k_node_base_w(), k_h_gap())) + } + } + let r = r + 1 + } + + // Direction inversions for BU / RL. + let need_flip_y = false + let need_flip_x = false + if str_eq(direction, "bottom-up") { let need_flip_y = true } + if str_eq(direction, "right-left") { let need_flip_x = true } + + if need_flip_y { + let max_y: el_val_t = fadd(fsub(cursor, k_v_gap()), k_margin()) + let i = 0 + while i < n { + let nid: String = get(id_list, i) + let pos: Map = el_map_get(result, "node_pos_" + nid) + let y: el_val_t = pos["y"] + let new_y: el_val_t = fadd(fsub(max_y, y), k_margin()) + let new_pos: Map = { "x": pos["x"], "y": new_y } + let result = el_map_set(result, "node_pos_" + nid, new_pos) + let i = i + 1 + } + } + if need_flip_x { + let max_x: el_val_t = fadd(fsub(cursor, k_h_gap()), k_margin()) + let i = 0 + while i < n { + let nid: String = get(id_list, i) + let pos: Map = el_map_get(result, "node_pos_" + nid) + let x: el_val_t = pos["x"] + let new_x: el_val_t = fadd(fsub(max_x, x), k_margin()) + let new_pos: Map = { "x": new_x, "y": pos["y"] } + let result = el_map_set(result, "node_pos_" + nid, new_pos) + let i = i + 1 + } + } + + // Group bounds. + let groups: [Map] = graph["groups"] + let gn: Int = el_list_len(groups) + let gid_list: [String] = el_list_empty() + let g = 0 + while g < gn { + let grp: Map = get(groups, g) + let gid: String = grp["id"] + let member_ids: [String] = grp["node_ids"] + let mn: Int = el_list_len(member_ids) + if mn > 0 { + let big: Int = 1000000000 + let neg: Int = 0 - 1000000000 + let min_x: el_val_t = int_to_float(big) + let min_y: el_val_t = int_to_float(big) + let max_x: el_val_t = int_to_float(neg) + let max_y: el_val_t = int_to_float(neg) + let mi = 0 + while mi < mn { + let mid: String = get(member_ids, mi) + let mpos: Map = el_map_get(result, "node_pos_" + mid) + let msz: Map = el_map_get(result, "node_size_" + mid) + let mid_present: String = mpos["x"] + if str_len(mid_present) >= 0 { + let cx: el_val_t = mpos["x"] + let cy: el_val_t = mpos["y"] + let mw: el_val_t = msz["w"] + let mh: el_val_t = msz["h"] + let left: el_val_t = fsub(cx, fdiv2(mw)) + let right: el_val_t = fadd(cx, fdiv2(mw)) + let top: el_val_t = fsub(cy, fdiv2(mh)) + let bot: el_val_t = fadd(cy, fdiv2(mh)) + if left < min_x { let min_x = left } + if top < min_y { let min_y = top } + if right > max_x { let max_x = right } + if bot > max_y { let max_y = bot } + } + let mi = mi + 1 + } + let bx: el_val_t = fsub(min_x, k_group_pad()) + let by: el_val_t = fsub(min_y, k_group_pad()) + let bw: el_val_t = fadd(fsub(max_x, min_x), fmul(k_group_pad(), int_to_float(2))) + let bh: el_val_t = fadd(fsub(max_y, min_y), fmul(k_group_pad(), int_to_float(2))) + let bounds: Map = { "x": bx, "y": by, "w": bw, "h": bh } + let result = el_map_set(result, "group_bounds_" + gid, bounds) + let gid_list = native_list_append(gid_list, gid) + } + let g = g + 1 + } + let result = el_map_set(result, "group_ids", gid_list) + + // Canvas size = max node-right / node-bottom + group-right / group-bottom. + let canvas_w: el_val_t = int_to_float(0) + let canvas_h: el_val_t = int_to_float(0) + let i = 0 + while i < n { + let nid: String = get(id_list, i) + let pos: Map = el_map_get(result, "node_pos_" + nid) + let sz: Map = el_map_get(result, "node_size_" + nid) + let right: el_val_t = fadd(pos["x"], fdiv2(sz["w"])) + let bottom: el_val_t = fadd(pos["y"], fdiv2(sz["h"])) + if right > canvas_w { let canvas_w = right } + if bottom > canvas_h { let canvas_h = bottom } + let i = i + 1 + } + let i = 0 + while i < el_list_len(gid_list) { + let gid: String = get(gid_list, i) + let b: Map = el_map_get(result, "group_bounds_" + gid) + let r: el_val_t = fadd(b["x"], b["w"]) + let bt: el_val_t = fadd(b["y"], b["h"]) + if r > canvas_w { let canvas_w = r } + if bt > canvas_h { let canvas_h = bt } + let i = i + 1 + } + let canvas: Map = { + "w": fadd(canvas_w, k_margin()), + "h": fadd(canvas_h, k_margin()) + } + let result = el_map_set(result, "canvas", canvas) + result +} + +// ── Smoke test ────────────────────────────────────────────────────────────── + +fn fl_to_str(v: el_val_t) -> String { + int_to_str(float_to_int(v)) +} + +fn smoke_fail(label: String, msg: String) -> Int { + println("FAIL " + label + ": " + msg) + state_set("smoke_failures", "1") + 0 +} + +fn make_test_node(id: String, label: String) -> Map { + { + "id": id, "label": label, "sublabel": "", + "shape": "rectangle", + "style_fill": "", "style_stroke": "", "style_color": "" + } +} + +fn make_test_edge(src: String, dst: String) -> Map { + { "from": src, "to": dst, "label": "", "line": "solid", "arrow": "forward" } +} + +fn make_test_graph(direction: String, ids: [String], src_dst: [String]) -> Map { + let nodes: [Map] = el_list_empty() + let i = 0 + while i < el_list_len(ids) { + let nid: String = get(ids, i) + let nodes = native_list_append(nodes, make_test_node(nid, nid)) + let i = i + 1 + } + let edges: [Map] = el_list_empty() + let i = 0 + while i + 1 < el_list_len(src_dst) { + let s: String = get(src_dst, i) + let d: String = get(src_dst, i + 1) + let edges = native_list_append(edges, make_test_edge(s, d)) + let i = i + 2 + } + { + "title": "T", "direction": direction, + "nodes": nodes, "edges": edges, "groups": el_list_empty() + } +} + +// Empty graph. +let g_empty: Map = { + "title": "e", "direction": "top-down", + "nodes": el_list_empty(), "edges": el_list_empty(), "groups": el_list_empty() +} +let r_empty: Map = arbor_layout(g_empty) +let canvas_empty: Map = r_empty["canvas"] +println("empty canvas w=" + fl_to_str(canvas_empty["w"])) + +// Single node. +let g_one: Map = make_test_graph("top-down", + ["solo"], el_list_empty()) +let r_one: Map = arbor_layout(g_one) +let pos_solo: Map = el_map_get(r_one, "node_pos_solo") +let x_solo: el_val_t = pos_solo["x"] +let y_solo: el_val_t = pos_solo["y"] +println("solo at x=" + fl_to_str(x_solo) + " y=" + fl_to_str(y_solo)) +if float_to_int(x_solo) <= 0 { smoke_fail("solo x", "expected > 0") } +if float_to_int(y_solo) <= 0 { smoke_fail("solo y", "expected > 0") } + +// Linear chain a→b→c top-down: ya < yb < yc. +let g_chain: Map = make_test_graph("top-down", + ["a", "b", "c"], ["a", "b", "b", "c"]) +let r_chain: Map = arbor_layout(g_chain) +let pa: Map = el_map_get(r_chain, "node_pos_a") +let pb: Map = el_map_get(r_chain, "node_pos_b") +let pc: Map = el_map_get(r_chain, "node_pos_c") +let ya: el_val_t = pa["y"] +let yb: el_val_t = pb["y"] +let yc: el_val_t = pc["y"] +println("td a.y=" + fl_to_str(ya) + " b.y=" + fl_to_str(yb) + " c.y=" + fl_to_str(yc)) +if float_to_int(ya) >= float_to_int(yb) { smoke_fail("td order", "a.y >= b.y") } +if float_to_int(yb) >= float_to_int(yc) { smoke_fail("td order", "b.y >= c.y") } + +// LR direction +let g_lr: Map = make_test_graph("left-right", + ["a", "b", "c"], ["a", "b", "b", "c"]) +let r_lr: Map = arbor_layout(g_lr) +let pa2: Map = el_map_get(r_lr, "node_pos_a") +let pc2: Map = el_map_get(r_lr, "node_pos_c") +let xa: el_val_t = pa2["x"] +let xc: el_val_t = pc2["x"] +println("lr a.x=" + fl_to_str(xa) + " c.x=" + fl_to_str(xc)) +if float_to_int(xa) >= float_to_int(xc) { smoke_fail("lr order", "a.x >= c.x") } + +// Bottom-up: a is below c. +let g_bu: Map = make_test_graph("bottom-up", + ["a", "b", "c"], ["a", "b", "b", "c"]) +let r_bu: Map = arbor_layout(g_bu) +let pa3: Map = el_map_get(r_bu, "node_pos_a") +let pc3: Map = el_map_get(r_bu, "node_pos_c") +let ya3: el_val_t = pa3["y"] +let yc3: el_val_t = pc3["y"] +println("bu a.y=" + fl_to_str(ya3) + " c.y=" + fl_to_str(yc3)) +if float_to_int(ya3) <= float_to_int(yc3) { smoke_fail("bu order", "a.y <= c.y") } + +// Canvas covers all nodes. +let canvas_chain: Map = r_chain["canvas"] +let cw: el_val_t = canvas_chain["w"] +let ch: el_val_t = canvas_chain["h"] +println("chain canvas w=" + fl_to_str(cw) + " h=" + fl_to_str(ch)) +if float_to_int(cw) <= 0 { smoke_fail("canvas w", "non-positive") } +if float_to_int(ch) <= 0 { smoke_fail("canvas h", "non-positive") } + +println("") +let f: String = state_get("smoke_failures") +if str_eq(f, "1") { + println("arbor-layout: FAILED") + exit_program(1) +} else { + println("arbor-layout: ok") +} diff --git a/arbor/vessels/arbor-parse/manifest.el b/arbor/vessels/arbor-parse/manifest.el new file mode 100644 index 0000000..52571d0 --- /dev/null +++ b/arbor/vessels/arbor-parse/manifest.el @@ -0,0 +1,19 @@ +// arbor-parse — hand-written recursive-descent parser for the .arbor source +// language. Produces an Arbor graph value consumable by arbor-layout and +// arbor-render. + +vessel "arbor-parse" { + version "0.1.0" + description "Recursive-descent parser for the .arbor diagram language" + authors ["Neuron Technologies"] + edition "2026" +} + +dependencies { + arbor-core "0.1" +} + +build { + entry "src/main.el" + output "dist/" +} diff --git a/arbor/vessels/arbor-parse/src/main.el b/arbor/vessels/arbor-parse/src/main.el new file mode 100644 index 0000000..9bff739 --- /dev/null +++ b/arbor/vessels/arbor-parse/src/main.el @@ -0,0 +1,763 @@ +// arbor-parse — recursive-descent parser for the .arbor source language. +// +// This vessel inlines a private copy of the small set of arbor-core helpers +// it needs (sanitize_id and constructors). El's import form today is purely +// syntactic concatenation, so each vessel that wants to be its own buildable +// unit carries its own copy of these helpers. They're tiny (well under 100 +// lines) and the duplication keeps each vessel hermetic. +// +// Public entry point: fn arbor_parse(source: String) -> Map +// +// Returns either a graph value or a parse-error map. Callers test for the +// "error" field: +// { "error": "..." , "line": Int, "text": "...source line..." } on failure +// { "title", "direction", "nodes", "edges", "groups" } on success + +// ── Sanitisation (copy of arbor-core's sanitize_id) ────────────────────────── + +fn is_alnum_underscore(ch: String) -> Bool { + let code: Int = str_char_code(ch, 0) + if code >= 48 { + if code <= 57 { return true } + } + if code >= 65 { + if code <= 90 { return true } + } + if code >= 97 { + if code <= 122 { return true } + } + if code == 95 { return true } + false +} + +fn is_ascii_digit(ch: String) -> Bool { + let code: Int = str_char_code(ch, 0) + if code >= 48 { + if code <= 57 { return true } + } + false +} + +fn sanitize_id(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { return "node" } + + let out = "" + let prev_underscore = false + let i = 0 + while i < n { + let ch: String = str_char_at(s, i) + if is_alnum_underscore(ch) { + let out = out + ch + let prev_underscore = false + } else { + if !prev_underscore { + let out = out + "_" + } + let prev_underscore = true + } + let i = i + 1 + } + + let m: Int = str_len(out) + let end = m + let stripping = true + while stripping { + if end <= 0 { + let stripping = false + } else { + let last: String = str_char_at(out, end - 1) + if last == "_" { + let end = end - 1 + } else { + let stripping = false + } + } + } + let out = str_slice(out, 0, end) + + if str_len(out) == 0 { return "node" } + + let first: String = str_char_at(out, 0) + if is_ascii_digit(first) { + let out = "n" + out + } + out +} + +fn shape_from_token(tok: String) -> String { + let t: String = str_trim(tok) + if t == "rect" { return "rect" } + if t == "rounded" { return "rounded" } + if t == "cylinder" { return "cylinder" } + if t == "diamond" { return "diamond" } + if t == "stadium" { return "stadium" } + if t == "primary" { return "primary" } + "" +} + +// ── Line preprocessing ────────────────────────────────────────────────────── +// +// Strip inline `// ...` comments, trim, drop empties. Returns a list of maps +// { "no": Int, "text": String }. + +fn preprocess(source: String) -> [Map] { + let lines: [String] = str_split(source, "\n") + let n: Int = el_list_len(lines) + let out: [Map] = el_list_empty() + let i = 0 + while i < n { + let raw: String = get(lines, i) + let cidx: Int = str_index_of(raw, "//") + let stripped = raw + if cidx >= 0 { + let stripped = str_slice(raw, 0, cidx) + } + let trimmed: String = str_trim(stripped) + if str_len(trimmed) > 0 { + let row: Map = { "no": i + 1, "text": trimmed } + let out = native_list_append(out, row) + } + let i = i + 1 + } + out +} + +// ── Quoted-string extraction ──────────────────────────────────────────────── +// +// Parses `"text"`-prefix from a string. Returns `{ "ok": Bool, "value": Str, +// "rest": Str }`. The `rest` field carries everything after the closing quote +// (so the caller can continue tokenising). + +fn parse_quoted(s: String) -> Map { + let t: String = str_trim(s) + if str_len(t) < 2 { + return { "ok": false, "value": "", "rest": s } + } + let first: String = str_char_at(t, 0) + if first != "\"" { + return { "ok": false, "value": "", "rest": s } + } + let body: String = str_slice(t, 1, str_len(t)) + let close: Int = str_index_of(body, "\"") + if close < 0 { + return { "ok": false, "value": "", "rest": s } + } + let inner: String = str_slice(body, 0, close) + let rest: String = str_slice(body, close + 1, str_len(body)) + { "ok": true, "value": inner, "rest": rest } +} + +// ── Identifier prefix split ───────────────────────────────────────────────── +// +// `split_identifier("foo bar")` → { "id": "foo", "rest": " bar" }. +// `split_identifier("a-b")` → { "id": "a", "rest": "-b" }. + +fn split_identifier(s: String) -> Map { + let n: Int = str_len(s) + let i = 0 + while i < n { + let ch: String = str_char_at(s, i) + if !is_alnum_underscore(ch) { + return { "id": str_slice(s, 0, i), "rest": str_slice(s, i, n) } + } + let i = i + 1 + } + { "id": s, "rest": "" } +} + +// ── Direction parsing ─────────────────────────────────────────────────────── + +fn parse_direction(s: String) -> String { + let t: String = str_trim(s) + if t == "top-down" { return "top-down" } + if t == "TD" { return "top-down" } + if t == "left-right" { return "left-right" } + if t == "LR" { return "left-right" } + if t == "right-left" { return "right-left" } + if t == "RL" { return "right-left" } + if t == "bottom-up" { return "bottom-up" } + if t == "BU" { return "bottom-up" } + "" +} + +// ── Edge-arrow detection ──────────────────────────────────────────────────── +// +// Detects the longest matching arrow token in a line, returning +// { "ok": Bool, "from_str": Str, "kind": Str, "rest": Str } + +fn extract_edge_parts(line: String) -> Map { + // Order: longest first to avoid partial matches. + let f1: Int = str_index_of(line, "-/->") + if f1 >= 0 { + return { "ok": true, + "from_str": str_slice(line, 0, f1), + "kind": "forbidden", + "rest": str_slice(line, f1 + 4, str_len(line)) } + } + let f2: Int = str_index_of(line, "<->") + if f2 >= 0 { + return { "ok": true, + "from_str": str_slice(line, 0, f2), + "kind": "bidirectional", + "rest": str_slice(line, f2 + 3, str_len(line)) } + } + let f3: Int = str_index_of(line, "-->") + if f3 >= 0 { + return { "ok": true, + "from_str": str_slice(line, 0, f3), + "kind": "dashed", + "rest": str_slice(line, f3 + 3, str_len(line)) } + } + let f4: Int = str_index_of(line, "->") + if f4 >= 0 { + return { "ok": true, + "from_str": str_slice(line, 0, f4), + "kind": "solid", + "rest": str_slice(line, f4 + 2, str_len(line)) } + } + { "ok": false, "from_str": "", "kind": "", "rest": "" } +} + +fn is_edge_line(line: String) -> Bool { + if str_contains(line, "->") { return true } + if str_contains(line, "<->") { return true } + false +} + +// ── Error helpers ─────────────────────────────────────────────────────────── + +fn make_error(line_no: Int, line_text: String, message: String) -> Map { + { "error": message, "line": line_no, "text": line_text } +} + +// ── Parse driver ──────────────────────────────────────────────────────────── +// +// State is held in process-local k/v rather than threaded through every +// function. Specifically: +// "title", "direction" — graph header +// "nodes_json", "edges_json", "groups_json" — accumulators (string lists) +// "group_stack_depth" — "0".."N" — open groups +// "group_stack__id" / "_label" / "_line" — frame data +// "group_stack__node_ids" — JSON array of ids inside frame +// "error" — non-empty if parse failed +// "error_line", "error_text" — context + +fn st_set_int(key: String, v: Int) -> Int { state_set(key, int_to_str(v)); 0 } +fn st_get_int(key: String) -> Int { + let s: String = state_get(key) + if str_eq(s, "") { return 0 } + str_to_int(s) +} + +// Encode/decode small string lists via "" delimiter (unit separator). +fn list_encode(xs: [String]) -> String { + let n: Int = el_list_len(xs) + let out = "" + let i = 0 + while i < n { + if i > 0 { let out = out + "" } + let out = out + get(xs, i) + let i = i + 1 + } + out +} + +fn list_decode(s: String) -> [String] { + if str_eq(s, "") { return el_list_empty() } + str_split(s, "") +} + +fn current_group_index() -> Int { + st_get_int("group_stack_depth") - 1 +} + +fn group_frame_key(idx: Int, suffix: String) -> String { + "gs_" + int_to_str(idx) + "_" + suffix +} + +fn open_group(id: String, label: String, line_no: Int) -> Int { + let depth: Int = st_get_int("group_stack_depth") + state_set(group_frame_key(depth, "id"), id) + state_set(group_frame_key(depth, "label"), label) + state_set(group_frame_key(depth, "line"), int_to_str(line_no)) + state_set(group_frame_key(depth, "ids"), "") + st_set_int("group_stack_depth", depth + 1) + 0 +} + +fn close_group_frame() -> Map { + let depth: Int = st_get_int("group_stack_depth") + if depth <= 0 { + return { "ok": false, "id": "", "label": "", "ids": "" } + } + let idx: Int = depth - 1 + let id: String = state_get(group_frame_key(idx, "id")) + let label: String = state_get(group_frame_key(idx, "label")) + let ids: String = state_get(group_frame_key(idx, "ids")) + state_del(group_frame_key(idx, "id")) + state_del(group_frame_key(idx, "label")) + state_del(group_frame_key(idx, "line")) + state_del(group_frame_key(idx, "ids")) + st_set_int("group_stack_depth", idx) + { "ok": true, "id": id, "label": label, "ids": ids } +} + +fn register_node_in_group(node_id: String) -> Int { + let depth: Int = st_get_int("group_stack_depth") + if depth <= 0 { return 0 } + let idx: Int = depth - 1 + let key: String = group_frame_key(idx, "ids") + let prev: String = state_get(key) + if str_eq(prev, "") { + state_set(key, node_id) + } else { + state_set(key, prev + "" + node_id) + } + 0 +} + +// Accumulator JSON-ish encoding for nodes/edges/groups. +// We render each entry as a small string and stash in state under a counter. + +fn store_node(id: String, label: String, shape: String) -> Int { + let n: Int = st_get_int("node_count") + state_set("node_id_" + int_to_str(n), id) + state_set("node_label_" + int_to_str(n), label) + state_set("node_shape_" + int_to_str(n), shape) + st_set_int("node_count", n + 1) + 0 +} + +fn store_edge(src: String, dst: String, label: String, kind: String) -> Int { + let n: Int = st_get_int("edge_count") + state_set("edge_from_" + int_to_str(n), src) + state_set("edge_to_" + int_to_str(n), dst) + state_set("edge_label_" + int_to_str(n), label) + state_set("edge_kind_" + int_to_str(n), kind) + st_set_int("edge_count", n + 1) + 0 +} + +fn store_group(id: String, label: String, ids: String) -> Int { + let n: Int = st_get_int("group_count") + state_set("group_id_" + int_to_str(n), id) + state_set("group_label_" + int_to_str(n), label) + state_set("group_ids_" + int_to_str(n), ids) + st_set_int("group_count", n + 1) + 0 +} + +fn set_error(msg: String, line_no: Int, line_text: String) -> Int { + state_set("parse_error", msg) + st_set_int("parse_error_line", line_no) + state_set("parse_error_text", line_text) + 0 +} + +fn has_error() -> Bool { + let m: String = state_get("parse_error") + if str_eq(m, "") { return false } + true +} + +// Reset state at the start of each parse pass. +fn reset_state() -> Int { + state_set("graph_title", "") + state_set("graph_direction", "top-down") + st_set_int("node_count", 0) + st_set_int("edge_count", 0) + st_set_int("group_count", 0) + st_set_int("group_stack_depth", 0) + state_set("parse_error", "") + st_set_int("parse_error_line", 0) + state_set("parse_error_text", "") + 0 +} + +// ── Statement-level parsing ───────────────────────────────────────────────── + +fn parse_node_stmt(line_no: Int, line: String) -> Int { + let id_split: Map = split_identifier(line) + let raw_id: String = id_split["id"] + if str_eq(raw_id, "") { + set_error("expected node id, edge, or keyword", line_no, line) + return 0 + } + let id: String = sanitize_id(raw_id) + let rest: String = str_trim(id_split["rest"]) + + // Optional shape: [token] + let shape = "rect" + let after_shape = rest + if str_len(rest) > 0 { + let lead: String = str_char_at(rest, 0) + if lead == "[" { + let close: Int = str_index_of(rest, "]") + if close < 0 { + set_error("unclosed `[` in shape token", line_no, line) + return 0 + } + let token: String = str_slice(rest, 1, close) + let parsed_shape: String = shape_from_token(token) + if str_eq(parsed_shape, "") { + set_error("unknown shape `" + token + "`", line_no, line) + return 0 + } + let shape = parsed_shape + let after_shape = str_trim(str_slice(rest, close + 1, str_len(rest))) + } + } + + // Optional quoted label. + let quoted: Map = parse_quoted(after_shape) + let label = raw_id + let ok: Bool = quoted["ok"] + if ok { + let label = quoted["value"] + } + + store_node(id, label, shape) + register_node_in_group(id) + 1 +} + +fn parse_edge_stmt(line_no: Int, line: String) -> Int { + let parts: Map = extract_edge_parts(line) + let ok: Bool = parts["ok"] + if !ok { + set_error("malformed edge — expected `->` `-->` `<->` or `-/->`", line_no, line) + return 0 + } + let from_str: String = parts["from_str"] + let rest_str: String = parts["rest"] + let kind: String = parts["kind"] + + let src: String = sanitize_id(str_trim(from_str)) + let rest_t: String = str_trim(rest_str) + + let id_split: Map = split_identifier(rest_t) + let to_raw: String = id_split["id"] + if str_eq(to_raw, "") { + set_error("edge missing target node id", line_no, line) + return 0 + } + let dst: String = sanitize_id(to_raw) + + let label_rest: String = str_trim(id_split["rest"]) + let quoted: Map = parse_quoted(label_rest) + let label = "" + let qok: Bool = quoted["ok"] + if qok { + let label = quoted["value"] + } + store_edge(src, dst, label, kind) + 1 +} + +fn parse_group_open(line_no: Int, line: String, rest: String) -> Int { + // Strip trailing `{`. + let trimmed: String = str_trim(rest) + let n: Int = str_len(trimmed) + let body = trimmed + if n > 0 { + let last: String = str_char_at(trimmed, n - 1) + if last == "{" { + let body = str_trim(str_slice(trimmed, 0, n - 1)) + } + } + + let id_split: Map = split_identifier(body) + let raw_id: String = id_split["id"] + if str_eq(raw_id, "") { + set_error("group declaration missing id", line_no, line) + return 0 + } + let label_rest: String = str_trim(id_split["rest"]) + let quoted: Map = parse_quoted(label_rest) + let label = raw_id + let qok: Bool = quoted["ok"] + if qok { + let label = quoted["value"] + } + open_group(raw_id, label, line_no) + 1 +} + +fn parse_close_brace(line_no: Int) -> Int { + let frame: Map = close_group_frame() + let frame_ok: Bool = frame["ok"] + if !frame_ok { + set_error("unexpected `}` — no open group", line_no, "}") + return 0 + } + store_group(frame["id"], frame["label"], frame["ids"]) + 1 +} + +fn parse_line_dispatch(line_no: Int, line: String) -> Int { + if line == "}" { return parse_close_brace(line_no) } + + if str_starts_with(line, "title:") { + let after: String = str_trim(str_slice(line, 6, str_len(line))) + let q: Map = parse_quoted(after) + let qok: Bool = q["ok"] + if !qok { + set_error("expected quoted string after `title:`", line_no, line) + return 0 + } + state_set("graph_title", q["value"]) + return 1 + } + + if str_starts_with(line, "direction:") { + let after: String = str_trim(str_slice(line, 10, str_len(line))) + let dir: String = parse_direction(after) + if str_eq(dir, "") { + set_error("unknown direction — expected top-down, left-right, right-left, or bottom-up", + line_no, line) + return 0 + } + state_set("graph_direction", dir) + return 1 + } + + if str_starts_with(line, "group ") { + let after: String = str_slice(line, 6, str_len(line)) + return parse_group_open(line_no, line, after) + } + + if is_edge_line(line) { + return parse_edge_stmt(line_no, line) + } + + parse_node_stmt(line_no, line) +} + +// ── Materialise accumulators into the final graph map ─────────────────────── + +fn build_graph_value() -> Map { + let n_nodes: Int = st_get_int("node_count") + let nodes: [Map] = el_list_empty() + let i = 0 + while i < n_nodes { + let s: String = int_to_str(i) + let node: Map = { + "id": state_get("node_id_" + s), + "label": state_get("node_label_" + s), + "shape": state_get("node_shape_" + s) + } + let nodes = native_list_append(nodes, node) + let i = i + 1 + } + + let n_edges: Int = st_get_int("edge_count") + let edges: [Map] = el_list_empty() + let i = 0 + while i < n_edges { + let s: String = int_to_str(i) + let edge: Map = { + "from": state_get("edge_from_" + s), + "to": state_get("edge_to_" + s), + "label": state_get("edge_label_" + s), + "kind": state_get("edge_kind_" + s) + } + let edges = native_list_append(edges, edge) + let i = i + 1 + } + + let n_groups: Int = st_get_int("group_count") + let groups: [Map] = el_list_empty() + let i = 0 + while i < n_groups { + let s: String = int_to_str(i) + let raw_ids: String = state_get("group_ids_" + s) + let id_list: [String] = list_decode(raw_ids) + let group: Map = { + "id": state_get("group_id_" + s), + "label": state_get("group_label_" + s), + "node_ids": id_list, + "direction": "" + } + let groups = native_list_append(groups, group) + let i = i + 1 + } + + { + "title": state_get("graph_title"), + "direction": state_get("graph_direction"), + "nodes": nodes, + "edges": edges, + "groups": groups + } +} + +// ── Public entry point ────────────────────────────────────────────────────── + +fn arbor_parse(source: String) -> Map { + reset_state() + let lines: [Map] = preprocess(source) + let n: Int = el_list_len(lines) + let i = 0 + let abort = false + while i < n { + if abort { + // skip — error already recorded + } else { + let row: Map = get(lines, i) + let line_no: Int = row["no"] + let text: String = row["text"] + parse_line_dispatch(line_no, text) + if has_error() { + let abort = true + } + } + let i = i + 1 + } + if !has_error() { + let depth: Int = st_get_int("group_stack_depth") + if depth > 0 { + let idx: Int = depth - 1 + let id: String = state_get(group_frame_key(idx, "id")) + let line_no: Int = st_get_int(group_frame_key(idx, "line")) + set_error("unclosed group '" + id + "' — missing closing `}`", + line_no, "group " + id) + } + } + if has_error() { + return { + "error": state_get("parse_error"), + "line": st_get_int("parse_error_line"), + "text": state_get("parse_error_text") + } + } + build_graph_value() +} + +// ── Smoke test ────────────────────────────────────────────────────────────── + +fn fail_msg(label: String, got: String, want: String) -> Int { + println("FAIL " + label + " got=[" + got + "] want=[" + want + "]") + state_set("smoke_failures", "1") + 0 +} + +fn check_eq(label: String, got: String, want: String) -> Int { + if got == want { + println("ok " + label) + return 1 + } + fail_msg(label, got, want) +} + +// Helper: a graph map is in the error state iff it has a non-empty "error". +fn parse_failed(g: Map) -> Bool { + let m: String = g["error"] + if str_eq(m, "") { return false } + // map_get returns NULL for missing keys; str_eq treats two NULLs as equal + // and NULL vs "" as not equal — guard explicitly. + if str_len(m) == 0 { return false } + true +} + +let src1 = "title: \"Test\"\ndirection: left-right\n\napi [rounded] \"REST API\"\ndb [cylinder] \"Postgres\"\n\napi -> db \"reads\"" +let g1: Map = arbor_parse(src1) +if parse_failed(g1) { + println("FAIL parse 1: " + g1["error"]) + state_set("smoke_failures", "1") +} +check_eq("title parsed", g1["title"], "Test") +check_eq("direction parsed", g1["direction"], "left-right") +let nodes1: [Map] = g1["nodes"] +let nn1: Int = el_list_len(nodes1) +check_eq("two nodes", int_to_str(nn1), "2") +let edges1: [Map] = g1["edges"] +let ne1: Int = el_list_len(edges1) +check_eq("one edge", int_to_str(ne1), "1") +let e0: Map = get(edges1, 0) +check_eq("edge from", e0["from"], "api") +check_eq("edge to", e0["to"], "db") +check_eq("edge label", e0["label"], "reads") +check_eq("edge kind", e0["kind"], "solid") +let n0: Map = get(nodes1, 0) +check_eq("node 0 shape", n0["shape"], "rounded") +check_eq("node 0 label", n0["label"], "REST API") + +// Test edge varieties +let src2 = "a \"A\"\nb \"B\"\na -> b\na --> b\na -/-> b\na <-> b" +let g2: Map = arbor_parse(src2) +let edges2: [Map] = g2["edges"] +check_eq("4 edges parsed", int_to_str(el_list_len(edges2)), "4") +let kinds = "" +let i = 0 +while i < el_list_len(edges2) { + let e: Map = get(edges2, i) + let k: String = e["kind"] + let kinds = kinds + k + "," + let i = i + 1 +} +check_eq("edge kinds", kinds, "solid,dashed,forbidden,bidirectional,") + +// Groups +let src3 = "group core \"Application Core\" {\n api [rounded] \"REST API\"\n svc \"Business Logic\"\n}\nstandalone \"Out\"" +let g3: Map = arbor_parse(src3) +let groups3: [Map] = g3["groups"] +check_eq("one group", int_to_str(el_list_len(groups3)), "1") +let grp0: Map = get(groups3, 0) +check_eq("group label", grp0["label"], "Application Core") +let gnids: [String] = grp0["node_ids"] +check_eq("group has 2 members", int_to_str(el_list_len(gnids)), "2") +let nodes3: [Map] = g3["nodes"] +check_eq("3 total nodes (incl standalone)", + int_to_str(el_list_len(nodes3)), "3") + +// Error: unknown shape +let src4 = "node [hexagon] \"X\"" +let g4: Map = arbor_parse(src4) +let err4: String = g4["error"] +if str_eq(err4, "") { + println("FAIL expected error for unknown shape") + state_set("smoke_failures", "1") +} else { + if str_contains(err4, "hexagon") { + println("ok error mentions hexagon: " + err4) + } else { + println("FAIL error wording: " + err4) + state_set("smoke_failures", "1") + } +} + +// Error: unclosed group +let src5 = "group g \"G\" {\n a \"A\"\n" +let g5: Map = arbor_parse(src5) +let err5: String = g5["error"] +if str_eq(err5, "") { + println("FAIL expected unclosed-group error") + state_set("smoke_failures", "1") +} else { + if str_contains(err5, "unclosed") { + println("ok unclosed group detected") + } else { + println("FAIL unclosed error wording: " + err5) + state_set("smoke_failures", "1") + } +} + +// Comments and inline comments +let src6 = "// header\na \"A\" // trailing\nb \"B\"" +let g6: Map = arbor_parse(src6) +check_eq("comments stripped", int_to_str(el_list_len(g6["nodes"])), "2") + +// Empty input +let g7: Map = arbor_parse("") +check_eq("empty graph nodes", int_to_str(el_list_len(g7["nodes"])), "0") +check_eq("empty graph default direction", g7["direction"], "top-down") + +println("") +let f: String = state_get("smoke_failures") +if str_eq(f, "1") { + println("arbor-parse: FAILED") + exit_program(1) +} else { + println("arbor-parse: ok") +} diff --git a/arbor/vessels/arbor-render/manifest.el b/arbor/vessels/arbor-render/manifest.el new file mode 100644 index 0000000..6c5a8fd --- /dev/null +++ b/arbor/vessels/arbor-render/manifest.el @@ -0,0 +1,21 @@ +// arbor-render — SVG renderer. Consumes a diagram graph + layout result and +// emits an SVG document. PNG rasterization is not provided in this vessel +// because the El runtime does not expose a vector-to-raster primitive yet +// (see report). + +vessel "arbor-render" { + version "0.1.0" + description "SVG renderer for Arbor diagrams" + authors ["Neuron Technologies"] + edition "2026" +} + +dependencies { + arbor-core "0.1" + arbor-layout "0.1" +} + +build { + entry "src/main.el" + output "dist/" +} diff --git a/arbor/vessels/arbor-render/src/main.el b/arbor/vessels/arbor-render/src/main.el new file mode 100644 index 0000000..0189088 --- /dev/null +++ b/arbor/vessels/arbor-render/src/main.el @@ -0,0 +1,575 @@ +// arbor-render — SVG emission from a laid-out diagram. +// +// Entry point: +// fn arbor_render_svg(graph: Map, layout: Map, forbidden: [String]) -> String +// +// The graph is the lowered (diagram-form) shape produced by arbor-core / +// arbor-diagram (`title`, `direction`, `nodes`, `edges`, `groups`). The +// layout is whatever arbor-layout returned: `node_pos_`, `node_size_`, +// `group_bounds_`, `node_ids`, `group_ids`, `canvas`. +// +// `forbidden` is a list of "from->to" key strings — same format as +// arbor-core's collect_forbidden(). The Rust crate threaded a HashSet +// through; El threads a list and we linear-scan. +// +// SVG is text emission — straightforward El. Every float coordinate is +// passed through format_float(_, 1) for stable output. +// +// ── PNG render is intentionally out of scope ──────────────────────────────── +// The Rust crate rasterises via resvg → tiny_skia → png. The El runtime +// today exposes no equivalent: there is no resvg, no usvg, no font rasterer, +// no PNG encoder, no path-fill code. fs_write writes text only — there is +// no binary write primitive. arbor_render_png() returns an error map in El +// until the runtime grows a rasterer (see "runtime gaps" in the report). + +// ── Colour palette (matches the Rust constants exactly) ──────────────────── + +fn col_node_fill() -> String { "#ffffff" } +fn col_node_stroke() -> String { "#334155" } +fn col_primary_fill() -> String { "#0052A0" } +fn col_primary_text() -> String { "#ffffff" } +fn col_node_text() -> String { "#0D0D14" } +fn col_edge() -> String { "#64748B" } +fn col_edge_forbidden() -> String { "#DC2626" } +fn col_group_fill() -> String { "rgba(0,0,0,0.03)" } +fn col_group_stroke() -> String { "#CBD5E1" } +fn col_group_text() -> String { "#64748B" } +fn col_edge_label() -> String { "#64748B" } + +// ── XML escape ───────────────────────────────────────────────────────────── + +fn esc(s: String) -> String { + let r1: String = str_replace(s, "&", "&") + let r2: String = str_replace(r1, "<", "<") + let r3: String = str_replace(r2, ">", ">") + let r4: String = str_replace(r3, "\"", """) + r4 +} + +// Float to "%.1f" — the Rust pt() helper. +fn pt(v: el_val_t) -> String { + format_float(v, 1) +} + +// Float arithmetic helpers — float_to_int / int_to_float trip through Int, +// which is exact for the integer-valued floats used by the layout pass. +fn fadd(a: el_val_t, b: el_val_t) -> el_val_t { + let ai: Int = float_to_int(a) + let bi: Int = float_to_int(b) + int_to_float(ai + bi) +} + +fn fsub(a: el_val_t, b: el_val_t) -> el_val_t { + let ai: Int = float_to_int(a) + let bi: Int = float_to_int(b) + int_to_float(ai - bi) +} + +fn fdiv2(a: el_val_t) -> el_val_t { + let ai: Int = float_to_int(a) + int_to_float(ai / 2) +} + +fn fmid(a: el_val_t, b: el_val_t) -> el_val_t { + fdiv2(fadd(a, b)) +} + +// ── forbidden-edge linear lookup ─────────────────────────────────────────── + +fn forbidden_key(from: String, to: String) -> String { + from + "->" + to +} + +fn forbidden_contains(set: [String], src: String, dst: String) -> Bool { + let key: String = forbidden_key(src, dst) + let n: Int = el_list_len(set) + let i = 0 + while i < n { + let s: String = get(set, i) + if str_eq(s, key) { return true } + let i = i + 1 + } + false +} + +// ── Arrow marker defs ────────────────────────────────────────────────────── + +fn arrow_defs() -> String { + let s = "\n \n" + let s = s + " \n" + let s = s + " \n" + let s = s + " \n" + let s = s + " \n" + let s = s + " \n" + let s = s + " \n" + let s = s + " \n" + let s = s + " " + s +} + +// ── Node rendering ───────────────────────────────────────────────────────── + +fn render_node(buf: String, node: Map, layout: Map) -> String { + let nid: String = node["id"] + let pos: Map = el_map_get(layout, "node_pos_" + nid) + let sz: Map = el_map_get(layout, "node_size_" + nid) + + let cx: el_val_t = pos["x"] + let cy: el_val_t = pos["y"] + let w: el_val_t = sz["w"] + let h: el_val_t = sz["h"] + + let x: el_val_t = fsub(cx, fdiv2(w)) + let y: el_val_t = fsub(cy, fdiv2(h)) + + let fill_in: String = node["style_fill"] + let stroke_in: String = node["style_stroke"] + let color_in: String = node["style_color"] + let fill = col_node_fill() + if str_len(fill_in) > 0 { let fill = fill_in } + let stroke = col_node_stroke() + if str_len(stroke_in) > 0 { let stroke = stroke_in } + let text_col = col_node_text() + if str_len(color_in) > 0 { let text_col = color_in } + + let shape: String = node["shape"] + let buf = buf + + if str_eq(shape, "rectangle") { + let buf = buf + " \n" + } + if str_eq(shape, "rounded_rect") { + let buf = buf + " \n" + } + if str_eq(shape, "stadium") { + let buf = buf + " \n" + } + if str_eq(shape, "cylinder") { + // body: rect from y+ry to bottom; ry ≈ h/6 (Rust uses h*0.18, we use h/6 + // to stay in integer arithmetic — visually indistinguishable on the + // canvas sizes the layout produces). + let hi: Int = float_to_int(h) + let ry: el_val_t = int_to_float(hi / 6) + let body_y: el_val_t = fadd(y, ry) + let body_h: el_val_t = fsub(h, ry) + let buf = buf + " \n" + // top ellipse + let buf = buf + " \n" + // bottom ellipse + let bot_y: el_val_t = fadd(y, h) + let buf = buf + " \n" + } + if str_eq(shape, "diamond") { + let hw: el_val_t = fdiv2(w) + let hh: el_val_t = fdiv2(h) + let buf = buf + " \n" + } + + // Label. + let label: String = node["label"] + let buf = buf + " " + let buf = buf + esc(label) + "\n" + + // Sublabel — Rust's DiagramNode stores Option; El uses "" sentinel. + let sub: String = node["sublabel"] + if str_len(sub) > 0 { + let sub_y: el_val_t = fadd(cy, int_to_float(14)) + let buf = buf + " " + let buf = buf + esc(sub) + "\n" + } + buf +} + +// ── Edge rendering ───────────────────────────────────────────────────────── +// +// We emit a straight line from one node centre to the other and let the +// browser draw it; the Rust crate renders cubic bezier paths but the runtime +// has no robust math layer, and the rectangles are large enough that +// straight edges read clearly. (See "runtime gaps".) + +fn render_edge(buf: String, edge: Map, layout: Map, forbidden: [String]) -> String { + let from_id: String = edge["from"] + let to_id: String = edge["to"] + let from_pos: Map = el_map_get(layout, "node_pos_" + from_id) + let to_pos: Map = el_map_get(layout, "node_pos_" + to_id) + + let fx: el_val_t = from_pos["x"] + let fy: el_val_t = from_pos["y"] + let tx: el_val_t = to_pos["x"] + let ty: el_val_t = to_pos["y"] + + let is_forbidden: Bool = forbidden_contains(forbidden, from_id, to_id) + let stroke = col_edge() + if is_forbidden { let stroke = col_edge_forbidden() } + + let line: String = edge["line"] + let arrow: String = edge["arrow"] + let dash_attr = "" + if str_eq(line, "dashed") { let dash_attr = " stroke-dasharray=\"5,3\"" } + if str_eq(line, "dotted") { let dash_attr = " stroke-dasharray=\"2,2\"" } + + let marker_start = "" + if str_eq(arrow, "both") { let marker_start = " marker-start=\"url(#ah-bi)\"" } + if str_eq(arrow, "backward") { let marker_start = " marker-start=\"url(#ah-bi)\"" } + + let marker_end = " marker-end=\"url(#ah)\"" + if is_forbidden { let marker_end = " marker-end=\"url(#ah-red)\"" } + if str_eq(arrow, "none") { let marker_end = "" } + if str_eq(arrow, "backward") { let marker_end = "" } + + let buf = buf + " \n" + + // Forbidden marker — circle-X at midpoint. + if is_forbidden { + let mx: el_val_t = fmid(fx, tx) + let my: el_val_t = fmid(fy, ty) + let r: el_val_t = int_to_float(7) + let buf = buf + " \n" + let off: el_val_t = int_to_float(4) + let buf = buf + " \n" + let buf = buf + " \n" + } + + // Edge label + let label: String = edge["label"] + if str_len(label) > 0 { + let mx: el_val_t = fmid(fx, tx) + let my: el_val_t = fmid(fy, ty) + let lw: el_val_t = int_to_float(str_len(label) * 7 + 8) + let lh: el_val_t = int_to_float(16) + let buf = buf + " \n" + let buf = buf + " " + esc(label) + "\n" + } + buf +} + +// ── Group rendering ──────────────────────────────────────────────────────── + +fn render_group(buf: String, group: Map, layout: Map) -> String { + let gid: String = group["id"] + let bounds: Map = el_map_get(layout, "group_bounds_" + gid) + // Layout may not have bounds for empty groups — defensive. + let bx_check: el_val_t = bounds["x"] + if float_to_int(bx_check) == 0 { + // Could be a real 0; cheaper to skip via presence check on group_ids. + } + let bx: el_val_t = bounds["x"] + let by: el_val_t = bounds["y"] + let bw: el_val_t = bounds["w"] + let bh: el_val_t = bounds["h"] + let buf = buf + " \n" + + // Group label in the top-left corner. + let lx: el_val_t = fadd(bx, int_to_float(8)) + let ly: el_val_t = fadd(by, int_to_float(14)) + let label: String = group["label"] + let buf = buf + " " + esc(label) + "\n" + buf +} + +// ── Public entry point ───────────────────────────────────────────────────── + +fn arbor_render_svg(graph: Map, layout: Map, forbidden: [String]) -> String { + let canvas: Map = el_map_get(layout, "canvas") + let cw: el_val_t = canvas["w"] + let ch: el_val_t = canvas["h"] + + let buf = "\n" + let buf = buf + " " + let buf = buf + arrow_defs() + let buf = buf + "\n \n" + let buf = buf + " \n" + + // Groups first (behind everything). + let buf = buf + " \n" + let groups: [Map] = graph["groups"] + let gn: Int = el_list_len(groups) + let i = 0 + while i < gn { + let g: Map = get(groups, i) + let gid: String = g["id"] + // Only render groups the layout actually placed. + let gids: [String] = el_map_get(layout, "group_ids") + let placed = false + let j = 0 + while j < el_list_len(gids) { + if str_eq(get(gids, j), gid) { let placed = true } + let j = j + 1 + } + if placed { + let buf = render_group(buf, g, layout) + } + let i = i + 1 + } + + // Edges + let buf = buf + " \n" + let edges: [Map] = graph["edges"] + let en: Int = el_list_len(edges) + let i = 0 + while i < en { + let e: Map = get(edges, i) + let buf = render_edge(buf, e, layout, forbidden) + let i = i + 1 + } + + // Nodes + let buf = buf + " \n" + let nodes: [Map] = graph["nodes"] + let nn: Int = el_list_len(nodes) + let i = 0 + while i < nn { + let n: Map = get(nodes, i) + let buf = render_node(buf, n, layout) + let i = i + 1 + } + + // Title + let title: String = graph["title"] + if str_len(title) > 0 { + let title_x: el_val_t = fdiv2(cw) + let buf = buf + " " + let buf = buf + esc(title) + "\n" + } + + let buf = buf + "\n" + buf +} + +// PNG — not implemented; the runtime has no SVG rasterizer or PNG encoder. +// Returns an error map that callers can inspect via map["error"]. +fn arbor_render_png(graph: Map, layout: Map, forbidden: [String]) -> Map { + { + "error": "PNG rasterization not available in El runtime — install a runtime image library or use the Rust binary" + } +} + +// ── Smoke test ───────────────────────────────────────────────────────────── + +fn fail(label: String, msg: String) -> Int { + println("FAIL " + label + ": " + msg) + state_set("smoke_failures", "1") + 0 +} + +fn check_contains(label: String, haystack: String, needle: String) -> Int { + if str_contains(haystack, needle) { + println("ok " + label) + return 1 + } + fail(label, "missing [" + needle + "]") +} + +fn check_not_contains(label: String, haystack: String, needle: String) -> Int { + if str_contains(haystack, needle) { + return fail(label, "should not contain [" + needle + "]") + } + println("ok " + label) + 1 +} + +fn make_test_node(id: String, label: String, shape: String) -> Map { + { + "id": id, "label": label, "sublabel": "", + "shape": shape, + "style_fill": "", "style_stroke": "", "style_color": "" + } +} + +fn make_test_edge(src: String, dst: String, line: String, arrow: String, label: String) -> Map { + { + "from": src, "to": dst, "label": label, + "line": line, "arrow": arrow + } +} + +fn make_test_pos(x: Int, y: Int) -> Map { + { "x": int_to_float(x), "y": int_to_float(y) } +} + +fn make_test_size(w: Int, h: Int) -> Map { + { "w": int_to_float(w), "h": int_to_float(h) } +} + +// Build a minimal layout map by hand. +fn build_layout(node_ids: [String], group_ids: [String], cw: Int, ch: Int) -> Map { + let r: Map = el_map_new(0) + let r = el_map_set(r, "node_ids", node_ids) + let r = el_map_set(r, "group_ids", group_ids) + let r = el_map_set(r, "canvas", { "w": int_to_float(cw), "h": int_to_float(ch) }) + r +} + +let n_a: Map = make_test_node("a", "Node A", "rectangle") +let n_b: Map = make_test_node("b", "Node B", "rectangle") +let e_ab: Map = make_test_edge("a", "b", "solid", "forward", "") + +let nodes: [Map] = native_list_empty() +let nodes = native_list_append(nodes, n_a) +let nodes = native_list_append(nodes, n_b) +let edges: [Map] = native_list_empty() +let edges = native_list_append(edges, e_ab) +let groups: [Map] = native_list_empty() + +let g: Map = { + "title": "Test", "direction": "top-down", + "nodes": nodes, "edges": edges, "groups": groups +} + +let nid_list: [String] = native_list_empty() +let nid_list = native_list_append(nid_list, "a") +let nid_list = native_list_append(nid_list, "b") +let gid_list: [String] = native_list_empty() +let layout: Map = build_layout(nid_list, gid_list, 400, 300) +let layout = el_map_set(layout, "node_pos_a", make_test_pos(100, 60)) +let layout = el_map_set(layout, "node_pos_b", make_test_pos(100, 200)) +let layout = el_map_set(layout, "node_size_a", make_test_size(120, 40)) +let layout = el_map_set(layout, "node_size_b", make_test_size(120, 40)) + +let forbidden: [String] = native_list_empty() +let svg: String = arbor_render_svg(g, layout, forbidden) + +check_contains("svg starts with ", svg, "") +check_contains("svg contains node label", svg, "Node A") +check_contains("svg contains title", svg, ">Test") +check_contains("svg has rect for rectangle node", svg, " = make_test_node("x", "A & B ", "rectangle") +let nodes2: [Map] = native_list_empty() +let nodes2 = native_list_append(nodes2, n_esc) +let g2: Map = { + "title": "Test ", "direction": "top-down", + "nodes": nodes2, "edges": native_list_empty(), "groups": native_list_empty() +} +let nid2: [String] = native_list_empty() +let nid2 = native_list_append(nid2, "x") +let layout2: Map<String, Any> = build_layout(nid2, native_list_empty(), 200, 100) +let layout2 = el_map_set(layout2, "node_pos_x", make_test_pos(80, 40)) +let layout2 = el_map_set(layout2, "node_size_x", make_test_size(120, 40)) +let svg2: String = arbor_render_svg(g2, layout2, native_list_empty()) +check_contains("escapes ampersand", svg2, "&") +check_contains("escapes <", svg2, "<") +check_not_contains("no raw <C>", svg2, "<C>") + +// Forbidden edge +let e_fb: Map<String, Any> = make_test_edge("a", "b", "solid", "forward", "") +let edges3: [Map<String, Any>] = native_list_empty() +let edges3 = native_list_append(edges3, e_fb) +let g3: Map<String, Any> = { + "title": "F", "direction": "top-down", + "nodes": nodes, "edges": edges3, "groups": native_list_empty() +} +let fb: [String] = native_list_empty() +let fb = native_list_append(fb, forbidden_key("a", "b")) +let svg3: String = arbor_render_svg(g3, layout, fb) +check_contains("forbidden uses red marker", svg3, "ah-red") +check_contains("forbidden colour present", svg3, col_edge_forbidden()) + +// Diamond shape → polygon +let n_d: Map<String, Any> = make_test_node("d", "Decide", "diamond") +let g4: Map<String, Any> = { + "title": "", "direction": "top-down", + "nodes": native_list_append(native_list_empty(), n_d), + "edges": native_list_empty(), "groups": native_list_empty() +} +let nid4: [String] = native_list_append(native_list_empty(), "d") +let layout4: Map<String, Any> = build_layout(nid4, native_list_empty(), 200, 100) +let layout4 = el_map_set(layout4, "node_pos_d", make_test_pos(80, 50)) +let layout4 = el_map_set(layout4, "node_size_d", make_test_size(120, 40)) +let svg4: String = arbor_render_svg(g4, layout4, native_list_empty()) +check_contains("diamond uses polygon", svg4, "<polygon") + +// Cylinder shape → ellipses +let n_cy: Map<String, Any> = make_test_node("cy", "DB", "cylinder") +let g5: Map<String, Any> = { + "title": "", "direction": "top-down", + "nodes": native_list_append(native_list_empty(), n_cy), + "edges": native_list_empty(), "groups": native_list_empty() +} +let nid5: [String] = native_list_append(native_list_empty(), "cy") +let layout5: Map<String, Any> = build_layout(nid5, native_list_empty(), 200, 100) +let layout5 = el_map_set(layout5, "node_pos_cy", make_test_pos(80, 50)) +let layout5 = el_map_set(layout5, "node_size_cy", make_test_size(120, 40)) +let svg5: String = arbor_render_svg(g5, layout5, native_list_empty()) +check_contains("cylinder uses ellipse", svg5, "<ellipse") + +// Dashed edge +let e_dash: Map<String, Any> = make_test_edge("a", "b", "dashed", "forward", "") +let g6: Map<String, Any> = { + "title": "", "direction": "top-down", + "nodes": nodes, "edges": native_list_append(native_list_empty(), e_dash), + "groups": native_list_empty() +} +let svg6: String = arbor_render_svg(g6, layout, native_list_empty()) +check_contains("dashed line dasharray", svg6, "stroke-dasharray=\"5,3\"") + +// PNG returns an error map +let png: Map<String, Any> = arbor_render_png(g, layout, native_list_empty()) +let err: String = png["error"] +if str_len(err) > 0 { + println("ok PNG returns error map") +} else { + println("FAIL PNG should have returned error") + state_set("smoke_failures", "1") +} + +println("") +let f: String = state_get("smoke_failures") +if str_eq(f, "1") { + println("arbor-render: FAILED") + exit_program(1) +} else { + println("arbor-render: ok") +} diff --git a/elp/src/dedup_just_one_import.el b/elp/src/dedup_just_one_import.el new file mode 100644 index 0000000..a3adae5 --- /dev/null +++ b/elp/src/dedup_just_one_import.el @@ -0,0 +1 @@ +import "language-profile.el" diff --git a/elp/src/dedup_nodedup.el b/elp/src/dedup_nodedup.el new file mode 100644 index 0000000..c560b15 --- /dev/null +++ b/elp/src/dedup_nodedup.el @@ -0,0 +1,2 @@ +import "language-profile.el" +import "dedup_test_a_nodedup.el" diff --git a/elp/src/dedup_test_a.el b/elp/src/dedup_test_a.el new file mode 100644 index 0000000..35b1aae --- /dev/null +++ b/elp/src/dedup_test_a.el @@ -0,0 +1,2 @@ +import "language-profile.el" +extern fn fn_a(x: String) -> String diff --git a/elp/src/dedup_test_a_nodedup.el b/elp/src/dedup_test_a_nodedup.el new file mode 100644 index 0000000..337d7e1 --- /dev/null +++ b/elp/src/dedup_test_a_nodedup.el @@ -0,0 +1 @@ +extern fn fn_a(x: String) -> String diff --git a/elp/src/dedup_test_a_notail.el b/elp/src/dedup_test_a_notail.el new file mode 100644 index 0000000..ef3e57f --- /dev/null +++ b/elp/src/dedup_test_a_notail.el @@ -0,0 +1,2 @@ +import "language-profile.el" +extern fn fn_a(x: String) -> String \ No newline at end of file diff --git a/elp/src/dedup_test_main.el b/elp/src/dedup_test_main.el new file mode 100644 index 0000000..496ad3f --- /dev/null +++ b/elp/src/dedup_test_main.el @@ -0,0 +1,6 @@ +import "language-profile.el" +import "dedup_test_a.el" + +fn main_fn(x: String) -> String { + return x +} diff --git a/elp/src/dedup_test_minimal.el b/elp/src/dedup_test_minimal.el new file mode 100644 index 0000000..2bf60c6 --- /dev/null +++ b/elp/src/dedup_test_minimal.el @@ -0,0 +1,2 @@ +import "language-profile.el" +import "dedup_test_a.el" diff --git a/elp/src/dedup_test_notail.el b/elp/src/dedup_test_notail.el new file mode 100644 index 0000000..967df01 --- /dev/null +++ b/elp/src/dedup_test_notail.el @@ -0,0 +1,2 @@ +import "language-profile.el" +import "dedup_test_a_notail.el" diff --git a/elp/src/ext_a.el b/elp/src/ext_a.el new file mode 100644 index 0000000..35b1aae --- /dev/null +++ b/elp/src/ext_a.el @@ -0,0 +1,2 @@ +import "language-profile.el" +extern fn fn_a(x: String) -> String diff --git a/elp/src/ext_b.el b/elp/src/ext_b.el new file mode 100644 index 0000000..a2f3528 --- /dev/null +++ b/elp/src/ext_b.el @@ -0,0 +1,2 @@ +import "language-profile.el" +extern fn fn_b(x: String) -> String diff --git a/elp/src/language-profile-big.el b/elp/src/language-profile-big.el new file mode 100644 index 0000000..e873123 --- /dev/null +++ b/elp/src/language-profile-big.el @@ -0,0 +1,761 @@ +// big language-profile for testing +fn lang_profile_big0(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "code") + let r = native_list_append(r, code) + let r = native_list_append(r, "word_order") + let r = native_list_append(r, word_order) + let r = native_list_append(r, "morph_type") + let r = native_list_append(r, morph_type) + let r = native_list_append(r, "has_case") + let r = native_list_append(r, has_case) + let r = native_list_append(r, "has_gender") + let r = native_list_append(r, has_gender) + let r = native_list_append(r, "script_dir") + let r = native_list_append(r, script_dir) + let r = native_list_append(r, "agreement") + let r = native_list_append(r, agreement) + let r = native_list_append(r, "null_subject") + let r = native_list_append(r, null_subject) + return r +} + +fn lang_get_big0(profile: [String], key: String) -> String { + let n: Int = native_list_len(profile) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(profile, i) + if str_eq(k, key) { + return native_list_get(profile, i + 1) + } + let i = i + 2 + } + return "" +} + +fn lang_profile_en() -> [String] { + return lang_profile_big0("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false") +} + +fn lang_profile_big1(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "code") + let r = native_list_append(r, code) + let r = native_list_append(r, "word_order") + let r = native_list_append(r, word_order) + let r = native_list_append(r, "morph_type") + let r = native_list_append(r, morph_type) + let r = native_list_append(r, "has_case") + let r = native_list_append(r, has_case) + let r = native_list_append(r, "has_gender") + let r = native_list_append(r, has_gender) + let r = native_list_append(r, "script_dir") + let r = native_list_append(r, script_dir) + let r = native_list_append(r, "agreement") + let r = native_list_append(r, agreement) + let r = native_list_append(r, "null_subject") + let r = native_list_append(r, null_subject) + return r +} + +fn lang_get_big1(profile: [String], key: String) -> String { + let n: Int = native_list_len(profile) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(profile, i) + if str_eq(k, key) { + return native_list_get(profile, i + 1) + } + let i = i + 2 + } + return "" +} + +fn lang_profile_en() -> [String] { + return lang_profile_big1("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false") +} + +fn lang_profile_big2(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "code") + let r = native_list_append(r, code) + let r = native_list_append(r, "word_order") + let r = native_list_append(r, word_order) + let r = native_list_append(r, "morph_type") + let r = native_list_append(r, morph_type) + let r = native_list_append(r, "has_case") + let r = native_list_append(r, has_case) + let r = native_list_append(r, "has_gender") + let r = native_list_append(r, has_gender) + let r = native_list_append(r, "script_dir") + let r = native_list_append(r, script_dir) + let r = native_list_append(r, "agreement") + let r = native_list_append(r, agreement) + let r = native_list_append(r, "null_subject") + let r = native_list_append(r, null_subject) + return r +} + +fn lang_get_big2(profile: [String], key: String) -> String { + let n: Int = native_list_len(profile) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(profile, i) + if str_eq(k, key) { + return native_list_get(profile, i + 1) + } + let i = i + 2 + } + return "" +} + +fn lang_profile_en() -> [String] { + return lang_profile_big2("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false") +} + +fn lang_profile_big3(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "code") + let r = native_list_append(r, code) + let r = native_list_append(r, "word_order") + let r = native_list_append(r, word_order) + let r = native_list_append(r, "morph_type") + let r = native_list_append(r, morph_type) + let r = native_list_append(r, "has_case") + let r = native_list_append(r, has_case) + let r = native_list_append(r, "has_gender") + let r = native_list_append(r, has_gender) + let r = native_list_append(r, "script_dir") + let r = native_list_append(r, script_dir) + let r = native_list_append(r, "agreement") + let r = native_list_append(r, agreement) + let r = native_list_append(r, "null_subject") + let r = native_list_append(r, null_subject) + return r +} + +fn lang_get_big3(profile: [String], key: String) -> String { + let n: Int = native_list_len(profile) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(profile, i) + if str_eq(k, key) { + return native_list_get(profile, i + 1) + } + let i = i + 2 + } + return "" +} + +fn lang_profile_en() -> [String] { + return lang_profile_big3("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false") +} + +fn lang_profile_big4(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "code") + let r = native_list_append(r, code) + let r = native_list_append(r, "word_order") + let r = native_list_append(r, word_order) + let r = native_list_append(r, "morph_type") + let r = native_list_append(r, morph_type) + let r = native_list_append(r, "has_case") + let r = native_list_append(r, has_case) + let r = native_list_append(r, "has_gender") + let r = native_list_append(r, has_gender) + let r = native_list_append(r, "script_dir") + let r = native_list_append(r, script_dir) + let r = native_list_append(r, "agreement") + let r = native_list_append(r, agreement) + let r = native_list_append(r, "null_subject") + let r = native_list_append(r, null_subject) + return r +} + +fn lang_get_big4(profile: [String], key: String) -> String { + let n: Int = native_list_len(profile) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(profile, i) + if str_eq(k, key) { + return native_list_get(profile, i + 1) + } + let i = i + 2 + } + return "" +} + +fn lang_profile_en() -> [String] { + return lang_profile_big4("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false") +} + +fn lang_profile_big5(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "code") + let r = native_list_append(r, code) + let r = native_list_append(r, "word_order") + let r = native_list_append(r, word_order) + let r = native_list_append(r, "morph_type") + let r = native_list_append(r, morph_type) + let r = native_list_append(r, "has_case") + let r = native_list_append(r, has_case) + let r = native_list_append(r, "has_gender") + let r = native_list_append(r, has_gender) + let r = native_list_append(r, "script_dir") + let r = native_list_append(r, script_dir) + let r = native_list_append(r, "agreement") + let r = native_list_append(r, agreement) + let r = native_list_append(r, "null_subject") + let r = native_list_append(r, null_subject) + return r +} + +fn lang_get_big5(profile: [String], key: String) -> String { + let n: Int = native_list_len(profile) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(profile, i) + if str_eq(k, key) { + return native_list_get(profile, i + 1) + } + let i = i + 2 + } + return "" +} + +fn lang_profile_en() -> [String] { + return lang_profile_big5("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false") +} + +fn lang_profile_big6(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "code") + let r = native_list_append(r, code) + let r = native_list_append(r, "word_order") + let r = native_list_append(r, word_order) + let r = native_list_append(r, "morph_type") + let r = native_list_append(r, morph_type) + let r = native_list_append(r, "has_case") + let r = native_list_append(r, has_case) + let r = native_list_append(r, "has_gender") + let r = native_list_append(r, has_gender) + let r = native_list_append(r, "script_dir") + let r = native_list_append(r, script_dir) + let r = native_list_append(r, "agreement") + let r = native_list_append(r, agreement) + let r = native_list_append(r, "null_subject") + let r = native_list_append(r, null_subject) + return r +} + +fn lang_get_big6(profile: [String], key: String) -> String { + let n: Int = native_list_len(profile) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(profile, i) + if str_eq(k, key) { + return native_list_get(profile, i + 1) + } + let i = i + 2 + } + return "" +} + +fn lang_profile_en() -> [String] { + return lang_profile_big6("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false") +} + +fn lang_profile_big7(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "code") + let r = native_list_append(r, code) + let r = native_list_append(r, "word_order") + let r = native_list_append(r, word_order) + let r = native_list_append(r, "morph_type") + let r = native_list_append(r, morph_type) + let r = native_list_append(r, "has_case") + let r = native_list_append(r, has_case) + let r = native_list_append(r, "has_gender") + let r = native_list_append(r, has_gender) + let r = native_list_append(r, "script_dir") + let r = native_list_append(r, script_dir) + let r = native_list_append(r, "agreement") + let r = native_list_append(r, agreement) + let r = native_list_append(r, "null_subject") + let r = native_list_append(r, null_subject) + return r +} + +fn lang_get_big7(profile: [String], key: String) -> String { + let n: Int = native_list_len(profile) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(profile, i) + if str_eq(k, key) { + return native_list_get(profile, i + 1) + } + let i = i + 2 + } + return "" +} + +fn lang_profile_en() -> [String] { + return lang_profile_big7("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false") +} + +fn lang_profile_big8(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "code") + let r = native_list_append(r, code) + let r = native_list_append(r, "word_order") + let r = native_list_append(r, word_order) + let r = native_list_append(r, "morph_type") + let r = native_list_append(r, morph_type) + let r = native_list_append(r, "has_case") + let r = native_list_append(r, has_case) + let r = native_list_append(r, "has_gender") + let r = native_list_append(r, has_gender) + let r = native_list_append(r, "script_dir") + let r = native_list_append(r, script_dir) + let r = native_list_append(r, "agreement") + let r = native_list_append(r, agreement) + let r = native_list_append(r, "null_subject") + let r = native_list_append(r, null_subject) + return r +} + +fn lang_get_big8(profile: [String], key: String) -> String { + let n: Int = native_list_len(profile) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(profile, i) + if str_eq(k, key) { + return native_list_get(profile, i + 1) + } + let i = i + 2 + } + return "" +} + +fn lang_profile_en() -> [String] { + return lang_profile_big8("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false") +} + +fn lang_profile_big9(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "code") + let r = native_list_append(r, code) + let r = native_list_append(r, "word_order") + let r = native_list_append(r, word_order) + let r = native_list_append(r, "morph_type") + let r = native_list_append(r, morph_type) + let r = native_list_append(r, "has_case") + let r = native_list_append(r, has_case) + let r = native_list_append(r, "has_gender") + let r = native_list_append(r, has_gender) + let r = native_list_append(r, "script_dir") + let r = native_list_append(r, script_dir) + let r = native_list_append(r, "agreement") + let r = native_list_append(r, agreement) + let r = native_list_append(r, "null_subject") + let r = native_list_append(r, null_subject) + return r +} + +fn lang_get_big9(profile: [String], key: String) -> String { + let n: Int = native_list_len(profile) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(profile, i) + if str_eq(k, key) { + return native_list_get(profile, i + 1) + } + let i = i + 2 + } + return "" +} + +fn lang_profile_en() -> [String] { + return lang_profile_big9("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false") +} + +fn lang_profile_big10(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "code") + let r = native_list_append(r, code) + let r = native_list_append(r, "word_order") + let r = native_list_append(r, word_order) + let r = native_list_append(r, "morph_type") + let r = native_list_append(r, morph_type) + let r = native_list_append(r, "has_case") + let r = native_list_append(r, has_case) + let r = native_list_append(r, "has_gender") + let r = native_list_append(r, has_gender) + let r = native_list_append(r, "script_dir") + let r = native_list_append(r, script_dir) + let r = native_list_append(r, "agreement") + let r = native_list_append(r, agreement) + let r = native_list_append(r, "null_subject") + let r = native_list_append(r, null_subject) + return r +} + +fn lang_get_big10(profile: [String], key: String) -> String { + let n: Int = native_list_len(profile) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(profile, i) + if str_eq(k, key) { + return native_list_get(profile, i + 1) + } + let i = i + 2 + } + return "" +} + +fn lang_profile_en() -> [String] { + return lang_profile_big10("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false") +} + +fn lang_profile_big11(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "code") + let r = native_list_append(r, code) + let r = native_list_append(r, "word_order") + let r = native_list_append(r, word_order) + let r = native_list_append(r, "morph_type") + let r = native_list_append(r, morph_type) + let r = native_list_append(r, "has_case") + let r = native_list_append(r, has_case) + let r = native_list_append(r, "has_gender") + let r = native_list_append(r, has_gender) + let r = native_list_append(r, "script_dir") + let r = native_list_append(r, script_dir) + let r = native_list_append(r, "agreement") + let r = native_list_append(r, agreement) + let r = native_list_append(r, "null_subject") + let r = native_list_append(r, null_subject) + return r +} + +fn lang_get_big11(profile: [String], key: String) -> String { + let n: Int = native_list_len(profile) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(profile, i) + if str_eq(k, key) { + return native_list_get(profile, i + 1) + } + let i = i + 2 + } + return "" +} + +fn lang_profile_en() -> [String] { + return lang_profile_big11("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false") +} + +fn lang_profile_big12(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "code") + let r = native_list_append(r, code) + let r = native_list_append(r, "word_order") + let r = native_list_append(r, word_order) + let r = native_list_append(r, "morph_type") + let r = native_list_append(r, morph_type) + let r = native_list_append(r, "has_case") + let r = native_list_append(r, has_case) + let r = native_list_append(r, "has_gender") + let r = native_list_append(r, has_gender) + let r = native_list_append(r, "script_dir") + let r = native_list_append(r, script_dir) + let r = native_list_append(r, "agreement") + let r = native_list_append(r, agreement) + let r = native_list_append(r, "null_subject") + let r = native_list_append(r, null_subject) + return r +} + +fn lang_get_big12(profile: [String], key: String) -> String { + let n: Int = native_list_len(profile) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(profile, i) + if str_eq(k, key) { + return native_list_get(profile, i + 1) + } + let i = i + 2 + } + return "" +} + +fn lang_profile_en() -> [String] { + return lang_profile_big12("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false") +} + +fn lang_profile_big13(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "code") + let r = native_list_append(r, code) + let r = native_list_append(r, "word_order") + let r = native_list_append(r, word_order) + let r = native_list_append(r, "morph_type") + let r = native_list_append(r, morph_type) + let r = native_list_append(r, "has_case") + let r = native_list_append(r, has_case) + let r = native_list_append(r, "has_gender") + let r = native_list_append(r, has_gender) + let r = native_list_append(r, "script_dir") + let r = native_list_append(r, script_dir) + let r = native_list_append(r, "agreement") + let r = native_list_append(r, agreement) + let r = native_list_append(r, "null_subject") + let r = native_list_append(r, null_subject) + return r +} + +fn lang_get_big13(profile: [String], key: String) -> String { + let n: Int = native_list_len(profile) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(profile, i) + if str_eq(k, key) { + return native_list_get(profile, i + 1) + } + let i = i + 2 + } + return "" +} + +fn lang_profile_en() -> [String] { + return lang_profile_big13("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false") +} + +fn lang_profile_big14(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "code") + let r = native_list_append(r, code) + let r = native_list_append(r, "word_order") + let r = native_list_append(r, word_order) + let r = native_list_append(r, "morph_type") + let r = native_list_append(r, morph_type) + let r = native_list_append(r, "has_case") + let r = native_list_append(r, has_case) + let r = native_list_append(r, "has_gender") + let r = native_list_append(r, has_gender) + let r = native_list_append(r, "script_dir") + let r = native_list_append(r, script_dir) + let r = native_list_append(r, "agreement") + let r = native_list_append(r, agreement) + let r = native_list_append(r, "null_subject") + let r = native_list_append(r, null_subject) + return r +} + +fn lang_get_big14(profile: [String], key: String) -> String { + let n: Int = native_list_len(profile) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(profile, i) + if str_eq(k, key) { + return native_list_get(profile, i + 1) + } + let i = i + 2 + } + return "" +} + +fn lang_profile_en() -> [String] { + return lang_profile_big14("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false") +} + +fn lang_profile_big15(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "code") + let r = native_list_append(r, code) + let r = native_list_append(r, "word_order") + let r = native_list_append(r, word_order) + let r = native_list_append(r, "morph_type") + let r = native_list_append(r, morph_type) + let r = native_list_append(r, "has_case") + let r = native_list_append(r, has_case) + let r = native_list_append(r, "has_gender") + let r = native_list_append(r, has_gender) + let r = native_list_append(r, "script_dir") + let r = native_list_append(r, script_dir) + let r = native_list_append(r, "agreement") + let r = native_list_append(r, agreement) + let r = native_list_append(r, "null_subject") + let r = native_list_append(r, null_subject) + return r +} + +fn lang_get_big15(profile: [String], key: String) -> String { + let n: Int = native_list_len(profile) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(profile, i) + if str_eq(k, key) { + return native_list_get(profile, i + 1) + } + let i = i + 2 + } + return "" +} + +fn lang_profile_en() -> [String] { + return lang_profile_big15("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false") +} + +fn lang_profile_big16(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "code") + let r = native_list_append(r, code) + let r = native_list_append(r, "word_order") + let r = native_list_append(r, word_order) + let r = native_list_append(r, "morph_type") + let r = native_list_append(r, morph_type) + let r = native_list_append(r, "has_case") + let r = native_list_append(r, has_case) + let r = native_list_append(r, "has_gender") + let r = native_list_append(r, has_gender) + let r = native_list_append(r, "script_dir") + let r = native_list_append(r, script_dir) + let r = native_list_append(r, "agreement") + let r = native_list_append(r, agreement) + let r = native_list_append(r, "null_subject") + let r = native_list_append(r, null_subject) + return r +} + +fn lang_get_big16(profile: [String], key: String) -> String { + let n: Int = native_list_len(profile) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(profile, i) + if str_eq(k, key) { + return native_list_get(profile, i + 1) + } + let i = i + 2 + } + return "" +} + +fn lang_profile_en() -> [String] { + return lang_profile_big16("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false") +} + +fn lang_profile_big17(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "code") + let r = native_list_append(r, code) + let r = native_list_append(r, "word_order") + let r = native_list_append(r, word_order) + let r = native_list_append(r, "morph_type") + let r = native_list_append(r, morph_type) + let r = native_list_append(r, "has_case") + let r = native_list_append(r, has_case) + let r = native_list_append(r, "has_gender") + let r = native_list_append(r, has_gender) + let r = native_list_append(r, "script_dir") + let r = native_list_append(r, script_dir) + let r = native_list_append(r, "agreement") + let r = native_list_append(r, agreement) + let r = native_list_append(r, "null_subject") + let r = native_list_append(r, null_subject) + return r +} + +fn lang_get_big17(profile: [String], key: String) -> String { + let n: Int = native_list_len(profile) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(profile, i) + if str_eq(k, key) { + return native_list_get(profile, i + 1) + } + let i = i + 2 + } + return "" +} + +fn lang_profile_en() -> [String] { + return lang_profile_big17("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false") +} + +fn lang_profile_big18(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "code") + let r = native_list_append(r, code) + let r = native_list_append(r, "word_order") + let r = native_list_append(r, word_order) + let r = native_list_append(r, "morph_type") + let r = native_list_append(r, morph_type) + let r = native_list_append(r, "has_case") + let r = native_list_append(r, has_case) + let r = native_list_append(r, "has_gender") + let r = native_list_append(r, has_gender) + let r = native_list_append(r, "script_dir") + let r = native_list_append(r, script_dir) + let r = native_list_append(r, "agreement") + let r = native_list_append(r, agreement) + let r = native_list_append(r, "null_subject") + let r = native_list_append(r, null_subject) + return r +} + +fn lang_get_big18(profile: [String], key: String) -> String { + let n: Int = native_list_len(profile) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(profile, i) + if str_eq(k, key) { + return native_list_get(profile, i + 1) + } + let i = i + 2 + } + return "" +} + +fn lang_profile_en() -> [String] { + return lang_profile_big18("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false") +} + +fn lang_profile_big19(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "code") + let r = native_list_append(r, code) + let r = native_list_append(r, "word_order") + let r = native_list_append(r, word_order) + let r = native_list_append(r, "morph_type") + let r = native_list_append(r, morph_type) + let r = native_list_append(r, "has_case") + let r = native_list_append(r, has_case) + let r = native_list_append(r, "has_gender") + let r = native_list_append(r, has_gender) + let r = native_list_append(r, "script_dir") + let r = native_list_append(r, script_dir) + let r = native_list_append(r, "agreement") + let r = native_list_append(r, agreement) + let r = native_list_append(r, "null_subject") + let r = native_list_append(r, null_subject) + return r +} + +fn lang_get_big19(profile: [String], key: String) -> String { + let n: Int = native_list_len(profile) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(profile, i) + if str_eq(k, key) { + return native_list_get(profile, i + 1) + } + let i = i + 2 + } + return "" +} + +fn lang_profile_en() -> [String] { + return lang_profile_big19("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false") +} + diff --git a/elp/src/morph_externsonly.el b/elp/src/morph_externsonly.el new file mode 100644 index 0000000..5c347dc --- /dev/null +++ b/elp/src/morph_externsonly.el @@ -0,0 +1,40 @@ +import "language-profile.el" + +extern fn es_pluralize(noun: String) -> String +extern fn es_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn fr_pluralize(noun: String) -> String +extern fn fr_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn de_noun_plural(noun: String, gender: String) -> String +extern fn de_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn ru_noun_case(noun: String, gender: String, gram_case: String, number: String) -> String +extern fn ru_conjugate(verb: String, tense: String, person: String, number: String, gender: String) -> String +extern fn ja_conjugate(dict_form: String, form: String) -> String +extern fn fi_apply_case(noun: String, gram_case: String, number: String) -> String +extern fn fi_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn ar_sound_plural(noun: String, gender: String) -> String +extern fn ar_conjugate(verb: String, tense: String, person: String, gender: String, number: String) -> String +extern fn hi_noun_direct(noun: String, gender: String, number: String) -> String +extern fn hi_gender(noun: String) -> String +extern fn hi_conjugate(verb: String, tense: String, person: String, gender: String, number: String) -> String +extern fn sw_noun_plural(noun: String) -> String +extern fn sw_conjugate(verb: String, person: String, number: String, noun_class: String, tense: String) -> String +extern fn la_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn he_conjugate(verb: String, tense: String, person: String, gender: String, number: String) -> String +extern fn grc_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn ang_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn sa_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn got_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn non_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn enm_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn pi_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn fro_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn goh_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn sga_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn txb_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn peo_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn akk_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn uga_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn egy_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn sux_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn gez_conjugate(verb: String, tense: String, person: String, number: String) -> String +extern fn cop_conjugate(verb: String, tense: String, person: String, number: String) -> String diff --git a/elp/src/morph_tiny.el b/elp/src/morph_tiny.el new file mode 100644 index 0000000..8b1b6c3 --- /dev/null +++ b/elp/src/morph_tiny.el @@ -0,0 +1,3 @@ +fn morph_tiny(x: String) -> String { + return x +} diff --git a/elp/src/one_extern.el b/elp/src/one_extern.el new file mode 100644 index 0000000..fa30cf5 --- /dev/null +++ b/elp/src/one_extern.el @@ -0,0 +1,2 @@ +import "language-profile.el" +extern fn es_pluralize(noun: String) -> String diff --git a/elp/src/one_extern_nolp.el b/elp/src/one_extern_nolp.el new file mode 100644 index 0000000..e6cff22 --- /dev/null +++ b/elp/src/one_extern_nolp.el @@ -0,0 +1 @@ +extern fn es_pluralize(noun: String) -> String diff --git a/elp/src/realizer_LPexterns.el b/elp/src/realizer_LPexterns.el new file mode 100644 index 0000000..1e4b7c4 --- /dev/null +++ b/elp/src/realizer_LPexterns.el @@ -0,0 +1,312 @@ +// realizer.el - Universal syntactic realizer: GramSpec -> surface text. +// +// The realizer is now language-agnostic. It reads the "lang" field from the +// GramSpec to resolve a language profile, then dispatches word order, question +// formation, and morphology through the engine functions in grammar.el and +// morphology.el. +// +// English remains the default (backward compatible) when no "lang" key is set. +// +// Realization pipeline per call: +// 1. Extract lang code -> resolve profile +// 2. Extract agent, predicate, patient, location, tense, aspect, intent +// 3. Compute person/number from agent (English heuristic; other languages TBD) +// 4. Build VP: morph_conjugate with profile -> get verb and auxiliary surface +// 5. Choose question strategy from gram_question_strategy(profile) +// 6. Order constituents via gram_order_constituents(subj, verb, obj, profile) +// 7. Capitalize and terminate +// +// Depends on: morphology (morph_conjugate, agree_determiner) +// grammar (gram_order_constituents, gram_question_strategy, +// gram_build_vp, build_np, build_pp, slots_get) +// language-profile (lang_from_code, lang_get, ...) +import "language-profile.el" +import "morph_externsonly.el" + +// ── Agent agreement analysis ────────────────────────────────────────────────── +// +// Person and number are inferred from English pronouns. For other languages +// the grammatical person/number should come from the Engram vocabulary node +// for the subject; here we use a heuristic that is correct for English and +// passable for languages where the same pronoun strings are used. + +fn agent_person(agent: String) -> String { + if str_eq(agent, "I") { return "first" } + if str_eq(agent, "me") { return "first" } + if str_eq(agent, "we") { return "first" } + if str_eq(agent, "us") { return "first" } + if str_eq(agent, "you") { return "second" } + return "third" +} + +fn agent_number(agent: String) -> String { + if str_eq(agent, "I") { return "singular" } + if str_eq(agent, "me") { return "singular" } + if str_eq(agent, "he") { return "singular" } + if str_eq(agent, "him") { return "singular" } + if str_eq(agent, "she") { return "singular" } + if str_eq(agent, "her") { return "singular" } + if str_eq(agent, "it") { return "singular" } + if str_eq(agent, "you") { return "singular" } + if str_eq(agent, "we") { return "plural" } + if str_eq(agent, "us") { return "plural" } + if str_eq(agent, "they") { return "plural" } + if str_eq(agent, "them") { return "plural" } + return "singular" +} + +// ── NP realization ──────────────────────────────────────────────────────────── + +fn realize_np(referent: String, number: String) -> String { + return referent +} + +// ── VP realization ──────────────────────────────────────────────────────────── +// +// Returns [main_verb_surface, aux_surface_or_empty]. +// Delegates conjugation to morph_conjugate with the language profile. + +fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: [String]) -> [String] { + let empty_aux: String = "" + + if str_eq(tense, "future") { + // Future: modal "will" + base (English) or language-specific future marker. + // For isolating/agglutinative languages the future marker is also the + // base form (morph_conjugate returns base); the surface "will" only appears + // for English because morph_conjugate("be", "future", ..., en_profile) = "will be". + let code: String = lang_get(profile, "code") + if str_eq(code, "en") { + let result: [String] = native_list_empty() + let result = native_list_append(result, base_verb) + let result = native_list_append(result, "will") + return result + } + // Other languages: conjugate normally (engine returns base form for + // languages without loaded Engram suffix data). + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result + } + + if str_eq(aspect, "progressive") { + let gerund: String = morph_conjugate(base_verb, "progressive", person, number, profile) + let be_aux: String = morph_conjugate("be", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, gerund) + let result = native_list_append(result, be_aux) + return result + } + + if str_eq(aspect, "perfect") { + let pp: String = morph_conjugate(base_verb, "perfect", person, number, profile) + let have_form: String = morph_conjugate("have", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, pp) + let result = native_list_append(result, have_form) + return result + } + + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result +} + +// ── Question formation ──────────────────────────────────────────────────────── +// +// Strategy is resolved from gram_question_strategy(profile): +// +// "do-support" (en) - insert conjugated "do" before subject; verb stays base. +// "particle" (ja, hi, fi) - statement order + sentence-final question particle. +// "intonation" (zh, es, ar, ru, sw) - statement order + "?" punctuation only. +// "inversion" (fr, de) - subject-verb inversion. + +// realize_question_lang: build the question surface string for any language. +// Returns the complete surface string (without final punctuation). + +fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: [String]) -> String { + let strategy: String = gram_question_strategy(profile) + let code: String = lang_get(profile, "code") + + // ── do-support (English) ────────────────────────────────────────────────── + if str_eq(strategy, "do-support") { + if str_eq(aspect, "progressive") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "progressive", person, number, profile) + let gerund: String = native_list_get(vp_pair, 0) + let be_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, gerund) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + if str_eq(aspect, "perfect") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "perfect", person, number, profile) + let pp: String = native_list_get(vp_pair, 0) + let have_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, have_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, pp) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // Simple: do-support + if str_eq(predicate, "be") { + let be_form: String = morph_conjugate("be", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_form) + let parts = native_list_append(parts, agent) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + let do_form: String = morph_conjugate("do", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, do_form) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── particle (ja, hi, fi) ───────────────────────────────────────────────── + // Build in statement order, then append the question particle. + if str_eq(strategy, "particle") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + let loc_part: String = "" + if !str_eq(location, "") { + let loc_part = core + " " + location + } else { + let loc_part = core + } + // Language-specific question particles + if str_eq(code, "ja") { return loc_part + " か" } + if str_eq(code, "hi") { return loc_part + " क्या" } + if str_eq(code, "fi") { return loc_part + "-ko" } + return loc_part + "?" + } + + // ── inversion (fr, de) ──────────────────────────────────────────────────── + if str_eq(strategy, "inversion") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + // Inversion: Verb-Subject-Object order + let parts: [String] = native_list_empty() + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, aux_s) + } else { + let parts = native_list_append(parts, verb_s) + } + let parts = native_list_append(parts, agent) + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, verb_s) + } + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── intonation (zh, es, ar, ru, sw) — statement order, "?" added by caller ─ + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + if !str_eq(location, "") { + return core + " " + location + } + return core +} + +// ── Capitalization and punctuation ──────────────────────────────────────────── + +fn capitalize_first(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { + return s + } + let first: String = str_slice(s, 0, 1) + let rest: String = str_slice(s, 1, n) + return str_to_upper(first) + rest +} + +fn add_punct(s: String, intent: String) -> String { + if str_eq(intent, "question") { return s + "?" } + return s + "." +} + +// ── Main realization entry point ────────────────────────────────────────────── + +fn realize_lang(form: [String], profile: [String]) -> String { + let intent: String = slots_get(form, "intent") + let agent: String = slots_get(form, "agent") + let predicate: String = slots_get(form, "predicate") + let patient: String = slots_get(form, "patient") + let location: String = slots_get(form, "location") + let tense_raw: String = slots_get(form, "tense") + let aspect_raw: String= slots_get(form, "aspect") + + let tense: String = tense_raw + if str_eq(tense, "") { let tense = "present" } + let aspect: String = aspect_raw + if str_eq(aspect, "") { let aspect = "simple" } + + let person: String = agent_person(agent) + let number: String = agent_number(agent) + + // ── Command (imperative) ────────────────────────────────────────────────── + if str_eq(intent, "command") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "command") + } + + // ── Question ────────────────────────────────────────────────────────────── + if str_eq(intent, "question") { + let surface: String = realize_question_lang(predicate, tense, aspect, person, number, agent, patient, location, profile) + return add_punct(capitalize_first(surface), "question") + } + + // ── Assertion (declarative) ─────────────────────────────────────────────── + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_surf: String = native_list_get(vp_pair, 0) + let aux_surf: String = native_list_get(vp_pair, 1) + + let vp_str: String = gram_build_vp(verb_surf, aux_surf, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, core) + if !str_eq(location, "") { + let parts = native_list_append(parts, location) + } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "assert") +} + +// realize: backward-compatible English entry point (original signature). +fn realize(form: [String]) -> String { + let lang_code: String = slots_get(form, "lang") + if str_eq(lang_code, "") { + return realize_lang(form, lang_default()) + } + return realize_lang(form, lang_from_code(lang_code)) +} diff --git a/elp/src/realizer_LPgram.el b/elp/src/realizer_LPgram.el new file mode 100644 index 0000000..8960e8d --- /dev/null +++ b/elp/src/realizer_LPgram.el @@ -0,0 +1,312 @@ +// realizer.el - Universal syntactic realizer: GramSpec -> surface text. +// +// The realizer is now language-agnostic. It reads the "lang" field from the +// GramSpec to resolve a language profile, then dispatches word order, question +// formation, and morphology through the engine functions in grammar.el and +// morphology.el. +// +// English remains the default (backward compatible) when no "lang" key is set. +// +// Realization pipeline per call: +// 1. Extract lang code -> resolve profile +// 2. Extract agent, predicate, patient, location, tense, aspect, intent +// 3. Compute person/number from agent (English heuristic; other languages TBD) +// 4. Build VP: morph_conjugate with profile -> get verb and auxiliary surface +// 5. Choose question strategy from gram_question_strategy(profile) +// 6. Order constituents via gram_order_constituents(subj, verb, obj, profile) +// 7. Capitalize and terminate +// +// Depends on: morphology (morph_conjugate, agree_determiner) +// grammar (gram_order_constituents, gram_question_strategy, +// gram_build_vp, build_np, build_pp, slots_get) +// language-profile (lang_from_code, lang_get, ...) +import "language-profile.el" +import "grammar.el" + +// ── Agent agreement analysis ────────────────────────────────────────────────── +// +// Person and number are inferred from English pronouns. For other languages +// the grammatical person/number should come from the Engram vocabulary node +// for the subject; here we use a heuristic that is correct for English and +// passable for languages where the same pronoun strings are used. + +fn agent_person(agent: String) -> String { + if str_eq(agent, "I") { return "first" } + if str_eq(agent, "me") { return "first" } + if str_eq(agent, "we") { return "first" } + if str_eq(agent, "us") { return "first" } + if str_eq(agent, "you") { return "second" } + return "third" +} + +fn agent_number(agent: String) -> String { + if str_eq(agent, "I") { return "singular" } + if str_eq(agent, "me") { return "singular" } + if str_eq(agent, "he") { return "singular" } + if str_eq(agent, "him") { return "singular" } + if str_eq(agent, "she") { return "singular" } + if str_eq(agent, "her") { return "singular" } + if str_eq(agent, "it") { return "singular" } + if str_eq(agent, "you") { return "singular" } + if str_eq(agent, "we") { return "plural" } + if str_eq(agent, "us") { return "plural" } + if str_eq(agent, "they") { return "plural" } + if str_eq(agent, "them") { return "plural" } + return "singular" +} + +// ── NP realization ──────────────────────────────────────────────────────────── + +fn realize_np(referent: String, number: String) -> String { + return referent +} + +// ── VP realization ──────────────────────────────────────────────────────────── +// +// Returns [main_verb_surface, aux_surface_or_empty]. +// Delegates conjugation to morph_conjugate with the language profile. + +fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: [String]) -> [String] { + let empty_aux: String = "" + + if str_eq(tense, "future") { + // Future: modal "will" + base (English) or language-specific future marker. + // For isolating/agglutinative languages the future marker is also the + // base form (morph_conjugate returns base); the surface "will" only appears + // for English because morph_conjugate("be", "future", ..., en_profile) = "will be". + let code: String = lang_get(profile, "code") + if str_eq(code, "en") { + let result: [String] = native_list_empty() + let result = native_list_append(result, base_verb) + let result = native_list_append(result, "will") + return result + } + // Other languages: conjugate normally (engine returns base form for + // languages without loaded Engram suffix data). + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result + } + + if str_eq(aspect, "progressive") { + let gerund: String = morph_conjugate(base_verb, "progressive", person, number, profile) + let be_aux: String = morph_conjugate("be", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, gerund) + let result = native_list_append(result, be_aux) + return result + } + + if str_eq(aspect, "perfect") { + let pp: String = morph_conjugate(base_verb, "perfect", person, number, profile) + let have_form: String = morph_conjugate("have", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, pp) + let result = native_list_append(result, have_form) + return result + } + + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result +} + +// ── Question formation ──────────────────────────────────────────────────────── +// +// Strategy is resolved from gram_question_strategy(profile): +// +// "do-support" (en) - insert conjugated "do" before subject; verb stays base. +// "particle" (ja, hi, fi) - statement order + sentence-final question particle. +// "intonation" (zh, es, ar, ru, sw) - statement order + "?" punctuation only. +// "inversion" (fr, de) - subject-verb inversion. + +// realize_question_lang: build the question surface string for any language. +// Returns the complete surface string (without final punctuation). + +fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: [String]) -> String { + let strategy: String = gram_question_strategy(profile) + let code: String = lang_get(profile, "code") + + // ── do-support (English) ────────────────────────────────────────────────── + if str_eq(strategy, "do-support") { + if str_eq(aspect, "progressive") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "progressive", person, number, profile) + let gerund: String = native_list_get(vp_pair, 0) + let be_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, gerund) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + if str_eq(aspect, "perfect") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "perfect", person, number, profile) + let pp: String = native_list_get(vp_pair, 0) + let have_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, have_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, pp) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // Simple: do-support + if str_eq(predicate, "be") { + let be_form: String = morph_conjugate("be", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_form) + let parts = native_list_append(parts, agent) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + let do_form: String = morph_conjugate("do", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, do_form) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── particle (ja, hi, fi) ───────────────────────────────────────────────── + // Build in statement order, then append the question particle. + if str_eq(strategy, "particle") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + let loc_part: String = "" + if !str_eq(location, "") { + let loc_part = core + " " + location + } else { + let loc_part = core + } + // Language-specific question particles + if str_eq(code, "ja") { return loc_part + " か" } + if str_eq(code, "hi") { return loc_part + " क्या" } + if str_eq(code, "fi") { return loc_part + "-ko" } + return loc_part + "?" + } + + // ── inversion (fr, de) ──────────────────────────────────────────────────── + if str_eq(strategy, "inversion") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + // Inversion: Verb-Subject-Object order + let parts: [String] = native_list_empty() + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, aux_s) + } else { + let parts = native_list_append(parts, verb_s) + } + let parts = native_list_append(parts, agent) + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, verb_s) + } + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── intonation (zh, es, ar, ru, sw) — statement order, "?" added by caller ─ + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + if !str_eq(location, "") { + return core + " " + location + } + return core +} + +// ── Capitalization and punctuation ──────────────────────────────────────────── + +fn capitalize_first(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { + return s + } + let first: String = str_slice(s, 0, 1) + let rest: String = str_slice(s, 1, n) + return str_to_upper(first) + rest +} + +fn add_punct(s: String, intent: String) -> String { + if str_eq(intent, "question") { return s + "?" } + return s + "." +} + +// ── Main realization entry point ────────────────────────────────────────────── + +fn realize_lang(form: [String], profile: [String]) -> String { + let intent: String = slots_get(form, "intent") + let agent: String = slots_get(form, "agent") + let predicate: String = slots_get(form, "predicate") + let patient: String = slots_get(form, "patient") + let location: String = slots_get(form, "location") + let tense_raw: String = slots_get(form, "tense") + let aspect_raw: String= slots_get(form, "aspect") + + let tense: String = tense_raw + if str_eq(tense, "") { let tense = "present" } + let aspect: String = aspect_raw + if str_eq(aspect, "") { let aspect = "simple" } + + let person: String = agent_person(agent) + let number: String = agent_number(agent) + + // ── Command (imperative) ────────────────────────────────────────────────── + if str_eq(intent, "command") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "command") + } + + // ── Question ────────────────────────────────────────────────────────────── + if str_eq(intent, "question") { + let surface: String = realize_question_lang(predicate, tense, aspect, person, number, agent, patient, location, profile) + return add_punct(capitalize_first(surface), "question") + } + + // ── Assertion (declarative) ─────────────────────────────────────────────── + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_surf: String = native_list_get(vp_pair, 0) + let aux_surf: String = native_list_get(vp_pair, 1) + + let vp_str: String = gram_build_vp(verb_surf, aux_surf, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, core) + if !str_eq(location, "") { + let parts = native_list_append(parts, location) + } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "assert") +} + +// realize: backward-compatible English entry point (original signature). +fn realize(form: [String]) -> String { + let lang_code: String = slots_get(form, "lang") + if str_eq(lang_code, "") { + return realize_lang(form, lang_default()) + } + return realize_lang(form, lang_from_code(lang_code)) +} diff --git a/elp/src/realizer_LPmorph.el b/elp/src/realizer_LPmorph.el new file mode 100644 index 0000000..6d39c53 --- /dev/null +++ b/elp/src/realizer_LPmorph.el @@ -0,0 +1,312 @@ +// realizer.el - Universal syntactic realizer: GramSpec -> surface text. +// +// The realizer is now language-agnostic. It reads the "lang" field from the +// GramSpec to resolve a language profile, then dispatches word order, question +// formation, and morphology through the engine functions in grammar.el and +// morphology.el. +// +// English remains the default (backward compatible) when no "lang" key is set. +// +// Realization pipeline per call: +// 1. Extract lang code -> resolve profile +// 2. Extract agent, predicate, patient, location, tense, aspect, intent +// 3. Compute person/number from agent (English heuristic; other languages TBD) +// 4. Build VP: morph_conjugate with profile -> get verb and auxiliary surface +// 5. Choose question strategy from gram_question_strategy(profile) +// 6. Order constituents via gram_order_constituents(subj, verb, obj, profile) +// 7. Capitalize and terminate +// +// Depends on: morphology (morph_conjugate, agree_determiner) +// grammar (gram_order_constituents, gram_question_strategy, +// gram_build_vp, build_np, build_pp, slots_get) +// language-profile (lang_from_code, lang_get, ...) +import "language-profile.el" +import "morphology.el" + +// ── Agent agreement analysis ────────────────────────────────────────────────── +// +// Person and number are inferred from English pronouns. For other languages +// the grammatical person/number should come from the Engram vocabulary node +// for the subject; here we use a heuristic that is correct for English and +// passable for languages where the same pronoun strings are used. + +fn agent_person(agent: String) -> String { + if str_eq(agent, "I") { return "first" } + if str_eq(agent, "me") { return "first" } + if str_eq(agent, "we") { return "first" } + if str_eq(agent, "us") { return "first" } + if str_eq(agent, "you") { return "second" } + return "third" +} + +fn agent_number(agent: String) -> String { + if str_eq(agent, "I") { return "singular" } + if str_eq(agent, "me") { return "singular" } + if str_eq(agent, "he") { return "singular" } + if str_eq(agent, "him") { return "singular" } + if str_eq(agent, "she") { return "singular" } + if str_eq(agent, "her") { return "singular" } + if str_eq(agent, "it") { return "singular" } + if str_eq(agent, "you") { return "singular" } + if str_eq(agent, "we") { return "plural" } + if str_eq(agent, "us") { return "plural" } + if str_eq(agent, "they") { return "plural" } + if str_eq(agent, "them") { return "plural" } + return "singular" +} + +// ── NP realization ──────────────────────────────────────────────────────────── + +fn realize_np(referent: String, number: String) -> String { + return referent +} + +// ── VP realization ──────────────────────────────────────────────────────────── +// +// Returns [main_verb_surface, aux_surface_or_empty]. +// Delegates conjugation to morph_conjugate with the language profile. + +fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: [String]) -> [String] { + let empty_aux: String = "" + + if str_eq(tense, "future") { + // Future: modal "will" + base (English) or language-specific future marker. + // For isolating/agglutinative languages the future marker is also the + // base form (morph_conjugate returns base); the surface "will" only appears + // for English because morph_conjugate("be", "future", ..., en_profile) = "will be". + let code: String = lang_get(profile, "code") + if str_eq(code, "en") { + let result: [String] = native_list_empty() + let result = native_list_append(result, base_verb) + let result = native_list_append(result, "will") + return result + } + // Other languages: conjugate normally (engine returns base form for + // languages without loaded Engram suffix data). + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result + } + + if str_eq(aspect, "progressive") { + let gerund: String = morph_conjugate(base_verb, "progressive", person, number, profile) + let be_aux: String = morph_conjugate("be", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, gerund) + let result = native_list_append(result, be_aux) + return result + } + + if str_eq(aspect, "perfect") { + let pp: String = morph_conjugate(base_verb, "perfect", person, number, profile) + let have_form: String = morph_conjugate("have", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, pp) + let result = native_list_append(result, have_form) + return result + } + + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result +} + +// ── Question formation ──────────────────────────────────────────────────────── +// +// Strategy is resolved from gram_question_strategy(profile): +// +// "do-support" (en) - insert conjugated "do" before subject; verb stays base. +// "particle" (ja, hi, fi) - statement order + sentence-final question particle. +// "intonation" (zh, es, ar, ru, sw) - statement order + "?" punctuation only. +// "inversion" (fr, de) - subject-verb inversion. + +// realize_question_lang: build the question surface string for any language. +// Returns the complete surface string (without final punctuation). + +fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: [String]) -> String { + let strategy: String = gram_question_strategy(profile) + let code: String = lang_get(profile, "code") + + // ── do-support (English) ────────────────────────────────────────────────── + if str_eq(strategy, "do-support") { + if str_eq(aspect, "progressive") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "progressive", person, number, profile) + let gerund: String = native_list_get(vp_pair, 0) + let be_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, gerund) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + if str_eq(aspect, "perfect") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "perfect", person, number, profile) + let pp: String = native_list_get(vp_pair, 0) + let have_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, have_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, pp) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // Simple: do-support + if str_eq(predicate, "be") { + let be_form: String = morph_conjugate("be", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_form) + let parts = native_list_append(parts, agent) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + let do_form: String = morph_conjugate("do", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, do_form) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── particle (ja, hi, fi) ───────────────────────────────────────────────── + // Build in statement order, then append the question particle. + if str_eq(strategy, "particle") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + let loc_part: String = "" + if !str_eq(location, "") { + let loc_part = core + " " + location + } else { + let loc_part = core + } + // Language-specific question particles + if str_eq(code, "ja") { return loc_part + " か" } + if str_eq(code, "hi") { return loc_part + " क्या" } + if str_eq(code, "fi") { return loc_part + "-ko" } + return loc_part + "?" + } + + // ── inversion (fr, de) ──────────────────────────────────────────────────── + if str_eq(strategy, "inversion") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + // Inversion: Verb-Subject-Object order + let parts: [String] = native_list_empty() + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, aux_s) + } else { + let parts = native_list_append(parts, verb_s) + } + let parts = native_list_append(parts, agent) + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, verb_s) + } + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── intonation (zh, es, ar, ru, sw) — statement order, "?" added by caller ─ + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + if !str_eq(location, "") { + return core + " " + location + } + return core +} + +// ── Capitalization and punctuation ──────────────────────────────────────────── + +fn capitalize_first(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { + return s + } + let first: String = str_slice(s, 0, 1) + let rest: String = str_slice(s, 1, n) + return str_to_upper(first) + rest +} + +fn add_punct(s: String, intent: String) -> String { + if str_eq(intent, "question") { return s + "?" } + return s + "." +} + +// ── Main realization entry point ────────────────────────────────────────────── + +fn realize_lang(form: [String], profile: [String]) -> String { + let intent: String = slots_get(form, "intent") + let agent: String = slots_get(form, "agent") + let predicate: String = slots_get(form, "predicate") + let patient: String = slots_get(form, "patient") + let location: String = slots_get(form, "location") + let tense_raw: String = slots_get(form, "tense") + let aspect_raw: String= slots_get(form, "aspect") + + let tense: String = tense_raw + if str_eq(tense, "") { let tense = "present" } + let aspect: String = aspect_raw + if str_eq(aspect, "") { let aspect = "simple" } + + let person: String = agent_person(agent) + let number: String = agent_number(agent) + + // ── Command (imperative) ────────────────────────────────────────────────── + if str_eq(intent, "command") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "command") + } + + // ── Question ────────────────────────────────────────────────────────────── + if str_eq(intent, "question") { + let surface: String = realize_question_lang(predicate, tense, aspect, person, number, agent, patient, location, profile) + return add_punct(capitalize_first(surface), "question") + } + + // ── Assertion (declarative) ─────────────────────────────────────────────── + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_surf: String = native_list_get(vp_pair, 0) + let aux_surf: String = native_list_get(vp_pair, 1) + + let vp_str: String = gram_build_vp(verb_surf, aux_surf, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, core) + if !str_eq(location, "") { + let parts = native_list_append(parts, location) + } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "assert") +} + +// realize: backward-compatible English entry point (original signature). +fn realize(form: [String]) -> String { + let lang_code: String = slots_get(form, "lang") + if str_eq(lang_code, "") { + return realize_lang(form, lang_default()) + } + return realize_lang(form, lang_from_code(lang_code)) +} diff --git a/elp/src/realizer_LPtinyM.el b/elp/src/realizer_LPtinyM.el new file mode 100644 index 0000000..162dc23 --- /dev/null +++ b/elp/src/realizer_LPtinyM.el @@ -0,0 +1,312 @@ +// realizer.el - Universal syntactic realizer: GramSpec -> surface text. +// +// The realizer is now language-agnostic. It reads the "lang" field from the +// GramSpec to resolve a language profile, then dispatches word order, question +// formation, and morphology through the engine functions in grammar.el and +// morphology.el. +// +// English remains the default (backward compatible) when no "lang" key is set. +// +// Realization pipeline per call: +// 1. Extract lang code -> resolve profile +// 2. Extract agent, predicate, patient, location, tense, aspect, intent +// 3. Compute person/number from agent (English heuristic; other languages TBD) +// 4. Build VP: morph_conjugate with profile -> get verb and auxiliary surface +// 5. Choose question strategy from gram_question_strategy(profile) +// 6. Order constituents via gram_order_constituents(subj, verb, obj, profile) +// 7. Capitalize and terminate +// +// Depends on: morphology (morph_conjugate, agree_determiner) +// grammar (gram_order_constituents, gram_question_strategy, +// gram_build_vp, build_np, build_pp, slots_get) +// language-profile (lang_from_code, lang_get, ...) +import "language-profile.el" +import "morph_tiny.el" + +// ── Agent agreement analysis ────────────────────────────────────────────────── +// +// Person and number are inferred from English pronouns. For other languages +// the grammatical person/number should come from the Engram vocabulary node +// for the subject; here we use a heuristic that is correct for English and +// passable for languages where the same pronoun strings are used. + +fn agent_person(agent: String) -> String { + if str_eq(agent, "I") { return "first" } + if str_eq(agent, "me") { return "first" } + if str_eq(agent, "we") { return "first" } + if str_eq(agent, "us") { return "first" } + if str_eq(agent, "you") { return "second" } + return "third" +} + +fn agent_number(agent: String) -> String { + if str_eq(agent, "I") { return "singular" } + if str_eq(agent, "me") { return "singular" } + if str_eq(agent, "he") { return "singular" } + if str_eq(agent, "him") { return "singular" } + if str_eq(agent, "she") { return "singular" } + if str_eq(agent, "her") { return "singular" } + if str_eq(agent, "it") { return "singular" } + if str_eq(agent, "you") { return "singular" } + if str_eq(agent, "we") { return "plural" } + if str_eq(agent, "us") { return "plural" } + if str_eq(agent, "they") { return "plural" } + if str_eq(agent, "them") { return "plural" } + return "singular" +} + +// ── NP realization ──────────────────────────────────────────────────────────── + +fn realize_np(referent: String, number: String) -> String { + return referent +} + +// ── VP realization ──────────────────────────────────────────────────────────── +// +// Returns [main_verb_surface, aux_surface_or_empty]. +// Delegates conjugation to morph_conjugate with the language profile. + +fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: [String]) -> [String] { + let empty_aux: String = "" + + if str_eq(tense, "future") { + // Future: modal "will" + base (English) or language-specific future marker. + // For isolating/agglutinative languages the future marker is also the + // base form (morph_conjugate returns base); the surface "will" only appears + // for English because morph_conjugate("be", "future", ..., en_profile) = "will be". + let code: String = lang_get(profile, "code") + if str_eq(code, "en") { + let result: [String] = native_list_empty() + let result = native_list_append(result, base_verb) + let result = native_list_append(result, "will") + return result + } + // Other languages: conjugate normally (engine returns base form for + // languages without loaded Engram suffix data). + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result + } + + if str_eq(aspect, "progressive") { + let gerund: String = morph_conjugate(base_verb, "progressive", person, number, profile) + let be_aux: String = morph_conjugate("be", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, gerund) + let result = native_list_append(result, be_aux) + return result + } + + if str_eq(aspect, "perfect") { + let pp: String = morph_conjugate(base_verb, "perfect", person, number, profile) + let have_form: String = morph_conjugate("have", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, pp) + let result = native_list_append(result, have_form) + return result + } + + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result +} + +// ── Question formation ──────────────────────────────────────────────────────── +// +// Strategy is resolved from gram_question_strategy(profile): +// +// "do-support" (en) - insert conjugated "do" before subject; verb stays base. +// "particle" (ja, hi, fi) - statement order + sentence-final question particle. +// "intonation" (zh, es, ar, ru, sw) - statement order + "?" punctuation only. +// "inversion" (fr, de) - subject-verb inversion. + +// realize_question_lang: build the question surface string for any language. +// Returns the complete surface string (without final punctuation). + +fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: [String]) -> String { + let strategy: String = gram_question_strategy(profile) + let code: String = lang_get(profile, "code") + + // ── do-support (English) ────────────────────────────────────────────────── + if str_eq(strategy, "do-support") { + if str_eq(aspect, "progressive") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "progressive", person, number, profile) + let gerund: String = native_list_get(vp_pair, 0) + let be_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, gerund) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + if str_eq(aspect, "perfect") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "perfect", person, number, profile) + let pp: String = native_list_get(vp_pair, 0) + let have_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, have_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, pp) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // Simple: do-support + if str_eq(predicate, "be") { + let be_form: String = morph_conjugate("be", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_form) + let parts = native_list_append(parts, agent) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + let do_form: String = morph_conjugate("do", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, do_form) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── particle (ja, hi, fi) ───────────────────────────────────────────────── + // Build in statement order, then append the question particle. + if str_eq(strategy, "particle") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + let loc_part: String = "" + if !str_eq(location, "") { + let loc_part = core + " " + location + } else { + let loc_part = core + } + // Language-specific question particles + if str_eq(code, "ja") { return loc_part + " か" } + if str_eq(code, "hi") { return loc_part + " क्या" } + if str_eq(code, "fi") { return loc_part + "-ko" } + return loc_part + "?" + } + + // ── inversion (fr, de) ──────────────────────────────────────────────────── + if str_eq(strategy, "inversion") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + // Inversion: Verb-Subject-Object order + let parts: [String] = native_list_empty() + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, aux_s) + } else { + let parts = native_list_append(parts, verb_s) + } + let parts = native_list_append(parts, agent) + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, verb_s) + } + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── intonation (zh, es, ar, ru, sw) — statement order, "?" added by caller ─ + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + if !str_eq(location, "") { + return core + " " + location + } + return core +} + +// ── Capitalization and punctuation ──────────────────────────────────────────── + +fn capitalize_first(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { + return s + } + let first: String = str_slice(s, 0, 1) + let rest: String = str_slice(s, 1, n) + return str_to_upper(first) + rest +} + +fn add_punct(s: String, intent: String) -> String { + if str_eq(intent, "question") { return s + "?" } + return s + "." +} + +// ── Main realization entry point ────────────────────────────────────────────── + +fn realize_lang(form: [String], profile: [String]) -> String { + let intent: String = slots_get(form, "intent") + let agent: String = slots_get(form, "agent") + let predicate: String = slots_get(form, "predicate") + let patient: String = slots_get(form, "patient") + let location: String = slots_get(form, "location") + let tense_raw: String = slots_get(form, "tense") + let aspect_raw: String= slots_get(form, "aspect") + + let tense: String = tense_raw + if str_eq(tense, "") { let tense = "present" } + let aspect: String = aspect_raw + if str_eq(aspect, "") { let aspect = "simple" } + + let person: String = agent_person(agent) + let number: String = agent_number(agent) + + // ── Command (imperative) ────────────────────────────────────────────────── + if str_eq(intent, "command") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "command") + } + + // ── Question ────────────────────────────────────────────────────────────── + if str_eq(intent, "question") { + let surface: String = realize_question_lang(predicate, tense, aspect, person, number, agent, patient, location, profile) + return add_punct(capitalize_first(surface), "question") + } + + // ── Assertion (declarative) ─────────────────────────────────────────────── + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_surf: String = native_list_get(vp_pair, 0) + let aux_surf: String = native_list_get(vp_pair, 1) + + let vp_str: String = gram_build_vp(verb_surf, aux_surf, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, core) + if !str_eq(location, "") { + let parts = native_list_append(parts, location) + } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "assert") +} + +// realize: backward-compatible English entry point (original signature). +fn realize(form: [String]) -> String { + let lang_code: String = slots_get(form, "lang") + if str_eq(lang_code, "") { + return realize_lang(form, lang_default()) + } + return realize_lang(form, lang_from_code(lang_code)) +} diff --git a/elp/src/realizer_allempty.el b/elp/src/realizer_allempty.el new file mode 100644 index 0000000..0d56125 --- /dev/null +++ b/elp/src/realizer_allempty.el @@ -0,0 +1,313 @@ +// realizer.el - Universal syntactic realizer: GramSpec -> surface text. +// +// The realizer is now language-agnostic. It reads the "lang" field from the +// GramSpec to resolve a language profile, then dispatches word order, question +// formation, and morphology through the engine functions in grammar.el and +// morphology.el. +// +// English remains the default (backward compatible) when no "lang" key is set. +// +// Realization pipeline per call: +// 1. Extract lang code -> resolve profile +// 2. Extract agent, predicate, patient, location, tense, aspect, intent +// 3. Compute person/number from agent (English heuristic; other languages TBD) +// 4. Build VP: morph_conjugate with profile -> get verb and auxiliary surface +// 5. Choose question strategy from gram_question_strategy(profile) +// 6. Order constituents via gram_order_constituents(subj, verb, obj, profile) +// 7. Capitalize and terminate +// +// Depends on: morphology (morph_conjugate, agree_determiner) +// grammar (gram_order_constituents, gram_question_strategy, +// gram_build_vp, build_np, build_pp, slots_get) +// language-profile (lang_from_code, lang_get, ...) +import "test_empty_gram.el" +import "test_empty_gram.el" +import "test_empty_gram.el" + +// ── Agent agreement analysis ────────────────────────────────────────────────── +// +// Person and number are inferred from English pronouns. For other languages +// the grammatical person/number should come from the Engram vocabulary node +// for the subject; here we use a heuristic that is correct for English and +// passable for languages where the same pronoun strings are used. + +fn agent_person(agent: String) -> String { + if str_eq(agent, "I") { return "first" } + if str_eq(agent, "me") { return "first" } + if str_eq(agent, "we") { return "first" } + if str_eq(agent, "us") { return "first" } + if str_eq(agent, "you") { return "second" } + return "third" +} + +fn agent_number(agent: String) -> String { + if str_eq(agent, "I") { return "singular" } + if str_eq(agent, "me") { return "singular" } + if str_eq(agent, "he") { return "singular" } + if str_eq(agent, "him") { return "singular" } + if str_eq(agent, "she") { return "singular" } + if str_eq(agent, "her") { return "singular" } + if str_eq(agent, "it") { return "singular" } + if str_eq(agent, "you") { return "singular" } + if str_eq(agent, "we") { return "plural" } + if str_eq(agent, "us") { return "plural" } + if str_eq(agent, "they") { return "plural" } + if str_eq(agent, "them") { return "plural" } + return "singular" +} + +// ── NP realization ──────────────────────────────────────────────────────────── + +fn realize_np(referent: String, number: String) -> String { + return referent +} + +// ── VP realization ──────────────────────────────────────────────────────────── +// +// Returns [main_verb_surface, aux_surface_or_empty]. +// Delegates conjugation to morph_conjugate with the language profile. + +fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: [String]) -> [String] { + let empty_aux: String = "" + + if str_eq(tense, "future") { + // Future: modal "will" + base (English) or language-specific future marker. + // For isolating/agglutinative languages the future marker is also the + // base form (morph_conjugate returns base); the surface "will" only appears + // for English because morph_conjugate("be", "future", ..., en_profile) = "will be". + let code: String = lang_get(profile, "code") + if str_eq(code, "en") { + let result: [String] = native_list_empty() + let result = native_list_append(result, base_verb) + let result = native_list_append(result, "will") + return result + } + // Other languages: conjugate normally (engine returns base form for + // languages without loaded Engram suffix data). + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result + } + + if str_eq(aspect, "progressive") { + let gerund: String = morph_conjugate(base_verb, "progressive", person, number, profile) + let be_aux: String = morph_conjugate("be", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, gerund) + let result = native_list_append(result, be_aux) + return result + } + + if str_eq(aspect, "perfect") { + let pp: String = morph_conjugate(base_verb, "perfect", person, number, profile) + let have_form: String = morph_conjugate("have", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, pp) + let result = native_list_append(result, have_form) + return result + } + + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result +} + +// ── Question formation ──────────────────────────────────────────────────────── +// +// Strategy is resolved from gram_question_strategy(profile): +// +// "do-support" (en) - insert conjugated "do" before subject; verb stays base. +// "particle" (ja, hi, fi) - statement order + sentence-final question particle. +// "intonation" (zh, es, ar, ru, sw) - statement order + "?" punctuation only. +// "inversion" (fr, de) - subject-verb inversion. + +// realize_question_lang: build the question surface string for any language. +// Returns the complete surface string (without final punctuation). + +fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: [String]) -> String { + let strategy: String = gram_question_strategy(profile) + let code: String = lang_get(profile, "code") + + // ── do-support (English) ────────────────────────────────────────────────── + if str_eq(strategy, "do-support") { + if str_eq(aspect, "progressive") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "progressive", person, number, profile) + let gerund: String = native_list_get(vp_pair, 0) + let be_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, gerund) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + if str_eq(aspect, "perfect") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "perfect", person, number, profile) + let pp: String = native_list_get(vp_pair, 0) + let have_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, have_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, pp) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // Simple: do-support + if str_eq(predicate, "be") { + let be_form: String = morph_conjugate("be", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_form) + let parts = native_list_append(parts, agent) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + let do_form: String = morph_conjugate("do", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, do_form) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── particle (ja, hi, fi) ───────────────────────────────────────────────── + // Build in statement order, then append the question particle. + if str_eq(strategy, "particle") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + let loc_part: String = "" + if !str_eq(location, "") { + let loc_part = core + " " + location + } else { + let loc_part = core + } + // Language-specific question particles + if str_eq(code, "ja") { return loc_part + " か" } + if str_eq(code, "hi") { return loc_part + " क्या" } + if str_eq(code, "fi") { return loc_part + "-ko" } + return loc_part + "?" + } + + // ── inversion (fr, de) ──────────────────────────────────────────────────── + if str_eq(strategy, "inversion") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + // Inversion: Verb-Subject-Object order + let parts: [String] = native_list_empty() + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, aux_s) + } else { + let parts = native_list_append(parts, verb_s) + } + let parts = native_list_append(parts, agent) + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, verb_s) + } + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── intonation (zh, es, ar, ru, sw) — statement order, "?" added by caller ─ + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + if !str_eq(location, "") { + return core + " " + location + } + return core +} + +// ── Capitalization and punctuation ──────────────────────────────────────────── + +fn capitalize_first(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { + return s + } + let first: String = str_slice(s, 0, 1) + let rest: String = str_slice(s, 1, n) + return str_to_upper(first) + rest +} + +fn add_punct(s: String, intent: String) -> String { + if str_eq(intent, "question") { return s + "?" } + return s + "." +} + +// ── Main realization entry point ────────────────────────────────────────────── + +fn realize_lang(form: [String], profile: [String]) -> String { + let intent: String = slots_get(form, "intent") + let agent: String = slots_get(form, "agent") + let predicate: String = slots_get(form, "predicate") + let patient: String = slots_get(form, "patient") + let location: String = slots_get(form, "location") + let tense_raw: String = slots_get(form, "tense") + let aspect_raw: String= slots_get(form, "aspect") + + let tense: String = tense_raw + if str_eq(tense, "") { let tense = "present" } + let aspect: String = aspect_raw + if str_eq(aspect, "") { let aspect = "simple" } + + let person: String = agent_person(agent) + let number: String = agent_number(agent) + + // ── Command (imperative) ────────────────────────────────────────────────── + if str_eq(intent, "command") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "command") + } + + // ── Question ────────────────────────────────────────────────────────────── + if str_eq(intent, "question") { + let surface: String = realize_question_lang(predicate, tense, aspect, person, number, agent, patient, location, profile) + return add_punct(capitalize_first(surface), "question") + } + + // ── Assertion (declarative) ─────────────────────────────────────────────── + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_surf: String = native_list_get(vp_pair, 0) + let aux_surf: String = native_list_get(vp_pair, 1) + + let vp_str: String = gram_build_vp(verb_surf, aux_surf, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, core) + if !str_eq(location, "") { + let parts = native_list_append(parts, location) + } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "assert") +} + +// realize: backward-compatible English entry point (original signature). +fn realize(form: [String]) -> String { + let lang_code: String = slots_get(form, "lang") + if str_eq(lang_code, "") { + return realize_lang(form, lang_default()) + } + return realize_lang(form, lang_from_code(lang_code)) +} diff --git a/elp/src/realizer_bigLP.el b/elp/src/realizer_bigLP.el new file mode 100644 index 0000000..e4398e2 --- /dev/null +++ b/elp/src/realizer_bigLP.el @@ -0,0 +1,311 @@ +// realizer.el - Universal syntactic realizer: GramSpec -> surface text. +// +// The realizer is now language-agnostic. It reads the "lang" field from the +// GramSpec to resolve a language profile, then dispatches word order, question +// formation, and morphology through the engine functions in grammar.el and +// morphology.el. +// +// English remains the default (backward compatible) when no "lang" key is set. +// +// Realization pipeline per call: +// 1. Extract lang code -> resolve profile +// 2. Extract agent, predicate, patient, location, tense, aspect, intent +// 3. Compute person/number from agent (English heuristic; other languages TBD) +// 4. Build VP: morph_conjugate with profile -> get verb and auxiliary surface +// 5. Choose question strategy from gram_question_strategy(profile) +// 6. Order constituents via gram_order_constituents(subj, verb, obj, profile) +// 7. Capitalize and terminate +// +// Depends on: morphology (morph_conjugate, agree_determiner) +// grammar (gram_order_constituents, gram_question_strategy, +// gram_build_vp, build_np, build_pp, slots_get) +// language-profile (lang_from_code, lang_get, ...) +import "language-profile-big.el" + +// ── Agent agreement analysis ────────────────────────────────────────────────── +// +// Person and number are inferred from English pronouns. For other languages +// the grammatical person/number should come from the Engram vocabulary node +// for the subject; here we use a heuristic that is correct for English and +// passable for languages where the same pronoun strings are used. + +fn agent_person(agent: String) -> String { + if str_eq(agent, "I") { return "first" } + if str_eq(agent, "me") { return "first" } + if str_eq(agent, "we") { return "first" } + if str_eq(agent, "us") { return "first" } + if str_eq(agent, "you") { return "second" } + return "third" +} + +fn agent_number(agent: String) -> String { + if str_eq(agent, "I") { return "singular" } + if str_eq(agent, "me") { return "singular" } + if str_eq(agent, "he") { return "singular" } + if str_eq(agent, "him") { return "singular" } + if str_eq(agent, "she") { return "singular" } + if str_eq(agent, "her") { return "singular" } + if str_eq(agent, "it") { return "singular" } + if str_eq(agent, "you") { return "singular" } + if str_eq(agent, "we") { return "plural" } + if str_eq(agent, "us") { return "plural" } + if str_eq(agent, "they") { return "plural" } + if str_eq(agent, "them") { return "plural" } + return "singular" +} + +// ── NP realization ──────────────────────────────────────────────────────────── + +fn realize_np(referent: String, number: String) -> String { + return referent +} + +// ── VP realization ──────────────────────────────────────────────────────────── +// +// Returns [main_verb_surface, aux_surface_or_empty]. +// Delegates conjugation to morph_conjugate with the language profile. + +fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: [String]) -> [String] { + let empty_aux: String = "" + + if str_eq(tense, "future") { + // Future: modal "will" + base (English) or language-specific future marker. + // For isolating/agglutinative languages the future marker is also the + // base form (morph_conjugate returns base); the surface "will" only appears + // for English because morph_conjugate("be", "future", ..., en_profile) = "will be". + let code: String = lang_get(profile, "code") + if str_eq(code, "en") { + let result: [String] = native_list_empty() + let result = native_list_append(result, base_verb) + let result = native_list_append(result, "will") + return result + } + // Other languages: conjugate normally (engine returns base form for + // languages without loaded Engram suffix data). + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result + } + + if str_eq(aspect, "progressive") { + let gerund: String = morph_conjugate(base_verb, "progressive", person, number, profile) + let be_aux: String = morph_conjugate("be", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, gerund) + let result = native_list_append(result, be_aux) + return result + } + + if str_eq(aspect, "perfect") { + let pp: String = morph_conjugate(base_verb, "perfect", person, number, profile) + let have_form: String = morph_conjugate("have", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, pp) + let result = native_list_append(result, have_form) + return result + } + + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result +} + +// ── Question formation ──────────────────────────────────────────────────────── +// +// Strategy is resolved from gram_question_strategy(profile): +// +// "do-support" (en) - insert conjugated "do" before subject; verb stays base. +// "particle" (ja, hi, fi) - statement order + sentence-final question particle. +// "intonation" (zh, es, ar, ru, sw) - statement order + "?" punctuation only. +// "inversion" (fr, de) - subject-verb inversion. + +// realize_question_lang: build the question surface string for any language. +// Returns the complete surface string (without final punctuation). + +fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: [String]) -> String { + let strategy: String = gram_question_strategy(profile) + let code: String = lang_get(profile, "code") + + // ── do-support (English) ────────────────────────────────────────────────── + if str_eq(strategy, "do-support") { + if str_eq(aspect, "progressive") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "progressive", person, number, profile) + let gerund: String = native_list_get(vp_pair, 0) + let be_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, gerund) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + if str_eq(aspect, "perfect") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "perfect", person, number, profile) + let pp: String = native_list_get(vp_pair, 0) + let have_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, have_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, pp) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // Simple: do-support + if str_eq(predicate, "be") { + let be_form: String = morph_conjugate("be", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_form) + let parts = native_list_append(parts, agent) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + let do_form: String = morph_conjugate("do", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, do_form) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── particle (ja, hi, fi) ───────────────────────────────────────────────── + // Build in statement order, then append the question particle. + if str_eq(strategy, "particle") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + let loc_part: String = "" + if !str_eq(location, "") { + let loc_part = core + " " + location + } else { + let loc_part = core + } + // Language-specific question particles + if str_eq(code, "ja") { return loc_part + " か" } + if str_eq(code, "hi") { return loc_part + " क्या" } + if str_eq(code, "fi") { return loc_part + "-ko" } + return loc_part + "?" + } + + // ── inversion (fr, de) ──────────────────────────────────────────────────── + if str_eq(strategy, "inversion") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + // Inversion: Verb-Subject-Object order + let parts: [String] = native_list_empty() + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, aux_s) + } else { + let parts = native_list_append(parts, verb_s) + } + let parts = native_list_append(parts, agent) + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, verb_s) + } + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── intonation (zh, es, ar, ru, sw) — statement order, "?" added by caller ─ + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + if !str_eq(location, "") { + return core + " " + location + } + return core +} + +// ── Capitalization and punctuation ──────────────────────────────────────────── + +fn capitalize_first(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { + return s + } + let first: String = str_slice(s, 0, 1) + let rest: String = str_slice(s, 1, n) + return str_to_upper(first) + rest +} + +fn add_punct(s: String, intent: String) -> String { + if str_eq(intent, "question") { return s + "?" } + return s + "." +} + +// ── Main realization entry point ────────────────────────────────────────────── + +fn realize_lang(form: [String], profile: [String]) -> String { + let intent: String = slots_get(form, "intent") + let agent: String = slots_get(form, "agent") + let predicate: String = slots_get(form, "predicate") + let patient: String = slots_get(form, "patient") + let location: String = slots_get(form, "location") + let tense_raw: String = slots_get(form, "tense") + let aspect_raw: String= slots_get(form, "aspect") + + let tense: String = tense_raw + if str_eq(tense, "") { let tense = "present" } + let aspect: String = aspect_raw + if str_eq(aspect, "") { let aspect = "simple" } + + let person: String = agent_person(agent) + let number: String = agent_number(agent) + + // ── Command (imperative) ────────────────────────────────────────────────── + if str_eq(intent, "command") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "command") + } + + // ── Question ────────────────────────────────────────────────────────────── + if str_eq(intent, "question") { + let surface: String = realize_question_lang(predicate, tense, aspect, person, number, agent, patient, location, profile) + return add_punct(capitalize_first(surface), "question") + } + + // ── Assertion (declarative) ─────────────────────────────────────────────── + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_surf: String = native_list_get(vp_pair, 0) + let aux_surf: String = native_list_get(vp_pair, 1) + + let vp_str: String = gram_build_vp(verb_surf, aux_surf, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, core) + if !str_eq(location, "") { + let parts = native_list_append(parts, location) + } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "assert") +} + +// realize: backward-compatible English entry point (original signature). +fn realize(form: [String]) -> String { + let lang_code: String = slots_get(form, "lang") + if str_eq(lang_code, "") { + return realize_lang(form, lang_default()) + } + return realize_lang(form, lang_from_code(lang_code)) +} diff --git a/elp/src/realizer_bothempty.el b/elp/src/realizer_bothempty.el new file mode 100644 index 0000000..ed40235 --- /dev/null +++ b/elp/src/realizer_bothempty.el @@ -0,0 +1,313 @@ +// realizer.el - Universal syntactic realizer: GramSpec -> surface text. +// +// The realizer is now language-agnostic. It reads the "lang" field from the +// GramSpec to resolve a language profile, then dispatches word order, question +// formation, and morphology through the engine functions in grammar.el and +// morphology.el. +// +// English remains the default (backward compatible) when no "lang" key is set. +// +// Realization pipeline per call: +// 1. Extract lang code -> resolve profile +// 2. Extract agent, predicate, patient, location, tense, aspect, intent +// 3. Compute person/number from agent (English heuristic; other languages TBD) +// 4. Build VP: morph_conjugate with profile -> get verb and auxiliary surface +// 5. Choose question strategy from gram_question_strategy(profile) +// 6. Order constituents via gram_order_constituents(subj, verb, obj, profile) +// 7. Capitalize and terminate +// +// Depends on: morphology (morph_conjugate, agree_determiner) +// grammar (gram_order_constituents, gram_question_strategy, +// gram_build_vp, build_np, build_pp, slots_get) +// language-profile (lang_from_code, lang_get, ...) +import "language-profile.el" +import "test_empty_gram.el" +import "test_empty_gram.el" + +// ── Agent agreement analysis ────────────────────────────────────────────────── +// +// Person and number are inferred from English pronouns. For other languages +// the grammatical person/number should come from the Engram vocabulary node +// for the subject; here we use a heuristic that is correct for English and +// passable for languages where the same pronoun strings are used. + +fn agent_person(agent: String) -> String { + if str_eq(agent, "I") { return "first" } + if str_eq(agent, "me") { return "first" } + if str_eq(agent, "we") { return "first" } + if str_eq(agent, "us") { return "first" } + if str_eq(agent, "you") { return "second" } + return "third" +} + +fn agent_number(agent: String) -> String { + if str_eq(agent, "I") { return "singular" } + if str_eq(agent, "me") { return "singular" } + if str_eq(agent, "he") { return "singular" } + if str_eq(agent, "him") { return "singular" } + if str_eq(agent, "she") { return "singular" } + if str_eq(agent, "her") { return "singular" } + if str_eq(agent, "it") { return "singular" } + if str_eq(agent, "you") { return "singular" } + if str_eq(agent, "we") { return "plural" } + if str_eq(agent, "us") { return "plural" } + if str_eq(agent, "they") { return "plural" } + if str_eq(agent, "them") { return "plural" } + return "singular" +} + +// ── NP realization ──────────────────────────────────────────────────────────── + +fn realize_np(referent: String, number: String) -> String { + return referent +} + +// ── VP realization ──────────────────────────────────────────────────────────── +// +// Returns [main_verb_surface, aux_surface_or_empty]. +// Delegates conjugation to morph_conjugate with the language profile. + +fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: [String]) -> [String] { + let empty_aux: String = "" + + if str_eq(tense, "future") { + // Future: modal "will" + base (English) or language-specific future marker. + // For isolating/agglutinative languages the future marker is also the + // base form (morph_conjugate returns base); the surface "will" only appears + // for English because morph_conjugate("be", "future", ..., en_profile) = "will be". + let code: String = lang_get(profile, "code") + if str_eq(code, "en") { + let result: [String] = native_list_empty() + let result = native_list_append(result, base_verb) + let result = native_list_append(result, "will") + return result + } + // Other languages: conjugate normally (engine returns base form for + // languages without loaded Engram suffix data). + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result + } + + if str_eq(aspect, "progressive") { + let gerund: String = morph_conjugate(base_verb, "progressive", person, number, profile) + let be_aux: String = morph_conjugate("be", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, gerund) + let result = native_list_append(result, be_aux) + return result + } + + if str_eq(aspect, "perfect") { + let pp: String = morph_conjugate(base_verb, "perfect", person, number, profile) + let have_form: String = morph_conjugate("have", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, pp) + let result = native_list_append(result, have_form) + return result + } + + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result +} + +// ── Question formation ──────────────────────────────────────────────────────── +// +// Strategy is resolved from gram_question_strategy(profile): +// +// "do-support" (en) - insert conjugated "do" before subject; verb stays base. +// "particle" (ja, hi, fi) - statement order + sentence-final question particle. +// "intonation" (zh, es, ar, ru, sw) - statement order + "?" punctuation only. +// "inversion" (fr, de) - subject-verb inversion. + +// realize_question_lang: build the question surface string for any language. +// Returns the complete surface string (without final punctuation). + +fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: [String]) -> String { + let strategy: String = gram_question_strategy(profile) + let code: String = lang_get(profile, "code") + + // ── do-support (English) ────────────────────────────────────────────────── + if str_eq(strategy, "do-support") { + if str_eq(aspect, "progressive") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "progressive", person, number, profile) + let gerund: String = native_list_get(vp_pair, 0) + let be_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, gerund) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + if str_eq(aspect, "perfect") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "perfect", person, number, profile) + let pp: String = native_list_get(vp_pair, 0) + let have_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, have_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, pp) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // Simple: do-support + if str_eq(predicate, "be") { + let be_form: String = morph_conjugate("be", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_form) + let parts = native_list_append(parts, agent) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + let do_form: String = morph_conjugate("do", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, do_form) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── particle (ja, hi, fi) ───────────────────────────────────────────────── + // Build in statement order, then append the question particle. + if str_eq(strategy, "particle") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + let loc_part: String = "" + if !str_eq(location, "") { + let loc_part = core + " " + location + } else { + let loc_part = core + } + // Language-specific question particles + if str_eq(code, "ja") { return loc_part + " か" } + if str_eq(code, "hi") { return loc_part + " क्या" } + if str_eq(code, "fi") { return loc_part + "-ko" } + return loc_part + "?" + } + + // ── inversion (fr, de) ──────────────────────────────────────────────────── + if str_eq(strategy, "inversion") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + // Inversion: Verb-Subject-Object order + let parts: [String] = native_list_empty() + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, aux_s) + } else { + let parts = native_list_append(parts, verb_s) + } + let parts = native_list_append(parts, agent) + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, verb_s) + } + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── intonation (zh, es, ar, ru, sw) — statement order, "?" added by caller ─ + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + if !str_eq(location, "") { + return core + " " + location + } + return core +} + +// ── Capitalization and punctuation ──────────────────────────────────────────── + +fn capitalize_first(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { + return s + } + let first: String = str_slice(s, 0, 1) + let rest: String = str_slice(s, 1, n) + return str_to_upper(first) + rest +} + +fn add_punct(s: String, intent: String) -> String { + if str_eq(intent, "question") { return s + "?" } + return s + "." +} + +// ── Main realization entry point ────────────────────────────────────────────── + +fn realize_lang(form: [String], profile: [String]) -> String { + let intent: String = slots_get(form, "intent") + let agent: String = slots_get(form, "agent") + let predicate: String = slots_get(form, "predicate") + let patient: String = slots_get(form, "patient") + let location: String = slots_get(form, "location") + let tense_raw: String = slots_get(form, "tense") + let aspect_raw: String= slots_get(form, "aspect") + + let tense: String = tense_raw + if str_eq(tense, "") { let tense = "present" } + let aspect: String = aspect_raw + if str_eq(aspect, "") { let aspect = "simple" } + + let person: String = agent_person(agent) + let number: String = agent_number(agent) + + // ── Command (imperative) ────────────────────────────────────────────────── + if str_eq(intent, "command") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "command") + } + + // ── Question ────────────────────────────────────────────────────────────── + if str_eq(intent, "question") { + let surface: String = realize_question_lang(predicate, tense, aspect, person, number, agent, patient, location, profile) + return add_punct(capitalize_first(surface), "question") + } + + // ── Assertion (declarative) ─────────────────────────────────────────────── + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_surf: String = native_list_get(vp_pair, 0) + let aux_surf: String = native_list_get(vp_pair, 1) + + let vp_str: String = gram_build_vp(verb_surf, aux_surf, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, core) + if !str_eq(location, "") { + let parts = native_list_append(parts, location) + } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "assert") +} + +// realize: backward-compatible English entry point (original signature). +fn realize(form: [String]) -> String { + let lang_code: String = slots_get(form, "lang") + if str_eq(lang_code, "") { + return realize_lang(form, lang_default()) + } + return realize_lang(form, lang_from_code(lang_code)) +} diff --git a/elp/src/realizer_emptygram.el b/elp/src/realizer_emptygram.el new file mode 100644 index 0000000..b9ddba6 --- /dev/null +++ b/elp/src/realizer_emptygram.el @@ -0,0 +1,313 @@ +// realizer.el - Universal syntactic realizer: GramSpec -> surface text. +// +// The realizer is now language-agnostic. It reads the "lang" field from the +// GramSpec to resolve a language profile, then dispatches word order, question +// formation, and morphology through the engine functions in grammar.el and +// morphology.el. +// +// English remains the default (backward compatible) when no "lang" key is set. +// +// Realization pipeline per call: +// 1. Extract lang code -> resolve profile +// 2. Extract agent, predicate, patient, location, tense, aspect, intent +// 3. Compute person/number from agent (English heuristic; other languages TBD) +// 4. Build VP: morph_conjugate with profile -> get verb and auxiliary surface +// 5. Choose question strategy from gram_question_strategy(profile) +// 6. Order constituents via gram_order_constituents(subj, verb, obj, profile) +// 7. Capitalize and terminate +// +// Depends on: morphology (morph_conjugate, agree_determiner) +// grammar (gram_order_constituents, gram_question_strategy, +// gram_build_vp, build_np, build_pp, slots_get) +// language-profile (lang_from_code, lang_get, ...) +import "language-profile.el" +import "morphology.el" +import "test_empty_gram.el" + +// ── Agent agreement analysis ────────────────────────────────────────────────── +// +// Person and number are inferred from English pronouns. For other languages +// the grammatical person/number should come from the Engram vocabulary node +// for the subject; here we use a heuristic that is correct for English and +// passable for languages where the same pronoun strings are used. + +fn agent_person(agent: String) -> String { + if str_eq(agent, "I") { return "first" } + if str_eq(agent, "me") { return "first" } + if str_eq(agent, "we") { return "first" } + if str_eq(agent, "us") { return "first" } + if str_eq(agent, "you") { return "second" } + return "third" +} + +fn agent_number(agent: String) -> String { + if str_eq(agent, "I") { return "singular" } + if str_eq(agent, "me") { return "singular" } + if str_eq(agent, "he") { return "singular" } + if str_eq(agent, "him") { return "singular" } + if str_eq(agent, "she") { return "singular" } + if str_eq(agent, "her") { return "singular" } + if str_eq(agent, "it") { return "singular" } + if str_eq(agent, "you") { return "singular" } + if str_eq(agent, "we") { return "plural" } + if str_eq(agent, "us") { return "plural" } + if str_eq(agent, "they") { return "plural" } + if str_eq(agent, "them") { return "plural" } + return "singular" +} + +// ── NP realization ──────────────────────────────────────────────────────────── + +fn realize_np(referent: String, number: String) -> String { + return referent +} + +// ── VP realization ──────────────────────────────────────────────────────────── +// +// Returns [main_verb_surface, aux_surface_or_empty]. +// Delegates conjugation to morph_conjugate with the language profile. + +fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: [String]) -> [String] { + let empty_aux: String = "" + + if str_eq(tense, "future") { + // Future: modal "will" + base (English) or language-specific future marker. + // For isolating/agglutinative languages the future marker is also the + // base form (morph_conjugate returns base); the surface "will" only appears + // for English because morph_conjugate("be", "future", ..., en_profile) = "will be". + let code: String = lang_get(profile, "code") + if str_eq(code, "en") { + let result: [String] = native_list_empty() + let result = native_list_append(result, base_verb) + let result = native_list_append(result, "will") + return result + } + // Other languages: conjugate normally (engine returns base form for + // languages without loaded Engram suffix data). + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result + } + + if str_eq(aspect, "progressive") { + let gerund: String = morph_conjugate(base_verb, "progressive", person, number, profile) + let be_aux: String = morph_conjugate("be", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, gerund) + let result = native_list_append(result, be_aux) + return result + } + + if str_eq(aspect, "perfect") { + let pp: String = morph_conjugate(base_verb, "perfect", person, number, profile) + let have_form: String = morph_conjugate("have", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, pp) + let result = native_list_append(result, have_form) + return result + } + + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result +} + +// ── Question formation ──────────────────────────────────────────────────────── +// +// Strategy is resolved from gram_question_strategy(profile): +// +// "do-support" (en) - insert conjugated "do" before subject; verb stays base. +// "particle" (ja, hi, fi) - statement order + sentence-final question particle. +// "intonation" (zh, es, ar, ru, sw) - statement order + "?" punctuation only. +// "inversion" (fr, de) - subject-verb inversion. + +// realize_question_lang: build the question surface string for any language. +// Returns the complete surface string (without final punctuation). + +fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: [String]) -> String { + let strategy: String = gram_question_strategy(profile) + let code: String = lang_get(profile, "code") + + // ── do-support (English) ────────────────────────────────────────────────── + if str_eq(strategy, "do-support") { + if str_eq(aspect, "progressive") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "progressive", person, number, profile) + let gerund: String = native_list_get(vp_pair, 0) + let be_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, gerund) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + if str_eq(aspect, "perfect") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "perfect", person, number, profile) + let pp: String = native_list_get(vp_pair, 0) + let have_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, have_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, pp) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // Simple: do-support + if str_eq(predicate, "be") { + let be_form: String = morph_conjugate("be", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_form) + let parts = native_list_append(parts, agent) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + let do_form: String = morph_conjugate("do", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, do_form) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── particle (ja, hi, fi) ───────────────────────────────────────────────── + // Build in statement order, then append the question particle. + if str_eq(strategy, "particle") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + let loc_part: String = "" + if !str_eq(location, "") { + let loc_part = core + " " + location + } else { + let loc_part = core + } + // Language-specific question particles + if str_eq(code, "ja") { return loc_part + " か" } + if str_eq(code, "hi") { return loc_part + " क्या" } + if str_eq(code, "fi") { return loc_part + "-ko" } + return loc_part + "?" + } + + // ── inversion (fr, de) ──────────────────────────────────────────────────── + if str_eq(strategy, "inversion") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + // Inversion: Verb-Subject-Object order + let parts: [String] = native_list_empty() + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, aux_s) + } else { + let parts = native_list_append(parts, verb_s) + } + let parts = native_list_append(parts, agent) + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, verb_s) + } + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── intonation (zh, es, ar, ru, sw) — statement order, "?" added by caller ─ + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + if !str_eq(location, "") { + return core + " " + location + } + return core +} + +// ── Capitalization and punctuation ──────────────────────────────────────────── + +fn capitalize_first(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { + return s + } + let first: String = str_slice(s, 0, 1) + let rest: String = str_slice(s, 1, n) + return str_to_upper(first) + rest +} + +fn add_punct(s: String, intent: String) -> String { + if str_eq(intent, "question") { return s + "?" } + return s + "." +} + +// ── Main realization entry point ────────────────────────────────────────────── + +fn realize_lang(form: [String], profile: [String]) -> String { + let intent: String = slots_get(form, "intent") + let agent: String = slots_get(form, "agent") + let predicate: String = slots_get(form, "predicate") + let patient: String = slots_get(form, "patient") + let location: String = slots_get(form, "location") + let tense_raw: String = slots_get(form, "tense") + let aspect_raw: String= slots_get(form, "aspect") + + let tense: String = tense_raw + if str_eq(tense, "") { let tense = "present" } + let aspect: String = aspect_raw + if str_eq(aspect, "") { let aspect = "simple" } + + let person: String = agent_person(agent) + let number: String = agent_number(agent) + + // ── Command (imperative) ────────────────────────────────────────────────── + if str_eq(intent, "command") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "command") + } + + // ── Question ────────────────────────────────────────────────────────────── + if str_eq(intent, "question") { + let surface: String = realize_question_lang(predicate, tense, aspect, person, number, agent, patient, location, profile) + return add_punct(capitalize_first(surface), "question") + } + + // ── Assertion (declarative) ─────────────────────────────────────────────── + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_surf: String = native_list_get(vp_pair, 0) + let aux_surf: String = native_list_get(vp_pair, 1) + + let vp_str: String = gram_build_vp(verb_surf, aux_surf, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, core) + if !str_eq(location, "") { + let parts = native_list_append(parts, location) + } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "assert") +} + +// realize: backward-compatible English entry point (original signature). +fn realize(form: [String]) -> String { + let lang_code: String = slots_get(form, "lang") + if str_eq(lang_code, "") { + return realize_lang(form, lang_default()) + } + return realize_lang(form, lang_from_code(lang_code)) +} diff --git a/elp/src/realizer_morphgram.el b/elp/src/realizer_morphgram.el new file mode 100644 index 0000000..2c7c55c --- /dev/null +++ b/elp/src/realizer_morphgram.el @@ -0,0 +1,312 @@ +// realizer.el - Universal syntactic realizer: GramSpec -> surface text. +// +// The realizer is now language-agnostic. It reads the "lang" field from the +// GramSpec to resolve a language profile, then dispatches word order, question +// formation, and morphology through the engine functions in grammar.el and +// morphology.el. +// +// English remains the default (backward compatible) when no "lang" key is set. +// +// Realization pipeline per call: +// 1. Extract lang code -> resolve profile +// 2. Extract agent, predicate, patient, location, tense, aspect, intent +// 3. Compute person/number from agent (English heuristic; other languages TBD) +// 4. Build VP: morph_conjugate with profile -> get verb and auxiliary surface +// 5. Choose question strategy from gram_question_strategy(profile) +// 6. Order constituents via gram_order_constituents(subj, verb, obj, profile) +// 7. Capitalize and terminate +// +// Depends on: morphology (morph_conjugate, agree_determiner) +// grammar (gram_order_constituents, gram_question_strategy, +// gram_build_vp, build_np, build_pp, slots_get) +// language-profile (lang_from_code, lang_get, ...) +import "morphology.el" +import "grammar.el" + +// ── Agent agreement analysis ────────────────────────────────────────────────── +// +// Person and number are inferred from English pronouns. For other languages +// the grammatical person/number should come from the Engram vocabulary node +// for the subject; here we use a heuristic that is correct for English and +// passable for languages where the same pronoun strings are used. + +fn agent_person(agent: String) -> String { + if str_eq(agent, "I") { return "first" } + if str_eq(agent, "me") { return "first" } + if str_eq(agent, "we") { return "first" } + if str_eq(agent, "us") { return "first" } + if str_eq(agent, "you") { return "second" } + return "third" +} + +fn agent_number(agent: String) -> String { + if str_eq(agent, "I") { return "singular" } + if str_eq(agent, "me") { return "singular" } + if str_eq(agent, "he") { return "singular" } + if str_eq(agent, "him") { return "singular" } + if str_eq(agent, "she") { return "singular" } + if str_eq(agent, "her") { return "singular" } + if str_eq(agent, "it") { return "singular" } + if str_eq(agent, "you") { return "singular" } + if str_eq(agent, "we") { return "plural" } + if str_eq(agent, "us") { return "plural" } + if str_eq(agent, "they") { return "plural" } + if str_eq(agent, "them") { return "plural" } + return "singular" +} + +// ── NP realization ──────────────────────────────────────────────────────────── + +fn realize_np(referent: String, number: String) -> String { + return referent +} + +// ── VP realization ──────────────────────────────────────────────────────────── +// +// Returns [main_verb_surface, aux_surface_or_empty]. +// Delegates conjugation to morph_conjugate with the language profile. + +fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: [String]) -> [String] { + let empty_aux: String = "" + + if str_eq(tense, "future") { + // Future: modal "will" + base (English) or language-specific future marker. + // For isolating/agglutinative languages the future marker is also the + // base form (morph_conjugate returns base); the surface "will" only appears + // for English because morph_conjugate("be", "future", ..., en_profile) = "will be". + let code: String = lang_get(profile, "code") + if str_eq(code, "en") { + let result: [String] = native_list_empty() + let result = native_list_append(result, base_verb) + let result = native_list_append(result, "will") + return result + } + // Other languages: conjugate normally (engine returns base form for + // languages without loaded Engram suffix data). + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result + } + + if str_eq(aspect, "progressive") { + let gerund: String = morph_conjugate(base_verb, "progressive", person, number, profile) + let be_aux: String = morph_conjugate("be", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, gerund) + let result = native_list_append(result, be_aux) + return result + } + + if str_eq(aspect, "perfect") { + let pp: String = morph_conjugate(base_verb, "perfect", person, number, profile) + let have_form: String = morph_conjugate("have", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, pp) + let result = native_list_append(result, have_form) + return result + } + + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result +} + +// ── Question formation ──────────────────────────────────────────────────────── +// +// Strategy is resolved from gram_question_strategy(profile): +// +// "do-support" (en) - insert conjugated "do" before subject; verb stays base. +// "particle" (ja, hi, fi) - statement order + sentence-final question particle. +// "intonation" (zh, es, ar, ru, sw) - statement order + "?" punctuation only. +// "inversion" (fr, de) - subject-verb inversion. + +// realize_question_lang: build the question surface string for any language. +// Returns the complete surface string (without final punctuation). + +fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: [String]) -> String { + let strategy: String = gram_question_strategy(profile) + let code: String = lang_get(profile, "code") + + // ── do-support (English) ────────────────────────────────────────────────── + if str_eq(strategy, "do-support") { + if str_eq(aspect, "progressive") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "progressive", person, number, profile) + let gerund: String = native_list_get(vp_pair, 0) + let be_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, gerund) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + if str_eq(aspect, "perfect") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "perfect", person, number, profile) + let pp: String = native_list_get(vp_pair, 0) + let have_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, have_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, pp) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // Simple: do-support + if str_eq(predicate, "be") { + let be_form: String = morph_conjugate("be", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_form) + let parts = native_list_append(parts, agent) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + let do_form: String = morph_conjugate("do", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, do_form) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── particle (ja, hi, fi) ───────────────────────────────────────────────── + // Build in statement order, then append the question particle. + if str_eq(strategy, "particle") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + let loc_part: String = "" + if !str_eq(location, "") { + let loc_part = core + " " + location + } else { + let loc_part = core + } + // Language-specific question particles + if str_eq(code, "ja") { return loc_part + " か" } + if str_eq(code, "hi") { return loc_part + " क्या" } + if str_eq(code, "fi") { return loc_part + "-ko" } + return loc_part + "?" + } + + // ── inversion (fr, de) ──────────────────────────────────────────────────── + if str_eq(strategy, "inversion") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + // Inversion: Verb-Subject-Object order + let parts: [String] = native_list_empty() + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, aux_s) + } else { + let parts = native_list_append(parts, verb_s) + } + let parts = native_list_append(parts, agent) + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, verb_s) + } + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── intonation (zh, es, ar, ru, sw) — statement order, "?" added by caller ─ + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + if !str_eq(location, "") { + return core + " " + location + } + return core +} + +// ── Capitalization and punctuation ──────────────────────────────────────────── + +fn capitalize_first(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { + return s + } + let first: String = str_slice(s, 0, 1) + let rest: String = str_slice(s, 1, n) + return str_to_upper(first) + rest +} + +fn add_punct(s: String, intent: String) -> String { + if str_eq(intent, "question") { return s + "?" } + return s + "." +} + +// ── Main realization entry point ────────────────────────────────────────────── + +fn realize_lang(form: [String], profile: [String]) -> String { + let intent: String = slots_get(form, "intent") + let agent: String = slots_get(form, "agent") + let predicate: String = slots_get(form, "predicate") + let patient: String = slots_get(form, "patient") + let location: String = slots_get(form, "location") + let tense_raw: String = slots_get(form, "tense") + let aspect_raw: String= slots_get(form, "aspect") + + let tense: String = tense_raw + if str_eq(tense, "") { let tense = "present" } + let aspect: String = aspect_raw + if str_eq(aspect, "") { let aspect = "simple" } + + let person: String = agent_person(agent) + let number: String = agent_number(agent) + + // ── Command (imperative) ────────────────────────────────────────────────── + if str_eq(intent, "command") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "command") + } + + // ── Question ────────────────────────────────────────────────────────────── + if str_eq(intent, "question") { + let surface: String = realize_question_lang(predicate, tense, aspect, person, number, agent, patient, location, profile) + return add_punct(capitalize_first(surface), "question") + } + + // ── Assertion (declarative) ─────────────────────────────────────────────── + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_surf: String = native_list_get(vp_pair, 0) + let aux_surf: String = native_list_get(vp_pair, 1) + + let vp_str: String = gram_build_vp(verb_surf, aux_surf, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, core) + if !str_eq(location, "") { + let parts = native_list_append(parts, location) + } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "assert") +} + +// realize: backward-compatible English entry point (original signature). +fn realize(form: [String]) -> String { + let lang_code: String = slots_get(form, "lang") + if str_eq(lang_code, "") { + return realize_lang(form, lang_default()) + } + return realize_lang(form, lang_from_code(lang_code)) +} diff --git a/elp/src/realizer_noimports.el b/elp/src/realizer_noimports.el new file mode 100644 index 0000000..b43030e --- /dev/null +++ b/elp/src/realizer_noimports.el @@ -0,0 +1,310 @@ +// realizer.el - Universal syntactic realizer: GramSpec -> surface text. +// +// The realizer is now language-agnostic. It reads the "lang" field from the +// GramSpec to resolve a language profile, then dispatches word order, question +// formation, and morphology through the engine functions in grammar.el and +// morphology.el. +// +// English remains the default (backward compatible) when no "lang" key is set. +// +// Realization pipeline per call: +// 1. Extract lang code -> resolve profile +// 2. Extract agent, predicate, patient, location, tense, aspect, intent +// 3. Compute person/number from agent (English heuristic; other languages TBD) +// 4. Build VP: morph_conjugate with profile -> get verb and auxiliary surface +// 5. Choose question strategy from gram_question_strategy(profile) +// 6. Order constituents via gram_order_constituents(subj, verb, obj, profile) +// 7. Capitalize and terminate +// +// Depends on: morphology (morph_conjugate, agree_determiner) +// grammar (gram_order_constituents, gram_question_strategy, +// gram_build_vp, build_np, build_pp, slots_get) +// language-profile (lang_from_code, lang_get, ...) + +// ── Agent agreement analysis ────────────────────────────────────────────────── +// +// Person and number are inferred from English pronouns. For other languages +// the grammatical person/number should come from the Engram vocabulary node +// for the subject; here we use a heuristic that is correct for English and +// passable for languages where the same pronoun strings are used. + +fn agent_person(agent: String) -> String { + if str_eq(agent, "I") { return "first" } + if str_eq(agent, "me") { return "first" } + if str_eq(agent, "we") { return "first" } + if str_eq(agent, "us") { return "first" } + if str_eq(agent, "you") { return "second" } + return "third" +} + +fn agent_number(agent: String) -> String { + if str_eq(agent, "I") { return "singular" } + if str_eq(agent, "me") { return "singular" } + if str_eq(agent, "he") { return "singular" } + if str_eq(agent, "him") { return "singular" } + if str_eq(agent, "she") { return "singular" } + if str_eq(agent, "her") { return "singular" } + if str_eq(agent, "it") { return "singular" } + if str_eq(agent, "you") { return "singular" } + if str_eq(agent, "we") { return "plural" } + if str_eq(agent, "us") { return "plural" } + if str_eq(agent, "they") { return "plural" } + if str_eq(agent, "them") { return "plural" } + return "singular" +} + +// ── NP realization ──────────────────────────────────────────────────────────── + +fn realize_np(referent: String, number: String) -> String { + return referent +} + +// ── VP realization ──────────────────────────────────────────────────────────── +// +// Returns [main_verb_surface, aux_surface_or_empty]. +// Delegates conjugation to morph_conjugate with the language profile. + +fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: [String]) -> [String] { + let empty_aux: String = "" + + if str_eq(tense, "future") { + // Future: modal "will" + base (English) or language-specific future marker. + // For isolating/agglutinative languages the future marker is also the + // base form (morph_conjugate returns base); the surface "will" only appears + // for English because morph_conjugate("be", "future", ..., en_profile) = "will be". + let code: String = lang_get(profile, "code") + if str_eq(code, "en") { + let result: [String] = native_list_empty() + let result = native_list_append(result, base_verb) + let result = native_list_append(result, "will") + return result + } + // Other languages: conjugate normally (engine returns base form for + // languages without loaded Engram suffix data). + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result + } + + if str_eq(aspect, "progressive") { + let gerund: String = morph_conjugate(base_verb, "progressive", person, number, profile) + let be_aux: String = morph_conjugate("be", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, gerund) + let result = native_list_append(result, be_aux) + return result + } + + if str_eq(aspect, "perfect") { + let pp: String = morph_conjugate(base_verb, "perfect", person, number, profile) + let have_form: String = morph_conjugate("have", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, pp) + let result = native_list_append(result, have_form) + return result + } + + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result +} + +// ── Question formation ──────────────────────────────────────────────────────── +// +// Strategy is resolved from gram_question_strategy(profile): +// +// "do-support" (en) - insert conjugated "do" before subject; verb stays base. +// "particle" (ja, hi, fi) - statement order + sentence-final question particle. +// "intonation" (zh, es, ar, ru, sw) - statement order + "?" punctuation only. +// "inversion" (fr, de) - subject-verb inversion. + +// realize_question_lang: build the question surface string for any language. +// Returns the complete surface string (without final punctuation). + +fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: [String]) -> String { + let strategy: String = gram_question_strategy(profile) + let code: String = lang_get(profile, "code") + + // ── do-support (English) ────────────────────────────────────────────────── + if str_eq(strategy, "do-support") { + if str_eq(aspect, "progressive") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "progressive", person, number, profile) + let gerund: String = native_list_get(vp_pair, 0) + let be_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, gerund) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + if str_eq(aspect, "perfect") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "perfect", person, number, profile) + let pp: String = native_list_get(vp_pair, 0) + let have_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, have_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, pp) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // Simple: do-support + if str_eq(predicate, "be") { + let be_form: String = morph_conjugate("be", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_form) + let parts = native_list_append(parts, agent) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + let do_form: String = morph_conjugate("do", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, do_form) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── particle (ja, hi, fi) ───────────────────────────────────────────────── + // Build in statement order, then append the question particle. + if str_eq(strategy, "particle") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + let loc_part: String = "" + if !str_eq(location, "") { + let loc_part = core + " " + location + } else { + let loc_part = core + } + // Language-specific question particles + if str_eq(code, "ja") { return loc_part + " か" } + if str_eq(code, "hi") { return loc_part + " क्या" } + if str_eq(code, "fi") { return loc_part + "-ko" } + return loc_part + "?" + } + + // ── inversion (fr, de) ──────────────────────────────────────────────────── + if str_eq(strategy, "inversion") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + // Inversion: Verb-Subject-Object order + let parts: [String] = native_list_empty() + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, aux_s) + } else { + let parts = native_list_append(parts, verb_s) + } + let parts = native_list_append(parts, agent) + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, verb_s) + } + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── intonation (zh, es, ar, ru, sw) — statement order, "?" added by caller ─ + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + if !str_eq(location, "") { + return core + " " + location + } + return core +} + +// ── Capitalization and punctuation ──────────────────────────────────────────── + +fn capitalize_first(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { + return s + } + let first: String = str_slice(s, 0, 1) + let rest: String = str_slice(s, 1, n) + return str_to_upper(first) + rest +} + +fn add_punct(s: String, intent: String) -> String { + if str_eq(intent, "question") { return s + "?" } + return s + "." +} + +// ── Main realization entry point ────────────────────────────────────────────── + +fn realize_lang(form: [String], profile: [String]) -> String { + let intent: String = slots_get(form, "intent") + let agent: String = slots_get(form, "agent") + let predicate: String = slots_get(form, "predicate") + let patient: String = slots_get(form, "patient") + let location: String = slots_get(form, "location") + let tense_raw: String = slots_get(form, "tense") + let aspect_raw: String= slots_get(form, "aspect") + + let tense: String = tense_raw + if str_eq(tense, "") { let tense = "present" } + let aspect: String = aspect_raw + if str_eq(aspect, "") { let aspect = "simple" } + + let person: String = agent_person(agent) + let number: String = agent_number(agent) + + // ── Command (imperative) ────────────────────────────────────────────────── + if str_eq(intent, "command") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "command") + } + + // ── Question ────────────────────────────────────────────────────────────── + if str_eq(intent, "question") { + let surface: String = realize_question_lang(predicate, tense, aspect, person, number, agent, patient, location, profile) + return add_punct(capitalize_first(surface), "question") + } + + // ── Assertion (declarative) ─────────────────────────────────────────────── + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_surf: String = native_list_get(vp_pair, 0) + let aux_surf: String = native_list_get(vp_pair, 1) + + let vp_str: String = gram_build_vp(verb_surf, aux_surf, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, core) + if !str_eq(location, "") { + let parts = native_list_append(parts, location) + } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "assert") +} + +// realize: backward-compatible English entry point (original signature). +fn realize(form: [String]) -> String { + let lang_code: String = slots_get(form, "lang") + if str_eq(lang_code, "") { + return realize_lang(form, lang_default()) + } + return realize_lang(form, lang_from_code(lang_code)) +} diff --git a/elp/src/realizer_oneext_nolp.el b/elp/src/realizer_oneext_nolp.el new file mode 100644 index 0000000..ec8f71c --- /dev/null +++ b/elp/src/realizer_oneext_nolp.el @@ -0,0 +1,312 @@ +// realizer.el - Universal syntactic realizer: GramSpec -> surface text. +// +// The realizer is now language-agnostic. It reads the "lang" field from the +// GramSpec to resolve a language profile, then dispatches word order, question +// formation, and morphology through the engine functions in grammar.el and +// morphology.el. +// +// English remains the default (backward compatible) when no "lang" key is set. +// +// Realization pipeline per call: +// 1. Extract lang code -> resolve profile +// 2. Extract agent, predicate, patient, location, tense, aspect, intent +// 3. Compute person/number from agent (English heuristic; other languages TBD) +// 4. Build VP: morph_conjugate with profile -> get verb and auxiliary surface +// 5. Choose question strategy from gram_question_strategy(profile) +// 6. Order constituents via gram_order_constituents(subj, verb, obj, profile) +// 7. Capitalize and terminate +// +// Depends on: morphology (morph_conjugate, agree_determiner) +// grammar (gram_order_constituents, gram_question_strategy, +// gram_build_vp, build_np, build_pp, slots_get) +// language-profile (lang_from_code, lang_get, ...) +import "language-profile.el" +import "one_extern_nolp.el" + +// ── Agent agreement analysis ────────────────────────────────────────────────── +// +// Person and number are inferred from English pronouns. For other languages +// the grammatical person/number should come from the Engram vocabulary node +// for the subject; here we use a heuristic that is correct for English and +// passable for languages where the same pronoun strings are used. + +fn agent_person(agent: String) -> String { + if str_eq(agent, "I") { return "first" } + if str_eq(agent, "me") { return "first" } + if str_eq(agent, "we") { return "first" } + if str_eq(agent, "us") { return "first" } + if str_eq(agent, "you") { return "second" } + return "third" +} + +fn agent_number(agent: String) -> String { + if str_eq(agent, "I") { return "singular" } + if str_eq(agent, "me") { return "singular" } + if str_eq(agent, "he") { return "singular" } + if str_eq(agent, "him") { return "singular" } + if str_eq(agent, "she") { return "singular" } + if str_eq(agent, "her") { return "singular" } + if str_eq(agent, "it") { return "singular" } + if str_eq(agent, "you") { return "singular" } + if str_eq(agent, "we") { return "plural" } + if str_eq(agent, "us") { return "plural" } + if str_eq(agent, "they") { return "plural" } + if str_eq(agent, "them") { return "plural" } + return "singular" +} + +// ── NP realization ──────────────────────────────────────────────────────────── + +fn realize_np(referent: String, number: String) -> String { + return referent +} + +// ── VP realization ──────────────────────────────────────────────────────────── +// +// Returns [main_verb_surface, aux_surface_or_empty]. +// Delegates conjugation to morph_conjugate with the language profile. + +fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: [String]) -> [String] { + let empty_aux: String = "" + + if str_eq(tense, "future") { + // Future: modal "will" + base (English) or language-specific future marker. + // For isolating/agglutinative languages the future marker is also the + // base form (morph_conjugate returns base); the surface "will" only appears + // for English because morph_conjugate("be", "future", ..., en_profile) = "will be". + let code: String = lang_get(profile, "code") + if str_eq(code, "en") { + let result: [String] = native_list_empty() + let result = native_list_append(result, base_verb) + let result = native_list_append(result, "will") + return result + } + // Other languages: conjugate normally (engine returns base form for + // languages without loaded Engram suffix data). + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result + } + + if str_eq(aspect, "progressive") { + let gerund: String = morph_conjugate(base_verb, "progressive", person, number, profile) + let be_aux: String = morph_conjugate("be", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, gerund) + let result = native_list_append(result, be_aux) + return result + } + + if str_eq(aspect, "perfect") { + let pp: String = morph_conjugate(base_verb, "perfect", person, number, profile) + let have_form: String = morph_conjugate("have", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, pp) + let result = native_list_append(result, have_form) + return result + } + + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result +} + +// ── Question formation ──────────────────────────────────────────────────────── +// +// Strategy is resolved from gram_question_strategy(profile): +// +// "do-support" (en) - insert conjugated "do" before subject; verb stays base. +// "particle" (ja, hi, fi) - statement order + sentence-final question particle. +// "intonation" (zh, es, ar, ru, sw) - statement order + "?" punctuation only. +// "inversion" (fr, de) - subject-verb inversion. + +// realize_question_lang: build the question surface string for any language. +// Returns the complete surface string (without final punctuation). + +fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: [String]) -> String { + let strategy: String = gram_question_strategy(profile) + let code: String = lang_get(profile, "code") + + // ── do-support (English) ────────────────────────────────────────────────── + if str_eq(strategy, "do-support") { + if str_eq(aspect, "progressive") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "progressive", person, number, profile) + let gerund: String = native_list_get(vp_pair, 0) + let be_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, gerund) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + if str_eq(aspect, "perfect") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "perfect", person, number, profile) + let pp: String = native_list_get(vp_pair, 0) + let have_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, have_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, pp) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // Simple: do-support + if str_eq(predicate, "be") { + let be_form: String = morph_conjugate("be", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_form) + let parts = native_list_append(parts, agent) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + let do_form: String = morph_conjugate("do", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, do_form) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── particle (ja, hi, fi) ───────────────────────────────────────────────── + // Build in statement order, then append the question particle. + if str_eq(strategy, "particle") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + let loc_part: String = "" + if !str_eq(location, "") { + let loc_part = core + " " + location + } else { + let loc_part = core + } + // Language-specific question particles + if str_eq(code, "ja") { return loc_part + " か" } + if str_eq(code, "hi") { return loc_part + " क्या" } + if str_eq(code, "fi") { return loc_part + "-ko" } + return loc_part + "?" + } + + // ── inversion (fr, de) ──────────────────────────────────────────────────── + if str_eq(strategy, "inversion") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + // Inversion: Verb-Subject-Object order + let parts: [String] = native_list_empty() + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, aux_s) + } else { + let parts = native_list_append(parts, verb_s) + } + let parts = native_list_append(parts, agent) + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, verb_s) + } + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── intonation (zh, es, ar, ru, sw) — statement order, "?" added by caller ─ + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + if !str_eq(location, "") { + return core + " " + location + } + return core +} + +// ── Capitalization and punctuation ──────────────────────────────────────────── + +fn capitalize_first(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { + return s + } + let first: String = str_slice(s, 0, 1) + let rest: String = str_slice(s, 1, n) + return str_to_upper(first) + rest +} + +fn add_punct(s: String, intent: String) -> String { + if str_eq(intent, "question") { return s + "?" } + return s + "." +} + +// ── Main realization entry point ────────────────────────────────────────────── + +fn realize_lang(form: [String], profile: [String]) -> String { + let intent: String = slots_get(form, "intent") + let agent: String = slots_get(form, "agent") + let predicate: String = slots_get(form, "predicate") + let patient: String = slots_get(form, "patient") + let location: String = slots_get(form, "location") + let tense_raw: String = slots_get(form, "tense") + let aspect_raw: String= slots_get(form, "aspect") + + let tense: String = tense_raw + if str_eq(tense, "") { let tense = "present" } + let aspect: String = aspect_raw + if str_eq(aspect, "") { let aspect = "simple" } + + let person: String = agent_person(agent) + let number: String = agent_number(agent) + + // ── Command (imperative) ────────────────────────────────────────────────── + if str_eq(intent, "command") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "command") + } + + // ── Question ────────────────────────────────────────────────────────────── + if str_eq(intent, "question") { + let surface: String = realize_question_lang(predicate, tense, aspect, person, number, agent, patient, location, profile) + return add_punct(capitalize_first(surface), "question") + } + + // ── Assertion (declarative) ─────────────────────────────────────────────── + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_surf: String = native_list_get(vp_pair, 0) + let aux_surf: String = native_list_get(vp_pair, 1) + + let vp_str: String = gram_build_vp(verb_surf, aux_surf, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, core) + if !str_eq(location, "") { + let parts = native_list_append(parts, location) + } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "assert") +} + +// realize: backward-compatible English entry point (original signature). +fn realize(form: [String]) -> String { + let lang_code: String = slots_get(form, "lang") + if str_eq(lang_code, "") { + return realize_lang(form, lang_default()) + } + return realize_lang(form, lang_from_code(lang_code)) +} diff --git a/elp/src/realizer_oneextern.el b/elp/src/realizer_oneextern.el new file mode 100644 index 0000000..dfc34e4 --- /dev/null +++ b/elp/src/realizer_oneextern.el @@ -0,0 +1,312 @@ +// realizer.el - Universal syntactic realizer: GramSpec -> surface text. +// +// The realizer is now language-agnostic. It reads the "lang" field from the +// GramSpec to resolve a language profile, then dispatches word order, question +// formation, and morphology through the engine functions in grammar.el and +// morphology.el. +// +// English remains the default (backward compatible) when no "lang" key is set. +// +// Realization pipeline per call: +// 1. Extract lang code -> resolve profile +// 2. Extract agent, predicate, patient, location, tense, aspect, intent +// 3. Compute person/number from agent (English heuristic; other languages TBD) +// 4. Build VP: morph_conjugate with profile -> get verb and auxiliary surface +// 5. Choose question strategy from gram_question_strategy(profile) +// 6. Order constituents via gram_order_constituents(subj, verb, obj, profile) +// 7. Capitalize and terminate +// +// Depends on: morphology (morph_conjugate, agree_determiner) +// grammar (gram_order_constituents, gram_question_strategy, +// gram_build_vp, build_np, build_pp, slots_get) +// language-profile (lang_from_code, lang_get, ...) +import "language-profile.el" +import "one_extern.el" + +// ── Agent agreement analysis ────────────────────────────────────────────────── +// +// Person and number are inferred from English pronouns. For other languages +// the grammatical person/number should come from the Engram vocabulary node +// for the subject; here we use a heuristic that is correct for English and +// passable for languages where the same pronoun strings are used. + +fn agent_person(agent: String) -> String { + if str_eq(agent, "I") { return "first" } + if str_eq(agent, "me") { return "first" } + if str_eq(agent, "we") { return "first" } + if str_eq(agent, "us") { return "first" } + if str_eq(agent, "you") { return "second" } + return "third" +} + +fn agent_number(agent: String) -> String { + if str_eq(agent, "I") { return "singular" } + if str_eq(agent, "me") { return "singular" } + if str_eq(agent, "he") { return "singular" } + if str_eq(agent, "him") { return "singular" } + if str_eq(agent, "she") { return "singular" } + if str_eq(agent, "her") { return "singular" } + if str_eq(agent, "it") { return "singular" } + if str_eq(agent, "you") { return "singular" } + if str_eq(agent, "we") { return "plural" } + if str_eq(agent, "us") { return "plural" } + if str_eq(agent, "they") { return "plural" } + if str_eq(agent, "them") { return "plural" } + return "singular" +} + +// ── NP realization ──────────────────────────────────────────────────────────── + +fn realize_np(referent: String, number: String) -> String { + return referent +} + +// ── VP realization ──────────────────────────────────────────────────────────── +// +// Returns [main_verb_surface, aux_surface_or_empty]. +// Delegates conjugation to morph_conjugate with the language profile. + +fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: [String]) -> [String] { + let empty_aux: String = "" + + if str_eq(tense, "future") { + // Future: modal "will" + base (English) or language-specific future marker. + // For isolating/agglutinative languages the future marker is also the + // base form (morph_conjugate returns base); the surface "will" only appears + // for English because morph_conjugate("be", "future", ..., en_profile) = "will be". + let code: String = lang_get(profile, "code") + if str_eq(code, "en") { + let result: [String] = native_list_empty() + let result = native_list_append(result, base_verb) + let result = native_list_append(result, "will") + return result + } + // Other languages: conjugate normally (engine returns base form for + // languages without loaded Engram suffix data). + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result + } + + if str_eq(aspect, "progressive") { + let gerund: String = morph_conjugate(base_verb, "progressive", person, number, profile) + let be_aux: String = morph_conjugate("be", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, gerund) + let result = native_list_append(result, be_aux) + return result + } + + if str_eq(aspect, "perfect") { + let pp: String = morph_conjugate(base_verb, "perfect", person, number, profile) + let have_form: String = morph_conjugate("have", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, pp) + let result = native_list_append(result, have_form) + return result + } + + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result +} + +// ── Question formation ──────────────────────────────────────────────────────── +// +// Strategy is resolved from gram_question_strategy(profile): +// +// "do-support" (en) - insert conjugated "do" before subject; verb stays base. +// "particle" (ja, hi, fi) - statement order + sentence-final question particle. +// "intonation" (zh, es, ar, ru, sw) - statement order + "?" punctuation only. +// "inversion" (fr, de) - subject-verb inversion. + +// realize_question_lang: build the question surface string for any language. +// Returns the complete surface string (without final punctuation). + +fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: [String]) -> String { + let strategy: String = gram_question_strategy(profile) + let code: String = lang_get(profile, "code") + + // ── do-support (English) ────────────────────────────────────────────────── + if str_eq(strategy, "do-support") { + if str_eq(aspect, "progressive") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "progressive", person, number, profile) + let gerund: String = native_list_get(vp_pair, 0) + let be_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, gerund) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + if str_eq(aspect, "perfect") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "perfect", person, number, profile) + let pp: String = native_list_get(vp_pair, 0) + let have_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, have_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, pp) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // Simple: do-support + if str_eq(predicate, "be") { + let be_form: String = morph_conjugate("be", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_form) + let parts = native_list_append(parts, agent) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + let do_form: String = morph_conjugate("do", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, do_form) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── particle (ja, hi, fi) ───────────────────────────────────────────────── + // Build in statement order, then append the question particle. + if str_eq(strategy, "particle") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + let loc_part: String = "" + if !str_eq(location, "") { + let loc_part = core + " " + location + } else { + let loc_part = core + } + // Language-specific question particles + if str_eq(code, "ja") { return loc_part + " か" } + if str_eq(code, "hi") { return loc_part + " क्या" } + if str_eq(code, "fi") { return loc_part + "-ko" } + return loc_part + "?" + } + + // ── inversion (fr, de) ──────────────────────────────────────────────────── + if str_eq(strategy, "inversion") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + // Inversion: Verb-Subject-Object order + let parts: [String] = native_list_empty() + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, aux_s) + } else { + let parts = native_list_append(parts, verb_s) + } + let parts = native_list_append(parts, agent) + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, verb_s) + } + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── intonation (zh, es, ar, ru, sw) — statement order, "?" added by caller ─ + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + if !str_eq(location, "") { + return core + " " + location + } + return core +} + +// ── Capitalization and punctuation ──────────────────────────────────────────── + +fn capitalize_first(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { + return s + } + let first: String = str_slice(s, 0, 1) + let rest: String = str_slice(s, 1, n) + return str_to_upper(first) + rest +} + +fn add_punct(s: String, intent: String) -> String { + if str_eq(intent, "question") { return s + "?" } + return s + "." +} + +// ── Main realization entry point ────────────────────────────────────────────── + +fn realize_lang(form: [String], profile: [String]) -> String { + let intent: String = slots_get(form, "intent") + let agent: String = slots_get(form, "agent") + let predicate: String = slots_get(form, "predicate") + let patient: String = slots_get(form, "patient") + let location: String = slots_get(form, "location") + let tense_raw: String = slots_get(form, "tense") + let aspect_raw: String= slots_get(form, "aspect") + + let tense: String = tense_raw + if str_eq(tense, "") { let tense = "present" } + let aspect: String = aspect_raw + if str_eq(aspect, "") { let aspect = "simple" } + + let person: String = agent_person(agent) + let number: String = agent_number(agent) + + // ── Command (imperative) ────────────────────────────────────────────────── + if str_eq(intent, "command") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "command") + } + + // ── Question ────────────────────────────────────────────────────────────── + if str_eq(intent, "question") { + let surface: String = realize_question_lang(predicate, tense, aspect, person, number, agent, patient, location, profile) + return add_punct(capitalize_first(surface), "question") + } + + // ── Assertion (declarative) ─────────────────────────────────────────────── + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_surf: String = native_list_get(vp_pair, 0) + let aux_surf: String = native_list_get(vp_pair, 1) + + let vp_str: String = gram_build_vp(verb_surf, aux_surf, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, core) + if !str_eq(location, "") { + let parts = native_list_append(parts, location) + } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "assert") +} + +// realize: backward-compatible English entry point (original signature). +fn realize(form: [String]) -> String { + let lang_code: String = slots_get(form, "lang") + if str_eq(lang_code, "") { + return realize_lang(form, lang_default()) + } + return realize_lang(form, lang_from_code(lang_code)) +} diff --git a/elp/src/realizer_onlyLP.el b/elp/src/realizer_onlyLP.el new file mode 100644 index 0000000..f74684e --- /dev/null +++ b/elp/src/realizer_onlyLP.el @@ -0,0 +1,311 @@ +// realizer.el - Universal syntactic realizer: GramSpec -> surface text. +// +// The realizer is now language-agnostic. It reads the "lang" field from the +// GramSpec to resolve a language profile, then dispatches word order, question +// formation, and morphology through the engine functions in grammar.el and +// morphology.el. +// +// English remains the default (backward compatible) when no "lang" key is set. +// +// Realization pipeline per call: +// 1. Extract lang code -> resolve profile +// 2. Extract agent, predicate, patient, location, tense, aspect, intent +// 3. Compute person/number from agent (English heuristic; other languages TBD) +// 4. Build VP: morph_conjugate with profile -> get verb and auxiliary surface +// 5. Choose question strategy from gram_question_strategy(profile) +// 6. Order constituents via gram_order_constituents(subj, verb, obj, profile) +// 7. Capitalize and terminate +// +// Depends on: morphology (morph_conjugate, agree_determiner) +// grammar (gram_order_constituents, gram_question_strategy, +// gram_build_vp, build_np, build_pp, slots_get) +// language-profile (lang_from_code, lang_get, ...) +import "language-profile.el" + +// ── Agent agreement analysis ────────────────────────────────────────────────── +// +// Person and number are inferred from English pronouns. For other languages +// the grammatical person/number should come from the Engram vocabulary node +// for the subject; here we use a heuristic that is correct for English and +// passable for languages where the same pronoun strings are used. + +fn agent_person(agent: String) -> String { + if str_eq(agent, "I") { return "first" } + if str_eq(agent, "me") { return "first" } + if str_eq(agent, "we") { return "first" } + if str_eq(agent, "us") { return "first" } + if str_eq(agent, "you") { return "second" } + return "third" +} + +fn agent_number(agent: String) -> String { + if str_eq(agent, "I") { return "singular" } + if str_eq(agent, "me") { return "singular" } + if str_eq(agent, "he") { return "singular" } + if str_eq(agent, "him") { return "singular" } + if str_eq(agent, "she") { return "singular" } + if str_eq(agent, "her") { return "singular" } + if str_eq(agent, "it") { return "singular" } + if str_eq(agent, "you") { return "singular" } + if str_eq(agent, "we") { return "plural" } + if str_eq(agent, "us") { return "plural" } + if str_eq(agent, "they") { return "plural" } + if str_eq(agent, "them") { return "plural" } + return "singular" +} + +// ── NP realization ──────────────────────────────────────────────────────────── + +fn realize_np(referent: String, number: String) -> String { + return referent +} + +// ── VP realization ──────────────────────────────────────────────────────────── +// +// Returns [main_verb_surface, aux_surface_or_empty]. +// Delegates conjugation to morph_conjugate with the language profile. + +fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: [String]) -> [String] { + let empty_aux: String = "" + + if str_eq(tense, "future") { + // Future: modal "will" + base (English) or language-specific future marker. + // For isolating/agglutinative languages the future marker is also the + // base form (morph_conjugate returns base); the surface "will" only appears + // for English because morph_conjugate("be", "future", ..., en_profile) = "will be". + let code: String = lang_get(profile, "code") + if str_eq(code, "en") { + let result: [String] = native_list_empty() + let result = native_list_append(result, base_verb) + let result = native_list_append(result, "will") + return result + } + // Other languages: conjugate normally (engine returns base form for + // languages without loaded Engram suffix data). + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result + } + + if str_eq(aspect, "progressive") { + let gerund: String = morph_conjugate(base_verb, "progressive", person, number, profile) + let be_aux: String = morph_conjugate("be", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, gerund) + let result = native_list_append(result, be_aux) + return result + } + + if str_eq(aspect, "perfect") { + let pp: String = morph_conjugate(base_verb, "perfect", person, number, profile) + let have_form: String = morph_conjugate("have", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, pp) + let result = native_list_append(result, have_form) + return result + } + + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result +} + +// ── Question formation ──────────────────────────────────────────────────────── +// +// Strategy is resolved from gram_question_strategy(profile): +// +// "do-support" (en) - insert conjugated "do" before subject; verb stays base. +// "particle" (ja, hi, fi) - statement order + sentence-final question particle. +// "intonation" (zh, es, ar, ru, sw) - statement order + "?" punctuation only. +// "inversion" (fr, de) - subject-verb inversion. + +// realize_question_lang: build the question surface string for any language. +// Returns the complete surface string (without final punctuation). + +fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: [String]) -> String { + let strategy: String = gram_question_strategy(profile) + let code: String = lang_get(profile, "code") + + // ── do-support (English) ────────────────────────────────────────────────── + if str_eq(strategy, "do-support") { + if str_eq(aspect, "progressive") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "progressive", person, number, profile) + let gerund: String = native_list_get(vp_pair, 0) + let be_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, gerund) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + if str_eq(aspect, "perfect") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "perfect", person, number, profile) + let pp: String = native_list_get(vp_pair, 0) + let have_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, have_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, pp) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // Simple: do-support + if str_eq(predicate, "be") { + let be_form: String = morph_conjugate("be", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_form) + let parts = native_list_append(parts, agent) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + let do_form: String = morph_conjugate("do", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, do_form) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── particle (ja, hi, fi) ───────────────────────────────────────────────── + // Build in statement order, then append the question particle. + if str_eq(strategy, "particle") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + let loc_part: String = "" + if !str_eq(location, "") { + let loc_part = core + " " + location + } else { + let loc_part = core + } + // Language-specific question particles + if str_eq(code, "ja") { return loc_part + " か" } + if str_eq(code, "hi") { return loc_part + " क्या" } + if str_eq(code, "fi") { return loc_part + "-ko" } + return loc_part + "?" + } + + // ── inversion (fr, de) ──────────────────────────────────────────────────── + if str_eq(strategy, "inversion") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + // Inversion: Verb-Subject-Object order + let parts: [String] = native_list_empty() + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, aux_s) + } else { + let parts = native_list_append(parts, verb_s) + } + let parts = native_list_append(parts, agent) + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, verb_s) + } + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── intonation (zh, es, ar, ru, sw) — statement order, "?" added by caller ─ + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + if !str_eq(location, "") { + return core + " " + location + } + return core +} + +// ── Capitalization and punctuation ──────────────────────────────────────────── + +fn capitalize_first(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { + return s + } + let first: String = str_slice(s, 0, 1) + let rest: String = str_slice(s, 1, n) + return str_to_upper(first) + rest +} + +fn add_punct(s: String, intent: String) -> String { + if str_eq(intent, "question") { return s + "?" } + return s + "." +} + +// ── Main realization entry point ────────────────────────────────────────────── + +fn realize_lang(form: [String], profile: [String]) -> String { + let intent: String = slots_get(form, "intent") + let agent: String = slots_get(form, "agent") + let predicate: String = slots_get(form, "predicate") + let patient: String = slots_get(form, "patient") + let location: String = slots_get(form, "location") + let tense_raw: String = slots_get(form, "tense") + let aspect_raw: String= slots_get(form, "aspect") + + let tense: String = tense_raw + if str_eq(tense, "") { let tense = "present" } + let aspect: String = aspect_raw + if str_eq(aspect, "") { let aspect = "simple" } + + let person: String = agent_person(agent) + let number: String = agent_number(agent) + + // ── Command (imperative) ────────────────────────────────────────────────── + if str_eq(intent, "command") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "command") + } + + // ── Question ────────────────────────────────────────────────────────────── + if str_eq(intent, "question") { + let surface: String = realize_question_lang(predicate, tense, aspect, person, number, agent, patient, location, profile) + return add_punct(capitalize_first(surface), "question") + } + + // ── Assertion (declarative) ─────────────────────────────────────────────── + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_surf: String = native_list_get(vp_pair, 0) + let aux_surf: String = native_list_get(vp_pair, 1) + + let vp_str: String = gram_build_vp(verb_surf, aux_surf, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, core) + if !str_eq(location, "") { + let parts = native_list_append(parts, location) + } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "assert") +} + +// realize: backward-compatible English entry point (original signature). +fn realize(form: [String]) -> String { + let lang_code: String = slots_get(form, "lang") + if str_eq(lang_code, "") { + return realize_lang(form, lang_default()) + } + return realize_lang(form, lang_from_code(lang_code)) +} diff --git a/elp/src/realizer_onlygram.el b/elp/src/realizer_onlygram.el new file mode 100644 index 0000000..da47879 --- /dev/null +++ b/elp/src/realizer_onlygram.el @@ -0,0 +1,311 @@ +// realizer.el - Universal syntactic realizer: GramSpec -> surface text. +// +// The realizer is now language-agnostic. It reads the "lang" field from the +// GramSpec to resolve a language profile, then dispatches word order, question +// formation, and morphology through the engine functions in grammar.el and +// morphology.el. +// +// English remains the default (backward compatible) when no "lang" key is set. +// +// Realization pipeline per call: +// 1. Extract lang code -> resolve profile +// 2. Extract agent, predicate, patient, location, tense, aspect, intent +// 3. Compute person/number from agent (English heuristic; other languages TBD) +// 4. Build VP: morph_conjugate with profile -> get verb and auxiliary surface +// 5. Choose question strategy from gram_question_strategy(profile) +// 6. Order constituents via gram_order_constituents(subj, verb, obj, profile) +// 7. Capitalize and terminate +// +// Depends on: morphology (morph_conjugate, agree_determiner) +// grammar (gram_order_constituents, gram_question_strategy, +// gram_build_vp, build_np, build_pp, slots_get) +// language-profile (lang_from_code, lang_get, ...) +import "grammar.el" + +// ── Agent agreement analysis ────────────────────────────────────────────────── +// +// Person and number are inferred from English pronouns. For other languages +// the grammatical person/number should come from the Engram vocabulary node +// for the subject; here we use a heuristic that is correct for English and +// passable for languages where the same pronoun strings are used. + +fn agent_person(agent: String) -> String { + if str_eq(agent, "I") { return "first" } + if str_eq(agent, "me") { return "first" } + if str_eq(agent, "we") { return "first" } + if str_eq(agent, "us") { return "first" } + if str_eq(agent, "you") { return "second" } + return "third" +} + +fn agent_number(agent: String) -> String { + if str_eq(agent, "I") { return "singular" } + if str_eq(agent, "me") { return "singular" } + if str_eq(agent, "he") { return "singular" } + if str_eq(agent, "him") { return "singular" } + if str_eq(agent, "she") { return "singular" } + if str_eq(agent, "her") { return "singular" } + if str_eq(agent, "it") { return "singular" } + if str_eq(agent, "you") { return "singular" } + if str_eq(agent, "we") { return "plural" } + if str_eq(agent, "us") { return "plural" } + if str_eq(agent, "they") { return "plural" } + if str_eq(agent, "them") { return "plural" } + return "singular" +} + +// ── NP realization ──────────────────────────────────────────────────────────── + +fn realize_np(referent: String, number: String) -> String { + return referent +} + +// ── VP realization ──────────────────────────────────────────────────────────── +// +// Returns [main_verb_surface, aux_surface_or_empty]. +// Delegates conjugation to morph_conjugate with the language profile. + +fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: [String]) -> [String] { + let empty_aux: String = "" + + if str_eq(tense, "future") { + // Future: modal "will" + base (English) or language-specific future marker. + // For isolating/agglutinative languages the future marker is also the + // base form (morph_conjugate returns base); the surface "will" only appears + // for English because morph_conjugate("be", "future", ..., en_profile) = "will be". + let code: String = lang_get(profile, "code") + if str_eq(code, "en") { + let result: [String] = native_list_empty() + let result = native_list_append(result, base_verb) + let result = native_list_append(result, "will") + return result + } + // Other languages: conjugate normally (engine returns base form for + // languages without loaded Engram suffix data). + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result + } + + if str_eq(aspect, "progressive") { + let gerund: String = morph_conjugate(base_verb, "progressive", person, number, profile) + let be_aux: String = morph_conjugate("be", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, gerund) + let result = native_list_append(result, be_aux) + return result + } + + if str_eq(aspect, "perfect") { + let pp: String = morph_conjugate(base_verb, "perfect", person, number, profile) + let have_form: String = morph_conjugate("have", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, pp) + let result = native_list_append(result, have_form) + return result + } + + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result +} + +// ── Question formation ──────────────────────────────────────────────────────── +// +// Strategy is resolved from gram_question_strategy(profile): +// +// "do-support" (en) - insert conjugated "do" before subject; verb stays base. +// "particle" (ja, hi, fi) - statement order + sentence-final question particle. +// "intonation" (zh, es, ar, ru, sw) - statement order + "?" punctuation only. +// "inversion" (fr, de) - subject-verb inversion. + +// realize_question_lang: build the question surface string for any language. +// Returns the complete surface string (without final punctuation). + +fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: [String]) -> String { + let strategy: String = gram_question_strategy(profile) + let code: String = lang_get(profile, "code") + + // ── do-support (English) ────────────────────────────────────────────────── + if str_eq(strategy, "do-support") { + if str_eq(aspect, "progressive") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "progressive", person, number, profile) + let gerund: String = native_list_get(vp_pair, 0) + let be_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, gerund) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + if str_eq(aspect, "perfect") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "perfect", person, number, profile) + let pp: String = native_list_get(vp_pair, 0) + let have_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, have_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, pp) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // Simple: do-support + if str_eq(predicate, "be") { + let be_form: String = morph_conjugate("be", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_form) + let parts = native_list_append(parts, agent) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + let do_form: String = morph_conjugate("do", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, do_form) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── particle (ja, hi, fi) ───────────────────────────────────────────────── + // Build in statement order, then append the question particle. + if str_eq(strategy, "particle") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + let loc_part: String = "" + if !str_eq(location, "") { + let loc_part = core + " " + location + } else { + let loc_part = core + } + // Language-specific question particles + if str_eq(code, "ja") { return loc_part + " か" } + if str_eq(code, "hi") { return loc_part + " क्या" } + if str_eq(code, "fi") { return loc_part + "-ko" } + return loc_part + "?" + } + + // ── inversion (fr, de) ──────────────────────────────────────────────────── + if str_eq(strategy, "inversion") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + // Inversion: Verb-Subject-Object order + let parts: [String] = native_list_empty() + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, aux_s) + } else { + let parts = native_list_append(parts, verb_s) + } + let parts = native_list_append(parts, agent) + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, verb_s) + } + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── intonation (zh, es, ar, ru, sw) — statement order, "?" added by caller ─ + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + if !str_eq(location, "") { + return core + " " + location + } + return core +} + +// ── Capitalization and punctuation ──────────────────────────────────────────── + +fn capitalize_first(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { + return s + } + let first: String = str_slice(s, 0, 1) + let rest: String = str_slice(s, 1, n) + return str_to_upper(first) + rest +} + +fn add_punct(s: String, intent: String) -> String { + if str_eq(intent, "question") { return s + "?" } + return s + "." +} + +// ── Main realization entry point ────────────────────────────────────────────── + +fn realize_lang(form: [String], profile: [String]) -> String { + let intent: String = slots_get(form, "intent") + let agent: String = slots_get(form, "agent") + let predicate: String = slots_get(form, "predicate") + let patient: String = slots_get(form, "patient") + let location: String = slots_get(form, "location") + let tense_raw: String = slots_get(form, "tense") + let aspect_raw: String= slots_get(form, "aspect") + + let tense: String = tense_raw + if str_eq(tense, "") { let tense = "present" } + let aspect: String = aspect_raw + if str_eq(aspect, "") { let aspect = "simple" } + + let person: String = agent_person(agent) + let number: String = agent_number(agent) + + // ── Command (imperative) ────────────────────────────────────────────────── + if str_eq(intent, "command") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "command") + } + + // ── Question ────────────────────────────────────────────────────────────── + if str_eq(intent, "question") { + let surface: String = realize_question_lang(predicate, tense, aspect, person, number, agent, patient, location, profile) + return add_punct(capitalize_first(surface), "question") + } + + // ── Assertion (declarative) ─────────────────────────────────────────────── + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_surf: String = native_list_get(vp_pair, 0) + let aux_surf: String = native_list_get(vp_pair, 1) + + let vp_str: String = gram_build_vp(verb_surf, aux_surf, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, core) + if !str_eq(location, "") { + let parts = native_list_append(parts, location) + } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "assert") +} + +// realize: backward-compatible English entry point (original signature). +fn realize(form: [String]) -> String { + let lang_code: String = slots_get(form, "lang") + if str_eq(lang_code, "") { + return realize_lang(form, lang_default()) + } + return realize_lang(form, lang_from_code(lang_code)) +} diff --git a/elp/src/realizer_onlymorph.el b/elp/src/realizer_onlymorph.el new file mode 100644 index 0000000..e18e2a5 --- /dev/null +++ b/elp/src/realizer_onlymorph.el @@ -0,0 +1,311 @@ +// realizer.el - Universal syntactic realizer: GramSpec -> surface text. +// +// The realizer is now language-agnostic. It reads the "lang" field from the +// GramSpec to resolve a language profile, then dispatches word order, question +// formation, and morphology through the engine functions in grammar.el and +// morphology.el. +// +// English remains the default (backward compatible) when no "lang" key is set. +// +// Realization pipeline per call: +// 1. Extract lang code -> resolve profile +// 2. Extract agent, predicate, patient, location, tense, aspect, intent +// 3. Compute person/number from agent (English heuristic; other languages TBD) +// 4. Build VP: morph_conjugate with profile -> get verb and auxiliary surface +// 5. Choose question strategy from gram_question_strategy(profile) +// 6. Order constituents via gram_order_constituents(subj, verb, obj, profile) +// 7. Capitalize and terminate +// +// Depends on: morphology (morph_conjugate, agree_determiner) +// grammar (gram_order_constituents, gram_question_strategy, +// gram_build_vp, build_np, build_pp, slots_get) +// language-profile (lang_from_code, lang_get, ...) +import "morphology.el" + +// ── Agent agreement analysis ────────────────────────────────────────────────── +// +// Person and number are inferred from English pronouns. For other languages +// the grammatical person/number should come from the Engram vocabulary node +// for the subject; here we use a heuristic that is correct for English and +// passable for languages where the same pronoun strings are used. + +fn agent_person(agent: String) -> String { + if str_eq(agent, "I") { return "first" } + if str_eq(agent, "me") { return "first" } + if str_eq(agent, "we") { return "first" } + if str_eq(agent, "us") { return "first" } + if str_eq(agent, "you") { return "second" } + return "third" +} + +fn agent_number(agent: String) -> String { + if str_eq(agent, "I") { return "singular" } + if str_eq(agent, "me") { return "singular" } + if str_eq(agent, "he") { return "singular" } + if str_eq(agent, "him") { return "singular" } + if str_eq(agent, "she") { return "singular" } + if str_eq(agent, "her") { return "singular" } + if str_eq(agent, "it") { return "singular" } + if str_eq(agent, "you") { return "singular" } + if str_eq(agent, "we") { return "plural" } + if str_eq(agent, "us") { return "plural" } + if str_eq(agent, "they") { return "plural" } + if str_eq(agent, "them") { return "plural" } + return "singular" +} + +// ── NP realization ──────────────────────────────────────────────────────────── + +fn realize_np(referent: String, number: String) -> String { + return referent +} + +// ── VP realization ──────────────────────────────────────────────────────────── +// +// Returns [main_verb_surface, aux_surface_or_empty]. +// Delegates conjugation to morph_conjugate with the language profile. + +fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: [String]) -> [String] { + let empty_aux: String = "" + + if str_eq(tense, "future") { + // Future: modal "will" + base (English) or language-specific future marker. + // For isolating/agglutinative languages the future marker is also the + // base form (morph_conjugate returns base); the surface "will" only appears + // for English because morph_conjugate("be", "future", ..., en_profile) = "will be". + let code: String = lang_get(profile, "code") + if str_eq(code, "en") { + let result: [String] = native_list_empty() + let result = native_list_append(result, base_verb) + let result = native_list_append(result, "will") + return result + } + // Other languages: conjugate normally (engine returns base form for + // languages without loaded Engram suffix data). + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result + } + + if str_eq(aspect, "progressive") { + let gerund: String = morph_conjugate(base_verb, "progressive", person, number, profile) + let be_aux: String = morph_conjugate("be", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, gerund) + let result = native_list_append(result, be_aux) + return result + } + + if str_eq(aspect, "perfect") { + let pp: String = morph_conjugate(base_verb, "perfect", person, number, profile) + let have_form: String = morph_conjugate("have", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, pp) + let result = native_list_append(result, have_form) + return result + } + + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result +} + +// ── Question formation ──────────────────────────────────────────────────────── +// +// Strategy is resolved from gram_question_strategy(profile): +// +// "do-support" (en) - insert conjugated "do" before subject; verb stays base. +// "particle" (ja, hi, fi) - statement order + sentence-final question particle. +// "intonation" (zh, es, ar, ru, sw) - statement order + "?" punctuation only. +// "inversion" (fr, de) - subject-verb inversion. + +// realize_question_lang: build the question surface string for any language. +// Returns the complete surface string (without final punctuation). + +fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: [String]) -> String { + let strategy: String = gram_question_strategy(profile) + let code: String = lang_get(profile, "code") + + // ── do-support (English) ────────────────────────────────────────────────── + if str_eq(strategy, "do-support") { + if str_eq(aspect, "progressive") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "progressive", person, number, profile) + let gerund: String = native_list_get(vp_pair, 0) + let be_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, gerund) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + if str_eq(aspect, "perfect") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "perfect", person, number, profile) + let pp: String = native_list_get(vp_pair, 0) + let have_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, have_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, pp) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // Simple: do-support + if str_eq(predicate, "be") { + let be_form: String = morph_conjugate("be", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_form) + let parts = native_list_append(parts, agent) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + let do_form: String = morph_conjugate("do", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, do_form) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── particle (ja, hi, fi) ───────────────────────────────────────────────── + // Build in statement order, then append the question particle. + if str_eq(strategy, "particle") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + let loc_part: String = "" + if !str_eq(location, "") { + let loc_part = core + " " + location + } else { + let loc_part = core + } + // Language-specific question particles + if str_eq(code, "ja") { return loc_part + " か" } + if str_eq(code, "hi") { return loc_part + " क्या" } + if str_eq(code, "fi") { return loc_part + "-ko" } + return loc_part + "?" + } + + // ── inversion (fr, de) ──────────────────────────────────────────────────── + if str_eq(strategy, "inversion") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + // Inversion: Verb-Subject-Object order + let parts: [String] = native_list_empty() + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, aux_s) + } else { + let parts = native_list_append(parts, verb_s) + } + let parts = native_list_append(parts, agent) + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, verb_s) + } + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── intonation (zh, es, ar, ru, sw) — statement order, "?" added by caller ─ + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + if !str_eq(location, "") { + return core + " " + location + } + return core +} + +// ── Capitalization and punctuation ──────────────────────────────────────────── + +fn capitalize_first(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { + return s + } + let first: String = str_slice(s, 0, 1) + let rest: String = str_slice(s, 1, n) + return str_to_upper(first) + rest +} + +fn add_punct(s: String, intent: String) -> String { + if str_eq(intent, "question") { return s + "?" } + return s + "." +} + +// ── Main realization entry point ────────────────────────────────────────────── + +fn realize_lang(form: [String], profile: [String]) -> String { + let intent: String = slots_get(form, "intent") + let agent: String = slots_get(form, "agent") + let predicate: String = slots_get(form, "predicate") + let patient: String = slots_get(form, "patient") + let location: String = slots_get(form, "location") + let tense_raw: String = slots_get(form, "tense") + let aspect_raw: String= slots_get(form, "aspect") + + let tense: String = tense_raw + if str_eq(tense, "") { let tense = "present" } + let aspect: String = aspect_raw + if str_eq(aspect, "") { let aspect = "simple" } + + let person: String = agent_person(agent) + let number: String = agent_number(agent) + + // ── Command (imperative) ────────────────────────────────────────────────── + if str_eq(intent, "command") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "command") + } + + // ── Question ────────────────────────────────────────────────────────────── + if str_eq(intent, "question") { + let surface: String = realize_question_lang(predicate, tense, aspect, person, number, agent, patient, location, profile) + return add_punct(capitalize_first(surface), "question") + } + + // ── Assertion (declarative) ─────────────────────────────────────────────── + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_surf: String = native_list_get(vp_pair, 0) + let aux_surf: String = native_list_get(vp_pair, 1) + + let vp_str: String = gram_build_vp(verb_surf, aux_surf, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, core) + if !str_eq(location, "") { + let parts = native_list_append(parts, location) + } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "assert") +} + +// realize: backward-compatible English entry point (original signature). +fn realize(form: [String]) -> String { + let lang_code: String = slots_get(form, "lang") + if str_eq(lang_code, "") { + return realize_lang(form, lang_default()) + } + return realize_lang(form, lang_from_code(lang_code)) +} diff --git a/elp/src/realizer_tinygram.el b/elp/src/realizer_tinygram.el new file mode 100644 index 0000000..e016902 --- /dev/null +++ b/elp/src/realizer_tinygram.el @@ -0,0 +1,313 @@ +// realizer.el - Universal syntactic realizer: GramSpec -> surface text. +// +// The realizer is now language-agnostic. It reads the "lang" field from the +// GramSpec to resolve a language profile, then dispatches word order, question +// formation, and morphology through the engine functions in grammar.el and +// morphology.el. +// +// English remains the default (backward compatible) when no "lang" key is set. +// +// Realization pipeline per call: +// 1. Extract lang code -> resolve profile +// 2. Extract agent, predicate, patient, location, tense, aspect, intent +// 3. Compute person/number from agent (English heuristic; other languages TBD) +// 4. Build VP: morph_conjugate with profile -> get verb and auxiliary surface +// 5. Choose question strategy from gram_question_strategy(profile) +// 6. Order constituents via gram_order_constituents(subj, verb, obj, profile) +// 7. Capitalize and terminate +// +// Depends on: morphology (morph_conjugate, agree_determiner) +// grammar (gram_order_constituents, gram_question_strategy, +// gram_build_vp, build_np, build_pp, slots_get) +// language-profile (lang_from_code, lang_get, ...) +import "language-profile.el" +import "morphology.el" +import "test_tiny_gram.el" + +// ── Agent agreement analysis ────────────────────────────────────────────────── +// +// Person and number are inferred from English pronouns. For other languages +// the grammatical person/number should come from the Engram vocabulary node +// for the subject; here we use a heuristic that is correct for English and +// passable for languages where the same pronoun strings are used. + +fn agent_person(agent: String) -> String { + if str_eq(agent, "I") { return "first" } + if str_eq(agent, "me") { return "first" } + if str_eq(agent, "we") { return "first" } + if str_eq(agent, "us") { return "first" } + if str_eq(agent, "you") { return "second" } + return "third" +} + +fn agent_number(agent: String) -> String { + if str_eq(agent, "I") { return "singular" } + if str_eq(agent, "me") { return "singular" } + if str_eq(agent, "he") { return "singular" } + if str_eq(agent, "him") { return "singular" } + if str_eq(agent, "she") { return "singular" } + if str_eq(agent, "her") { return "singular" } + if str_eq(agent, "it") { return "singular" } + if str_eq(agent, "you") { return "singular" } + if str_eq(agent, "we") { return "plural" } + if str_eq(agent, "us") { return "plural" } + if str_eq(agent, "they") { return "plural" } + if str_eq(agent, "them") { return "plural" } + return "singular" +} + +// ── NP realization ──────────────────────────────────────────────────────────── + +fn realize_np(referent: String, number: String) -> String { + return referent +} + +// ── VP realization ──────────────────────────────────────────────────────────── +// +// Returns [main_verb_surface, aux_surface_or_empty]. +// Delegates conjugation to morph_conjugate with the language profile. + +fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: [String]) -> [String] { + let empty_aux: String = "" + + if str_eq(tense, "future") { + // Future: modal "will" + base (English) or language-specific future marker. + // For isolating/agglutinative languages the future marker is also the + // base form (morph_conjugate returns base); the surface "will" only appears + // for English because morph_conjugate("be", "future", ..., en_profile) = "will be". + let code: String = lang_get(profile, "code") + if str_eq(code, "en") { + let result: [String] = native_list_empty() + let result = native_list_append(result, base_verb) + let result = native_list_append(result, "will") + return result + } + // Other languages: conjugate normally (engine returns base form for + // languages without loaded Engram suffix data). + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result + } + + if str_eq(aspect, "progressive") { + let gerund: String = morph_conjugate(base_verb, "progressive", person, number, profile) + let be_aux: String = morph_conjugate("be", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, gerund) + let result = native_list_append(result, be_aux) + return result + } + + if str_eq(aspect, "perfect") { + let pp: String = morph_conjugate(base_verb, "perfect", person, number, profile) + let have_form: String = morph_conjugate("have", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, pp) + let result = native_list_append(result, have_form) + return result + } + + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result +} + +// ── Question formation ──────────────────────────────────────────────────────── +// +// Strategy is resolved from gram_question_strategy(profile): +// +// "do-support" (en) - insert conjugated "do" before subject; verb stays base. +// "particle" (ja, hi, fi) - statement order + sentence-final question particle. +// "intonation" (zh, es, ar, ru, sw) - statement order + "?" punctuation only. +// "inversion" (fr, de) - subject-verb inversion. + +// realize_question_lang: build the question surface string for any language. +// Returns the complete surface string (without final punctuation). + +fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: [String]) -> String { + let strategy: String = gram_question_strategy(profile) + let code: String = lang_get(profile, "code") + + // ── do-support (English) ────────────────────────────────────────────────── + if str_eq(strategy, "do-support") { + if str_eq(aspect, "progressive") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "progressive", person, number, profile) + let gerund: String = native_list_get(vp_pair, 0) + let be_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, gerund) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + if str_eq(aspect, "perfect") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "perfect", person, number, profile) + let pp: String = native_list_get(vp_pair, 0) + let have_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, have_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, pp) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // Simple: do-support + if str_eq(predicate, "be") { + let be_form: String = morph_conjugate("be", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_form) + let parts = native_list_append(parts, agent) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + let do_form: String = morph_conjugate("do", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, do_form) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── particle (ja, hi, fi) ───────────────────────────────────────────────── + // Build in statement order, then append the question particle. + if str_eq(strategy, "particle") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + let loc_part: String = "" + if !str_eq(location, "") { + let loc_part = core + " " + location + } else { + let loc_part = core + } + // Language-specific question particles + if str_eq(code, "ja") { return loc_part + " か" } + if str_eq(code, "hi") { return loc_part + " क्या" } + if str_eq(code, "fi") { return loc_part + "-ko" } + return loc_part + "?" + } + + // ── inversion (fr, de) ──────────────────────────────────────────────────── + if str_eq(strategy, "inversion") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + // Inversion: Verb-Subject-Object order + let parts: [String] = native_list_empty() + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, aux_s) + } else { + let parts = native_list_append(parts, verb_s) + } + let parts = native_list_append(parts, agent) + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, verb_s) + } + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── intonation (zh, es, ar, ru, sw) — statement order, "?" added by caller ─ + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + if !str_eq(location, "") { + return core + " " + location + } + return core +} + +// ── Capitalization and punctuation ──────────────────────────────────────────── + +fn capitalize_first(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { + return s + } + let first: String = str_slice(s, 0, 1) + let rest: String = str_slice(s, 1, n) + return str_to_upper(first) + rest +} + +fn add_punct(s: String, intent: String) -> String { + if str_eq(intent, "question") { return s + "?" } + return s + "." +} + +// ── Main realization entry point ────────────────────────────────────────────── + +fn realize_lang(form: [String], profile: [String]) -> String { + let intent: String = slots_get(form, "intent") + let agent: String = slots_get(form, "agent") + let predicate: String = slots_get(form, "predicate") + let patient: String = slots_get(form, "patient") + let location: String = slots_get(form, "location") + let tense_raw: String = slots_get(form, "tense") + let aspect_raw: String= slots_get(form, "aspect") + + let tense: String = tense_raw + if str_eq(tense, "") { let tense = "present" } + let aspect: String = aspect_raw + if str_eq(aspect, "") { let aspect = "simple" } + + let person: String = agent_person(agent) + let number: String = agent_number(agent) + + // ── Command (imperative) ────────────────────────────────────────────────── + if str_eq(intent, "command") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "command") + } + + // ── Question ────────────────────────────────────────────────────────────── + if str_eq(intent, "question") { + let surface: String = realize_question_lang(predicate, tense, aspect, person, number, agent, patient, location, profile) + return add_punct(capitalize_first(surface), "question") + } + + // ── Assertion (declarative) ─────────────────────────────────────────────── + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_surf: String = native_list_get(vp_pair, 0) + let aux_surf: String = native_list_get(vp_pair, 1) + + let vp_str: String = gram_build_vp(verb_surf, aux_surf, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, core) + if !str_eq(location, "") { + let parts = native_list_append(parts, location) + } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "assert") +} + +// realize: backward-compatible English entry point (original signature). +fn realize(form: [String]) -> String { + let lang_code: String = slots_get(form, "lang") + if str_eq(lang_code, "") { + return realize_lang(form, lang_default()) + } + return realize_lang(form, lang_from_code(lang_code)) +} diff --git a/elp/src/realizer_twodedup.el b/elp/src/realizer_twodedup.el new file mode 100644 index 0000000..9b8cb9f --- /dev/null +++ b/elp/src/realizer_twodedup.el @@ -0,0 +1,313 @@ +// realizer.el - Universal syntactic realizer: GramSpec -> surface text. +// +// The realizer is now language-agnostic. It reads the "lang" field from the +// GramSpec to resolve a language profile, then dispatches word order, question +// formation, and morphology through the engine functions in grammar.el and +// morphology.el. +// +// English remains the default (backward compatible) when no "lang" key is set. +// +// Realization pipeline per call: +// 1. Extract lang code -> resolve profile +// 2. Extract agent, predicate, patient, location, tense, aspect, intent +// 3. Compute person/number from agent (English heuristic; other languages TBD) +// 4. Build VP: morph_conjugate with profile -> get verb and auxiliary surface +// 5. Choose question strategy from gram_question_strategy(profile) +// 6. Order constituents via gram_order_constituents(subj, verb, obj, profile) +// 7. Capitalize and terminate +// +// Depends on: morphology (morph_conjugate, agree_determiner) +// grammar (gram_order_constituents, gram_question_strategy, +// gram_build_vp, build_np, build_pp, slots_get) +// language-profile (lang_from_code, lang_get, ...) +import "language-profile.el" +import "ext_a.el" +import "ext_b.el" + +// ── Agent agreement analysis ────────────────────────────────────────────────── +// +// Person and number are inferred from English pronouns. For other languages +// the grammatical person/number should come from the Engram vocabulary node +// for the subject; here we use a heuristic that is correct for English and +// passable for languages where the same pronoun strings are used. + +fn agent_person(agent: String) -> String { + if str_eq(agent, "I") { return "first" } + if str_eq(agent, "me") { return "first" } + if str_eq(agent, "we") { return "first" } + if str_eq(agent, "us") { return "first" } + if str_eq(agent, "you") { return "second" } + return "third" +} + +fn agent_number(agent: String) -> String { + if str_eq(agent, "I") { return "singular" } + if str_eq(agent, "me") { return "singular" } + if str_eq(agent, "he") { return "singular" } + if str_eq(agent, "him") { return "singular" } + if str_eq(agent, "she") { return "singular" } + if str_eq(agent, "her") { return "singular" } + if str_eq(agent, "it") { return "singular" } + if str_eq(agent, "you") { return "singular" } + if str_eq(agent, "we") { return "plural" } + if str_eq(agent, "us") { return "plural" } + if str_eq(agent, "they") { return "plural" } + if str_eq(agent, "them") { return "plural" } + return "singular" +} + +// ── NP realization ──────────────────────────────────────────────────────────── + +fn realize_np(referent: String, number: String) -> String { + return referent +} + +// ── VP realization ──────────────────────────────────────────────────────────── +// +// Returns [main_verb_surface, aux_surface_or_empty]. +// Delegates conjugation to morph_conjugate with the language profile. + +fn realize_vp_lang(base_verb: String, tense: String, aspect: String, person: String, number: String, profile: [String]) -> [String] { + let empty_aux: String = "" + + if str_eq(tense, "future") { + // Future: modal "will" + base (English) or language-specific future marker. + // For isolating/agglutinative languages the future marker is also the + // base form (morph_conjugate returns base); the surface "will" only appears + // for English because morph_conjugate("be", "future", ..., en_profile) = "will be". + let code: String = lang_get(profile, "code") + if str_eq(code, "en") { + let result: [String] = native_list_empty() + let result = native_list_append(result, base_verb) + let result = native_list_append(result, "will") + return result + } + // Other languages: conjugate normally (engine returns base form for + // languages without loaded Engram suffix data). + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result + } + + if str_eq(aspect, "progressive") { + let gerund: String = morph_conjugate(base_verb, "progressive", person, number, profile) + let be_aux: String = morph_conjugate("be", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, gerund) + let result = native_list_append(result, be_aux) + return result + } + + if str_eq(aspect, "perfect") { + let pp: String = morph_conjugate(base_verb, "perfect", person, number, profile) + let have_form: String = morph_conjugate("have", tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, pp) + let result = native_list_append(result, have_form) + return result + } + + let surf: String = morph_conjugate(base_verb, tense, person, number, profile) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result +} + +// ── Question formation ──────────────────────────────────────────────────────── +// +// Strategy is resolved from gram_question_strategy(profile): +// +// "do-support" (en) - insert conjugated "do" before subject; verb stays base. +// "particle" (ja, hi, fi) - statement order + sentence-final question particle. +// "intonation" (zh, es, ar, ru, sw) - statement order + "?" punctuation only. +// "inversion" (fr, de) - subject-verb inversion. + +// realize_question_lang: build the question surface string for any language. +// Returns the complete surface string (without final punctuation). + +fn realize_question_lang(predicate: String, tense: String, aspect: String, person: String, number: String, agent: String, patient: String, location: String, profile: [String]) -> String { + let strategy: String = gram_question_strategy(profile) + let code: String = lang_get(profile, "code") + + // ── do-support (English) ────────────────────────────────────────────────── + if str_eq(strategy, "do-support") { + if str_eq(aspect, "progressive") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "progressive", person, number, profile) + let gerund: String = native_list_get(vp_pair, 0) + let be_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, gerund) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + if str_eq(aspect, "perfect") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, "perfect", person, number, profile) + let pp: String = native_list_get(vp_pair, 0) + let have_aux: String = native_list_get(vp_pair, 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, have_aux) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, pp) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // Simple: do-support + if str_eq(predicate, "be") { + let be_form: String = morph_conjugate("be", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, be_form) + let parts = native_list_append(parts, agent) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + let do_form: String = morph_conjugate("do", tense, person, number, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, do_form) + let parts = native_list_append(parts, agent) + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── particle (ja, hi, fi) ───────────────────────────────────────────────── + // Build in statement order, then append the question particle. + if str_eq(strategy, "particle") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + let loc_part: String = "" + if !str_eq(location, "") { + let loc_part = core + " " + location + } else { + let loc_part = core + } + // Language-specific question particles + if str_eq(code, "ja") { return loc_part + " か" } + if str_eq(code, "hi") { return loc_part + " क्या" } + if str_eq(code, "fi") { return loc_part + "-ko" } + return loc_part + "?" + } + + // ── inversion (fr, de) ──────────────────────────────────────────────────── + if str_eq(strategy, "inversion") { + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + // Inversion: Verb-Subject-Object order + let parts: [String] = native_list_empty() + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, aux_s) + } else { + let parts = native_list_append(parts, verb_s) + } + let parts = native_list_append(parts, agent) + if !str_eq(aux_s, "") { + let parts = native_list_append(parts, verb_s) + } + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") + } + + // ── intonation (zh, es, ar, ru, sw) — statement order, "?" added by caller ─ + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_s: String = native_list_get(vp_pair, 0) + let aux_s: String = native_list_get(vp_pair, 1) + let vp_str: String = gram_build_vp(verb_s, aux_s, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + if !str_eq(location, "") { + return core + " " + location + } + return core +} + +// ── Capitalization and punctuation ──────────────────────────────────────────── + +fn capitalize_first(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { + return s + } + let first: String = str_slice(s, 0, 1) + let rest: String = str_slice(s, 1, n) + return str_to_upper(first) + rest +} + +fn add_punct(s: String, intent: String) -> String { + if str_eq(intent, "question") { return s + "?" } + return s + "." +} + +// ── Main realization entry point ────────────────────────────────────────────── + +fn realize_lang(form: [String], profile: [String]) -> String { + let intent: String = slots_get(form, "intent") + let agent: String = slots_get(form, "agent") + let predicate: String = slots_get(form, "predicate") + let patient: String = slots_get(form, "patient") + let location: String = slots_get(form, "location") + let tense_raw: String = slots_get(form, "tense") + let aspect_raw: String= slots_get(form, "aspect") + + let tense: String = tense_raw + if str_eq(tense, "") { let tense = "present" } + let aspect: String = aspect_raw + if str_eq(aspect, "") { let aspect = "simple" } + + let person: String = agent_person(agent) + let number: String = agent_number(agent) + + // ── Command (imperative) ────────────────────────────────────────────────── + if str_eq(intent, "command") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "command") + } + + // ── Question ────────────────────────────────────────────────────────────── + if str_eq(intent, "question") { + let surface: String = realize_question_lang(predicate, tense, aspect, person, number, agent, patient, location, profile) + return add_punct(capitalize_first(surface), "question") + } + + // ── Assertion (declarative) ─────────────────────────────────────────────── + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_surf: String = native_list_get(vp_pair, 0) + let aux_surf: String = native_list_get(vp_pair, 1) + + let vp_str: String = gram_build_vp(verb_surf, aux_surf, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, core) + if !str_eq(location, "") { + let parts = native_list_append(parts, location) + } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "assert") +} + +// realize: backward-compatible English entry point (original signature). +fn realize(form: [String]) -> String { + let lang_code: String = slots_get(form, "lang") + if str_eq(lang_code, "") { + return realize_lang(form, lang_default()) + } + return realize_lang(form, lang_from_code(lang_code)) +} diff --git a/elp/src/semantics_allempty.el b/elp/src/semantics_allempty.el new file mode 100644 index 0000000..6c07506 --- /dev/null +++ b/elp/src/semantics_allempty.el @@ -0,0 +1,311 @@ +// semantics.el - Semantic layer: SemFrame -> GramSpec (slot map for realizer). +// +// Bridges from intent/meaning representation to the grammar layer. +// A SemFrame is a slot map ([String] key-value list) with these keys: +// +// "intent" - "assert" | "query" | "describe" | "greet" +// "subject" - subject referent (pronoun or noun phrase) +// "object" - object referent (optional, "" if absent) +// "modifiers" - semicolon-separated modifier strings (e.g. "in the park;quickly") +// "lang" - ISO 639-1 language code (optional, defaults to "en") +// +// sem_to_spec converts a SemFrame into a realizer slot map ready for realize(). +// sem_realize is the end-to-end shortcut: frame -> realized text. +// +// All existing function signatures are preserved; new *_lang variants add an +// explicit lang_code parameter. +// +// Depends on: grammar (slots_*, realize), language-profile (lang_from_code) +import "test_empty_gram.el" +import "test_empty_gram.el" +import "test_empty_gram.el" + +// ── SemFrame constructors ───────────────────────────────────────────────────── + +// Build a SemFrame with all four core fields. Language defaults to "en". +fn sem_frame(intent: String, subject: String, obj: String, modifiers: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "intent") + let r = native_list_append(r, intent) + let r = native_list_append(r, "subject") + let r = native_list_append(r, subject) + let r = native_list_append(r, "object") + let r = native_list_append(r, obj) + let r = native_list_append(r, "modifiers") + let r = native_list_append(r, modifiers) + let r = native_list_append(r, "lang") + let r = native_list_append(r, "en") + return r +} + +// Build a SemFrame with an explicit language code. +fn sem_frame_lang(intent: String, subject: String, obj: String, modifiers: String, lang_code: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "intent") + let r = native_list_append(r, intent) + let r = native_list_append(r, "subject") + let r = native_list_append(r, subject) + let r = native_list_append(r, "object") + let r = native_list_append(r, obj) + let r = native_list_append(r, "modifiers") + let r = native_list_append(r, modifiers) + let r = native_list_append(r, "lang") + let r = native_list_append(r, lang_code) + return r +} + +// Convenience: no object, no modifiers, English. +fn sem_frame_simple(intent: String, subject: String) -> [String] { + return sem_frame(intent, subject, "", "") +} + +// Convenience: with object, no modifiers, English. +fn sem_frame_obj(intent: String, subject: String, obj: String) -> [String] { + return sem_frame(intent, subject, obj, "") +} + +// ── SemFrame field accessors ────────────────────────────────────────────────── + +fn sem_intent(frame: [String]) -> String { + return slots_get(frame, "intent") +} + +fn sem_subject(frame: [String]) -> String { + return slots_get(frame, "subject") +} + +fn sem_object(frame: [String]) -> String { + return slots_get(frame, "object") +} + +fn sem_modifiers(frame: [String]) -> String { + return slots_get(frame, "modifiers") +} + +fn sem_lang(frame: [String]) -> String { + let code: String = slots_get(frame, "lang") + if str_eq(code, "") { + return "en" + } + return code +} + +// ── Modifier helpers ────────────────────────────────────────────────────────── + +fn sem_first_modifier(mods: String) -> String { + let n: Int = str_len(mods) + if n == 0 { + return "" + } + let i: Int = 0 + let running: Bool = true + while running { + if i >= n { + let running = false + } else { + let c: String = str_slice(mods, i, i + 1) + if str_eq(c, ";") { + let running = false + } else { + let i = i + 1 + } + } + } + return str_slice(mods, 0, i) +} + +// ── Intent mapping ──────────────────────────────────────────────────────────── + +fn sem_intent_to_realize(intent: String) -> String { + if str_eq(intent, "assert") { return "assert" } + if str_eq(intent, "query") { return "question" } + if str_eq(intent, "describe") { return "assert" } + if str_eq(intent, "greet") { return "greet" } + return "assert" +} + +// ── sem_to_spec: SemFrame -> realizer slot map ──────────────────────────────── +// +// The "lang" key from the SemFrame is forwarded into the GramSpec so that +// realize() can resolve the correct language profile. + +fn sem_to_spec(frame: [String]) -> [String] { + let intent: String = sem_intent(frame) + let subject: String = sem_subject(frame) + let obj: String = sem_object(frame) + let mods: String = sem_modifiers(frame) + let lang_code: String = sem_lang(frame) + let location: String = sem_first_modifier(mods) + + if str_eq(intent, "greet") { + let spec: [String] = native_list_empty() + let spec = native_list_append(spec, "intent") + let spec = native_list_append(spec, "greet") + let spec = native_list_append(spec, "agent") + let spec = native_list_append(spec, subject) + let spec = native_list_append(spec, "predicate") + let spec = native_list_append(spec, "") + let spec = native_list_append(spec, "patient") + let spec = native_list_append(spec, "") + let spec = native_list_append(spec, "location") + let spec = native_list_append(spec, "") + let spec = native_list_append(spec, "tense") + let spec = native_list_append(spec, "present") + let spec = native_list_append(spec, "aspect") + let spec = native_list_append(spec, "simple") + let spec = native_list_append(spec, "lang") + let spec = native_list_append(spec, lang_code) + return spec + } + + if str_eq(intent, "describe") { + let spec: [String] = native_list_empty() + let spec = native_list_append(spec, "intent") + let spec = native_list_append(spec, "assert") + let spec = native_list_append(spec, "agent") + let spec = native_list_append(spec, subject) + let spec = native_list_append(spec, "predicate") + let spec = native_list_append(spec, "be") + let spec = native_list_append(spec, "patient") + let spec = native_list_append(spec, obj) + let spec = native_list_append(spec, "location") + let spec = native_list_append(spec, location) + let spec = native_list_append(spec, "tense") + let spec = native_list_append(spec, "present") + let spec = native_list_append(spec, "aspect") + let spec = native_list_append(spec, "simple") + let spec = native_list_append(spec, "lang") + let spec = native_list_append(spec, lang_code) + return spec + } + + let realize_intent: String = sem_intent_to_realize(intent) + let spec: [String] = native_list_empty() + let spec = native_list_append(spec, "intent") + let spec = native_list_append(spec, realize_intent) + let spec = native_list_append(spec, "agent") + let spec = native_list_append(spec, subject) + let spec = native_list_append(spec, "predicate") + let spec = native_list_append(spec, obj) + let spec = native_list_append(spec, "patient") + let spec = native_list_append(spec, "") + let spec = native_list_append(spec, "location") + let spec = native_list_append(spec, location) + let spec = native_list_append(spec, "tense") + let spec = native_list_append(spec, "present") + let spec = native_list_append(spec, "aspect") + let spec = native_list_append(spec, "simple") + let spec = native_list_append(spec, "lang") + let spec = native_list_append(spec, lang_code) + return spec +} + +// ── sem_to_spec_full ────────────────────────────────────────────────────────── + +fn sem_to_spec_full(frame: [String], verb: String, tense: String, aspect: String) -> [String] { + let intent: String = sem_intent(frame) + let subject: String = sem_subject(frame) + let obj: String = sem_object(frame) + let mods: String = sem_modifiers(frame) + let lang_code: String = sem_lang(frame) + let location: String = sem_first_modifier(mods) + + if str_eq(intent, "greet") { + return sem_to_spec(frame) + } + + if str_eq(intent, "describe") { + let spec: [String] = native_list_empty() + let spec = native_list_append(spec, "intent") + let spec = native_list_append(spec, "assert") + let spec = native_list_append(spec, "agent") + let spec = native_list_append(spec, subject) + let spec = native_list_append(spec, "predicate") + let spec = native_list_append(spec, "be") + let spec = native_list_append(spec, "patient") + let spec = native_list_append(spec, obj) + let spec = native_list_append(spec, "location") + let spec = native_list_append(spec, location) + let spec = native_list_append(spec, "tense") + let spec = native_list_append(spec, tense) + let spec = native_list_append(spec, "aspect") + let spec = native_list_append(spec, aspect) + let spec = native_list_append(spec, "lang") + let spec = native_list_append(spec, lang_code) + return spec + } + + let realize_intent: String = sem_intent_to_realize(intent) + let spec: [String] = native_list_empty() + let spec = native_list_append(spec, "intent") + let spec = native_list_append(spec, realize_intent) + let spec = native_list_append(spec, "agent") + let spec = native_list_append(spec, subject) + let spec = native_list_append(spec, "predicate") + let spec = native_list_append(spec, verb) + let spec = native_list_append(spec, "patient") + let spec = native_list_append(spec, obj) + let spec = native_list_append(spec, "location") + let spec = native_list_append(spec, location) + let spec = native_list_append(spec, "tense") + let spec = native_list_append(spec, tense) + let spec = native_list_append(spec, "aspect") + let spec = native_list_append(spec, aspect) + let spec = native_list_append(spec, "lang") + let spec = native_list_append(spec, lang_code) + return spec +} + +// ── Greet realization helper ────────────────────────────────────────────────── + +fn sem_realize_greet(subject: String) -> String { + if str_eq(subject, "") { + return "Hello." + } + return "Hello, " + subject + "." +} + +// ── sem_realize: SemFrame -> text ───────────────────────────────────────────── + +fn sem_realize(frame: [String]) -> String { + let intent: String = sem_intent(frame) + + if str_eq(intent, "greet") { + return sem_realize_greet(sem_subject(frame)) + } + + let spec: [String] = sem_to_spec(frame) + return realize(spec) +} + +// ── sem_realize_full ────────────────────────────────────────────────────────── + +fn sem_realize_full(frame: [String], verb: String, tense: String, aspect: String) -> String { + let intent: String = sem_intent(frame) + + if str_eq(intent, "greet") { + return sem_realize_greet(sem_subject(frame)) + } + + let spec: [String] = sem_to_spec_full(frame, verb, tense, aspect) + return realize(spec) +} + +// ── sem_realize_lang: realize in an explicitly specified language ────────────── +// +// Convenience for callers that want to specify the output language without +// constructing a full SemFrame. The lang_code overrides whatever "lang" is +// set in the frame. + +fn sem_realize_lang(frame: [String], lang_code: String) -> String { + let intent: String = sem_intent(frame) + + if str_eq(intent, "greet") { + return sem_realize_greet(sem_subject(frame)) + } + + // Inject the lang_code into the frame before converting to spec. + let patched: [String] = slots_set(frame, "lang", lang_code) + let spec: [String] = sem_to_spec(patched) + return realize(spec) +} diff --git a/elp/src/semantics_noimports.el b/elp/src/semantics_noimports.el new file mode 100644 index 0000000..76924b2 --- /dev/null +++ b/elp/src/semantics_noimports.el @@ -0,0 +1,308 @@ +// semantics.el - Semantic layer: SemFrame -> GramSpec (slot map for realizer). +// +// Bridges from intent/meaning representation to the grammar layer. +// A SemFrame is a slot map ([String] key-value list) with these keys: +// +// "intent" - "assert" | "query" | "describe" | "greet" +// "subject" - subject referent (pronoun or noun phrase) +// "object" - object referent (optional, "" if absent) +// "modifiers" - semicolon-separated modifier strings (e.g. "in the park;quickly") +// "lang" - ISO 639-1 language code (optional, defaults to "en") +// +// sem_to_spec converts a SemFrame into a realizer slot map ready for realize(). +// sem_realize is the end-to-end shortcut: frame -> realized text. +// +// All existing function signatures are preserved; new *_lang variants add an +// explicit lang_code parameter. +// +// Depends on: grammar (slots_*, realize), language-profile (lang_from_code) + +// ── SemFrame constructors ───────────────────────────────────────────────────── + +// Build a SemFrame with all four core fields. Language defaults to "en". +fn sem_frame(intent: String, subject: String, obj: String, modifiers: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "intent") + let r = native_list_append(r, intent) + let r = native_list_append(r, "subject") + let r = native_list_append(r, subject) + let r = native_list_append(r, "object") + let r = native_list_append(r, obj) + let r = native_list_append(r, "modifiers") + let r = native_list_append(r, modifiers) + let r = native_list_append(r, "lang") + let r = native_list_append(r, "en") + return r +} + +// Build a SemFrame with an explicit language code. +fn sem_frame_lang(intent: String, subject: String, obj: String, modifiers: String, lang_code: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "intent") + let r = native_list_append(r, intent) + let r = native_list_append(r, "subject") + let r = native_list_append(r, subject) + let r = native_list_append(r, "object") + let r = native_list_append(r, obj) + let r = native_list_append(r, "modifiers") + let r = native_list_append(r, modifiers) + let r = native_list_append(r, "lang") + let r = native_list_append(r, lang_code) + return r +} + +// Convenience: no object, no modifiers, English. +fn sem_frame_simple(intent: String, subject: String) -> [String] { + return sem_frame(intent, subject, "", "") +} + +// Convenience: with object, no modifiers, English. +fn sem_frame_obj(intent: String, subject: String, obj: String) -> [String] { + return sem_frame(intent, subject, obj, "") +} + +// ── SemFrame field accessors ────────────────────────────────────────────────── + +fn sem_intent(frame: [String]) -> String { + return slots_get(frame, "intent") +} + +fn sem_subject(frame: [String]) -> String { + return slots_get(frame, "subject") +} + +fn sem_object(frame: [String]) -> String { + return slots_get(frame, "object") +} + +fn sem_modifiers(frame: [String]) -> String { + return slots_get(frame, "modifiers") +} + +fn sem_lang(frame: [String]) -> String { + let code: String = slots_get(frame, "lang") + if str_eq(code, "") { + return "en" + } + return code +} + +// ── Modifier helpers ────────────────────────────────────────────────────────── + +fn sem_first_modifier(mods: String) -> String { + let n: Int = str_len(mods) + if n == 0 { + return "" + } + let i: Int = 0 + let running: Bool = true + while running { + if i >= n { + let running = false + } else { + let c: String = str_slice(mods, i, i + 1) + if str_eq(c, ";") { + let running = false + } else { + let i = i + 1 + } + } + } + return str_slice(mods, 0, i) +} + +// ── Intent mapping ──────────────────────────────────────────────────────────── + +fn sem_intent_to_realize(intent: String) -> String { + if str_eq(intent, "assert") { return "assert" } + if str_eq(intent, "query") { return "question" } + if str_eq(intent, "describe") { return "assert" } + if str_eq(intent, "greet") { return "greet" } + return "assert" +} + +// ── sem_to_spec: SemFrame -> realizer slot map ──────────────────────────────── +// +// The "lang" key from the SemFrame is forwarded into the GramSpec so that +// realize() can resolve the correct language profile. + +fn sem_to_spec(frame: [String]) -> [String] { + let intent: String = sem_intent(frame) + let subject: String = sem_subject(frame) + let obj: String = sem_object(frame) + let mods: String = sem_modifiers(frame) + let lang_code: String = sem_lang(frame) + let location: String = sem_first_modifier(mods) + + if str_eq(intent, "greet") { + let spec: [String] = native_list_empty() + let spec = native_list_append(spec, "intent") + let spec = native_list_append(spec, "greet") + let spec = native_list_append(spec, "agent") + let spec = native_list_append(spec, subject) + let spec = native_list_append(spec, "predicate") + let spec = native_list_append(spec, "") + let spec = native_list_append(spec, "patient") + let spec = native_list_append(spec, "") + let spec = native_list_append(spec, "location") + let spec = native_list_append(spec, "") + let spec = native_list_append(spec, "tense") + let spec = native_list_append(spec, "present") + let spec = native_list_append(spec, "aspect") + let spec = native_list_append(spec, "simple") + let spec = native_list_append(spec, "lang") + let spec = native_list_append(spec, lang_code) + return spec + } + + if str_eq(intent, "describe") { + let spec: [String] = native_list_empty() + let spec = native_list_append(spec, "intent") + let spec = native_list_append(spec, "assert") + let spec = native_list_append(spec, "agent") + let spec = native_list_append(spec, subject) + let spec = native_list_append(spec, "predicate") + let spec = native_list_append(spec, "be") + let spec = native_list_append(spec, "patient") + let spec = native_list_append(spec, obj) + let spec = native_list_append(spec, "location") + let spec = native_list_append(spec, location) + let spec = native_list_append(spec, "tense") + let spec = native_list_append(spec, "present") + let spec = native_list_append(spec, "aspect") + let spec = native_list_append(spec, "simple") + let spec = native_list_append(spec, "lang") + let spec = native_list_append(spec, lang_code) + return spec + } + + let realize_intent: String = sem_intent_to_realize(intent) + let spec: [String] = native_list_empty() + let spec = native_list_append(spec, "intent") + let spec = native_list_append(spec, realize_intent) + let spec = native_list_append(spec, "agent") + let spec = native_list_append(spec, subject) + let spec = native_list_append(spec, "predicate") + let spec = native_list_append(spec, obj) + let spec = native_list_append(spec, "patient") + let spec = native_list_append(spec, "") + let spec = native_list_append(spec, "location") + let spec = native_list_append(spec, location) + let spec = native_list_append(spec, "tense") + let spec = native_list_append(spec, "present") + let spec = native_list_append(spec, "aspect") + let spec = native_list_append(spec, "simple") + let spec = native_list_append(spec, "lang") + let spec = native_list_append(spec, lang_code) + return spec +} + +// ── sem_to_spec_full ────────────────────────────────────────────────────────── + +fn sem_to_spec_full(frame: [String], verb: String, tense: String, aspect: String) -> [String] { + let intent: String = sem_intent(frame) + let subject: String = sem_subject(frame) + let obj: String = sem_object(frame) + let mods: String = sem_modifiers(frame) + let lang_code: String = sem_lang(frame) + let location: String = sem_first_modifier(mods) + + if str_eq(intent, "greet") { + return sem_to_spec(frame) + } + + if str_eq(intent, "describe") { + let spec: [String] = native_list_empty() + let spec = native_list_append(spec, "intent") + let spec = native_list_append(spec, "assert") + let spec = native_list_append(spec, "agent") + let spec = native_list_append(spec, subject) + let spec = native_list_append(spec, "predicate") + let spec = native_list_append(spec, "be") + let spec = native_list_append(spec, "patient") + let spec = native_list_append(spec, obj) + let spec = native_list_append(spec, "location") + let spec = native_list_append(spec, location) + let spec = native_list_append(spec, "tense") + let spec = native_list_append(spec, tense) + let spec = native_list_append(spec, "aspect") + let spec = native_list_append(spec, aspect) + let spec = native_list_append(spec, "lang") + let spec = native_list_append(spec, lang_code) + return spec + } + + let realize_intent: String = sem_intent_to_realize(intent) + let spec: [String] = native_list_empty() + let spec = native_list_append(spec, "intent") + let spec = native_list_append(spec, realize_intent) + let spec = native_list_append(spec, "agent") + let spec = native_list_append(spec, subject) + let spec = native_list_append(spec, "predicate") + let spec = native_list_append(spec, verb) + let spec = native_list_append(spec, "patient") + let spec = native_list_append(spec, obj) + let spec = native_list_append(spec, "location") + let spec = native_list_append(spec, location) + let spec = native_list_append(spec, "tense") + let spec = native_list_append(spec, tense) + let spec = native_list_append(spec, "aspect") + let spec = native_list_append(spec, aspect) + let spec = native_list_append(spec, "lang") + let spec = native_list_append(spec, lang_code) + return spec +} + +// ── Greet realization helper ────────────────────────────────────────────────── + +fn sem_realize_greet(subject: String) -> String { + if str_eq(subject, "") { + return "Hello." + } + return "Hello, " + subject + "." +} + +// ── sem_realize: SemFrame -> text ───────────────────────────────────────────── + +fn sem_realize(frame: [String]) -> String { + let intent: String = sem_intent(frame) + + if str_eq(intent, "greet") { + return sem_realize_greet(sem_subject(frame)) + } + + let spec: [String] = sem_to_spec(frame) + return realize(spec) +} + +// ── sem_realize_full ────────────────────────────────────────────────────────── + +fn sem_realize_full(frame: [String], verb: String, tense: String, aspect: String) -> String { + let intent: String = sem_intent(frame) + + if str_eq(intent, "greet") { + return sem_realize_greet(sem_subject(frame)) + } + + let spec: [String] = sem_to_spec_full(frame, verb, tense, aspect) + return realize(spec) +} + +// ── sem_realize_lang: realize in an explicitly specified language ────────────── +// +// Convenience for callers that want to specify the output language without +// constructing a full SemFrame. The lang_code overrides whatever "lang" is +// set in the frame. + +fn sem_realize_lang(frame: [String], lang_code: String) -> String { + let intent: String = sem_intent(frame) + + if str_eq(intent, "greet") { + return sem_realize_greet(sem_subject(frame)) + } + + // Inject the lang_code into the frame before converting to spec. + let patched: [String] = slots_set(frame, "lang", lang_code) + let spec: [String] = sem_to_spec(patched) + return realize(spec) +} diff --git a/elp/src/test_empty_gram.el b/elp/src/test_empty_gram.el new file mode 100644 index 0000000..e69de29 diff --git a/elp/src/test_tiny_gram.el b/elp/src/test_tiny_gram.el new file mode 100644 index 0000000..823226d --- /dev/null +++ b/elp/src/test_tiny_gram.el @@ -0,0 +1,3 @@ +fn gram_fn(x: String) -> String { + return x +}