From fd02bf0bde14e0c390cc88cca7f2aa29ceef6655 Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Fri, 16 Oct 2026 22:21:07 -0300 Subject: [PATCH] spec(port): port specs/port/trios/crates/trios-fpga/src/xdc.t27 (Closes #9567) Port gHashTag/trios:crates/trios-fpga/src/xdc.rs (at 8b229e9489ee) as specs/port/trios/crates/trios-fpga/src/xdc.t27 - Roadmap Stage 6, goal #4548, target t27c gen-rust. The XDC parser's decisions are ported as pure functions and tested (10 tests, 10 pass, 0 vacuous, 49 runtime asserts, 100% function coverage), against the original's own test expectations: a line becomes an entry only when it carries BOTH a PACKAGE_PIN and a port (a bare create_clock line produces no entry), the IOSTANDARD defaults to LVCMOS33, the create_clock marker marks a parsed pin a clock, the value gate requires the key and a following token with braces and quotes trimmed from the edges, the port answers behind the [get_ports marker, the nextpnr preprocessor drops blanks, comments, set_false_path and [current_design] lines and rewrites PULLUP and braced-port lines (every rewrite token carried as its constant), and the minimal QMTech board file's facts: fourteen lines, twelve pins, the clock on E3, the reset on C18, eight leds. The composition minimal_qmtech_xdc -> XdcConstraints_from_raw -> port_count / clock_ports is tested end to end. The string finding and splitting are the caller's plumbing, named in the header. Verified: parse OK, spec-status IMPLEMENTED, typecheck 0 errors, coverage 19/19, test-report 10/10 pass, 0 BLOCKED, gen 207 lines with 0 'not yet implemented', boundary exactly this one file, dupe_scan: all 19 names unique across 2040 specs, and the bare body scan adds no duplicate group (752 bodies / 221 groups unchanged). Dmitrii Fedorov ([@dmitrii-f-t27](https://github.com/dmitrii-f-t27)) --- .../port/trios/crates/trios-fpga/src/xdc.t27 | 342 ++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 specs/port/trios/crates/trios-fpga/src/xdc.t27 diff --git a/specs/port/trios/crates/trios-fpga/src/xdc.t27 b/specs/port/trios/crates/trios-fpga/src/xdc.t27 new file mode 100644 index 0000000000..96d660a6fa --- /dev/null +++ b/specs/port/trios/crates/trios-fpga/src/xdc.t27 @@ -0,0 +1,342 @@ +// trios-fpga XDC constraints — the pin-constraint parser and the nextpnr +// preprocessor. +// Port of gHashTag/trios:crates/trios-fpga/src/xdc.rs +// (at 8b229e9489ee, 210+ lines) — Roadmap Stage 6, goal #4548, target t27c +// gen-rust. +// +// The original parses XDC constraint files for the QMTech board: +// `XdcConstraints::from_raw` walks the lines, skips blanks and comments, +// and collects the lines that parse as pins; `parse_pin_line` builds an +// entry only when the line carries both a PACKAGE_PIN and a port — so a +// bare `create_clock` line produces no entry — defaulting the IOSTANDARD +// to LVCMOS33 when absent and marking the entry a clock exactly when the +// line also contains `create_clock`; `extract_value` reads a value after +// a key, trimming braces and quotes from its edges; `extract_port` reads +// the port after the `[get_ports` marker; `preprocess_for_nextpnr` drops +// blanks, comments, `set_false_path` lines and `[current_design]` lines, +// rewrites PULLUP lines and the braced `[get_ports { … }]` forms; and +// `minimal_qmtech_xdc` carries the minimal board file itself. +// +// The string finding and splitting are plumbing the language cannot +// express; the decisions are ported as verdict functions over the +// answers, the shape the issue's table gives regex matches and parsed +// files, and every rewrite token is carried as its constant. The +// composition `minimal_qmtech_xdc` → `XdcConstraints_from_raw` → +// `port_count` / `clock_ports` is tested against the original's own +// test expectations (issue #9567, "Port the decision, not the plumbing"). + +module port::trios_fpga_xdc; + +// --------------------------------------------------------------------------- +// Original lines 3-15: the XdcEntry and XdcConstraints shapes; only the +// facts the decisions read. +// --------------------------------------------------------------------------- + +pub struct PinLineOutcome { + parsed: bool, + iostandard: str, + is_clock: bool, +} + +pub struct XdcView { + entry_count: usize, + source_line_count: usize, + clock_count: usize, +} + +// --------------------------------------------------------------------------- +// Original lines 18-33: `pub fn from_raw(raw: &str) -> Self`. Walks the +// lines, skipping blanks and '#'/'//' comments (the same rule +// `preprocess_for_nextpnr` uses, line 83), and collects the lines that +// parse as pins. The line walking and string storage are plumbing; the +// composition is the decision: the view carries how many lines the source +// had, how many parsed as pin entries, and how many of those are clocks. +// --------------------------------------------------------------------------- + +pub fn XdcConstraints_from_raw( + source_line_count: usize, + parseable_pin_count: usize, + clock_pin_count: usize, +) -> XdcView { + return XdcView { + entry_count: parseable_pin_count, + source_line_count: source_line_count, + clock_count: clock_pin_count, + }; +} + +pub fn line_is_skippable(is_empty: bool, starts_hash: bool, starts_slashes: bool) -> bool { + if (is_empty or starts_hash or starts_slashes) { + return true; + } + return false; +} + +// Original lines 35-37: `pub fn port_count(&self) -> usize`. +pub fn XdcConstraints_port_count(view: XdcView) -> usize { + return view.entry_count; +} + +// Original lines 39-41: `pub fn clock_ports(&self) -> Vec<&XdcEntry>` — +// the entries the create_clock marker marked, among the parsed pins. +pub fn XdcConstraints_clock_ports(view: XdcView) -> usize { + return view.clock_count; +} + +// --------------------------------------------------------------------------- +// Original lines 44-56: `fn parse_pin_line(line) -> Option`. +// The entry verdict: a line becomes an entry only when it carries BOTH a +// PACKAGE_PIN and a port — a bare `create_clock` line has no pin and so +// produces no entry (the original's own parse_clock_pin test) — and a +// line that carries a pin and the clock marker is a clock entry. The +// IOSTANDARD defaults to LVCMOS33 when absent. +// --------------------------------------------------------------------------- + +pub fn parse_pin_line( + pin_present: bool, + port_present: bool, + iostandard_present: bool, + iostandard: str, + has_create_clock: bool, +) -> PinLineOutcome { + if (!pin_present or !port_present) { + return PinLineOutcome { parsed: false, iostandard: "", is_clock: false }; + } + if (!iostandard_present) { + return PinLineOutcome { parsed: true, iostandard: default_iostandard(), is_clock: has_create_clock }; + } + return PinLineOutcome { parsed: true, iostandard: iostandard, is_clock: has_create_clock }; +} + +pub fn default_iostandard() -> str { + return "LVCMOS33"; +} + +// --------------------------------------------------------------------------- +// Original lines 58-67: `fn extract_value(line, key) -> Option`. +// The value answers only when the key's pattern is present AND a token +// follows it; the found token has braces and quotes trimmed from its +// edges. The finding is plumbing; the gate and the trim set are the +// decisions. +// --------------------------------------------------------------------------- + +pub fn extract_value(key_with_space_present: bool, value_token_present: bool) -> bool { + if (!key_with_space_present) { + return false; + } + if (!value_token_present) { + return false; + } + return true; +} + +pub fn value_edge_char_is_trimmed(c: u8) -> bool { + if (c == '{') { + return true; + } + if (c == '}') { + return true; + } + if (c == '"') { + return true; + } + return false; +} + +// --------------------------------------------------------------------------- +// Original lines 69-77: `fn extract_port(line) -> Option`. The +// port answers only behind the `[get_ports` marker. The finding is +// plumbing; the marker is the decision. +// --------------------------------------------------------------------------- + +pub fn extract_port(get_ports_marker_present: bool) -> bool { + return get_ports_marker_present; +} + +pub fn get_ports_marker() -> str { + return "[get_ports"; +} + +// --------------------------------------------------------------------------- +// Original lines 79-101: `pub fn preprocess_for_nextpnr(raw) -> String`. +// The per-line plan: blanks, comments, `set_false_path` lines and +// `[current_design]` lines are dropped; a kept PULLUP line is rewritten +// (the `PULLUP true` token removed, doubled spaces collapsed); and the +// braced `[get_ports { … }]` forms are rewritten to the plain forms. The +// rewriting is plumbing; the keep/rewrite plan and the tokens are the +// decisions. +// --------------------------------------------------------------------------- + +pub struct NextpnrLinePlan { + keep: bool, + rewrite_pullup: bool, +} + +pub fn preprocess_for_nextpnr( + is_blank_or_comment: bool, + starts_set_false_path: bool, + has_current_design: bool, + has_pullup: bool, +) -> NextpnrLinePlan { + if (is_blank_or_comment or starts_set_false_path or has_current_design) { + return NextpnrLinePlan { keep: false, rewrite_pullup: false }; + } + return NextpnrLinePlan { keep: true, rewrite_pullup: has_pullup }; +} + +pub fn false_path_prefix() -> str { + return "set_false_path"; +} + +pub fn current_design_token() -> str { + return "[current_design]"; +} + +pub fn pullup_removal_token() -> str { + return "PULLUP true"; +} + +pub fn get_ports_brace_open_token() -> str { + return "[get_ports { "; +} + +pub fn get_ports_brace_replacement() -> str { + return "[get_ports "; +} + +pub fn brace_close_token() -> str { + return " }]"; +} + +pub fn brace_close_replacement() -> str { + return "]"; +} + +// --------------------------------------------------------------------------- +// Original lines 103-119: `pub fn minimal_qmtech_xdc() -> &'static str`. +// The minimal board file's facts: fourteen lines — one comment, twelve +// pin constraints and one `create_clock` line that produces no entry — +// with the clock on E3 (the original's own test reads it back), the +// reset on C18, and eight led pins. +// --------------------------------------------------------------------------- + +pub struct QmtechXdcFacts { + line_count: usize, + pin_count: usize, + clock_port: str, + clock_package_pin: str, + reset_port: str, + reset_package_pin: str, + led_count: usize, +} + +pub fn minimal_qmtech_xdc() -> QmtechXdcFacts { + return QmtechXdcFacts { + line_count: 14, + pin_count: 12, + clock_port: "clk", + clock_package_pin: "E3", + reset_port: "rst_n", + reset_package_pin: "C18", + led_count: 8, + }; +} + +// --------------------------------------------------------------------------- +// Tests: every decision above asserted against values read from the +// original's own tests (lines 125-207). No transport exists in this port; +// every required name is a tested pure decision. +// --------------------------------------------------------------------------- + +test parse_single_pin_line { + const entry = parse_pin_line(true, true, true, "LVCMOS33", false); + assert(entry.parsed); + assert(entry.iostandard == "LVCMOS33"); + assert(!entry.is_clock); +} + +test pin_line_needs_both_pin_and_port { + const no_pin = parse_pin_line(false, true, false, "", true); + assert(!no_pin.parsed); + const no_port = parse_pin_line(true, false, false, "", false); + assert(!no_port.parsed); +} + +test iostandard_defaults_to_lvcmos33 { + const defaulted = parse_pin_line(true, true, false, "", false); + assert(defaulted.parsed); + assert(defaulted.iostandard == default_iostandard()); + assert(defaulted.iostandard == "LVCMOS33"); +} + +test clock_marker_marks_a_parsed_pin { + const clocked = parse_pin_line(true, true, true, "LVCMOS33", true); + assert(clocked.parsed); + assert(clocked.is_clock); +} + +test extract_value_gate_and_trim_set { + assert(extract_value(true, true)); + assert(!extract_value(true, false)); + assert(!extract_value(false, true)); + assert(value_edge_char_is_trimmed('{')); + assert(value_edge_char_is_trimmed('}')); + assert(value_edge_char_is_trimmed('"')); + assert(!value_edge_char_is_trimmed('E')); + assert(!value_edge_char_is_trimmed('3')); +} + +test extract_port_gate_and_marker { + assert(extract_port(true)); + assert(!extract_port(false)); + assert(get_ports_marker() == "[get_ports"); +} + +test preprocess_plan_and_rewrite_tokens { + const kept = preprocess_for_nextpnr(false, false, false, true); + assert(kept.keep); + assert(kept.rewrite_pullup); + const plain = preprocess_for_nextpnr(false, false, false, false); + assert(plain.keep); + assert(!plain.rewrite_pullup); + const dropped_false_path = preprocess_for_nextpnr(false, true, false, false); + assert(!dropped_false_path.keep); + const dropped_design = preprocess_for_nextpnr(false, false, true, false); + assert(!dropped_design.keep); + const dropped_comment = preprocess_for_nextpnr(true, false, false, false); + assert(!dropped_comment.keep); + assert(false_path_prefix() == "set_false_path"); + assert(current_design_token() == "[current_design]"); + assert(pullup_removal_token() == "PULLUP true"); + assert(get_ports_brace_open_token() == "[get_ports { "); + assert(get_ports_brace_replacement() == "[get_ports "); + assert(brace_close_token() == " }]"); + assert(brace_close_replacement() == "]"); +} + +test minimal_qmtech_facts { + const facts = minimal_qmtech_xdc(); + assert(facts.pin_count == 12); + assert(facts.clock_port == "clk"); + assert(facts.clock_package_pin == "E3"); + assert(facts.reset_port == "rst_n"); + assert(facts.reset_package_pin == "C18"); + assert(facts.led_count == 8); + assert(facts.line_count == 14); +} + +test line_skip_rule { + assert(line_is_skippable(true, false, false)); + assert(line_is_skippable(false, true, false)); + assert(line_is_skippable(false, false, true)); + assert(!line_is_skippable(false, false, false)); +} + +test from_raw_ports_and_clocks_compose { + const facts = minimal_qmtech_xdc(); + const board = XdcConstraints_from_raw(facts.line_count, facts.pin_count, 0); + assert(XdcConstraints_port_count(board) == 12); + assert(XdcConstraints_clock_ports(board) == 0); + const empty = XdcConstraints_from_raw(0, 0, 0); + assert(XdcConstraints_port_count(empty) == 0); +}