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/.
1021 lines
35 KiB
EmacsLisp
1021 lines
35 KiB
EmacsLisp
// 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 <file.arbor> [-o out.svg] → write SVG to disk
|
|
// arbor check <file.arbor> → parse and report counts
|
|
// arbor ls <file.arbor> → list nodes / edges / groups
|
|
// arbor fmt <file.arbor> [--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<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 }
|
|
}
|
|
|
|
fn cli_split_id(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 !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<String, Any> {
|
|
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<String, Any> {
|
|
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<String, Any> = 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<String, Any> = 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<String, Any> = 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<String, Any> = 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<String, Any> = 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<String, Any> = 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<String, Any> = 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<String, Any> = 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<String, Any> = 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<String, Any>] {
|
|
let lines: [String] = str_split(source, "\n")
|
|
let n: Int = el_list_len(lines)
|
|
let out: [Map<String, Any>] = 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<String, Any> = { "no": i + 1, "text": trimmed }
|
|
let out = native_list_append(out, row)
|
|
}
|
|
let i = i + 1
|
|
}
|
|
out
|
|
}
|
|
|
|
fn cli_parse(source: String) -> Map<String, Any> {
|
|
cli_reset()
|
|
let lines: [Map<String, Any>] = 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<String, Any> = 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 = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" + int_to_str(cw)
|
|
let buf = buf + "\" height=\"" + int_to_str(ch) + "\" viewBox=\"0 0 "
|
|
let buf = buf + int_to_str(cw) + " " + int_to_str(ch) + "\">\n"
|
|
let buf = buf + " <defs>\n"
|
|
let buf = buf + " <marker id=\"ah\" markerWidth=\"10\" markerHeight=\"7\" refX=\"9\" refY=\"3.5\" orient=\"auto\">\n"
|
|
let buf = buf + " <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"#64748B\"/>\n"
|
|
let buf = buf + " </marker>\n"
|
|
let buf = buf + " </defs>\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 + " <line x1=\"" + int_to_str(fx) + "\" y1=\"" + int_to_str(fy)
|
|
let buf = buf + "\" x2=\"" + int_to_str(tx) + "\" y2=\"" + int_to_str(ty)
|
|
let buf = buf + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\""
|
|
let buf = buf + dash + " marker-end=\"url(#ah)\"/>\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 + " <text x=\"" + int_to_str(mx) + "\" y=\"" + int_to_str(my)
|
|
let buf = buf + "\" text-anchor=\"middle\" font-family=\"sans-serif\" font-size=\"11\" fill=\"#64748B\">"
|
|
let buf = buf + cli_esc(label) + "</text>\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 + " <rect x=\"" + int_to_str(x) + "\" y=\"" + int_to_str(y)
|
|
let buf = buf + "\" width=\"120\" height=\"40\""
|
|
let buf = buf + " rx=\"" + int_to_str(rx) + "\" fill=\"" + fill + "\" stroke=\"" + stroke
|
|
let buf = buf + "\" stroke-width=\"1.5\"/>\n"
|
|
let buf = buf + " <text x=\"" + int_to_str(cx) + "\" y=\"" + int_to_str(cy)
|
|
let buf = buf + "\" text-anchor=\"middle\" dominant-baseline=\"middle\""
|
|
let buf = buf + " font-family=\"sans-serif\" font-size=\"13\" fill=\"" + txt + "\">"
|
|
let buf = buf + cli_esc(label) + "</text>\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 + " <text x=\"" + int_to_str(tx) + "\" y=\"22\" text-anchor=\"middle\""
|
|
let buf = buf + " font-family=\"sans-serif\" font-size=\"15\" font-weight=\"600\" fill=\"#0D0D14\">"
|
|
let buf = buf + cli_esc(title) + "</text>\n"
|
|
}
|
|
let buf = buf + "</svg>\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 <file.arbor> [-o <output>]")
|
|
println(" arbor check <file.arbor>")
|
|
println(" arbor ls <file.arbor>")
|
|
println(" arbor fmt <file.arbor> [--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<String, Any> = 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<String, Any> = 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<String, Any> = 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<String, Any> = 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<String, Any> = 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, "<svg") { smoke_fail("svg head", "missing <svg") } else { println("ok svg head") }
|
|
if !str_contains(svg, "REST API") { smoke_fail("svg label", "missing REST API") } else { println("ok svg has label") }
|
|
if !str_contains(svg, "</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)
|
|
}
|