From dd92d0d7820db636463cdd9d98f62e6b8b385c83 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 05:03:09 +0700 Subject: [PATCH] t27f: dependencies view -- the specs around one spec, from t27c use-edges (Closes #10680) specs/t27f/spec_deps.t27 lays out what a spec imports (layers to the right) and who imports it (layers to the left), every edge exact, with a cycle flag through the focus and a whole refusal past the caller's room or depth. 13 tests, 4 mutants caught on the lab. Corpus run over 806 specs found the igla race backend <-> rtl mutual import. Co-Authored-By: Claude Opus 5.5 --- .claude/skills/t27f/SKILL.md | 7 + .trinity/seals/t27f_t27f_spec_deps.json | 37 ++ docs/reports/t27b_expectations.json | 1 + specs/OWNERS.md | 2 +- specs/t27f/spec_deps.t27 | 446 ++++++++++++++++++++++++ 5 files changed, 492 insertions(+), 1 deletion(-) create mode 100644 .trinity/seals/t27f_t27f_spec_deps.json create mode 100644 specs/t27f/spec_deps.t27 diff --git a/.claude/skills/t27f/SKILL.md b/.claude/skills/t27f/SKILL.md index 506a16c904..eb5da0af3a 100644 --- a/.claude/skills/t27f/SKILL.md +++ b/.claude/skills/t27f/SKILL.md @@ -81,6 +81,7 @@ the diagram core in detail, then the other surfaces and the topic-map applicatio | 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 | +| Dependencies view: the specs around one spec | `specs/t27f/spec_deps.t27` (t27#10680) | spec with tests: reads `t27c use-edges --specs-dir specs`; layer 0 is the focus, +k the specs k imports away (right), -k the specs k importers away (left), each spec once in its shortest-path layer; every edge-list line between two drawn specs is one `exact` edge, a repeated line drawn once; `cycle=1` when the focus imports itself or is imported by a spec in its own import closure; a walk past the caller's room or depth is refused whole (`refused\tfull` / `refused\tdeep`), never cut. Limits: `use` lines t27c does not resolve are not drawn (#7176); cycles that miss the focus are not reported; a spec on a cycle through the focus is drawn on both sides; no host draws it yet. Corpus on master e607228f7: 806 specs, 2461 edge lines, nothing refused at depth 32, deepest walks 6, one real cycle (igla race `backend` <-> `rtl`) | | 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 1024 nodes or 4096 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 1024 nodes and 4096 edges, u16 indices (all 2225 specs in specs/ fit, largest 775 nodes and 3194 edges after derive; anything past the cap is refused, never truncated), takes a `[]const u8` slice rather than `[65536]u8` plus a length. 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 1024 nodes or 4096 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 | @@ -194,6 +195,12 @@ Traps from this tick (ffmpeg on the lab): and UNKNOWN opaque nodes for undeclared qualified call targets, by its three stated rules. Anything the rules do not reach (SUGGESTED explanations, call-result flow, anything larger) is still a manual reading, and no host runs either pass yet. Name that gap rather than hide it. +5. For the specs **around** a spec (what it imports, who imports it), run `t27c use-edges + --specs-dir specs` on the lab and lay it out with `deps_view` in `specs/t27f/spec_deps.t27`. + Trap: `tsort` prints loops on **stderr** and still prints an order on stdout, so "tsort gave + an order" is not "acyclic". An earlier issue body said there was no cycle, and the + spec's corpus run found igla race `backend.t27` <-> `rtl.t27`. Read stderr, or use the + spec's `cycle=` flag. ## Application: topic map (gHashTag/trinity#1677) diff --git a/.trinity/seals/t27f_t27f_spec_deps.json b/.trinity/seals/t27f_t27f_spec_deps.json new file mode 100644 index 0000000000..7e6df30ce1 --- /dev/null +++ b/.trinity/seals/t27f_t27f_spec_deps.json @@ -0,0 +1,37 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/ci/affected.t27", + "spec_hash": "sha256:38a7bdc245f1974f0d5f1a55a45966decd70098afdb89e882f568da3650ee9f9" + }, + { + "spec": "specs/policy/text_lines.t27", + "spec_hash": "sha256:13c90895a4a44c760cce6b3b65f72f937298633062f040f8489486108ab0c2d1" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:46e8cd0521a7d466c8d72b0072d7124fe4e125980d15053b4ff0f5e1e0e8f4e6", + "gen_hash_rust": "sha256:6402828161a1d1ddff1787a41297d158c0fd08b54540a4a606980b414ea8f2d2", + "gen_hash_verilog": "sha256:6a2d9fc3d039e9a989aa364d45502d701169bb5074a9726d7365d460702672a8", + "gen_hash_zig": "sha256:b26cd46a5e8acf928349c46c6f3a7a4ded7c18e7e0c14e9276a59dba54aed396", + "module": "t27f_spec_deps", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T22:02:00Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:54eeee44e930711d5a1740e9cde8c11720a05baf17951360adf14a17925b4575", + "spec_path": "specs/t27f/spec_deps.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 13, + "total": 13, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 237ea5e84e..7e85c76fba 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -1483,6 +1483,7 @@ {"path": "specs/t27f/reel_particles_mux.t27", "t27b": "pass"}, {"path": "specs/t27f/reel_t27_basics.t27", "t27b": "pass"}, {"path": "specs/t27f/reel_topics.t27", "t27b": "pass"}, + {"path": "specs/t27f/spec_deps.t27", "t27b": "pass"}, {"path": "specs/t27f/spec_graph.t27", "t27b": "pass"}, {"path": "specs/t27f/spec_graph_derive.t27", "t27b": "pass"}, {"path": "specs/t27f/spec_graph_export.t27", "t27b": "pass"}, diff --git a/specs/OWNERS.md b/specs/OWNERS.md index 4ed09320e0..86505a5068 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, t27's frontend (definition in `.claude/skills/t27f/SKILL.md`): `t27f/frontend.t27` (module `T27fFrontend`): the law of every T27F surface -- what draws it today, which surface answers an intent, the stand-in when it cannot be made, that nothing undrawn is claimed made, and which surfaces TRI CLAW serves. Its core is the visual spec of a .t27 file (gHashTag/trinity#1648, RFC #1649): `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. The application drawn through 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. Map surfaces: `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, t27's frontend (definition in `.claude/skills/t27f/SKILL.md`): `t27f/frontend.t27` (module `T27fFrontend`): the law of every T27F surface -- what draws it today, which surface answers an intent, the stand-in when it cannot be made, that nothing undrawn is claimed made, and which surfaces TRI CLAW serves. Its core is the visual spec of a .t27 file (gHashTag/trinity#1648, RFC #1649): `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/spec_deps.t27` (module `t27f_spec_deps`): the dependencies view, the specs around one spec from the `t27c use-edges` list -- import layers right, importer layers left, exact edges, a cycle flag through the focus, refusal past room or depth; numbers only; `t27f/ui_plan.t27` (module `t27f_ui_plan`): the renderer-neutral UI plan IR and the trusted widget catalog. The application drawn through 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. Map surfaces: `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_deps.t27 b/specs/t27f/spec_deps.t27 new file mode 100644 index 0000000000..37005ad3b7 --- /dev/null +++ b/specs/t27f/spec_deps.t27 @@ -0,0 +1,446 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/t27f/spec_deps.t27 -- the T27F dependencies view: one spec's imports and importers, in layers +; (gHashTag/t27#10680; epic gHashTag/trinity#1648, RFC gHashTag/trinity#1649). T27F is the visual spec +; of .t27 (.claude/skills/t27f/SKILL.md, "What T27F is"). The RFC names a dependencies view beside the +; source, architecture, data-flow and test views. specs/t27f/spec_graph_view.t27 lays out one spec's +; own declarations; this file lays out the specs around it. +; -- +; Input: the path-keyed edge list `t27c use-edges --specs-dir specs` prints, one `importerimported` +; line per `use` line that resolves (bootstrap/src/use_resolve.rs). The same list feeds +; specs/ci/affected.t27 (every importer, transitively) and specs/claw/t27f_gate.t27 (one hop). The set +; helpers are affected.t27's, imported, not copied. +; -- +; The rules, each with a test below: +; - layer 0 is the focus. Layer +k holds the specs k imports away, layer -k the specs k importers +; away. Each spec is in one layer per side, the layer of its shortest path, in edge-list order; +; - every edge drawn is a line of the edge list between two drawn specs, drawn once however often +; the list repeats it, and labelled exact: t27c resolved it, nothing here is inferred; +; - the focus is flagged as on a cycle when it imports itself or a spec it imports imports it; +; - a neighbourhood that does not fit the caller's room, or is deeper than the depth asked for, is +; refused whole. Nothing is cut off. +; -- +; The honest limits: +; - a `use` line t27c does not resolve is not in the edge list, so it is not drawn (#7176 counted +; 119 such lines in 70 specs); +; - the flag says only whether the focus is on a cycle. A cycle among other specs is not reported, +; and a spec on a cycle through the focus is drawn on both sides; +; - x and y are numbers for a host, in the spacing of spec_graph_view.t27. No host draws them yet. +; -- +; Measured on the Railway lab, master e607228f7 plus this file: 2461 edge lines over 806 specs. Every +; spec walks inside DEPS_MAX_DEPTH: the deepest import walk is 6 (specs/t27f/lesson_reel.t27, 18 +; specs) and the deepest importer walk is 6 (specs/verified/reuse.t27, 50 specs). The widest view is +; specs/base/types.t27, with 400 importers. Two specs are flagged on a cycle, and both flags are +; true: specs/igla/race/backend.t27 and specs/igla/race/rtl.t27 import each other. `tsort` reports +; the same loop on stderr. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module t27f_spec_deps; + +use policy::text_lines; +use ci::affected; + +/// Which way a walk follows the edges: to what a spec imports, or to what imports it. +pub const DEPS_IMPORTS : u8 = 1; +pub const DEPS_IMPORTERS : u8 = 2; + +/// What a walk says. +pub const DEPS_OK : u8 = 0; +pub const DEPS_ERR_FULL : u8 = 1; +pub const DEPS_ERR_DEEP : u8 = 2; + +/// The deepest walk asked for; a deeper request is read as this one. A walk's bounds[] needs +/// DEPS_BOUNDS entries. +pub const DEPS_MAX_DEPTH : usize = 32; +pub const DEPS_BOUNDS : usize = 35; + +/// Spacing, in the units of spec_graph_view.t27 (VIEW_MARGIN, VIEW_COL_STEP, VIEW_ROW_STEP). +pub const DEPS_MARGIN : usize = 40; +pub const DEPS_COL_STEP : usize = 260; +pub const DEPS_ROW_STEP : usize = 64; + +pub const DEPS_DIGITS : str = "0123456789$"; +pub const DEPS_HEAD : str = "deps\t$"; +pub const DEPS_NODE : str = "node\t$"; +pub const DEPS_EDGE : str = "edge\t$"; +pub const DEPS_EXACT : str = "\texact\n$"; +pub const DEPS_REFUSED : str = "refused\t$"; +pub const DEPS_WHY_FULL : str = "full\t$"; +pub const DEPS_WHY_DEEP : str = "deep\t$"; + +/// Length of a '$'-terminated literal. +pub fn deps_len(lit: []const u8) -> usize { + var k : usize = 0; + while (lit[k] != LIST_END) { k += 1; } + return k; +} + +/// Is s[f..t] one of the newline-terminated lines of set[lo..hi]? +pub fn deps_in_range(set: []const u8, lo: usize, hi: usize, s: []const u8, f: usize, t: usize) -> bool { + var i : usize = lo; + while (i < hi) { + const e : usize = line_end(set, i, hi); + if (same(set, i, e, s, f, t)) { return true; } + i = e + 1; + } + return false; +} + +/// How many lines set[lo..hi] holds. +pub fn deps_count(set: []const u8, lo: usize, hi: usize) -> usize { + var c : usize = 0; + var i : usize = lo; + while (i < hi) { + if (set[i] == '\n') { c += 1; } + i += 1; + } + return c; +} + +/// Walk the edge list buf[0..n] from focus[ff..ft] in direction dir, breadth first, into set[0..scap]: +/// layer k is set[bounds[k]..bounds[k + 1]], one spec per line, layer 0 the focus. A spec already in the +/// set is not added again, so each spec lands in the layer of its shortest path. DEPS_ERR_FULL when +/// the set does not fit, DEPS_ERR_DEEP when layer depth + 1 is not empty. +pub fn deps_walk(buf: []const u8, n: usize, focus: []const u8, ff: usize, ft: usize, dir: u8, depth: usize, set: []u8, scap: usize, bounds: []usize) -> u8 { + var d : usize = depth; + if (d > DEPS_MAX_DEPTH) { d = DEPS_MAX_DEPTH; } + bounds[0] = 0; + var sn : usize = set_add(set, 0, scap, focus, ff, ft); + if (sn > scap) { return DEPS_ERR_FULL; } + bounds[1] = sn; + var k : usize = 1; + while (k <= d + 1) { + const lo : usize = bounds[k - 1]; + const hi : usize = bounds[k]; + var s : usize = 0; + while (s < n and sn <= scap) { + const e : usize = line_end(buf, s, n); + const t : usize = text_end(buf, s, e); + const p : usize = first_tab(buf, s, t); + if (p < t and p > s) { + if (dir == DEPS_IMPORTS) { + if (deps_in_range(set, lo, hi, buf, s, p)) { sn = set_add(set, sn, scap, buf, p + 1, t); } + } else { + if (deps_in_range(set, lo, hi, buf, p + 1, t)) { sn = set_add(set, sn, scap, buf, s, p); } + } + } + s = e + 1; + } + if (sn > scap) { return DEPS_ERR_FULL; } + bounds[k + 1] = sn; + if (sn == hi) { return DEPS_OK; } + if (k == d + 1) { return DEPS_ERR_DEEP; } + k += 1; + } + return DEPS_OK; +} + +/// The last layer that is not empty, after a walk that said DEPS_OK. +pub fn deps_depth(bounds: []const usize, depth: usize) -> usize { + var d : usize = depth; + if (d > DEPS_MAX_DEPTH) { d = DEPS_MAX_DEPTH; } + var k : usize = 1; + while (k <= d and bounds[k + 1] > bounds[k]) { k += 1; } + return k - 1; +} + +/// Is the focus on a cycle: does some line of the edge list run from a spec of its import set +/// imp[0..isn] (the focus included) back to the focus? +pub fn deps_on_cycle(buf: []const u8, n: usize, focus: []const u8, ff: usize, ft: usize, imp: []const u8, isn: usize) -> bool { + var s : usize = 0; + while (s < n) { + const e : usize = line_end(buf, s, n); + const t : usize = text_end(buf, s, e); + const p : usize = first_tab(buf, s, t); + if (p < t and p > s) { + if (same(buf, p + 1, t, focus, ff, ft) and deps_in_range(imp, 0, isn, buf, s, p)) { return true; } + } + s = e + 1; + } + return false; +} + +/// Write buf[f..t]. +pub fn deps_put_span(out: []u8, w: usize, cap: usize, buf: []const u8, f: usize, t: usize) -> usize { + var p : usize = w; + var i : usize = f; + while (i < t) { + p = put(out, p, cap, buf[i]); + i += 1; + } + return p; +} + +/// Write a '$'-terminated literal. +pub fn deps_put_lit(out: []u8, w: usize, cap: usize, lit: []const u8) -> usize { + return deps_put_span(out, w, cap, lit, 0, deps_len(lit)); +} + +/// Write v in decimal. +pub fn deps_put_dec(out: []u8, w: usize, cap: usize, v: usize) -> usize { + var d : usize = 1; + while (v / d >= 10) { d = d * 10; } + var p : usize = w; + while (d > 0) { + p = put(out, p, cap, DEPS_DIGITS[(v / d) % 10]); + d = d / 10; + } + return p; +} + +/// Write the node lines of one layer, set[lo..hi]: `nodelayerxypath`, the layer +/// with a '-' when neg, the column col, one row per spec. +pub fn deps_put_layer(out: []u8, w: usize, cap: usize, set: []const u8, lo: usize, hi: usize, layer: usize, neg: bool, col: usize) -> usize { + var p : usize = w; + var row : usize = 0; + var i : usize = lo; + while (i < hi) { + const e : usize = line_end(set, i, hi); + p = deps_put_lit(out, p, cap, DEPS_NODE); + if (neg) { p = put(out, p, cap, '-'); } + p = deps_put_dec(out, p, cap, layer); + p = put(out, p, cap, '\t'); + p = deps_put_dec(out, p, cap, DEPS_MARGIN + col * DEPS_COL_STEP); + p = put(out, p, cap, '\t'); + p = deps_put_dec(out, p, cap, DEPS_MARGIN + row * DEPS_ROW_STEP); + p = put(out, p, cap, '\t'); + p = deps_put_span(out, p, cap, set, i, e); + p = put(out, p, cap, '\n'); + row += 1; + i = e + 1; + } + return p; +} + +/// The dependencies view of focus[ff..ft] over the edge list buf[0..n], written to out[0..cap]: +/// depsfocuscycle=0|1imports=Nimporters=Ndepth=I/R +/// node lines, importer layers first (deepest left), then the focus, then the import layers; +/// edgeimporterimportedexact for each distinct line between two drawn specs. +/// A walk that is refused writes one line, refusedfull|deepfocus. imp/ib and rev/rb hold the +/// two walks, eset the edges already written. Returns the length, or more than cap when the view does +/// not fit; the caller then refuses it. +pub fn deps_view(buf: []const u8, n: usize, focus: []const u8, ff: usize, ft: usize, depth: usize, imp: []u8, icap: usize, ib: []usize, rev: []u8, rcap: usize, rb: []usize, eset: []u8, ecap: usize, out: []u8, cap: usize) -> usize { + const si : u8 = deps_walk(buf, n, focus, ff, ft, DEPS_IMPORTS, depth, imp, icap, ib); + const sr : u8 = deps_walk(buf, n, focus, ff, ft, DEPS_IMPORTERS, depth, rev, rcap, rb); + var w : usize = 0; + if (si != DEPS_OK or sr != DEPS_OK) { + w = deps_put_lit(out, w, cap, DEPS_REFUSED); + if (si == DEPS_ERR_FULL or sr == DEPS_ERR_FULL) { + w = deps_put_lit(out, w, cap, DEPS_WHY_FULL); + } else { + w = deps_put_lit(out, w, cap, DEPS_WHY_DEEP); + } + w = deps_put_span(out, w, cap, focus, ff, ft); + return put(out, w, cap, '\n'); + } + const di : usize = deps_depth(ib, depth); + const dr : usize = deps_depth(rb, depth); + const isn : usize = ib[di + 1]; + const rsn : usize = rb[dr + 1]; + w = deps_put_lit(out, w, cap, DEPS_HEAD); + w = deps_put_span(out, w, cap, focus, ff, ft); + w = put(out, w, cap, '\t'); + w = put(out, w, cap, 'c'); + w = put(out, w, cap, 'y'); + w = put(out, w, cap, 'c'); + w = put(out, w, cap, 'l'); + w = put(out, w, cap, 'e'); + w = put(out, w, cap, '='); + if (deps_on_cycle(buf, n, focus, ff, ft, imp, isn)) { w = put(out, w, cap, '1'); } else { w = put(out, w, cap, '0'); } + w = put(out, w, cap, '\t'); + w = deps_put_lit(out, w, cap, "imports=$"); + w = deps_put_dec(out, w, cap, deps_count(imp, 0, isn) - 1); + w = put(out, w, cap, '\t'); + w = deps_put_lit(out, w, cap, "importers=$"); + w = deps_put_dec(out, w, cap, deps_count(rev, 0, rsn) - 1); + w = put(out, w, cap, '\t'); + w = deps_put_lit(out, w, cap, "depth=$"); + w = deps_put_dec(out, w, cap, di); + w = put(out, w, cap, '/'); + w = deps_put_dec(out, w, cap, dr); + w = put(out, w, cap, '\n'); + var k : usize = dr; + while (k >= 1) { + w = deps_put_layer(out, w, cap, rev, rb[k], rb[k + 1], k, true, dr - k); + k -= 1; + } + w = deps_put_layer(out, w, cap, imp, ib[0], ib[1], 0, false, dr); + k = 1; + while (k <= di) { + w = deps_put_layer(out, w, cap, imp, ib[k], ib[k + 1], k, false, dr + k); + k += 1; + } + var en : usize = 0; + var s : usize = 0; + while (s < n) { + const e : usize = line_end(buf, s, n); + const t : usize = text_end(buf, s, e); + const p : usize = first_tab(buf, s, t); + if (p < t and p > s) { + const a_in : bool = deps_in_range(imp, 0, isn, buf, s, p) or deps_in_range(rev, 0, rsn, buf, s, p); + const b_in : bool = deps_in_range(imp, 0, isn, buf, p + 1, t) or deps_in_range(rev, 0, rsn, buf, p + 1, t); + if (a_in and b_in) { + const before : usize = en; + en = set_add(eset, en, ecap, buf, s, t); + if (en > ecap) { return cap + 1; } + if (en > before) { + w = deps_put_lit(out, w, cap, DEPS_EDGE); + w = deps_put_span(out, w, cap, buf, s, t); + w = deps_put_lit(out, w, cap, DEPS_EXACT); + } + } + } + s = e + 1; + } + return w; +} + +// ---- tests ---- + +// A diamond under the focus a (a imports b and c, both import d), two importers above it (e imports +// a, f imports e), a pair the focus never reaches (x, y), and a repeated line (a imports b twice). +pub const DEPS_FIXTURE : str = "a\tb\na\tc\nb\td\nc\td\ne\ta\nf\te\nx\ty\na\tb\n$"; + +test the_import_layers_of_a_diamond_list_the_shared_spec_once { + var set : [64]u8 = undefined; + var b : [35]usize = undefined; + const g : str = DEPS_FIXTURE; + assert deps_walk(g, deps_len(g), "a", 0, 1, DEPS_IMPORTS, 8, &set, 64, &b) == DEPS_OK; + assert deps_depth(&b, 8) == 2; + assert deps_count(&set, b[1], b[2]) == 2; + assert deps_count(&set, b[2], b[3]) == 1; + assert deps_in_range(&set, b[2], b[3], "d", 0, 1) == true; + assert b[3] == 8; +} + +test the_importer_layers_walk_the_other_way { + var set : [64]u8 = undefined; + var b : [35]usize = undefined; + const g : str = DEPS_FIXTURE; + assert deps_walk(g, deps_len(g), "a", 0, 1, DEPS_IMPORTERS, 8, &set, 64, &b) == DEPS_OK; + assert deps_depth(&b, 8) == 2; + assert deps_in_range(&set, b[1], b[2], "e", 0, 1) == true; + assert deps_in_range(&set, b[2], b[3], "f", 0, 1) == true; + assert deps_in_range(&set, 0, b[3], "b", 0, 1) == false; + assert deps_in_range(&set, 0, b[3], "x", 0, 1) == false; +} + +test negative_control_a_dag_does_not_flag_a_cycle { + var set : [64]u8 = undefined; + var b : [35]usize = undefined; + const g : str = DEPS_FIXTURE; + assert deps_walk(g, deps_len(g), "a", 0, 1, DEPS_IMPORTS, 8, &set, 64, &b) == DEPS_OK; + assert deps_on_cycle(g, deps_len(g), "a", 0, 1, &set, b[deps_depth(&b, 8) + 1]) == false; +} + +test a_spec_that_imports_the_focus_back_flags_it { + var set : [64]u8 = undefined; + var b : [35]usize = undefined; + const g : str = "a\tb\na\tc\nb\td\nc\td\nd\ta\n$"; + assert deps_walk(g, deps_len(g), "a", 0, 1, DEPS_IMPORTS, 8, &set, 64, &b) == DEPS_OK; + assert deps_on_cycle(g, deps_len(g), "a", 0, 1, &set, b[deps_depth(&b, 8) + 1]) == true; +} + +// The shape found on the corpus: two specs that import each other (igla::race backend and rtl). +test two_specs_that_import_each_other_are_both_flagged { + var set : [64]u8 = undefined; + var b : [35]usize = undefined; + const g : str = "a\tt\na\tb\nb\tt\nb\ta\nc\tb\n$"; + assert deps_walk(g, deps_len(g), "a", 0, 1, DEPS_IMPORTS, 8, &set, 64, &b) == DEPS_OK; + assert deps_on_cycle(g, deps_len(g), "a", 0, 1, &set, b[deps_depth(&b, 8) + 1]) == true; + assert deps_walk(g, deps_len(g), "b", 0, 1, DEPS_IMPORTS, 8, &set, 64, &b) == DEPS_OK; + assert deps_on_cycle(g, deps_len(g), "b", 0, 1, &set, b[deps_depth(&b, 8) + 1]) == true; + assert deps_walk(g, deps_len(g), "c", 0, 1, DEPS_IMPORTS, 8, &set, 64, &b) == DEPS_OK; + assert deps_on_cycle(g, deps_len(g), "c", 0, 1, &set, b[deps_depth(&b, 8) + 1]) == false; +} + +test a_self_import_flags_the_focus { + var set : [64]u8 = undefined; + var b : [35]usize = undefined; + const g : str = "a\ta\n$"; + assert deps_walk(g, deps_len(g), "a", 0, 1, DEPS_IMPORTS, 8, &set, 64, &b) == DEPS_OK; + assert deps_depth(&b, 8) == 0; + assert deps_on_cycle(g, deps_len(g), "a", 0, 1, &set, b[1]) == true; +} + +test negative_control_a_cycle_the_focus_is_not_on_does_not_flag_it { + var set : [64]u8 = undefined; + var b : [35]usize = undefined; + const g : str = "a\tb\nb\tc\nc\tb\n$"; + assert deps_walk(g, deps_len(g), "a", 0, 1, DEPS_IMPORTS, 8, &set, 64, &b) == DEPS_OK; + assert deps_on_cycle(g, deps_len(g), "a", 0, 1, &set, b[deps_depth(&b, 8) + 1]) == false; + assert deps_walk(g, deps_len(g), "b", 0, 1, DEPS_IMPORTS, 8, &set, 64, &b) == DEPS_OK; + assert deps_on_cycle(g, deps_len(g), "b", 0, 1, &set, b[deps_depth(&b, 8) + 1]) == true; +} + +test negative_control_a_set_that_does_not_fit_is_refused { + var set : [64]u8 = undefined; + var b : [35]usize = undefined; + const g : str = DEPS_FIXTURE; + assert deps_walk(g, deps_len(g), "a", 0, 1, DEPS_IMPORTS, 8, &set, 5, &b) == DEPS_ERR_FULL; + assert deps_walk(g, deps_len(g), "a", 0, 1, DEPS_IMPORTS, 8, &set, 8, &b) == DEPS_OK; +} + +test negative_control_a_walk_deeper_than_asked_is_refused { + var set : [64]u8 = undefined; + var b : [35]usize = undefined; + const g : str = "a\tb\nb\tc\nc\td\n$"; + assert deps_walk(g, deps_len(g), "a", 0, 1, DEPS_IMPORTS, 2, &set, 64, &b) == DEPS_ERR_DEEP; + assert deps_walk(g, deps_len(g), "a", 0, 1, DEPS_IMPORTS, 3, &set, 64, &b) == DEPS_OK; + assert deps_depth(&b, 3) == 3; +} + +test the_view_lays_out_importers_left_and_imports_right_with_exact_edges { + var imp : [64]u8 = undefined; + var ib : [35]usize = undefined; + var rev : [64]u8 = undefined; + var rb : [35]usize = undefined; + var es : [128]u8 = undefined; + var out : [512]u8 = undefined; + const g : str = DEPS_FIXTURE; + const w : usize = deps_view(g, deps_len(g), "a", 0, 1, 8, &imp, 64, &ib, &rev, 64, &rb, &es, 128, &out, 512); + const want : str = "deps\ta\tcycle=0\timports=3\timporters=2\tdepth=2/2\nnode\t-2\t40\t40\tf\nnode\t-1\t300\t40\te\nnode\t0\t560\t40\ta\nnode\t1\t820\t40\tb\nnode\t1\t820\t104\tc\nnode\t2\t1080\t40\td\nedge\ta\tb\texact\nedge\ta\tc\texact\nedge\tb\td\texact\nedge\tc\td\texact\nedge\te\ta\texact\nedge\tf\te\texact\n$"; + assert w == deps_len(want); + assert same(&out, 0, w, want, 0, deps_len(want)) == true; +} + +test negative_control_a_repeated_line_is_one_edge { + var imp : [64]u8 = undefined; + var ib : [35]usize = undefined; + var rev : [64]u8 = undefined; + var rb : [35]usize = undefined; + var es : [128]u8 = undefined; + var out : [256]u8 = undefined; + const g : str = "a\tb\na\tb\na\tb\n$"; + const w : usize = deps_view(g, deps_len(g), "a", 0, 1, 8, &imp, 64, &ib, &rev, 64, &rb, &es, 128, &out, 256); + assert w <= 256; + assert deps_count(&out, 0, w) == 4; +} + +test negative_control_a_refused_walk_writes_one_refusal_line { + var imp : [64]u8 = undefined; + var ib : [35]usize = undefined; + var rev : [64]u8 = undefined; + var rb : [35]usize = undefined; + var es : [128]u8 = undefined; + var out : [256]u8 = undefined; + const g : str = DEPS_FIXTURE; + const w : usize = deps_view(g, deps_len(g), "a", 0, 1, 8, &imp, 5, &ib, &rev, 64, &rb, &es, 128, &out, 256); + const want : str = "refused\tfull\ta\n$"; + assert w == deps_len(want); + assert same(&out, 0, w, want, 0, deps_len(want)) == true; + const w2 : usize = deps_view(g, deps_len(g), "a", 0, 1, 1, &imp, 64, &ib, &rev, 64, &rb, &es, 128, &out, 256); + const want2 : str = "refused\tdeep\ta\n$"; + assert same(&out, 0, w2, want2, 0, deps_len(want2)) == true; +} + +test negative_control_a_view_that_does_not_fit_returns_more_than_cap { + var imp : [64]u8 = undefined; + var ib : [35]usize = undefined; + var rev : [64]u8 = undefined; + var rb : [35]usize = undefined; + var es : [128]u8 = undefined; + var out : [32]u8 = undefined; + const g : str = DEPS_FIXTURE; + assert deps_view(g, deps_len(g), "a", 0, 1, 8, &imp, 64, &ib, &rev, 64, &rb, &es, 128, &out, 32) > 32; + assert deps_view(g, deps_len(g), "a", 0, 1, 8, &imp, 64, &ib, &rev, 64, &rb, &es, 4, &out, 32) > 32; +}