Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
342 changes: 342 additions & 0 deletions specs/port/trios/crates/trios-fpga/src/xdc.t27
Original file line number Diff line number Diff line change
@@ -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<XdcEntry>`.
// 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<String>`.
// 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<String>`. 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);
}
Loading