From a8c9342e34b162dc277be692f75c5a52fdccb295 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 11:56:52 +0700 Subject: [PATCH] spec(t27f): readable big graphs -- wrapped columns, kind fold, expand kind, hit testing (Closes #10218) New specs/t27f/spec_graph_fold.t27 next to the graph view. Columns taller than N = 12 rows wrap into sub-columns so the canvas aspect is near phi; a graph that fits is laid out exactly as the view lays it out. A fold shows one box per node kind with the exact count and one arc per pair of kinds with the exact edge count, labelled by the weakest member. expand_graph keeps one kind and its direct neighbours verbatim with an origin map. wrap_hit and the line span functions take a click to a node and its lines. Colour constants of the view are not touched. Co-Authored-By: Claude Sonnet 5.5 --- .claude/skills/t27f/SKILL.md | 1 + specs/OWNERS.md | 2 +- specs/t27f/spec_graph_fold.t27 | 1593 ++++++++++++++++++++++++++++++++ 3 files changed, 1595 insertions(+), 1 deletion(-) create mode 100644 specs/t27f/spec_graph_fold.t27 diff --git a/.claude/skills/t27f/SKILL.md b/.claude/skills/t27f/SKILL.md index d6d94a81ef..e2ab197b02 100644 --- a/.claude/skills/t27f/SKILL.md +++ b/.claude/skills/t27f/SKILL.md @@ -60,6 +60,7 @@ the reels. Describing T27F as #1677 is the error this section exists to stop (ow |---|---|---| | Spec -> graph IR schema (the core) | `specs/t27f/spec_graph.t27` (t27#10020) | spec with tests: node / edge kinds, labels, spans, `validate_graph`, line -> node mapping, node -> ui_plan widget, one hand-read fixture (`specs/tutorial/08_modules_and_arrays.t27`) | | Graph view: layout of a spec graph | `specs/t27f/spec_graph_view.t27` (t27#10034) | spec with tests: one column per node kind, size / colour / dash / row order from the inference label, UNKNOWN nodes alone in a band, every edge with a source line, node -> widget through `node_widget`; numbers only, laid out on the hand-read fixture; drawn by the MCP tool `t27f_graph` (gHashTag/t27-mcp#9) | +| Graph view for big specs: wrap, fold, expand, hit | `specs/t27f/spec_graph_fold.t27` (t27#10218) | spec with tests: a kind column taller than N = 12 rows wraps into sub-columns so the canvas aspect is near phi (identical to the view when nothing wraps); a fold to one box per kind with the exact count and one arc per pair of kinds with the exact edge count, each labelled by its weakest member; `expand_graph` = one kind plus its direct neighbours, copied verbatim with an origin map; `wrap_hit` / `wrap_hit_line_start` / `wrap_hit_line_end` take a click to a node and its lines; numbers only, the view's colours are not touched | | Spec -> graph IR exporter | `specs/t27f/spec_graph_export.t27` (t27#10162) | spec with tests: `export_graph(path, src)` is a deterministic byte scanner over the source, `export_verdict` is `validate_graph` of the result. One node per top-level declaration (a leading `pub ` is allowed), spans to the matching closing brace, every node and edge EXACT or (unclassifiable) OPAQUE + UNKNOWN, CALLS / USES / CONSTRAINS edges with the line of the use, refusal past 192 nodes or 254 edges. On the tutorial 08 snapshot it equals the hand-read fixture on all 14 declaration nodes and all 9 EXACT edges; it does not emit the fixture's `std.testing` UNKNOWN node, nor its 7 DERIVED edges (4 data flow, 3 tested_by), because a scanner never derives; the derive pass below adds them. Limits: textual matching without scope or type resolution, no block comments, no struct/enum bodies, no cross-spec `use` resolution, capacity 192 nodes and 254 edges (about 98 percent of specs in specs/ fit; the rest are refused), takes a `[]const u8` slice rather than `[65536]u8` plus a length. Not wired to a host: the MCP tool does not call it. AST/IR audit is trinity#1650, pilot choice #1651 | | Graph IR derive pass | `specs/t27f/spec_graph_derive.t27` (t27#10191) | spec with tests: `derive_graph(g, src)` appends what the exporter must not claim and changes nothing it holds. R1 DERIVED `tested_by` = every EXACT CALLS edge from a test, invariant or bench to a fn read backwards (no line). R2 DERIVED `data_flow` = a const or `use` that an EXACT USES edge ties to a fn and that is named inside one of the fn's return statements (from `return` to the next `;`), on the line of the first such name. R3 UNKNOWN opaque node = an undeclared qualified call target of three or more segments (`std.testing.expectEqual(` names `std.testing`), spanning the lines that call through it. Exporter plus derive equal the tutorial 08 fixture as sets: 15 of 15 nodes, 16 of 16 edges, no special case. Limits: names matched as text; call results and flow through locals are not derived; R3 needs 3 or more segments (two cannot be told from a method call); a graph that does not validate, or a source of another line count, comes back unchanged; past 192 nodes or 254 edges the result is refused with the too-many verdict. Not wired to a host: the MCP tool does not call it | | UI plan IR + trusted widget catalog | `specs/t27f/ui_plan.t27` (t27#9962) | spec with tests; no renderer draws a plan yet | diff --git a/specs/OWNERS.md b/specs/OWNERS.md index c42f775cf5..9936d72acb 100644 --- a/specs/OWNERS.md +++ b/specs/OWNERS.md @@ -28,7 +28,7 @@ | `docs/` | **Z-Zeta** (with **T-Queen** for `chapters/queen.t27`) | The system documentation of `t27.ai/#/docs` as `.t27` specs (`docs/system.t27`, `KIND = "docs"`; `docs/chapters/.t27`, `KIND = "docs-chapter"`): chapter ids, sections, sources, the figure and table the site generates; English prose in `docs/system/.md`, Russian via `i18n/docs-ru.t27` | | `ui/` | **T-Queen** | The site's viewport contract as a `.t27` spec (`ui/viewport.t27`, `KIND = "viewport"`): tiers by CSS viewport width, panes per tier, the 44 px touch minimum, the six-size QA matrix and the Queen rail capacity read from `Queen.css`; the site (gHashTag/trinity) generates `viewport.generated.ts` / `.css` from a vendored copy and evaluates the spec's own test blocks; `ui/queen_evidence.t27` (`module ui_queen_evidence`): the evidence constants of the Queen's FFN and Ternary Check panel, from which the site generates `queenEvidence.generated.ts`; canonical here since S10, byte-identical to the copy the site carried alone | | `catalog/` | **T-Queen** | How the site's spec catalog and the Queen map find the repositories that write `.t27` (`catalog/discovery.t27`, `KIND = "discovery"`): the GitHub owners scanned, the exclusions, what makes a candidate qualify (a declaration in the compiler's AST, not a file extension) and the limits; the site (gHashTag/trinity) vendors a byte-identical copy and `scripts/discover-t27-worlds.mjs` reads it through the compiler wasm before touching GitHub | -| `t27f/` | **T-Queen** | T27F, the visual spec of a .t27 file (gHashTag/trinity#1648, RFC #1649; definition in `.claude/skills/t27f/SKILL.md`): `t27f/spec_graph.t27` (module `t27f_spec_graph`): the core graph IR a spec becomes -- node and edge kinds, inference labels, source spans, `validate_graph`, source mapping both ways and the node -> ui_plan widget binding, with one hand-read fixture; `t27f/spec_graph_export.t27` (module `t27f_spec_graph_export`): the exporter, a deterministic byte scanner from spec source to that graph -- declarations, brace-matched spans, exact calls/uses/constrains edges, refusal past the IR capacity, checked against the hand-read fixture; `t27f/spec_graph_derive.t27` (module `t27f_spec_graph_derive`): the derive pass, a second deterministic scan that appends DERIVED tested_by and data_flow edges and UNKNOWN opaque nodes for undeclared qualified call targets by three stated rules and never changes an exact fact -- exporter plus derive equal the tutorial 08 fixture; `t27f/spec_graph_view.t27` (module `t27f_spec_graph_view`): the graph view, a deterministic layout of a spec graph -- one column per node kind, style and row order from the inference label, UNKNOWN nodes drawn alone, every edge with a source line; numbers only, a host draws them; `t27f/ui_plan.t27` (module `t27f_ui_plan`): the renderer-neutral UI plan IR and the trusted widget catalog. Applications built on it: the topic-to-value map of gHashTag/trinity#1677 (`t27f/topic_engine.t27`, module `T27fTopicEngine`): the 3 x 9 = 27 lesson shape, intake and gap rules, keyword routing to one domain, the Bee dispatch cap, the evidence and approval gates, and progression that is never money; driven by the Claude Code skill `.claude/skills/t27f/SKILL.md` through the t27 MCP server; `t27f/reel.t27` (module `T27fReel`): the video format of a topic map and the still views of a Claude Code answer -- honeycomb geometry, focus+context camera, scene catalog, per-target limits, gap styling, the posting gate and the channel per host; numbers only, a host draws them; `t27f/deck.t27` (module `T27fDeck`): the default look of a reel, every scene a presentation slide -- layouts per scene, regions, type sizes, glass card, palette kept off the status colours, contrast and card fades | +| `t27f/` | **T-Queen** | T27F, the visual spec of a .t27 file (gHashTag/trinity#1648, RFC #1649; definition in `.claude/skills/t27f/SKILL.md`): `t27f/spec_graph.t27` (module `t27f_spec_graph`): the core graph IR a spec becomes -- node and edge kinds, inference labels, source spans, `validate_graph`, source mapping both ways and the node -> ui_plan widget binding, with one hand-read fixture; `t27f/spec_graph_export.t27` (module `t27f_spec_graph_export`): the exporter, a deterministic byte scanner from spec source to that graph -- declarations, brace-matched spans, exact calls/uses/constrains edges, refusal past the IR capacity, checked against the hand-read fixture; `t27f/spec_graph_derive.t27` (module `t27f_spec_graph_derive`): the derive pass, a second deterministic scan that appends DERIVED tested_by and data_flow edges and UNKNOWN opaque nodes for undeclared qualified call targets by three stated rules and never changes an exact fact -- exporter plus derive equal the tutorial 08 fixture; `t27f/spec_graph_view.t27` (module `t27f_spec_graph_view`): the graph view, a deterministic layout of a spec graph -- one column per node kind, style and row order from the inference label, UNKNOWN nodes drawn alone, every edge with a source line; numbers only, a host draws them; `t27f/spec_graph_fold.t27` (module `t27f_spec_graph_fold`): the same view for big specs -- columns taller than N rows wrap to near phi, a fold to one box per node kind with exact counts and kind-to-kind edge counts, expand one kind with its direct neighbours, and hit testing from a point to a node and its source lines; `t27f/ui_plan.t27` (module `t27f_ui_plan`): the renderer-neutral UI plan IR and the trusted widget catalog. Applications built on it: the topic-to-value map of gHashTag/trinity#1677 (`t27f/topic_engine.t27`, module `T27fTopicEngine`): the 3 x 9 = 27 lesson shape, intake and gap rules, keyword routing to one domain, the Bee dispatch cap, the evidence and approval gates, and progression that is never money; driven by the Claude Code skill `.claude/skills/t27f/SKILL.md` through the t27 MCP server; `t27f/reel.t27` (module `T27fReel`): the video format of a topic map and the still views of a Claude Code answer -- honeycomb geometry, focus+context camera, scene catalog, per-target limits, gap styling, the posting gate and the channel per host; numbers only, a host draws them; `t27f/deck.t27` (module `T27fDeck`): the default look of a reel, every scene a presentation slide -- layouts per scene, regions, type sizes, glass card, palette kept off the status colours, contrast and card fades | | `t27mcp/` | **T-Queen** | The decisions of the t27 MCP server (gHashTag/t27-mcp) as `.t27` specs, compiled to wasm that the server runs: `t27mcp/check_hints.t27` (module `T27mcpCheckHints`): which short "write this instead" hint a compiler message gets for foreign syntax (`in` as a name, `as u3`, `x++`, `a ? b : c`, `match`), and the line number a message names; `t27mcp/examples.t27` (module `T27mcpExamples`): which short slice of the published corpus, pinned to one commit, answers a question about clocked or combinational modules, `fn`, `test` blocks and the reset styles; `t27mcp/spec_brief.t27` (module `T27mcpSpecBrief`): which manifest fields a short spec row keeps and why a failing spec fails (a bit set of reasons plus their text templates); the server's JavaScript is I/O glue and holds no rule | | `trinity/` | **T-Queen** + **A-Architect** | The project manifest of gHashTag/trinity as `.t27` specs (`trinity/project.t27`, `KIND = "project"`; `trinity/capabilities/.t27`, `KIND = "capability"`): the pinned consumer revision, profiles, dispositions and evidence tags, dialect and build counts, dependency pins and the work packages of gHashTag/trinity#988; each card names its owner, canonical spec, implementation and generated paths, build targets, acceptance command and evidence; `tools/trinity_manifest.py` derives the inventory from a clean pinned checkout and holds the cards to it; `trinity/compiler_matrix.t27` (`KIND = "compiler-matrix"`) names the pinned compiler, the backends, the stages and the features of the executable subset with their fixtures under `bootstrap/tests/fixtures/trinity_matrix/`, and `tools/trinity_compiler_matrix.py` measures them; `trinity/build_graph.t27` (`KIND = "build-graph"`) declares the consumer's toolchain, pins, profiles, generated-file boundaries and receipt policy, and `tools/trinity_build_receipt.py` derives the graph, writes the receipts and judges drift; the consumer vendors a byte-identical copy and generates its manifest from it (S12). Shared contracts stay in their own subtrees (`numeric/`, `vsa/`, `queen/`, `tools/`, `docs/`, `ui/`) and are cited, never copied | diff --git a/specs/t27f/spec_graph_fold.t27 b/specs/t27f/spec_graph_fold.t27 new file mode 100644 index 0000000000..33833d6a1f --- /dev/null +++ b/specs/t27f/spec_graph_fold.t27 @@ -0,0 +1,1593 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/t27f/spec_graph_fold.t27 -- the T27F graph view for big specs: wrapped columns, a fold to +; one box per node kind, "expand this kind", and hit testing (gHashTag/t27#10218; epic +; gHashTag/trinity#1648, RFC gHashTag/trinity#1649). specs/t27f/spec_graph_view.t27 draws one column +; per node kind and one row per node, so a spec with 300 fns is a column about 19,000 px tall. This +; file reads the same SpecGraph and the same view geometry and adds three things, each a separate +; section below; it does not edit the view, and it does not touch the view's colours or the +; label-to-style table, which a host restyles on its own. +; WRAP. A kind column with more than WRAP_MAX_ROWS rows (N) is split into sub-columns of R rows, and +; R is chosen from N up to the tallest column so that width:height is as near phi as the grid +; allows (WRAP_ASPECT_MILLI, 1618). A graph whose tallest column has N rows or fewer keeps R = N and +; is laid out exactly as the view lays it out: the tests compare every box and edge end on both +; fixtures. A wrapped kind takes as many columns as it has sub-columns; later kinds shift right. +; UNKNOWN nodes keep their band below the known boxes and wrap in the same R. The rule is a pure +; function of node counts (WrapPlan), so a 300-fn layout is tested on counts although MAX_NODES +; does not yet allow a 300-fn graph; the graph-level test fills the graph to whatever MAX_NODES is. +; An edge leaves the side of the from-box that faces the to-box, judged by sub-column instead of +; kind column, and view_edge_line is reused unchanged. +; FOLD. One box per node kind present, in the view's column order, with the exact number of nodes of +; the kind (a host writes "fn x 214" from fold_kind_count and the kind name), and one aggregated +; edge per ordered pair of kinds with the exact number of edges between them. Nothing is invented: +; the counts sum to node_count and edge_count (a test checks it). A box or an aggregated edge that +; holds nodes of several labels is drawn with the WEAKEST label among its members +; (fold_kind_label, fold_edge_label), so a fold never shows a guess as a fact; the per-label +; counts are fold_kind_label_count. The aggregate itself is not called DERIVED: it is a count of +; what the graph says, and its label is only the weakest member's. Aggregated edges are arcs: a +; forward arc (same or later column) above the boxes, a backward arc below. +; EXPAND. expand_graph(g, k) is a SpecGraph holding the nodes of kind k and every node an edge ties +; directly to one, in the original order, with every edge that has an end of kind k; node and edge +; fields are copied unchanged and expand_origin maps a new node number back to the old one. It is +; laid out with the wrap functions. It is a sub-graph, so node 0 need not be the module and +; validate_graph can refuse it; layout_safe is the guard this file uses. +; HIT. wrap_hit maps a point to the lowest node number whose box holds it, or NO_INDEX; +; wrap_hit_line_start / wrap_hit_line_end give that node's source span, so a host needs no layout +; math to go from a click to a line. fold_hit and expand_hit do the same for the other two views. +; Not claimed: nothing here routes an edge around a box, no host draws the fold yet, and the 300-fn +; graph of the issue exists as counts (plan_*), not yet as a SpecGraph, until MAX_NODES is raised. +; The edge end points assume the endpoint arrays hold node numbers below MAX_NODES; expand_graph +; narrows a new number into the array element type with `as u8` and must follow if that type grows. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module t27f_spec_graph_fold; + +use t27f::spec_graph::SpecGraph; +use t27f::spec_graph::blank_graph; +use t27f::spec_graph::with_node; +use t27f::spec_graph::with_edge; +use t27f::spec_graph::tutorial_08_graph; +use t27f::spec_graph::validate_graph; +use t27f::spec_graph::GRAPH_OK; +use t27f::spec_graph::ERR_MODULE; +use t27f::spec_graph::MAX_NODES; +use t27f::spec_graph::MAX_EDGES; +use t27f::spec_graph::MEASURED_NODES_P95; +use t27f::spec_graph::NO_INDEX; +use t27f::spec_graph::NO_LINE; +use t27f::spec_graph::NODE_KINDS; +use t27f::spec_graph::NODE_MODULE; +use t27f::spec_graph::NODE_CONST; +use t27f::spec_graph::NODE_FN; +use t27f::spec_graph::NODE_TEST; +use t27f::spec_graph::NODE_INVARIANT; +use t27f::spec_graph::NODE_BENCH; +use t27f::spec_graph::NODE_USE; +use t27f::spec_graph::NODE_OPAQUE; +use t27f::spec_graph::EDGE_CALLS; +use t27f::spec_graph::EDGE_USES; +use t27f::spec_graph::EDGE_TESTED_BY; +use t27f::spec_graph::EDGE_DATA_FLOW; +use t27f::spec_graph::EDGE_KINDS; +use t27f::spec_graph::LABEL_EXACT; +use t27f::spec_graph::LABEL_DERIVED; +use t27f::spec_graph::LABEL_SUGGESTED; +use t27f::spec_graph::LABEL_UNKNOWN; +use t27f::spec_graph::LABEL_KINDS; +use t27f::spec_graph_view::VIEW_OK; +use t27f::spec_graph_view::VIEW_MARGIN; +use t27f::spec_graph_view::VIEW_COL_W; +use t27f::spec_graph_view::VIEW_COL_STEP; +use t27f::spec_graph_view::VIEW_ROW_STEP; +use t27f::spec_graph_view::VIEW_BAND_GAP; +use t27f::spec_graph_view::VIEW_KIND_ORDER; +use t27f::spec_graph_view::VIEW_LABEL_H; +use t27f::spec_graph_view::VIEW_LABEL_INDENT; +use t27f::spec_graph_view::VIEW_LANE_STEP; +use t27f::spec_graph_view::VIEW_LANE_HALF; +use t27f::spec_graph_view::view_is_unknown; +use t27f::spec_graph_view::view_style; +use t27f::spec_graph_view::view_kind_present; +use t27f::spec_graph_view::view_column; +use t27f::spec_graph_view::view_column_count; +use t27f::spec_graph_view::view_row; +use t27f::spec_graph_view::view_node_w; +use t27f::spec_graph_view::view_node_h; +use t27f::spec_graph_view::view_node_x; +use t27f::spec_graph_view::view_node_y; +use t27f::spec_graph_view::view_width; +use t27f::spec_graph_view::view_height; +use t27f::spec_graph_view::view_edge_line; +use t27f::spec_graph_view::view_edge_x1; +use t27f::spec_graph_view::view_edge_y1; +use t27f::spec_graph_view::view_edge_x2; +use t27f::spec_graph_view::view_edge_y2; +use t27f::spec_graph_view::view_overlap_count; +use t27f::spec_graph_view::view_verdict; +use t27f::spec_graph_view::view_fx; + +/// N: a kind column with more rows than this is wrapped. It must be at least 1. +pub const WRAP_MAX_ROWS : u32 = 12; +/// The aspect ratio the wrapped view aims for, width * 1000 / height; phi is 1.618. A graph is "near +/// phi" when its aspect is within WRAP_ASPECT_TOLERANCE_MILLI of it, a quarter of phi. +pub const WRAP_ASPECT_MILLI : u32 = 1618; +pub const WRAP_ASPECT_TOLERANCE_MILLI : u32 = 404; + +/// Fold geometry in pixels. A kind box is as wide as a view column and FOLD_BOX_H tall; an arc between +/// kinds rises FOLD_ARC_STEP for every column it crosses, a self loop rises FOLD_LOOP_RISE, and an +/// arc end sits FOLD_ANCHOR to the side of the box centre. +pub const FOLD_BOX_H : u32 = 64; +pub const FOLD_ARC_STEP : u32 = 36; +pub const FOLD_LOOP_RISE : u32 = 28; +pub const FOLD_ANCHOR : u32 = 24; + +/// Fold levels a host can show: every node (wrapped), one box per kind, or one kind expanded. +pub const FOLD_LEVEL_FULL : u32 = 0; +pub const FOLD_LEVEL_KINDS : u32 = 1; +pub const FOLD_LEVEL_EXPAND : u32 = 2; + +/// Node counts of the wide fixture: tutorial 08 plus this many extra fns and tests. +pub const WIDE_FNS : u32 = 150; +pub const WIDE_TESTS : u32 = 20; + +/// Per-kind counts of a graph, the whole input of the wrap rule. known[k] counts nodes of kind k +/// whose label the view can read, unk[k] those it draws as UNKNOWN; rows is R, the rows of a +/// sub-column. +pub struct WrapPlan { + known: [8]u32, + unk: [8]u32, + rows: u32, +} + +pub fn fold_min(a: u32, b: u32) -> u32 { + if (a < b) { return a; } + return b; +} + +pub fn fold_max(a: u32, b: u32) -> u32 { + if (a > b) { return a; } + return b; +} + +pub fn fold_gap(a: u32, b: u32) -> u32 { + if (a > b) { return a - b; } + return b - a; +} + +pub fn fold_ceil(a: u32, b: u32) -> u32 { + if (b == 0) { return 0; } + return (a + b - 1) / b; +} + +/// --------------------------------------------------------------------------------------------- +/// WRAP: the rule on counts +/// --------------------------------------------------------------------------------------------- + +pub fn plan_empty() -> WrapPlan { + return WrapPlan { known: [0; 8], unk: [0; 8], rows: WRAP_MAX_ROWS }; +} + +/// The rows of a sub-column; never 0. +pub fn plan_r(p: WrapPlan) -> u32 { + return fold_max(p.rows, 1); +} + +pub fn plan_count(p: WrapPlan, k: u32, unk: bool) -> u32 { + if (unk) { return p.unk[k as usize]; } + return p.known[k as usize]; +} + +/// The tallest group, known or UNKNOWN, over all kinds. +pub fn plan_most(p: WrapPlan, unk: bool) -> u32 { + var m : u32 = 0; + var k : u32 = 0; + while (k < (NODE_KINDS as u32)) { + m = fold_max(m, plan_count(p, k, unk)); + k = k + 1; + } + return m; +} + +pub fn plan_tallest(p: WrapPlan) -> u32 { + return fold_max(plan_most(p, false), plan_most(p, true)); +} + +/// Sub-columns kind k takes when sub-columns hold r rows: the larger of its known and UNKNOWN +/// groups, since the two groups share the sub-columns, one above the other. 0 for an absent kind. +pub fn plan_span_at(p: WrapPlan, k: u32, r: u32) -> u32 { + return fold_max(fold_ceil(p.known[k as usize], r), fold_ceil(p.unk[k as usize], r)); +} + +pub fn plan_cols_at(p: WrapPlan, r: u32) -> u32 { + var c : u32 = 0; + var k : u32 = 0; + while (k < (NODE_KINDS as u32)) { + c = c + plan_span_at(p, k, r); + k = k + 1; + } + return c; +} + +pub fn plan_width_at(p: WrapPlan, r: u32) -> u32 { + let c : u32 = plan_cols_at(p, r); + if (c == 0) { return 2 * VIEW_MARGIN; } + return 2 * VIEW_MARGIN + (c - 1) * VIEW_COL_STEP + VIEW_COL_W; +} + +/// The canvas height when sub-columns hold r rows, with every box at the tallest known style, so it +/// is never below the exact height of the same graph (wrap_height). +pub fn plan_height_at(p: WrapPlan, r: u32) -> u32 { + let mk : u32 = plan_most(p, false); + let mu : u32 = plan_most(p, true); + let d : u32 = fold_min(mk, r); + var h : u32 = 2 * VIEW_MARGIN; + if (mk > 0) { + h = VIEW_MARGIN + (d - 1) * VIEW_ROW_STEP + VIEW_LABEL_H[LABEL_EXACT as usize] + VIEW_MARGIN; + } + if (mu > 0) { + h = VIEW_MARGIN + d * VIEW_ROW_STEP + VIEW_BAND_GAP + (fold_min(mu, r) - 1) * VIEW_ROW_STEP + VIEW_LABEL_H[LABEL_UNKNOWN as usize] + VIEW_MARGIN; + } + return h; +} + +/// How far the aspect at r rows is from phi, in thousandths. +pub fn plan_dev(p: WrapPlan, r: u32) -> u32 { + let a : u32 = plan_width_at(p, r) * 1000 / plan_height_at(p, r); + return fold_gap(a, WRAP_ASPECT_MILLI); +} + +/// p with its rows chosen: N when no column is taller than N, else the r in N..tallest whose aspect +/// is nearest phi (the smallest such r on a tie). +pub fn plan_solve(p: WrapPlan) -> WrapPlan { + var q : WrapPlan = p; + let top : u32 = plan_tallest(p); + var best : u32 = WRAP_MAX_ROWS; + if (top > WRAP_MAX_ROWS) { + var best_dev : u32 = plan_dev(p, WRAP_MAX_ROWS); + var r : u32 = WRAP_MAX_ROWS + 1; + while (r <= top) { + let d : u32 = plan_dev(p, r); + if (d < best_dev) { + best_dev = d; + best = r; + } + r = r + 1; + } + } + q.rows = best; + return q; +} + +/// Sub-columns before kind k, counting only kinds that come earlier in VIEW_KIND_ORDER. +pub fn plan_start(p: WrapPlan, k: u32) -> u32 { + var s : u32 = 0; + var kk : u32 = 0; + while (kk < (NODE_KINDS as u32)) { + if (VIEW_KIND_ORDER[kk as usize] < VIEW_KIND_ORDER[k as usize]) { s = s + plan_span_at(p, kk, plan_r(p)); } + kk = kk + 1; + } + return s; +} + +/// The sub-column of the ordinal-th node of kind k in its group. +pub fn plan_sub(p: WrapPlan, k: u32, ordinal: u32) -> u32 { + return plan_start(p, k) + ordinal / plan_r(p); +} + +pub fn plan_slot_x(p: WrapPlan, k: u32, ordinal: u32) -> u32 { + return VIEW_MARGIN + plan_sub(p, k, ordinal) * VIEW_COL_STEP; +} + +/// The top of the UNKNOWN band: below the deepest known sub-column, as the view puts it below the +/// deepest known column. +pub fn plan_band_top(p: WrapPlan) -> u32 { + return VIEW_MARGIN + fold_min(plan_most(p, false), plan_r(p)) * VIEW_ROW_STEP + VIEW_BAND_GAP; +} + +pub fn plan_slot_y(p: WrapPlan, unk: bool, ordinal: u32) -> u32 { + let r : u32 = ordinal % plan_r(p); + if (unk) { return plan_band_top(p) + r * VIEW_ROW_STEP; } + return VIEW_MARGIN + r * VIEW_ROW_STEP; +} + +pub fn plan_slot_h(unk: bool) -> u32 { + if (unk) { return VIEW_LABEL_H[LABEL_UNKNOWN as usize]; } + return VIEW_LABEL_H[LABEL_EXACT as usize]; +} + +/// Two slots, each (kind, UNKNOWN or known, ordinal), share a pixel. A slot is a full column wide. +pub fn plan_slot_overlap(p: WrapPlan, ka: u32, ua: bool, oa: u32, kb: u32, ub: bool, ob: u32) -> bool { + let ax : u32 = plan_slot_x(p, ka, oa); + let ay : u32 = plan_slot_y(p, ua, oa); + let bx : u32 = plan_slot_x(p, kb, ob); + let by : u32 = plan_slot_y(p, ub, ob); + return ax < bx + VIEW_COL_W and bx < ax + VIEW_COL_W and ay < by + plan_slot_h(ub) and by < ay + plan_slot_h(ua); +} + +/// Pairs of slots that share a pixel, over every slot the plan holds: 0 for a sound plan. +pub fn plan_overlap_count(p: WrapPlan) -> u32 { + var c : u32 = 0; + var a : u32 = 0; + while (a < 16) { + let ka : u32 = a / 2; + let ua : bool = (a % 2) == 1; + var oa : u32 = 0; + while (oa < plan_count(p, ka, ua)) { + var b : u32 = a; + while (b < 16) { + let kb : u32 = b / 2; + let ub : bool = (b % 2) == 1; + var ob : u32 = 0; + if (b == a) { ob = oa + 1; } + while (ob < plan_count(p, kb, ub)) { + if (plan_slot_overlap(p, ka, ua, oa, kb, ub, ob)) { c = c + 1; } + ob = ob + 1; + } + b = b + 1; + } + oa = oa + 1; + } + a = a + 1; + } + return c; +} + +pub fn plan_slots(p: WrapPlan) -> u32 { + var c : u32 = 0; + var k : u32 = 0; + while (k < (NODE_KINDS as u32)) { + c = c + p.known[k as usize] + p.unk[k as usize]; + k = k + 1; + } + return c; +} + +/// --------------------------------------------------------------------------------------------- +/// WRAP: the layout of a graph +/// --------------------------------------------------------------------------------------------- + +/// The graph is safe to lay out: counts inside the arrays, every node kind and edge kind known, and +/// every edge end a real node. It is weaker than validate_graph on purpose, so an expanded +/// sub-graph (no module at node 0) passes. +pub fn layout_safe(g: SpecGraph) -> bool { + if (g.node_count > MAX_NODES or g.edge_count > MAX_EDGES) { return false; } + var n : u32 = 0; + while (n < g.node_count) { + if (g.node_kind[n as usize] >= NODE_KINDS) { return false; } + n = n + 1; + } + var j : u32 = 0; + while (j < g.edge_count) { + let k : usize = j as usize; + if (g.edge_kind[k] >= EDGE_KINDS) { return false; } + if ((g.edge_from[k] as u32) >= g.node_count or (g.edge_to[k] as u32) >= g.node_count) { return false; } + j = j + 1; + } + return true; +} + +/// The counts of g with R solved. A graph that is not layout_safe gives the empty plan. +pub fn wrap_plan(g: SpecGraph) -> WrapPlan { + var p : WrapPlan = plan_empty(); + if (layout_safe(g) == false) { return p; } + var n : u32 = 0; + while (n < g.node_count) { + let k : usize = g.node_kind[n as usize] as usize; + if (view_is_unknown(g.node_label[n as usize])) { + p.unk[k] = p.unk[k] + 1; + } else { + p.known[k] = p.known[k] + 1; + } + n = n + 1; + } + return plan_solve(p); +} + +pub fn wrap_rows(g: SpecGraph) -> u32 { + return wrap_plan(g).rows; +} + +/// The sub-column of node n: its kind's first sub-column plus its row divided by R. +pub fn wrap_sub(g: SpecGraph, p: WrapPlan, n: u32) -> u32 { + return plan_sub(p, g.node_kind[n as usize] as u32, view_row(g, n)); +} + +pub fn wrap_x(g: SpecGraph, p: WrapPlan, n: u32) -> u32 { + let s : u8 = view_style(g.node_label[n as usize]); + return VIEW_MARGIN + wrap_sub(g, p, n) * VIEW_COL_STEP + VIEW_LABEL_INDENT[s as usize]; +} + +pub fn wrap_y(g: SpecGraph, p: WrapPlan, n: u32) -> u32 { + return plan_slot_y(p, view_is_unknown(g.node_label[n as usize]), view_row(g, n)); +} + +pub fn wrap_node_x(g: SpecGraph, n: u32) -> u32 { + if (layout_safe(g) == false or n >= g.node_count) { return 0; } + return wrap_x(g, wrap_plan(g), n); +} + +pub fn wrap_node_y(g: SpecGraph, n: u32) -> u32 { + if (layout_safe(g) == false or n >= g.node_count) { return 0; } + return wrap_y(g, wrap_plan(g), n); +} + +pub fn wrap_width(g: SpecGraph) -> u32 { + let p : WrapPlan = wrap_plan(g); + return plan_width_at(p, plan_r(p)); +} + +/// The exact canvas height: the lowest box, whatever its style, plus a margin. +pub fn wrap_height(g: SpecGraph) -> u32 { + let p : WrapPlan = wrap_plan(g); + var bottom : u32 = VIEW_MARGIN; + var n : u32 = 0; + while (n < g.node_count and layout_safe(g)) { + bottom = fold_max(bottom, wrap_y(g, p, n) + view_node_h(g, n)); + n = n + 1; + } + return bottom + VIEW_MARGIN; +} + +/// width * 1000 / height of the wrapped canvas. +pub fn wrap_aspect_milli(g: SpecGraph) -> u32 { + return wrap_width(g) * 1000 / wrap_height(g); +} + +/// Boxes a and b share at least one pixel. +pub fn wrap_overlap(g: SpecGraph, p: WrapPlan, a: u32, b: u32) -> bool { + let ax : u32 = wrap_x(g, p, a); + let ay : u32 = wrap_y(g, p, a); + let bx : u32 = wrap_x(g, p, b); + let by : u32 = wrap_y(g, p, b); + return ax < bx + view_node_w(g, b) and bx < ax + view_node_w(g, a) and ay < by + view_node_h(g, b) and by < ay + view_node_h(g, a); +} + +pub fn wrap_overlap_count(g: SpecGraph) -> u32 { + if (layout_safe(g) == false) { return 0; } + let p : WrapPlan = wrap_plan(g); + var c : u32 = 0; + var a : u32 = 0; + while (a < g.node_count) { + var b : u32 = a + 1; + while (b < g.node_count) { + if (wrap_overlap(g, p, a, b)) { c = c + 1; } + b = b + 1; + } + a = a + 1; + } + return c; +} + +/// An edge leaves the side of its from-box that faces the to-box, judged by sub-column, and enters +/// the facing side of the to-box; inside one sub-column both ends are on the right side. +pub fn wrap_ex1(g: SpecGraph, p: WrapPlan, j: u32) -> u32 { + let f : u32 = g.edge_from[j as usize] as u32; + let t : u32 = g.edge_to[j as usize] as u32; + if (wrap_sub(g, p, f) > wrap_sub(g, p, t)) { return wrap_x(g, p, f); } + return wrap_x(g, p, f) + view_node_w(g, f); +} + +pub fn wrap_ex2(g: SpecGraph, p: WrapPlan, j: u32) -> u32 { + let f : u32 = g.edge_from[j as usize] as u32; + let t : u32 = g.edge_to[j as usize] as u32; + if (wrap_sub(g, p, f) < wrap_sub(g, p, t)) { return wrap_x(g, p, t); } + return wrap_x(g, p, t) + view_node_w(g, t); +} + +/// The vertical centre of node n, shifted to the lane of an edge of kind ek (the view's lane rule). +pub fn wrap_lane_y(g: SpecGraph, p: WrapPlan, n: u32, ek: u8) -> u32 { + return wrap_y(g, p, n) + view_node_h(g, n) / 2 + VIEW_LANE_STEP * (ek as u32) - VIEW_LANE_HALF; +} + +pub fn wrap_ey1(g: SpecGraph, p: WrapPlan, j: u32) -> u32 { + return wrap_lane_y(g, p, g.edge_from[j as usize] as u32, g.edge_kind[j as usize]); +} + +pub fn wrap_ey2(g: SpecGraph, p: WrapPlan, j: u32) -> u32 { + return wrap_lane_y(g, p, g.edge_to[j as usize] as u32, g.edge_kind[j as usize]); +} + +pub fn wrap_edge_x1(g: SpecGraph, j: u32) -> u32 { + if (layout_safe(g) == false or j >= g.edge_count) { return 0; } + return wrap_ex1(g, wrap_plan(g), j); +} + +pub fn wrap_edge_y1(g: SpecGraph, j: u32) -> u32 { + if (layout_safe(g) == false or j >= g.edge_count) { return 0; } + return wrap_ey1(g, wrap_plan(g), j); +} + +pub fn wrap_edge_x2(g: SpecGraph, j: u32) -> u32 { + if (layout_safe(g) == false or j >= g.edge_count) { return 0; } + return wrap_ex2(g, wrap_plan(g), j); +} + +pub fn wrap_edge_y2(g: SpecGraph, j: u32) -> u32 { + if (layout_safe(g) == false or j >= g.edge_count) { return 0; } + return wrap_ey2(g, wrap_plan(g), j); +} + +/// The point (x, y) lies on or inside the box of node n. +pub fn wrap_point_in(g: SpecGraph, p: WrapPlan, n: u32, x: u32, y: u32) -> bool { + let nx : u32 = wrap_x(g, p, n); + let ny : u32 = wrap_y(g, p, n); + return nx <= x and x <= nx + view_node_w(g, n) and ny <= y and y <= ny + view_node_h(g, n); +} + +/// Every edge end lands on the box of its own endpoint. +pub fn wrap_edges_attach(g: SpecGraph) -> bool { + if (layout_safe(g) == false) { return false; } + let p : WrapPlan = wrap_plan(g); + var j : u32 = 0; + while (j < g.edge_count) { + let f : u32 = g.edge_from[j as usize] as u32; + let t : u32 = g.edge_to[j as usize] as u32; + if (wrap_point_in(g, p, f, wrap_ex1(g, p, j), wrap_ey1(g, p, j)) == false) { return false; } + if (wrap_point_in(g, p, t, wrap_ex2(g, p, j), wrap_ey2(g, p, j)) == false) { return false; } + j = j + 1; + } + return true; +} + +/// --------------------------------------------------------------------------------------------- +/// HIT: a point to a node, a node to its lines +/// --------------------------------------------------------------------------------------------- + +/// The lowest node number whose box holds (x, y), or NO_INDEX: a miss, or a graph that is not +/// layout_safe. The wrapped boxes never overlap, so "lowest" only fixes the answer for a graph a +/// host built by hand. +pub fn wrap_hit(g: SpecGraph, x: u32, y: u32) -> u32 { + if (layout_safe(g) == false) { return NO_INDEX; } + let p : WrapPlan = wrap_plan(g); + var n : u32 = 0; + while (n < g.node_count) { + if (wrap_point_in(g, p, n, x, y)) { return n; } + n = n + 1; + } + return NO_INDEX; +} + +/// The first and last source line of node n; NO_LINE for a node that is not there or has no span. +pub fn wrap_node_line_start(g: SpecGraph, n: u32) -> u32 { + if (n >= g.node_count or n >= MAX_NODES) { return NO_LINE; } + return g.line_start[n as usize]; +} + +pub fn wrap_node_line_end(g: SpecGraph, n: u32) -> u32 { + if (n >= g.node_count or n >= MAX_NODES) { return NO_LINE; } + return g.line_end[n as usize]; +} + +/// Click to line: the first line of the span of the node under (x, y), NO_LINE when there is none. +pub fn wrap_hit_line_start(g: SpecGraph, x: u32, y: u32) -> u32 { + let n : u32 = wrap_hit(g, x, y); + if (n == NO_INDEX) { return NO_LINE; } + return wrap_node_line_start(g, n); +} + +pub fn wrap_hit_line_end(g: SpecGraph, x: u32, y: u32) -> u32 { + let n : u32 = wrap_hit(g, x, y); + if (n == NO_INDEX) { return NO_LINE; } + return wrap_node_line_end(g, n); +} + +/// --------------------------------------------------------------------------------------------- +/// FOLD: one box per kind, one arc per pair of kinds +/// --------------------------------------------------------------------------------------------- + +/// The level a host opens: every node when the spec has at most MEASURED_NODES_P95 of them (the +/// measured 95th percentile of the corpus, spec_graph.t27), else one box per kind. The threshold is a +/// presentation choice, not a measurement of readability. +pub fn fold_level(g: SpecGraph) -> u32 { + if (g.node_count <= MEASURED_NODES_P95) { return FOLD_LEVEL_FULL; } + return FOLD_LEVEL_KINDS; +} + +/// How many nodes of kind k the graph holds; 0 for a kind outside the vocabulary or an unsafe graph. +pub fn fold_kind_count(g: SpecGraph, k: u8) -> u32 { + if (k >= NODE_KINDS or layout_safe(g) == false) { return 0; } + var c : u32 = 0; + var n : u32 = 0; + while (n < g.node_count) { + if (g.node_kind[n as usize] == k) { c = c + 1; } + n = n + 1; + } + return c; +} + +/// How many nodes of kind k are drawn with label l (an unreadable label counts as UNKNOWN). +pub fn fold_kind_label_count(g: SpecGraph, k: u8, l: u8) -> u32 { + if (k >= NODE_KINDS or layout_safe(g) == false) { return 0; } + var c : u32 = 0; + var n : u32 = 0; + while (n < g.node_count) { + if (g.node_kind[n as usize] == k and view_style(g.node_label[n as usize]) == l) { c = c + 1; } + n = n + 1; + } + return c; +} + +/// The weakest label among the nodes of kind k, or LABEL_KINDS when there are none. +pub fn fold_kind_label(g: SpecGraph, k: u8) -> u8 { + var l : u32 = LABEL_KINDS as u32; + while (l > 0) { + l = l - 1; + if (fold_kind_label_count(g, k, l as u8) > 0) { return l as u8; } + } + return LABEL_KINDS; +} + +/// The kind whose box is in column c of the fold, or NO_INDEX. +pub fn fold_kind_at(g: SpecGraph, c: u32) -> u32 { + if (layout_safe(g) == false) { return NO_INDEX; } + var k : u32 = 0; + while (k < (NODE_KINDS as u32)) { + if (view_kind_present(g, k as u8) and view_column(g, k as u8) == c) { return k; } + k = k + 1; + } + return NO_INDEX; +} + +/// The room above the boxes for the highest arc; the same room is kept below for the backward arcs. +pub fn fold_rise(g: SpecGraph) -> u32 { + return fold_max(view_column_count(g), 2) * FOLD_ARC_STEP - FOLD_ARC_STEP; +} + +/// The fold has the view's columns, so it is as wide as the view. +pub fn fold_width(g: SpecGraph) -> u32 { + return view_width(g); +} + +pub fn fold_height(g: SpecGraph) -> u32 { + if (view_column_count(g) == 0) { return 2 * VIEW_MARGIN; } + return 2 * VIEW_MARGIN + 2 * fold_rise(g) + FOLD_BOX_H; +} + +/// Box geometry of kind k: 0 for a kind with no node. +pub fn fold_box_x(g: SpecGraph, k: u8) -> u32 { + if (fold_kind_count(g, k) == 0) { return 0; } + return VIEW_MARGIN + view_column(g, k) * VIEW_COL_STEP; +} + +pub fn fold_box_y(g: SpecGraph, k: u8) -> u32 { + if (fold_kind_count(g, k) == 0) { return 0; } + return VIEW_MARGIN + fold_rise(g); +} + +pub fn fold_box_w(g: SpecGraph, k: u8) -> u32 { + if (fold_kind_count(g, k) == 0) { return 0; } + return VIEW_COL_W; +} + +pub fn fold_box_h(g: SpecGraph, k: u8) -> u32 { + if (fold_kind_count(g, k) == 0) { return 0; } + return FOLD_BOX_H; +} + +/// The kind whose box holds (x, y), or NO_INDEX. +pub fn fold_hit(g: SpecGraph, x: u32, y: u32) -> u32 { + var k : u32 = 0; + while (k < (NODE_KINDS as u32)) { + let kk : u8 = k as u8; + let bx : u32 = fold_box_x(g, kk); + let by : u32 = fold_box_y(g, kk); + if (fold_kind_count(g, kk) > 0 and bx <= x and x <= bx + fold_box_w(g, kk) and by <= y and y <= by + fold_box_h(g, kk)) { return k; } + k = k + 1; + } + return NO_INDEX; +} + +/// Edges from a node of kind a to a node of kind b, of any edge kind; of edge kind ek. +pub fn fold_edge_count(g: SpecGraph, a: u8, b: u8) -> u32 { + if (layout_safe(g) == false) { return 0; } + var c : u32 = 0; + var j : u32 = 0; + while (j < g.edge_count) { + let k : usize = j as usize; + if (g.node_kind[g.edge_from[k] as usize] == a and g.node_kind[g.edge_to[k] as usize] == b) { c = c + 1; } + j = j + 1; + } + return c; +} + +pub fn fold_edge_count_kind(g: SpecGraph, a: u8, b: u8, ek: u8) -> u32 { + if (layout_safe(g) == false) { return 0; } + var c : u32 = 0; + var j : u32 = 0; + while (j < g.edge_count) { + let k : usize = j as usize; + if (g.edge_kind[k] == ek and g.node_kind[g.edge_from[k] as usize] == a and g.node_kind[g.edge_to[k] as usize] == b) { c = c + 1; } + j = j + 1; + } + return c; +} + +/// The weakest label among the edges from kind a to kind b, or LABEL_KINDS when there are none. +pub fn fold_edge_label(g: SpecGraph, a: u8, b: u8) -> u8 { + if (layout_safe(g) == false) { return LABEL_KINDS; } + var w : u32 = LABEL_KINDS as u32; + var j : u32 = 0; + while (j < g.edge_count) { + let k : usize = j as usize; + if (g.node_kind[g.edge_from[k] as usize] == a and g.node_kind[g.edge_to[k] as usize] == b) { + let l : u32 = view_style(g.edge_label[k]) as u32; + if (w == (LABEL_KINDS as u32) or l > w) { w = l; } + } + j = j + 1; + } + return w as u8; +} + +/// An arc is forward when its to-box is in the same column or a later one: drawn above the boxes. +pub fn fold_arc_forward(g: SpecGraph, a: u8, b: u8) -> bool { + return view_column(g, a) <= view_column(g, b); +} + +/// The arc ends. A forward arc leaves right of the from-box centre and enters left of the to-box +/// centre, a backward one the other way round, a self loop leaves left of the centre and enters right +/// of it. All 0 for a pair with no edge. +pub fn fold_edge_x1(g: SpecGraph, a: u8, b: u8) -> u32 { + if (fold_edge_count(g, a, b) == 0) { return 0; } + let cx : u32 = fold_box_x(g, a) + VIEW_COL_W / 2; + if (view_column(g, a) < view_column(g, b)) { return cx + FOLD_ANCHOR; } + return cx - FOLD_ANCHOR; +} + +pub fn fold_edge_x2(g: SpecGraph, a: u8, b: u8) -> u32 { + if (fold_edge_count(g, a, b) == 0) { return 0; } + let cx : u32 = fold_box_x(g, b) + VIEW_COL_W / 2; + if (view_column(g, a) < view_column(g, b)) { return cx - FOLD_ANCHOR; } + return cx + FOLD_ANCHOR; +} + +/// Where the arc touches the boxes: the top edge for a forward arc, the bottom edge for a backward one. +pub fn fold_edge_y(g: SpecGraph, a: u8, b: u8) -> u32 { + if (fold_edge_count(g, a, b) == 0) { return 0; } + if (fold_arc_forward(g, a, b)) { return fold_box_y(g, a); } + return fold_box_y(g, a) + FOLD_BOX_H; +} + +/// The y of the arc's far point, half way between its ends. +pub fn fold_edge_apex(g: SpecGraph, a: u8, b: u8) -> u32 { + if (fold_edge_count(g, a, b) == 0) { return 0; } + let ca : u32 = view_column(g, a); + let cb : u32 = view_column(g, b); + if (ca == cb) { return fold_box_y(g, a) - FOLD_LOOP_RISE; } + if (ca < cb) { return fold_box_y(g, a) - (cb - ca) * FOLD_ARC_STEP; } + return fold_box_y(g, a) + FOLD_BOX_H + (ca - cb) * FOLD_ARC_STEP; +} + +/// --------------------------------------------------------------------------------------------- +/// EXPAND: kind k and its direct neighbours +/// --------------------------------------------------------------------------------------------- + +/// The sub-graph of kind k: its nodes and every node an edge ties directly to one, in the original +/// order, and every edge with an end of kind k, renumbered; fields are copied unchanged. A graph +/// that is not layout_safe, or a kind outside the vocabulary, gives a graph with no node. +pub fn expand_graph(g: SpecGraph, k: u8) -> SpecGraph { + var h : SpecGraph = blank_graph(); + h.ir_version = g.ir_version; + h.spec_path = g.spec_path; + h.line_count = g.line_count; + if (k >= NODE_KINDS or layout_safe(g) == false) { return h; } + // Scratch: a blank graph's line_start[n] is 0 for a dropped node, then 1 for a kept node, then + // the new node number plus 1. Its size follows MAX_NODES. + var map : SpecGraph = blank_graph(); + var n : u32 = 0; + while (n < g.node_count) { + if (g.node_kind[n as usize] == k) { map.line_start[n as usize] = 1; } + n = n + 1; + } + var j : u32 = 0; + while (j < g.edge_count) { + let e : usize = j as usize; + if (g.node_kind[g.edge_from[e] as usize] == k or g.node_kind[g.edge_to[e] as usize] == k) { + map.line_start[g.edge_from[e] as usize] = 1; + map.line_start[g.edge_to[e] as usize] = 1; + } + j = j + 1; + } + n = 0; + while (n < g.node_count) { + let q : usize = n as usize; + if (map.line_start[q] != 0) { + map.line_start[q] = h.node_count + 1; + h = with_node(h, g.node_kind[q], g.node_label[q], g.node_name[q], g.line_start[q], g.line_end[q]); + } + n = n + 1; + } + j = 0; + while (j < g.edge_count) { + let e : usize = j as usize; + if (g.node_kind[g.edge_from[e] as usize] == k or g.node_kind[g.edge_to[e] as usize] == k) { + let nf : u32 = map.line_start[g.edge_from[e] as usize] - 1; + let nt : u32 = map.line_start[g.edge_to[e] as usize] - 1; + h = with_edge(h, g.edge_kind[e], nf as u8, nt as u8, g.edge_label[e], g.edge_line[e]); + } + j = j + 1; + } + return h; +} + +/// The node number in g of node i of expand_graph(g, k), or NO_INDEX when i is past it. +pub fn expand_origin(g: SpecGraph, k: u8, i: u32) -> u32 { + let h : SpecGraph = expand_graph(g, k); + if (i >= h.node_count) { return NO_INDEX; } + var seen : u32 = 0; + var n : u32 = 0; + while (n < g.node_count) { + let q : usize = n as usize; + var kept : bool = g.node_kind[q] == k; + var j : u32 = 0; + while (j < g.edge_count and kept == false) { + let e : usize = j as usize; + if ((g.edge_from[e] as u32) == n or (g.edge_to[e] as u32) == n) { + if (g.node_kind[g.edge_from[e] as usize] == k or g.node_kind[g.edge_to[e] as usize] == k) { kept = true; } + } + j = j + 1; + } + if (kept) { + if (seen == i) { return n; } + seen = seen + 1; + } + n = n + 1; + } + return NO_INDEX; +} + +/// The expanded kind laid out with the wrap functions: the node of g under (x, y), or NO_INDEX. +pub fn expand_hit(g: SpecGraph, k: u8, x: u32, y: u32) -> u32 { + let h : SpecGraph = expand_graph(g, k); + let i : u32 = wrap_hit(h, x, y); + if (i == NO_INDEX) { return NO_INDEX; } + return expand_origin(g, k, i); +} + +/// --------------------------------------------------------------------------------------------- +/// Host entry points +/// --------------------------------------------------------------------------------------------- + +/// The wide fixture: tutorial 08 plus WIDE_FNS fns, each calling the one before, and WIDE_TESTS +/// tests, test i calling extra fn i. A hand-made graph for the wrap, not a spec's. +pub fn wide_graph() -> SpecGraph { + var g : SpecGraph = tutorial_08_graph(); + let base : u32 = g.node_count; + var i : u32 = 0; + while (i < WIDE_FNS) { + let s : u32 = 1 + (i % 90); + g = with_node(g, NODE_FN, LABEL_EXACT, "wide_fn", s, s + 2); + i = i + 1; + } + i = 0; + while (i < WIDE_TESTS) { + let s : u32 = 1 + (i % 90); + g = with_node(g, NODE_TEST, LABEL_EXACT, "wide_test", s, s + 2); + i = i + 1; + } + i = 1; + while (i < WIDE_FNS) { + let s : u32 = 1 + (i % 90); + g = with_edge(g, EDGE_CALLS, (base + i) as u8, (base + i - 1) as u8, LABEL_EXACT, s); + i = i + 1; + } + i = 0; + while (i < WIDE_TESTS) { + let s : u32 = 1 + (i % 90); + g = with_edge(g, EDGE_CALLS, (base + WIDE_FNS + i) as u8, (base + i) as u8, LABEL_EXACT, s); + i = i + 1; + } + return g; +} + +/// A wasm export carries scalars only, so these name a fixture: 0 and 1 are the view's, 2 is +/// wide_graph, any other number is the blank graph. An index past the graph returns 0, and a kind +/// outside the vocabulary has no box, no count and no neighbour. +pub fn fold_fx(which: u32) -> SpecGraph { + if (which == 2) { return wide_graph(); } + return view_fx(which); +} + +pub fn expand_fx(which: u32, k: u32) -> SpecGraph { + return expand_graph(fold_fx(which), fold_kind_u8(k)); +} + +/// A kind number from a host as a kind code; a number past the vocabulary becomes NODE_KINDS, which +/// every function here treats as "no such kind". +pub fn fold_kind_u8(k: u32) -> u8 { + if (k >= (NODE_KINDS as u32)) { return NODE_KINDS; } + return k as u8; +} + +pub fn fx_wrap_rows(which: u32) -> u32 { + return wrap_rows(fold_fx(which)); +} + +pub fn fx_wrap_width(which: u32) -> u32 { + return wrap_width(fold_fx(which)); +} + +pub fn fx_wrap_height(which: u32) -> u32 { + return wrap_height(fold_fx(which)); +} + +pub fn fx_wrap_aspect_milli(which: u32) -> u32 { + return wrap_aspect_milli(fold_fx(which)); +} + +pub fn fx_wrap_node_count(which: u32) -> u32 { + return fold_fx(which).node_count; +} + +pub fn fx_wrap_edge_count(which: u32) -> u32 { + return fold_fx(which).edge_count; +} + +pub fn fx_wrap_node_x(which: u32, n: u32) -> u32 { + return wrap_node_x(fold_fx(which), n); +} + +pub fn fx_wrap_node_y(which: u32, n: u32) -> u32 { + return wrap_node_y(fold_fx(which), n); +} + +pub fn fx_wrap_edge_x1(which: u32, j: u32) -> u32 { + return wrap_edge_x1(fold_fx(which), j); +} + +pub fn fx_wrap_edge_y1(which: u32, j: u32) -> u32 { + return wrap_edge_y1(fold_fx(which), j); +} + +pub fn fx_wrap_edge_x2(which: u32, j: u32) -> u32 { + return wrap_edge_x2(fold_fx(which), j); +} + +pub fn fx_wrap_edge_y2(which: u32, j: u32) -> u32 { + return wrap_edge_y2(fold_fx(which), j); +} + +pub fn fx_wrap_hit(which: u32, x: u32, y: u32) -> u32 { + return wrap_hit(fold_fx(which), x, y); +} + +pub fn fx_wrap_hit_line_start(which: u32, x: u32, y: u32) -> u32 { + return wrap_hit_line_start(fold_fx(which), x, y); +} + +pub fn fx_wrap_hit_line_end(which: u32, x: u32, y: u32) -> u32 { + return wrap_hit_line_end(fold_fx(which), x, y); +} + +pub fn fx_wrap_node_line_start(which: u32, n: u32) -> u32 { + return wrap_node_line_start(fold_fx(which), n); +} + +pub fn fx_wrap_node_line_end(which: u32, n: u32) -> u32 { + return wrap_node_line_end(fold_fx(which), n); +} + +pub fn fx_fold_level(which: u32) -> u32 { + return fold_level(fold_fx(which)); +} + +pub fn fx_fold_width(which: u32) -> u32 { + return fold_width(fold_fx(which)); +} + +pub fn fx_fold_height(which: u32) -> u32 { + return fold_height(fold_fx(which)); +} + +pub fn fx_fold_columns(which: u32) -> u32 { + return view_column_count(fold_fx(which)); +} + +pub fn fx_fold_kind_at(which: u32, c: u32) -> u32 { + return fold_kind_at(fold_fx(which), c); +} + +pub fn fx_fold_kind_count(which: u32, k: u32) -> u32 { + return fold_kind_count(fold_fx(which), fold_kind_u8(k)); +} + +pub fn fx_fold_kind_label(which: u32, k: u32) -> u32 { + return fold_kind_label(fold_fx(which), fold_kind_u8(k)) as u32; +} + +pub fn fx_fold_kind_label_count(which: u32, k: u32, l: u32) -> u32 { + return fold_kind_label_count(fold_fx(which), fold_kind_u8(k), fold_kind_u8(l)); +} + +pub fn fx_fold_box_x(which: u32, k: u32) -> u32 { + return fold_box_x(fold_fx(which), fold_kind_u8(k)); +} + +pub fn fx_fold_box_y(which: u32, k: u32) -> u32 { + return fold_box_y(fold_fx(which), fold_kind_u8(k)); +} + +pub fn fx_fold_box_w(which: u32, k: u32) -> u32 { + return fold_box_w(fold_fx(which), fold_kind_u8(k)); +} + +pub fn fx_fold_box_h(which: u32, k: u32) -> u32 { + return fold_box_h(fold_fx(which), fold_kind_u8(k)); +} + +pub fn fx_fold_hit(which: u32, x: u32, y: u32) -> u32 { + return fold_hit(fold_fx(which), x, y); +} + +pub fn fx_fold_edge_count(which: u32, a: u32, b: u32) -> u32 { + return fold_edge_count(fold_fx(which), fold_kind_u8(a), fold_kind_u8(b)); +} + +pub fn fx_fold_edge_count_kind(which: u32, a: u32, b: u32, ek: u32) -> u32 { + return fold_edge_count_kind(fold_fx(which), fold_kind_u8(a), fold_kind_u8(b), ek as u8); +} + +pub fn fx_fold_edge_label(which: u32, a: u32, b: u32) -> u32 { + return fold_edge_label(fold_fx(which), fold_kind_u8(a), fold_kind_u8(b)) as u32; +} + +pub fn fx_fold_edge_x1(which: u32, a: u32, b: u32) -> u32 { + return fold_edge_x1(fold_fx(which), fold_kind_u8(a), fold_kind_u8(b)); +} + +pub fn fx_fold_edge_x2(which: u32, a: u32, b: u32) -> u32 { + return fold_edge_x2(fold_fx(which), fold_kind_u8(a), fold_kind_u8(b)); +} + +pub fn fx_fold_edge_y(which: u32, a: u32, b: u32) -> u32 { + return fold_edge_y(fold_fx(which), fold_kind_u8(a), fold_kind_u8(b)); +} + +pub fn fx_fold_edge_apex(which: u32, a: u32, b: u32) -> u32 { + return fold_edge_apex(fold_fx(which), fold_kind_u8(a), fold_kind_u8(b)); +} + +pub fn fx_expand_node_count(which: u32, k: u32) -> u32 { + return expand_fx(which, k).node_count; +} + +pub fn fx_expand_edge_count(which: u32, k: u32) -> u32 { + return expand_fx(which, k).edge_count; +} + +pub fn fx_expand_origin(which: u32, k: u32, i: u32) -> u32 { + return expand_origin(fold_fx(which), fold_kind_u8(k), i); +} + +pub fn fx_expand_width(which: u32, k: u32) -> u32 { + return wrap_width(expand_fx(which, k)); +} + +pub fn fx_expand_height(which: u32, k: u32) -> u32 { + return wrap_height(expand_fx(which, k)); +} + +pub fn fx_expand_node_x(which: u32, k: u32, i: u32) -> u32 { + return wrap_node_x(expand_fx(which, k), i); +} + +pub fn fx_expand_node_y(which: u32, k: u32, i: u32) -> u32 { + return wrap_node_y(expand_fx(which, k), i); +} + +pub fn fx_expand_edge_x1(which: u32, k: u32, j: u32) -> u32 { + return wrap_edge_x1(expand_fx(which, k), j); +} + +pub fn fx_expand_edge_y1(which: u32, k: u32, j: u32) -> u32 { + return wrap_edge_y1(expand_fx(which, k), j); +} + +pub fn fx_expand_edge_x2(which: u32, k: u32, j: u32) -> u32 { + return wrap_edge_x2(expand_fx(which, k), j); +} + +pub fn fx_expand_edge_y2(which: u32, k: u32, j: u32) -> u32 { + return wrap_edge_y2(expand_fx(which, k), j); +} + +pub fn fx_expand_hit(which: u32, k: u32, x: u32, y: u32) -> u32 { + return expand_hit(fold_fx(which), fold_kind_u8(k), x, y); +} + +/// --- tests --- + +test the_wrap_leaves_the_fixtures_as_the_view_draws_them { + var which : u32 = 0; + while (which < 2) { + let g : SpecGraph = view_fx(which); + assert view_verdict(g) == VIEW_OK; + assert wrap_rows(g) == WRAP_MAX_ROWS; + assert wrap_width(g) == view_width(g); + assert wrap_height(g) == view_height(g); + var same : u32 = 0; + var n : u32 = 0; + while (n < g.node_count) { + if (wrap_node_x(g, n) == view_node_x(g, n) and wrap_node_y(g, n) == view_node_y(g, n)) { same = same + 1; } + n = n + 1; + } + assert same == g.node_count; + var edges : u32 = 0; + var j : u32 = 0; + while (j < g.edge_count) { + let ok : bool = wrap_edge_x1(g, j) == view_edge_x1(g, j) and wrap_edge_y1(g, j) == view_edge_y1(g, j) and wrap_edge_x2(g, j) == view_edge_x2(g, j) and wrap_edge_y2(g, j) == view_edge_y2(g, j); + if (ok) { edges = edges + 1; } + j = j + 1; + } + assert edges == g.edge_count; + assert wrap_overlap_count(g) == 0; + assert wrap_edges_attach(g); + which = which + 1; + } + // the first fixture is 1860 x 484, as in the view, and its aspect is nowhere near phi: a graph + // that fits in N rows is not bent to phi + let t : SpecGraph = tutorial_08_graph(); + assert wrap_width(t) == 1860; + assert wrap_height(t) == 484; + assert wrap_aspect_milli(t) == 3842; +} + +test a_300_fn_plan_fits_the_aspect_and_no_slot_overlaps { + var p : WrapPlan = plan_empty(); + p.known[NODE_MODULE as usize] = 1; + p.known[NODE_USE as usize] = 1; + p.known[NODE_CONST as usize] = 3; + p.known[NODE_FN as usize] = 300; + p.known[NODE_TEST as usize] = 20; + p.known[NODE_INVARIANT as usize] = 1; + p.unk[NODE_OPAQUE as usize] = 1; + assert plan_slots(p) == 327; + assert plan_tallest(p) == 300; + let q : WrapPlan = plan_solve(p); + // 35 rows, 9 fn sub-columns, 15 columns: 3940 x 2404, aspect 1638, 20 from phi + assert q.rows == 35; + assert plan_cols_at(q, q.rows) == 15; + assert plan_width_at(q, q.rows) == 3940; + assert plan_height_at(q, q.rows) == 2404; + assert plan_dev(q, q.rows) == 20; + assert plan_dev(q, q.rows) <= WRAP_ASPECT_TOLERANCE_MILLI; + assert plan_overlap_count(q) == 0; + // negative control: left as one 300-row column the same graph is 96:1000 wide, far off phi + assert plan_dev(p, 300) > WRAP_ASPECT_TOLERANCE_MILLI; + assert plan_width_at(p, 300) == 1860; + assert plan_height_at(p, 300) == 19364; + // negative control: two slots at one position overlap; one slot against itself does too + assert plan_slot_overlap(q, NODE_FN as u32, false, 0, NODE_FN as u32, false, 0); + assert plan_slot_overlap(q, NODE_FN as u32, false, 0, NODE_FN as u32, false, 1) == false; + // negative control: a plan forced to 1 row per sub-column lays slots on top of the next kind + // only if its starts are wrong; here each kind still starts after the previous one + var one : WrapPlan = q; + one.rows = 1; + assert plan_overlap_count(one) == 0; + // a 300-fn plan with N = 12 rows would be 25 sub-columns: no wrap needed below N, a wrap above + assert plan_span_at(q, NODE_FN as u32, WRAP_MAX_ROWS) == 25; + assert plan_span_at(q, NODE_FN as u32, q.rows) == 9; + assert plan_span_at(q, NODE_BENCH as u32, q.rows) == 0; +} + +test the_wide_graph_wraps_to_near_phi { + let g : SpecGraph = wide_graph(); + assert g.node_count == 185; + assert g.edge_count == 185; + assert validate_graph(g) == GRAPH_OK; + assert view_verdict(g) == VIEW_OK; + assert layout_safe(g); + // 153 fns in 28-row sub-columns: 6 of them, 12 columns, 3160 x 1956, aspect 1615 + assert wrap_rows(g) == 28; + assert wrap_width(g) == 3160; + assert wrap_height(g) == 1956; + assert wrap_aspect_milli(g) == 1615; + assert fold_gap(wrap_aspect_milli(g), WRAP_ASPECT_MILLI) <= WRAP_ASPECT_TOLERANCE_MILLI; + assert wrap_height(g) <= plan_height_at(wrap_plan(g), 28); + assert wrap_overlap_count(g) == 0; + assert wrap_edges_attach(g); + // the unwrapped view of the same graph is one 153-row column, nearly 10,000 px tall + assert view_height(g) == 9956; + assert view_overlap_count(g) == 0; + assert wrap_height(g) * 4 < view_height(g); + // fn ordinal 27 (node 39) ends the first sub-column; ordinal 28 (node 40) starts the second + assert wrap_node_x(g, 39) == 820; + assert wrap_node_y(g, 39) == 1768; + assert wrap_node_x(g, 40) == 1080; + assert wrap_node_y(g, 40) == 40; + // the call from node 40 back to node 39 crosses a sub-column: it leaves the left side of 40 and + // enters the right side of 39 + assert wrap_edge_x1(g, 40) == 1080; + assert wrap_edge_x2(g, 40) == 1040; + assert wrap_edge_y1(g, 40) == 54; + assert wrap_edge_y2(g, 40) == 1782; + // the UNKNOWN node keeps its band below the known boxes, at the last column + assert wrap_node_x(g, 14) == 2910; + assert wrap_node_y(g, 14) == 1880; + // later kinds shift right by the extra fn sub-columns: tests start at sub-column 9 + assert wrap_node_x(g, 10) == 40 + 9 * 260; +} + +test the_graph_filled_to_capacity_keeps_its_aspect { + var g : SpecGraph = tutorial_08_graph(); + // as many fns as MAX_NODES allows, at most 300: the 300-fn graph of the issue once the cap allows it + let want : u32 = fold_min(300, MAX_NODES - g.node_count); + var i : u32 = 0; + while (i < want) { + let s : u32 = 1 + (i % 90); + g = with_node(g, NODE_FN, LABEL_EXACT, "big_fn", s, s + 2); + i = i + 1; + } + assert g.node_count == 15 + want; + assert layout_safe(g); + assert fold_kind_count(g, NODE_FN) == 3 + want; + assert fold_gap(wrap_aspect_milli(g), WRAP_ASPECT_MILLI) <= WRAP_ASPECT_TOLERANCE_MILLI; + assert wrap_overlap_count(g) == 0; + // the unwrapped view is far taller than the wrapped canvas is wide + assert view_height(g) > wrap_height(g); + // negative control: a graph past the arrays is not laid out + var over : SpecGraph = g; + over.node_count = MAX_NODES + 1; + assert layout_safe(over) == false; + assert wrap_overlap_count(over) == 0; + assert wrap_node_x(over, 0) == 0; +} + +test hit_testing_maps_a_point_to_a_node_and_its_lines { + let g : SpecGraph = wide_graph(); + // the module box is at (40, 40), 220 x 44: its corner pixels belong to it + assert wrap_hit(g, 40, 40) == 0; + assert wrap_hit(g, 260, 84) == 0; + assert wrap_hit(g, 261, 84) == NO_INDEX; + // node 40, the first row of the second fn sub-column, spans lines 26 to 28 + assert wrap_hit(g, 1100, 50) == 40; + assert wrap_node_line_start(g, 40) == 26; + assert wrap_node_line_end(g, 40) == 28; + assert wrap_hit_line_start(g, 1100, 50) == 26; + assert wrap_hit_line_end(g, 1100, 50) == 28; + // the UNKNOWN node std.testing (lines 79 to 89) is hit in its band, not above it + assert wrap_hit(g, 2920, 1890) == 14; + assert wrap_hit_line_start(g, 2920, 1890) == 79; + assert wrap_hit_line_end(g, 2920, 1890) == 89; + // negative controls: the gap between two rows, the margin, a point past the canvas + assert wrap_hit(g, 1100, 95) == NO_INDEX; + assert wrap_hit(g, 1, 1) == NO_INDEX; + assert wrap_hit(g, 100000, 100000) == NO_INDEX; + assert wrap_hit_line_start(g, 1, 1) == NO_LINE; + assert wrap_hit_line_end(g, 1, 1) == NO_LINE; + assert wrap_node_line_start(g, 185) == NO_LINE; + // a graph that is not layout_safe gives no hit + var bad : SpecGraph = g; + bad.edge_to[0] = 200; + assert layout_safe(bad) == false; + assert wrap_hit(bad, 40, 40) == NO_INDEX; + // every node is hit at its own corner (no two boxes share a pixel, so each box is reachable) + var found : u32 = 0; + var n : u32 = 0; + while (n < g.node_count) { + if (wrap_hit(g, wrap_node_x(g, n), wrap_node_y(g, n)) == n) { found = found + 1; } + n = n + 1; + } + assert found == g.node_count; +} + +test the_fold_shows_one_box_per_kind_with_exact_counts { + let g : SpecGraph = tutorial_08_graph(); + assert fold_level(g) == FOLD_LEVEL_FULL; + assert fold_kind_count(g, NODE_MODULE) == 1; + assert fold_kind_count(g, NODE_USE) == 1; + assert fold_kind_count(g, NODE_CONST) == 5; + assert fold_kind_count(g, NODE_FN) == 3; + assert fold_kind_count(g, NODE_TEST) == 3; + assert fold_kind_count(g, NODE_INVARIANT) == 1; + assert fold_kind_count(g, NODE_OPAQUE) == 1; + assert fold_kind_count(g, NODE_BENCH) == 0; + // the counts lose nothing and invent nothing: they sum to the graph's own counts + var total : u32 = 0; + var k : u32 = 0; + while (k < (NODE_KINDS as u32)) { + total = total + fold_kind_count(g, k as u8); + k = k + 1; + } + assert total == g.node_count; + var edges : u32 = 0; + var a : u32 = 0; + while (a < (NODE_KINDS as u32)) { + var b : u32 = 0; + while (b < (NODE_KINDS as u32)) { + edges = edges + fold_edge_count(g, a as u8, b as u8); + b = b + 1; + } + a = a + 1; + } + assert edges == g.edge_count; + // labels: one UNKNOWN opaque region, everything else exact; the box takes the weakest label + assert fold_kind_label_count(g, NODE_OPAQUE, LABEL_UNKNOWN) == 1; + assert fold_kind_label_count(g, NODE_FN, LABEL_EXACT) == 3; + assert fold_kind_label(g, NODE_FN) == LABEL_EXACT; + assert fold_kind_label(g, NODE_OPAQUE) == LABEL_UNKNOWN; + assert fold_kind_label(g, NODE_BENCH) == LABEL_KINDS; + // a single derived fn makes its box derived, though two of its three nodes stay exact + var d : SpecGraph = tutorial_08_graph(); + d.node_label[9] = LABEL_DERIVED; + assert fold_kind_label(d, NODE_FN) == LABEL_DERIVED; + assert fold_kind_label_count(d, NODE_FN, LABEL_EXACT) == 2; + assert fold_kind_label_count(d, NODE_FN, LABEL_DERIVED) == 1; + // boxes sit in one row in the view's column order, below the room for the arcs + assert fold_rise(g) == 216; + assert fold_width(g) == view_width(g); + assert fold_height(g) == 576; + assert fold_box_x(g, NODE_MODULE) == 40; + assert fold_box_x(g, NODE_FN) == 820; + assert fold_box_x(g, NODE_OPAQUE) == 1600; + assert fold_box_y(g, NODE_FN) == 256; + assert fold_box_w(g, NODE_FN) == 220; + assert fold_box_h(g, NODE_FN) == 64; + assert fold_kind_at(g, 0) == NODE_MODULE as u32; + assert fold_kind_at(g, 3) == NODE_FN as u32; + assert fold_kind_at(g, 6) == NODE_OPAQUE as u32; + assert fold_kind_at(g, 7) == NO_INDEX; + // negative controls: an absent kind has no box and no arc, a kind past the vocabulary neither + assert fold_box_w(g, NODE_BENCH) == 0; + assert fold_box_x(g, NODE_BENCH) == 0; + assert fold_kind_count(g, NODE_KINDS) == 0; + assert fold_edge_count(g, NODE_BENCH, NODE_FN) == 0; + assert fold_edge_label(g, NODE_BENCH, NODE_FN) == LABEL_KINDS; + assert fold_edge_x1(g, NODE_BENCH, NODE_FN) == 0; +} + +test the_fold_aggregates_edges_between_kinds { + let g : SpecGraph = tutorial_08_graph(); + // uses: module -> use 1; fn -> const 4. data_flow const -> fn 4. calls test -> fn 3. + // tested_by fn -> test 3. constrains invariant -> const 1. Sixteen edges in all. + assert fold_edge_count(g, NODE_MODULE, NODE_USE) == 1; + assert fold_edge_count(g, NODE_FN, NODE_CONST) == 4; + assert fold_edge_count(g, NODE_CONST, NODE_FN) == 4; + assert fold_edge_count(g, NODE_TEST, NODE_FN) == 3; + assert fold_edge_count(g, NODE_FN, NODE_TEST) == 3; + assert fold_edge_count(g, NODE_INVARIANT, NODE_CONST) == 1; + assert fold_edge_count(g, NODE_FN, NODE_FN) == 0; + assert fold_edge_count_kind(g, NODE_FN, NODE_CONST, EDGE_USES) == 4; + assert fold_edge_count_kind(g, NODE_FN, NODE_CONST, EDGE_CALLS) == 0; + assert fold_edge_count_kind(g, NODE_CONST, NODE_FN, EDGE_DATA_FLOW) == 4; + assert fold_edge_count_kind(g, NODE_FN, NODE_TEST, EDGE_TESTED_BY) == 3; + assert fold_edge_count_kind(g, NODE_TEST, NODE_FN, EDGE_CALLS) == 3; + // the label of an aggregate is its weakest member's: fn -> const is all exact, const -> fn all derived + assert fold_edge_label(g, NODE_FN, NODE_CONST) == LABEL_EXACT; + assert fold_edge_label(g, NODE_CONST, NODE_FN) == LABEL_DERIVED; + assert fold_edge_label(g, NODE_FN, NODE_TEST) == LABEL_DERIVED; + assert fold_edge_label(g, NODE_TEST, NODE_FN) == LABEL_EXACT; + // a suggested edge makes its pair suggested + var s : SpecGraph = tutorial_08_graph(); + s.edge_label[1] = LABEL_SUGGESTED; + assert fold_edge_label(s, NODE_FN, NODE_CONST) == LABEL_SUGGESTED; + // arcs. fn (column 3) -> const (column 2) runs backward: below the boxes, from the left of the + // fn box to the right of the const box, one column wide + assert fold_arc_forward(g, NODE_FN, NODE_CONST) == false; + assert fold_edge_x1(g, NODE_FN, NODE_CONST) == 906; + assert fold_edge_x2(g, NODE_FN, NODE_CONST) == 694; + assert fold_edge_y(g, NODE_FN, NODE_CONST) == 320; + assert fold_edge_apex(g, NODE_FN, NODE_CONST) == 356; + // const -> fn runs forward: above the boxes, over the same two columns + assert fold_arc_forward(g, NODE_CONST, NODE_FN); + assert fold_edge_x1(g, NODE_CONST, NODE_FN) == 694; + assert fold_edge_x2(g, NODE_CONST, NODE_FN) == 906; + assert fold_edge_y(g, NODE_CONST, NODE_FN) == 256; + assert fold_edge_apex(g, NODE_CONST, NODE_FN) == 220; + // module (column 0) -> use (column 1) spans one column; invariant (5) -> const (2) spans three + assert fold_edge_apex(g, NODE_MODULE, NODE_USE) == 220; + assert fold_edge_apex(g, NODE_INVARIANT, NODE_CONST) == 320 + 3 * 36; + // the highest arc stays inside the room kept for it + assert fold_rise(g) >= 6 * FOLD_ARC_STEP; +} + +test the_fold_hit_maps_a_point_to_a_kind { + let g : SpecGraph = tutorial_08_graph(); + assert fold_hit(g, 50, 260) == NODE_MODULE as u32; + assert fold_hit(g, 900, 300) == NODE_FN as u32; + assert fold_hit(g, 1700, 300) == NODE_OPAQUE as u32; + // negative controls: the room above the boxes (where arcs run), a gap between boxes, and the + // column an absent kind (bench) would have had + assert fold_hit(g, 900, 100) == NO_INDEX; + assert fold_hit(g, 285, 300) == NO_INDEX; + assert fold_hit(g, 5, 5) == NO_INDEX; + assert fold_hit(blank_graph(), 100, 100) == NO_INDEX; +} + +test a_wide_spec_opens_folded { + let g : SpecGraph = wide_graph(); + assert fold_level(g) == FOLD_LEVEL_KINDS; + assert fold_kind_count(g, NODE_FN) == 153; + assert fold_kind_count(g, NODE_TEST) == 23; + // 149 fn -> fn calls, plus 3 tutorial calls from tests and 20 wide ones from tests + assert fold_edge_count(g, NODE_FN, NODE_FN) == 149; + assert fold_edge_count(g, NODE_TEST, NODE_FN) == 23; + assert fold_edge_count_kind(g, NODE_FN, NODE_FN, EDGE_CALLS) == 149; + assert fold_edge_apex(g, NODE_FN, NODE_FN) == 256 - 28; + // the fold stays seven boxes wide whatever the graph's size + assert fold_width(g) == 1860; + assert fold_height(g) == 576; + // the pin of the issue: a fold needs no row per node + assert fold_height(g) * 2 < wrap_height(g); +} + +test expand_keeps_the_kind_and_its_neighbours_verbatim { + let g : SpecGraph = tutorial_08_graph(); + let e : SpecGraph = expand_graph(g, NODE_FN); + // the three fns, their four consts, their three tests: ten nodes; every edge that touches a fn, + // fourteen of sixteen (the use statement and the invariant's constraint are not) + assert e.node_count == 10; + assert e.edge_count == 14; + assert layout_safe(e); + const ep : str = e.spec_path; + const gp : str = g.spec_path; + assert (ep == gp); + assert e.line_count == g.line_count; + assert expand_origin(g, NODE_FN, 0) == 2; + assert expand_origin(g, NODE_FN, 2) == 4; + assert expand_origin(g, NODE_FN, 3) == 6; + assert expand_origin(g, NODE_FN, 5) == 8; + assert expand_origin(g, NODE_FN, 9) == 12; + assert expand_origin(g, NODE_FN, 10) == NO_INDEX; + var same : u32 = 0; + var i : u32 = 0; + while (i < e.node_count) { + let o : usize = expand_origin(g, NODE_FN, i) as usize; + let q : usize = i as usize; + const en : str = e.node_name[q]; + const gn : str = g.node_name[o]; + if (e.node_kind[q] == g.node_kind[o] and e.node_label[q] == g.node_label[o] and (en == gn) and e.line_start[q] == g.line_start[o] and e.line_end[q] == g.line_end[o]) { same = same + 1; } + i = i + 1; + } + assert same == 10; + // edge j of the sub-graph is edge j + 1 of the graph, ends renumbered, everything else copied + var kept : u32 = 0; + var j : u32 = 0; + while (j < e.edge_count) { + let a : usize = j as usize; + let o : usize = (j + 1) as usize; + let ends : bool = expand_origin(g, NODE_FN, e.edge_from[a] as u32) == (g.edge_from[o] as u32) and expand_origin(g, NODE_FN, e.edge_to[a] as u32) == (g.edge_to[o] as u32); + if (ends and e.edge_kind[a] == g.edge_kind[o] and e.edge_label[a] == g.edge_label[o] and e.edge_line[a] == g.edge_line[o]) { kept = kept + 1; } + j = j + 1; + } + assert kept == 14; + // a derived edge keeps its missing line, and the view still reads the call it is read back from + assert e.edge_line[11] == NO_LINE; + assert view_edge_line(e, 11) == 79; + // negative controls. A sub-graph without the module is not a valid SpecGraph, but is drawable. + assert validate_graph(e) == ERR_MODULE; + // the module's own neighbourhood does hold the module at node 0 + let m : SpecGraph = expand_graph(g, NODE_MODULE); + assert m.node_count == 2; + assert m.edge_count == 1; + assert validate_graph(m) == GRAPH_OK; + // the invariant touches one const and nothing else + let v : SpecGraph = expand_graph(g, NODE_INVARIANT); + assert v.node_count == 2; + assert v.edge_count == 1; + assert expand_origin(g, NODE_INVARIANT, 0) == 2; + assert expand_origin(g, NODE_INVARIANT, 1) == 13; + // the UNKNOWN region has no edge, so it expands to itself; an absent kind to nothing + let u : SpecGraph = expand_graph(g, NODE_OPAQUE); + assert u.node_count == 1; + assert u.edge_count == 0; + assert expand_origin(g, NODE_OPAQUE, 0) == 14; + assert expand_graph(g, NODE_BENCH).node_count == 0; + assert expand_graph(g, NODE_KINDS).node_count == 0; + // a graph that is not layout_safe expands to nothing + var bad : SpecGraph = g; + bad.edge_from[0] = 200; + assert expand_graph(bad, NODE_FN).node_count == 0; +} + +test the_expanded_kind_is_laid_out_and_hit_through_the_origin_map { + let g : SpecGraph = tutorial_08_graph(); + let e : SpecGraph = expand_graph(g, NODE_FN); + // consts, fns, tests: three columns; the deepest is the four consts + assert wrap_rows(e) == WRAP_MAX_ROWS; + assert wrap_width(e) == 820; + assert wrap_height(e) == 316; + assert wrap_overlap_count(e) == 0; + assert wrap_edges_attach(e); + // the first fn (scaled_width, node 4 of g) is the first row of the middle column + assert wrap_node_x(e, 2) == 300; + assert wrap_node_y(e, 2) == 40; + assert expand_hit(g, NODE_FN, 310, 50) == 4; + assert expand_hit(g, NODE_FN, 310, 120) == 8; + // a click on a const box maps back to the const's number in g + assert expand_hit(g, NODE_FN, 50, 50) == 2; + // negative controls: a gap, and a click where a box of another kind (the invariant) was dropped + assert expand_hit(g, NODE_FN, 285, 50) == NO_INDEX; + assert expand_hit(g, NODE_FN, 1400, 50) == NO_INDEX; + // the wide graph expands a kind into a short canvas + let w : SpecGraph = wide_graph(); + let t : SpecGraph = expand_graph(w, NODE_TEST); + // the 23 tests, and the 23 fns they call (3 of the tutorial, 20 extra) + assert t.node_count == 46; + assert wrap_height(t) < wrap_height(w); +} + +test host_entry_points_match_the_functions { + let g : SpecGraph = tutorial_08_graph(); + let w : SpecGraph = wide_graph(); + assert fx_wrap_rows(0) == 12; + assert fx_wrap_rows(2) == 28; + assert fx_wrap_width(0) == 1860; + assert fx_wrap_height(0) == 484; + assert fx_wrap_width(2) == wrap_width(w); + assert fx_wrap_height(2) == wrap_height(w); + assert fx_wrap_aspect_milli(2) == 1615; + assert fx_wrap_node_count(2) == 185; + assert fx_wrap_edge_count(2) == 185; + assert fx_wrap_node_x(2, 40) == 1080; + assert fx_wrap_node_y(2, 40) == 40; + assert fx_wrap_edge_x1(2, 40) == 1080; + assert fx_wrap_edge_y1(2, 40) == 54; + assert fx_wrap_edge_x2(2, 40) == 1040; + assert fx_wrap_edge_y2(2, 40) == 1782; + assert fx_wrap_hit(2, 1100, 50) == 40; + assert fx_wrap_hit_line_start(2, 1100, 50) == 26; + assert fx_wrap_hit_line_end(2, 1100, 50) == 28; + assert fx_wrap_node_line_start(0, 8) == 66; + assert fx_wrap_node_line_end(0, 8) == 68; + // past the graph, a host gets 0 or NO_INDEX, not a neighbour's numbers + assert fx_wrap_node_x(0, 15) == 0; + assert fx_wrap_edge_x1(0, 16) == 0; + assert fx_wrap_hit(0, 1, 1) == NO_INDEX; + assert fx_wrap_width(9) == 80; + assert fx_fold_level(0) == FOLD_LEVEL_FULL; + assert fx_fold_level(2) == FOLD_LEVEL_KINDS; + assert fx_fold_width(0) == 1860; + assert fx_fold_height(0) == 576; + assert fx_fold_columns(0) == 7; + assert fx_fold_kind_at(0, 3) == 2; + assert fx_fold_kind_count(0, 2) == 3; + assert fx_fold_kind_count(2, 2) == 153; + assert fx_fold_kind_count(0, 9) == 0; + assert fx_fold_kind_label(0, 7) == 3; + assert fx_fold_kind_label_count(0, 7, 3) == 1; + assert fx_fold_box_x(0, 2) == 820; + assert fx_fold_box_y(0, 2) == 256; + assert fx_fold_box_w(0, 2) == 220; + assert fx_fold_box_h(0, 2) == 64; + assert fx_fold_hit(0, 900, 300) == 2; + assert fx_fold_edge_count(0, 2, 1) == 4; + assert fx_fold_edge_count_kind(0, 2, 1, 1) == 4; + assert fx_fold_edge_label(0, 1, 2) == 1; + assert fx_fold_edge_x1(0, 2, 1) == 906; + assert fx_fold_edge_x2(0, 2, 1) == 694; + assert fx_fold_edge_y(0, 2, 1) == 320; + assert fx_fold_edge_apex(0, 2, 1) == 356; + assert fx_expand_node_count(0, 2) == 10; + assert fx_expand_edge_count(0, 2) == 14; + assert fx_expand_origin(0, 2, 5) == 8; + assert fx_expand_width(0, 2) == 820; + assert fx_expand_height(0, 2) == 316; + assert fx_expand_node_x(0, 2, 2) == 300; + assert fx_expand_node_y(0, 2, 2) == 40; + assert fx_expand_edge_x1(0, 2, 0) == wrap_edge_x1(expand_graph(g, NODE_FN), 0); + assert fx_expand_edge_y1(0, 2, 0) == wrap_edge_y1(expand_graph(g, NODE_FN), 0); + assert fx_expand_edge_x2(0, 2, 0) == wrap_edge_x2(expand_graph(g, NODE_FN), 0); + assert fx_expand_edge_y2(0, 2, 0) == wrap_edge_y2(expand_graph(g, NODE_FN), 0); + assert fx_expand_hit(0, 2, 310, 50) == 4; + // a kind past the vocabulary expands to nothing + assert fx_expand_node_count(0, 9) == 0; +} + +invariant wrap_constants_are_sound { + assert WRAP_MAX_ROWS >= 1; + assert WRAP_ASPECT_TOLERANCE_MILLI * 4 <= WRAP_ASPECT_MILLI + 4; + assert WRAP_ASPECT_TOLERANCE_MILLI < WRAP_ASPECT_MILLI; + // a wrapped column's rows are spaced more than a box is tall, as the view's are + assert VIEW_LABEL_H[LABEL_EXACT as usize] < VIEW_ROW_STEP; + assert VIEW_COL_W < VIEW_COL_STEP; +} + +invariant fold_constants_are_sound { + // an arc end stays inside the box top it sits on, and a self loop rises less than a column arc + assert FOLD_ANCHOR < VIEW_COL_W / 2; + assert FOLD_LOOP_RISE < FOLD_ARC_STEP; + assert FOLD_LOOP_RISE <= fold_rise(blank_graph()) + FOLD_ARC_STEP; + // the three levels are distinct, and the fixtures are in the range the wide one is not + assert FOLD_LEVEL_FULL != FOLD_LEVEL_KINDS; + assert FOLD_LEVEL_KINDS != FOLD_LEVEL_EXPAND; + assert MEASURED_NODES_P95 > 15; +}