90ddbdbfc3
Brings the remaining foundation repos that were not included in the original monorepo consolidation: - arbor/vessels/ — 6 vessels (arbor-cli, arbor-core, arbor-diagram, arbor-layout, arbor-parse, arbor-render) with manifests + src/main.el - dharma/ — CGI Provenance Registry package (flat layout, 14 .el files across registry/, sandbox/, training/, validation/, tests/) - forge/ — consciousness channel tool (8 src .el files + new manifest.el) - elp/src/ — 36 test fixture files not carried over in original merge (dedup_*, realizer_*, semantics_*, morph_*, ext_*, one_extern_* helpers) el-ide, engram, elql are already complete in ide/, engram/, ql/.
764 lines
25 KiB
EmacsLisp
764 lines
25 KiB
EmacsLisp
// 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<String, Any>
|
|
//
|
|
// 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<String, Any>] {
|
|
let lines: [String] = str_split(source, "\n")
|
|
let n: Int = el_list_len(lines)
|
|
let out: [Map<String, Any>] = 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<String, Any> = { "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<String, Any> {
|
|
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<String, Any> {
|
|
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<String, Any> {
|
|
// 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<String, Any> {
|
|
{ "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_<i>_id" / "_label" / "_line" — frame data
|
|
// "group_stack_<i>_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<String, Any> {
|
|
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<String, Any> = 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<String, Any> = 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<String, Any> = 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<String, Any> = 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<String, Any> = 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<String, Any> = 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<String, Any> = 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<String, Any> = 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<String, Any> = 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<String, Any> {
|
|
let n_nodes: Int = st_get_int("node_count")
|
|
let nodes: [Map<String, Any>] = el_list_empty()
|
|
let i = 0
|
|
while i < n_nodes {
|
|
let s: String = int_to_str(i)
|
|
let node: Map<String, Any> = {
|
|
"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<String, Any>] = el_list_empty()
|
|
let i = 0
|
|
while i < n_edges {
|
|
let s: String = int_to_str(i)
|
|
let edge: Map<String, Any> = {
|
|
"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<String, Any>] = 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<String, Any> = {
|
|
"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<String, Any> {
|
|
reset_state()
|
|
let lines: [Map<String, Any>] = 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<String, Any> = 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<String, Any>) -> 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<String, Any> = 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<String, Any>] = g1["nodes"]
|
|
let nn1: Int = el_list_len(nodes1)
|
|
check_eq("two nodes", int_to_str(nn1), "2")
|
|
let edges1: [Map<String, Any>] = g1["edges"]
|
|
let ne1: Int = el_list_len(edges1)
|
|
check_eq("one edge", int_to_str(ne1), "1")
|
|
let e0: Map<String, Any> = 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<String, Any> = 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<String, Any> = arbor_parse(src2)
|
|
let edges2: [Map<String, Any>] = 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<String, Any> = 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<String, Any> = arbor_parse(src3)
|
|
let groups3: [Map<String, Any>] = g3["groups"]
|
|
check_eq("one group", int_to_str(el_list_len(groups3)), "1")
|
|
let grp0: Map<String, Any> = 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<String, Any>] = 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<String, Any> = 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<String, Any> = 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<String, Any> = arbor_parse(src6)
|
|
check_eq("comments stripped", int_to_str(el_list_len(g6["nodes"])), "2")
|
|
|
|
// Empty input
|
|
let g7: Map<String, Any> = 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")
|
|
}
|