diff --git a/AGENTS.md b/AGENTS.md index 4b994f7d..13d8b082 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,11 @@ ## Documentation +- 8-bit ALU and binary/BCD/7-segment compiler roadmap, module contracts, verification, and parallel ownership: `docs/alu_display_compiler_plan.md` +- ALU/display implementation, physical-port regressions and row-COW profiling: `docs/alu_display_compiler_progress.md` +- SAT Exact candidates in Global PnR, verified external interfaces, provenance and controlled whole-decoder compaction measurements: `docs/exact_global_candidates.md` +- One monolithic Exact model for the full four-input/seven-output decoder, flattened RTL graph, bounded attempts and UNKNOWN interpretation: `docs/whole_decoder_exact.md` +- Whole-decoder window construction/compaction runner, live-frontier measurements, interface contracts and failed construction checkpoints: `docs/whole_decoder_construct.md` - Verilog RTL interface design notes: `docs/verilog_rtl_interface_design.md` - RCIR language and lowering design: `docs/intermediate_representation_design.md` - Physical design intent and local-cell recipes: `docs/physical_design_intent.md` diff --git a/crates/rsdsl/src/program.rs b/crates/rsdsl/src/program.rs index 02380ecd..e2baf271 100644 --- a/crates/rsdsl/src/program.rs +++ b/crates/rsdsl/src/program.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use std::io::{self, Write}; -use crate::ast::{File, Header}; +use crate::ast::{File, Header, ItemKind}; use crate::diag::{Diagnostic, Error, SourceMap, Span}; use crate::formula::{Lit, VarOrigin, F}; use crate::ground::{Dim, GroundOptions, NameRef, Program, VarIndexKind}; @@ -50,6 +50,22 @@ impl Model { &self.name } + /// Whether an instance may supply this external fact. + pub fn accepts_fact(&self, name: &str) -> bool { + self.file + .items + .iter() + .any(|item| matches!(&item.kind, ItemKind::Fact {name:n,derived:None,..} if n==name)) + } + + /// Whether an instance may supply values for this external domain. + pub fn accepts_domain(&self, name: &str) -> bool { + self.file + .items + .iter() + .any(|item| matches!(&item.kind, ItemKind::Domain {name:n,value:None,..} if n==name)) + } + /// Grounds against an instance built in Rust. pub fn ground(&self, instance: &Instance, options: GroundOptions) -> Result { let mut program = diff --git a/docs/alu_display_compiler_plan.md b/docs/alu_display_compiler_plan.md new file mode 100644 index 00000000..8c6e5769 --- /dev/null +++ b/docs/alu_display_compiler_plan.md @@ -0,0 +1,156 @@ +# Verilog → 8-bit ALU → 십진수 표시판 개발 계획 + +작성: 2026-11-08. 기준: master `008197f`, 작업 브랜치 `codex/8bit-alu-display`. +이 문서는 구현 계획이다. 아래 완료 기준은 아직 달성했다고 주장하는 항목이 아니다. + +이 브랜치의 draft PR에는 7-segment 디코더부터 unsigned 8-bit ALU와 BCD/표시 연결까지 계속 구현을 쌓는다. 현재 검증된 체크포인트와 남은 물리 문제는 [구현 기록](alu_display_compiler_progress.md)에 구분해서 남긴다. 작은 디코더의 성공만으로 ALU 작업 완료나 merge 준비 상태로 표시하지 않는다. + +## 목표와 범위 + +일반 Verilog 모듈로 작성한 unsigned 8-bit ALU와 이진수→BCD→7-segment 회로를 논리 IR, 물리 배치/배선, NBT까지 연결한다. 처음에는 CPU나 ISA 없이 두 입력과 연산 선택 스위치를 사용한다. 표시 범위는 000~255, 세 자리 직접 구동으로 시작한다. + +BCD 변환기와 디코더는 재사용 가능한 RTL 예제로 정의한다. 컴파일러가 특정 모듈 이름을 알아보고 특별 취급하지 않는다. 검증된 물리 셀을 재사용하는 최적화는 기능과 포트 계약을 확인하는 target mapping 계층에 둔다. + +## 현재 확인된 공백 + +| 영역 | 현재 상태 | 필요한 작업 | +|---|---|---| +| Verilog 표현식 | `~`, `&`, `^`, `\|`, `+` 중심. `a[3]`은 `a_3` 이름으로 파싱됨 | 실제 선택 AST, 폭 계산, 상수, 연결, 조건식 | +| 폭과 인덱스 | RTL에서 선언 범위를 폭으로 축약 | unsigned 확장/절단과 지원 범위 명시 | +| 일반 조합 process | `always @*` 처리에 latch 패턴 중심 경로가 있음 | 완전 할당 조합 process와 latch 구분 | +| 논리→물리 | 일반 조합 leaf는 scalar 중심. `Add`, `Inc`, `Mux` 일반 mapping 미완성 | bus 분해, 작은 leaf, 연산 mapping | +| 계층 | cell과 instance 혼합 및 중첩 계층 제약 | flat composite와 작은 leaf 정의로 정규화 | +| 가산기 | CarryTiling 실험/검증 존재 | production IR/PnR 연결과 배선 가능한 포트 | +| 표시 출력 | 이름과 위치를 담는 `outputs.v1` 메타데이터 있음 | 관찰 가능한 21개 endpoint와 표시판 위치 연결 | +| 실제 램프 | `BlockKind`에 lamp 없음 | block/NBT/simulator/WASM/viewer 지원 | + +근거 경로: `src/verilog/{ast,parser,rtl,synth}.rs`, `src/ir/{logical_adapter,logical_lowering}.rs`, `src/transform/place_and_route/local_placer/exact/tiling.rs`, `src/transform/place_and_route/global_pnr/`, `src/world/block.rs`, `src/output.rs`, `tools/nbt-viewer/src/main.ts`. + +기존 `test/alu.v`는 완성된 8-bit ALU fixture가 아니다. 새 예제는 `test/alu_display/`에 둔다. CLI에서 출력 경로 없이 Verilog를 읽는 성공은 IR 로드 성공이며 NBT 컴파일 성공과 구분한다. + +## 고정할 모듈 계약 + +### ALU + +`alu8(a[7:0], b[7:0], op[2:0]) → y[7:0], z, n, c, v` + +| op | 동작 | c | v | +|---|---|---|---| +| 000 | a + b | 9번째 carry | signed 덧셈 overflow | +| 001 | a − b | borrow가 없으면 1 | signed 뺄셈 overflow | +| 010 | a AND b | 0 | 0 | +| 011 | a OR b | 0 | 0 | +| 100 | a XOR b | 0 | 0 | +| 101 | a 전달 | 0 | 0 | +| 110 | a를 왼쪽으로 1bit, 빈자리 0 | a[7] | 0 | +| 111 | a를 오른쪽으로 1bit, 빈자리 0 | a[0] | 0 | + +결과는 하위 8bit, `z=(y==0)`, `n=y[7]`. ADD overflow는 입력 부호가 같고 결과 부호가 다를 때, SUB overflow는 입력 부호가 다르고 결과 부호가 a와 다를 때다. unsigned 인터페이스에도 signed 해석용 v를 제공한다. + +첫 물리 단계는 ADD/SUB와 y, 다음은 나머지 연산과 z/n, 마지막은 c/v다. ADD/SUB는 `b XOR {8{sub}}`, 초기 carry=sub인 하나의 carry chain을 공유하도록 RTL을 작성한다. 반복 연결 문법 지원 전에는 명시적 연결을 사용한다. 컴파일러가 별개 ADD/SUB 회로를 자동 공유한다고 가정하지 않는다. + +### 표시 논리 + +| 모듈 | 입력 | 출력 | 계약 | +|---|---|---|---| +| `add3_if_ge5` | nibble[3:0] | corrected[3:0] | 5 이상이면 3을 더한 하위 4bit, 아니면 그대로 | +| `bin8_to_bcd` | value[7:0] | hundreds/tens/ones 각각 [3:0] | 100h+10t+o=value, 각 자리 0~9 | +| `bcd_to_7seg` | digit[3:0], blank | seg[6:0] | active-high, bit 0부터 a,b,c,d,e,f,g | +| `display_u8` | value[7:0] | 세 자리 seg 각각 [6:0] | 기본 000~255, 21개 독립 출력 | +| `alu8_display` | a,b,op | ALU 결과/flags와 세 자리 seg | y를 unsigned 십진수로 표시 | + +디코더 마스크는 0~9 순서로 `3f,06,5b,4f,66,6d,7d,07,7f,6f` (hex). blank=1 또는 digit=10~15이면 모두 0이다. don't-care를 사용하지 않는다. 선행 0 생략은 후속 옵션으로 둔다. + +Double Dabble은 BCD=0에서 시작해 각 자리의 add3 보정 후 다음 입력 bit를 MSB부터 shift-in하는 8단계를 명시적으로 펼친다. 초기 구현에는 나눗셈, 나머지, 배열, generate, for, parameter가 필요 없다. 중간 보조 함수는 전체 16개 입력에서 정의하고, 실제 BCD 단계의 도달 가능한 값은 별도로 확인한다. + +물리 endpoint 이름은 `hundreds_a`~`ones_g`로 통일한다. 첫 단계는 viewer가 상태를 읽을 수 있는 dust/repeater 등의 출력을 사용한다. 현재 endpoint 메타데이터는 위치를 기록할 뿐, 원하는 표시판 좌표까지 자동 배선하는 기능이 아니다. 실제 표시판에는 고정 sink 위치/접근 방향/점유 공간 계약을 추가한다. viewer의 기존 compiler→NBT 좌표 변환도 재사용한다. + +## 프런트엔드와 IR 설계 순서 + +1. 첫 subset은 unsigned `[N-1:0]` packed vector다. 다른 범위와 signed, x/z, 동적 선택은 명확한 오류로 거부한다. 미지원 표현식을 조용히 다른 의미로 해석하지 않는다. +2. sized binary/hex/decimal 상수, 정적 bit/part select, concatenation, `?:`, `==`, `>=`, `-`를 추가한다. `{a,b}`에서 a는 상위 bit다. 고정 shift는 select와 concat으로 표현한다. +3. 폭 추론과 operand 확장, 결과 절단을 명시적으로 elaboration한다. unsized 상수는 Verilog 규칙을 구현하기 전까지 새 subset에서 거부하고 fixture는 sized 상수로 작성한다. 기존 최소 폭 상수 처리와의 호환성 변경은 테스트로 드러낸다. +4. carry는 `{1'b0,a}+{1'b0,b}`처럼 9bit 연산으로 드러낸다. 같은 폭 unsigned 덧셈의 결과 폭, 조건식의 분기 폭, 비교의 1bit 결과, assignment 절단을 각각 검증한다. +5. `.digit(bcd[3:0])` 같은 입력 포트의 표현식 바인딩을 지원한다. 출력 연결은 첫 단계에서 이름 있는 wire로 제한한다. +6. typed LogicalDesign을 유지한 채 hierarchy를 정규화하고 bus를 target mapping에서 작은 leaf로 분해한다. scalar LogicGraph로 전체 설계를 우회하지 않는다. +7. 연속 할당 기반 예제가 관통한 다음 `always @*`, blocking assignment, if/else, case/default를 확장한다. 기본값과 순차적인 덮어쓰기를 보존하고 의도치 않은 불완전 할당을 진단한다. 기존 latch 경로의 회귀를 확인한다. + +공유 IR enum/API 변경은 통합 담당자가 먼저 확정한다. subtraction/comparison/select를 기존 IR 연산으로 정규화할지 새 연산으로 남길지도 이 단계에서 결정하고 frontend와 mapper가 각자 다른 계약을 만들지 않게 한다. + +## 물리 구현 전략 + +- 전체 19-input ALU를 하나의 exact leaf로 풀지 않는다. decoder, bitwise 연산, mux, carry slice 등 작은 단위로 나누고 op decode를 공유한다. +- CarryTiling은 바로 연결할 수 있는 완제품으로 취급하지 않는다. 현재 chain 조립에는 스위치, carry polarity adapter, 출력 torch가 포함된다. `LayoutCandidate`로 옮기면서 외부 입력 스위치를 router 구동 포트로 바꾸고 carry 극성, 접근점, support/blocked cell을 보존한다. +- 기존 XOR/half-adder macro와 candidate cache를 활용한다. 새 물리 recipe의 revision을 cache key/version에 반영한다. +- 1→2→4→8bit에서 논리 정확성, 배선 성공, 안정화 시간, torch burnout을 함께 확인한다. 기존 tile의 8/16bit 일부 입력 검증을 전수 검증으로 확대 해석하지 않는다. +- broadcast fanout과 bus routing 비용을 먼저 계측한다. local solver 캐시가 빨라져도 전역 candidate 조합/배치/배선 비용은 남는다. +- 큰 회로용 NBT vector runner는 케이스마다 truth table을 재생성하지 않도록 예상값 계산과 실행을 분리한다. + +## 현재 bit mapping 구현과 실험 경계 + +`LogicalDesign::lower_to_routable()`의 일반 조합회로 mapping은 기본적으로 Boolean 연산 1개, 경계 입력 최대 2개의 primitive leaf를 사용한다. fanout-free cone 병합은 `lower_to_routable_with_bit_mapping_limits(4, 3)`으로 명시적으로 켠다. 이 제한의 gate 수는 NOR 변환 전 Boolean 연산 수이며, 물리 블록 수나 변환 후 gate 수와 같지 않다. + +조건식 decoder에서 4/3 병합은 instance를 112개에서 68개로 줄였고 32개 입력에서 primitive mapping 및 독립 oracle과 일치했다. 그러나 3-input AND cone의 local candidate 생성이 제한된 탐색 설정에서 실패했다. 따라서 이 수치로 물리 컴파일 성공이나 속도 개선을 주장하지 않는다. 경계 입력 2개로 제한한 SOP decoder는 70개 instance이며, 세 종류의 primitive가 각각 local candidate 4개를 생성했다. 전체 배선과 exported NBT 검증은 별도 단계다. + +계층은 개별 cell의 입력 준비 상태를 검사하기 전에 flat netlist로 펼친다. 서로 다른 child의 독립 출력끼리 교차 연결된 합법적인 조합 DAG를 모듈 단위 순환으로 오판하지 않기 위함이다. 일반 bus 포트 이름은 `digit_0`처럼 유지하지만, 이미 같은 이름의 scalar 포트가 있으면 해당 bus bit만 `digit[0]`으로 표기한다. 그 이름도 사용 중이면 `$` 접두사를 붙여 충돌을 피한다. scalar 포트 이름은 보존하며 생성된 모든 이름은 유일하다. + +상수 접기, select/concat/resize 배선, 구조 공유를 적용한다. 사용되지 않는 top input bit는 interface에 남기고 배선하지 않는다. 상수 출력은 기존 입력을 읽는 항진식/모순식 leaf로 표현하며, 입력이 전혀 없는 상수 전용 회로는 아직 명확한 오류를 반환한다. pass-through 출력에는 관찰 가능한 buffer leaf를 둔다. + +## 단계별 완료 기준 + +| 단계 | 결과물 | 통과 기준 | +|---|---|---| +| M0 계약/재현성 | 포트/폭 계약, 독립 oracle, fixture 골격 | clean checkout에서 필요한 release test가 재현됨 | +| M1 작은 관통 | decoder RTL→IR→NBT→7개 출력 관찰 | digit×blank 32개 논리 전수, 물리 안정값과 전이 검사 | +| M2 bus 산술 | ADD/SUB mapping과 carry adapter | 1,2,4bit 단계 통과 후 8bit 물리 성공; 논리 전수 oracle 일치 | +| M3 ALU | 8개 op와 flags, snapshot | 524,288개 논리 입력 전수; NBT 경계/연속 전이 검사 | +| M4 표시 논리 | double dabble와 decoder 3개 | 256개 변환 논리 전수, 표시 출력 21개 물리 검증 | +| M5 결합 | alu8_display NBT와 재현 명령 | y/flags/표시값 일치, 시간·크기·안정화 보고서 | +| M6 표시판 | 실제 lamp와 고정 panel sink | simulator/NBT/viewer 일치, 인접 segment 간섭 검증 | + +M2/M3의 산술 작업과 M4의 논리 fixture는 M0 이후 병렬 가능하다. M4의 물리 실행은 M1과 bus mapping을 사용한다. M5는 M3/M4 완료 후 진행한다. 작은 단계를 통과하지 않은 상태에서 큰 ALU 탐색 시간을 무작정 늘리지 않는다. + +M0에서 `src/ir/debug.rs`의 `include_str!`가 참조하는 `test/counter.snapshot/counter.v`, `test/d-flip-flop.snapshot/d-flip-flop.v`의 tracked fixture 여부를 정리한다. 현재 로컬에는 있지만 Git에 없는 입력에 의존하는 테스트가 있으므로 기존 snapshot 디렉터리를 통째로 추가하지 말고 최소 입력 fixture로 고친다. + +## 병렬 작업 소유권 + +첫 구현 묶음은 통합 담당 1명 + 에이전트 3명으로 진행한다. 같은 checkout에서 각 파일의 작성자를 한 명으로 고정한다. + +| 담당 | 소유 범위 | 다른 담당자에게 제공 | +|---|---|---| +| 통합 | 공유 IR 계약, 테스트 연결, 단계별 실행/보고서 | 합의된 연산/폭 API와 검증 명령 | +| Frontend | `src/verilog/`, parser/elaboration 테스트 | typed IR, 미지원 문법 진단 | +| Mapping | `src/ir/logical_adapter.rs`, `logical_lowering.rs`, mapping 테스트 | flat composite와 scalar leaf/산술 mapping | +| Fixtures/reference | 새 `test/alu_display/`, 독립 oracle 테스트 파일 | decoder/BCD/ALU RTL와 기대값 | + +후속 묶음은 physical carry adapter, reusable vector runner, viewer/panel을 나눈다. `global_pnr`와 cache 변경은 physical 담당 한 명이 맡는다. lamp 도입은 block enum/NBT/simulation/WASM에 걸친 작업이므로 공유 파일 변경을 묶어서 순차 통합한다. agent마다 임의 커밋하거나 다른 담당자의 파일을 동시에 수정하지 않는다. + +## 검증과 성능 판단 + +논리 검증은 독립 정수 모델로 수행한다. decoder 32개, add3 16개, converter 256개, ALU 256×256×8=524,288개를 전수 비교한다. 가능하면 이후 외부 Verilog simulator를 두 번째 oracle로 추가한다. 현재 환경에서 iverilog/verilator/yosys 설치는 확인되지 않았으며 이를 이미 사용한 검증으로 기록하지 않는다. + +물리 검증은 별도다. 작은 leaf에는 전수 입력과 전수 전이, 큰 회로에는 경계값과 seed 고정 연속 전이를 사용한다. 현재 모든 입력 순서쌍 검사기를 19-input ALU에 적용하면 약 2.75×10¹¹ 전이가 되어 사용할 수 없다. 무작위 물리 검사를 완전한 증명이라고 부르지 않는다. + +필수 물리 사례: 긴 carry 전파, 0/255 경계, SUB borrow, op 변경, `9↔10`, `99↔100`, `199↔200`, `255↔0`. 입력 변경 중 조합 glitch는 허용하고 안정화 이후 결과로 판정한다. timeout, event limit, 미안정화, burnout은 모두 별도로 실패/미확정으로 기록한다. 탐색 timeout을 UNSAT으로 해석하지 않는다. + +각 단계에서 cold/warm cache, local candidate 생성 시간, global placement/routing 시간, 실패 원인, 블록 수, bounding box, 안정화 tick을 기록한다. 동일 입력/설정/seed로 비교하고, 최적화는 여러 seed에서 성공률과 시간 분포를 함께 본다. 첫 작은 회로 측정 후 시간/event 예산을 정하고 초과 시 결과를 남겨 다음 단계 진입을 막는다. + +빌드/테스트/실행은 모두 release 모드다. 기존 compilation snapshot과 candidate cache를 재사용한다. 저장소에는 최소 RTL/골든 fixture/측정 요약을 남기고 대형 탐색 trace는 기본적으로 로컬 산출물로 둔다. + +## 후속 확장: 순차 변환기와 CPU + +8-cycle Double Dabble은 조합 버전과 별도의 후속 목표다. 여러 레지스터, reset, FSM, nonblocking assignment 지원이 필요하다. 계약은 `clk/reset/start/value → busy/done/bcd`로 한다. idle에서 start를 받은 edge에 입력을 저장하고, 이후 8개의 edge에서 처리한다. 마지막 edge에서 결과를 공개하고 done을 한 cycle 올린다. 결과는 다음 완료까지 유지한다. reset은 동기식 최우선, busy 중 start는 무시하고 완료 edge에서 새 요청은 받지 않는다. clock 주기는 물리 안정화 측정으로 정한다. + +CPU를 붙일 때는 accumulator, 입력/결과 register, 작은 instruction ROM부터 확장한다. MIPS 호환성이나 명령어 집합 구현을 이번 ALU/표시 컴파일러 완료 조건에 포함하지 않는다. + +## 산술 물리 경로 진척 — 2026-11-08 + +`examples/adder_nbt.rs`에서 unsigned `a,b → sum[N:0]`를 Verilog부터 NBT까지 연결했다. 이 결과는 ADD 경로이며 ADD/SUB 공유, 전체 ALU의 8개 op/flags 또는 BCD/표시판 완료를 뜻하지 않는다. 물리 설정은 `10x10x5`, ranked beam 256, A* expansion 20,000이며 seed 조정 없이 box/beam을 바꾸었다. 작은 box의 XOR는 출력 diode reservation에서 탈락했고, 입력 처리 전의 실패였음을 debug stage 로그로 확인했다. + +- 2bit cold: PnR 6.507초, 645블록. 저장 NBT의 정적 16개 입력과 256개 ordered transition 전수 통과. +- 4bit warm: PnR 7.648초, 1,925블록. local candidate 17개 instance 모두 cache 재사용. 정적 256개 입력 전수와 연속 walk 256개 통과. +- 8bit warm: PnR 38.709초, 3,433블록. directed carry/boundary 33개와 고정 seed 무작위 256개, 총 289개 정적 사례와 같은 연속 walk 통과. 8bit 전체 입력/전이의 완전 검증은 아니다. + +8bit 초기 walk 실패는 simulator의 redstone normalization에서 OFF만 통지하고 새 ON을 누락한 문제로 재현되었다. fixpoint 전후 ON/OFF 통지를 수정한 뒤 원래 NBT를 다시 읽어 검증했다. NBT 및 입력 순서는 바꾸지 않았다. `--verify-only`로 2/4/8bit를 모두 재검증했고, 기존 실패/성공/status 기록을 `diagnostics/prior-*`로 보존한 뒤 `verification.json`에 simulator 수정 후 재검증 여부와 파일 fingerprint를 남겼다. 검증 원본은 독립 정수 `a+b`이며 graph 자기평가를 사용하지 않는다. + +명령과 coverage 범위는 `test/alu_display/README.md`의 unsigned 가산기 절을 따른다. artifact는 `target/alu-display/` 아래 두며, `verification.json`이 실제 통과를 나타낸다. source/IR 로딩 성공이나 NBT 생성만으로 검증 완료를 선언하지 않는다. diff --git a/docs/alu_display_compiler_progress.md b/docs/alu_display_compiler_progress.md new file mode 100644 index 00000000..0a86ca61 --- /dev/null +++ b/docs/alu_display_compiler_progress.md @@ -0,0 +1,91 @@ +# ALU / 표시 컴파일러 구현 기록 + +기준 브랜치: `codex/8bit-alu-display`. 목표와 인터페이스는 [개발 계획](alu_display_compiler_plan.md), 재현 명령은 [fixture 안내](../test/alu_display/README.md)에 있다. + +## 논리 경로 + +- unsigned sized b/h/d 상수, bit/part select, concat, 조건식, `==`, `>=`, `-`, 입력 포트 표현식을 파싱하고 RTL에서 폭 확장/절단을 명시한다. +- Logical IR에도 비교/선택/연결/resize를 보존한다. 계층은 먼저 펼친 뒤 cell 의존 순서로 처리한다. 독립 child 출력의 교차 연결을 모듈 단위의 가짜 cycle로 거절하던 문제를 회귀 검사한다. +- 버스 bit와 scalar 이름이 충돌하면 생성한 bit 이름을 escape한다. `a[0]`과 scalar `a_0`가 같은 입력으로 합쳐지지 않는다. +- 상수 접기와 구조 공유 후 기본적으로 1gate/2input leaf로 내린다. 작은 cone grouping은 명시적 API의 실험 옵션이다. 조건식 decoder에서 4gate/3input grouping은 112→68 instances지만 일부 leaf의 물리 후보를 찾지 못해 기본값으로 사용하지 않는다. + +RTL 독립 정답 검사: decoder 두 표현 각각 32개, Double Dabble 256개, ALU/flags 524,288개 전수 비교가 통과했다. 별도로 생성된 Routable 논리는 decoder32개, 4bit 산술/비교 등512개, 계층16개, 상수/전달4개, 검토 중 발견한 계층·이름 충돌 회귀를 검사한다. `display_u8`는 8개 scalar 입력과21개 출력으로 lowering하고 Logical text 왕복을 검사한다. + +## 물리 경로에서 발견한 문제 + +1. 외부 Cobble 입력의 self hard-power source가 전원 정규화와 attached torch 재평가에서 빠졌다. 외부 ON이 지워져 NOT 후보가 모두 truth 검사에서 탈락했다. 두 계산에 해당 전원을 포함하고 반복 ON/OFF 회귀를 추가했다. +2. input diode의 첫 fanout 가지가 아직 연결하지 않은 다른 sink까지 ON이어야 한다고 요구했다. 합법적인 접촉 위치와 현재 가지의 전원 의무를 분리했다. 두 가지를 차례로 연결하고 ON/OFF를 검사한다. +3. diode의 repeater와 target이 route path에서 빠져 최종 contact 검사에서 정상 연결을 거절했다. 두 adapter 생성 경로에 실제 경로를 기록한다. +4. input support에 붙은 torch의 정상 반응을 foreign short로 판정했다. 허용 범위를 그 support에 물리적으로 붙은 torch로 한정했다. 생성 NOT/AND/OR 후보의 입력 접근과 독립 NOT의 실제 반전, 무관한 인접 torch의 거절을 검사한다. +5. floor-level diode 포트에는 지지 블록을 둘 아래 공간이 필요하다. 생성한 전역 배치에 support layer를 예약한 뒤 명시적 physical intent를 적용한다. + +6. 상세 배선의 `ConnectPosition` 판정 방향이 뒤집혀 있었다. terminal→sink 방향으로 고쳤으며 방향성이 있는 repeater 회귀로 검사한다. +7. 전역 배선은 기존 Cobble support를 재사용할 수 있어야 한다. `SupportReusePolicy`를 명시하고 전역 경로에서만 켠다. 기존 local placer의 기본 `FreshOnly`는 유지한다. +8. dust 강도 정규화의 중간 OFF 상태가 이벤트로 전달되고 마지막 ON 복구가 누락됐다. fixpoint 시작/끝의 실제 ON/OFF 변화만 전달한다. 저장된 8bit 가산기의 특정 입력 전이 실패가 NBT 변경 없이 해결됐다. +9. Cobble 전원 카운트를 정정할 때도 consumer에 변화가 전달되지 않았다. 꺼진 Cobble 옆 리피터가 ON으로 남는 작은 회귀를 추가하고, 전체/강한 전원 각각의 ON/OFF 변화만 통지한다. 기존 decoder NBT의 48개 전이 실패가 파일 수정 없이 모두 해결됐다. + +실험 옵션인 입출력 diode 예약과 verified n-ary recipe는 생성 후 진리표를 검사한다. 의미상의 단일 minterm/maxterm을 식별하므로 모듈 이름에 의존하지 않는다. literal absorption은 별도 lowering API이며 기본 1gate/2input mapping을 바꾸지 않는다. recipe 출력 위 진입 공간은 `blocked_cells`로 예약하고 cache/복원에도 보존한다. 배선의 불변 `RoutingSearchContext`가 이 예약을 전달한다. + +동일 경계에 일렬로 붙은 입력은 먼저 연결한 fanout bus가 나머지 입력을 가둘 수 있다. 실험 옵션 `nary_stagger_inputs`는 입력을 계단식으로 노출하고 repeater로 강도를 복원한다. 4bit minterm을 2/4/10개 공유하는 probe가 각각 8/16/40개 배선을 완료하고 NBT 재읽기 후16개 입력을 검사했다. `PinAccess`는 같은 방향의 깊은 핀을 먼저 연결하고, 입력 앞 접근 공간은 해당 sink만 사용하도록 예약하는 선택적 전략이다. 기본 순서는 유지한다. + +내부 fanout은 근처 tap을 먼저 시도하되 실패하면 원래 출력/접근점으로 돌아간다. tap을 제한할 때는 완료한 sink를 제외하고 아직 연결하지 않은 sink까지의 거리로 순위를 매긴다. 두 보완 모두 별도의 실제 신호 회귀가 있다. + +꺼진 dust 출력은 기존에 시작 세기를2로 보수적으로 잡았다. 같은 실패 prefix에서2는 막히고15는124칸 경로를 찾았다. `RestoredDirect`는 활성 상태에서15를 보장하는 출력만의 명시적 계약이다. n-ary recipe와 준비된 IO driver는 진리표에서 이를 인증한 뒤 표시한다. 일반 dust/다른 접근점/중간 tap에는 이 약속을 적용하지 않는다. cache v10과 candidate library v2는 계약을 보존한다. 리피터를 기존 dust 옆에 배치할 때 이전 dust의 연결 모양을 갱신하지 않던 상세 배선 오류도 직각 배선 회귀로 수정했다. + +## 디코더 관통 성공 + +`bcd_to_7seg_minimized.v`와 `blank=0`인 일반 RTL wrapper를 컴파일해 `target/alu-display/decoder-digit-final/decoder.nbt`를 생성했다. 실제 기록은 PnR23.308초,20개 cell,62개 배선,5,703블록이다. 16개 정적 입력을 검사하고 저장한 NBT를 다시 읽어 모든256개 입력 전이(연속512회 입력 적용), invalid BCD10~15 OFF, burnout 부재를 확인했다. `verification.json`이 성공 증거다. 전수 검사가 작은 probe에만 한정된 결과가 아니다. + +원래 shared-minterm식은18개 cell/90개 연결이었다. 재현 가능한 prime implicant/최소 cover 탐색으로 동등한 POS식(20개 cell/62개 연결)을 만들었다. 최대 fanout은10→11로 증가하므로 모든 지표가 좋아진 것은 아니다. 생성 도구는 `tools/alu_display/minimize_decoder.py`이며 결과는 일반 Verilog이고 컴파일러 이름 기반 특례가 아니다. invalid10~15를 don't-care로 없애지 않았다. + +blank 제어를 포함한 전체32상태/1,024전이 NBT는 미완료다. 한 배치는32개 정적 입력과69개 배선을 통과했지만 연속 전이 중 burnout이 검출됐다. runner의 배치 선택 callback에도 전체 전이 검증을 추가해 잘못된 배치를 거절한다. 실제 램프 표시판도 후속 단계다. 현재 성공은 기본 숫자 표시용7개 논리 segment 출력이다. + +## 실제 가산기 NBT + +Verilog에서 시작해 공통 lowering/PnR/NBT 경로로 2/4/8bit 가산기를 생성했다. 측정값과 재현 명령은 fixture 안내에 있다. 2bit는 cold PnR6.507초,16개 정적 입력과256개 모든 입력 전이, 4bit는 warm7.648초,256개 정적 입력과256개 연속 walk가 통과했다. 8bit는 warm38.709초,3,433블록이며289개 정적 입력과289개 연속 walk를 검사했다. 8bit 검사는 경계 사례와 seed가 고정된 무작위256개를 포함하며 전수 증명은 아니다. + +8bit는 처음 연속 walk에서 실패했다. 원인을 simulator의 정규화 이벤트에서 고친 뒤 기존 `adder.nbt`를 수정하거나 재컴파일하지 않고 같은 검사를 통과했다. `verification.json`에는 NBT fingerprint와 `nbt_unchanged`를 남긴다. 전체 ALU와 Double Dabble의 물리 컴파일 성공을 뜻하지는 않는다. + +## 공유 ADD/SUB + +`test/alu_display/add_sub{1,2,4,8}.v`는 `b XOR subtract`, 초기 carry=`subtract`를 사용하는 하나의 명시적인 carry chain이다. 별개 ADD/SUB의 자동 공유를 가정하지 않는다. 출력 `c`는 덧셈 carry 또는 뺄셈에서 borrow 없음이다. `examples/add_sub_nbt.rs`는 독립 정수 연산으로 검증한다. + +- 2bit:12개 cell/24개 배선,1,333블록. PnR6.040초,32개 입력 및 모든1,024개 입력 전이/NBT 재읽기 통과. transition callback이 burnout 배치를 거절한 뒤 다음 배치를 선택했다. +- 4bit:24개 cell/48개 배선,2,906블록. PnR77.012초,512개 입력 전수 및512회 연속 walk/NBT 재읽기 통과. cache v10의3개 unique 후보는 재생성하고21개 instance를 재사용했으므로 완전 warm 측정은 아니다. 모든262,144개 입력 순서쌍 검사는 아니다. +- 8bit:48개 cell, 후보48개 재사용. 동일20,000 expansion 예산과12개 배치/순서 시도에서136.36초 후 배선 실패했다. 성공 NBT/검증 결과가 없으며,8bit 덧셈 단독 성공과 구분한다. + +다음 물리 병목은 ADD/SUB의8bit 전역 배선과 decoder의 blank 포함 전이 안정성이다. 전체 ALU의8개 연산/flags 및 Double Dabble/세 자리 표시 연결은 아직 물리 완료가 아니다. + +## 배선 복사 비용 + +실행 중 primitive decoder의 1초 스택 샘플에서85개 중70개가 상세 배선의 World3D deep clone 경로였다. World3D를 plane/행의 두 단계 Arc로 바꾸고 수정한 plane의 행 포인터와 해당 행의 block만 복사한다. clone 격리와 sparse 왕복을 검사했다. + +`cargo run --release --example world_clone_benchmark`의 동일64×64×8 world,1,000 clone,각 clone에서 서로 다른 두 행 수정,5회 중앙값 측정은 기존49.039ms / COW1.029ms였다. checksum은 동일하다. 47.68배는 이 복사 microbenchmark 수치이며 전체 PnR 배속이 아니다. + +## 검증 범위 + +release mode만 사용한다. 최종 `cargo test --release --workspace --all-targets`는 core444개 통과/실패0/ignored56개(216.54초)와 workspace 및 integration 검사를 통과했다. `PinAccess`, 출력 강도 계약, 직각 repeater, 두 정규화 알림 회귀를 포함한다. workspace native check에는 nbt-sim-wasm도 포함하며 viewer용 WASM release와 viewer production build도 통과했다. 검색이 무거운 ignored 측정을 전부 실행한 결과로 해석하지 않는다. + +전체 탐색 trace와 실패 NBT는 기본적으로 저장소에 추가하지 않는다. 작은 RTL/검증 코드와 요약을 남기고 `target/alu-display/`에 IR, NBT, cache, 검증 결과를 생성한다. + +## Exact 후보를 연결한 압축 + +이후 Global PnR에 opt-in Exact SAT 후보 공급을 연결했다. 실제 SAT 셀6개를 채택한36×64×56 NBT는4,187블록이며16개 입력과 모든256개 전이를 통과했다. 기존2층 대비25.7%, 같은 자유3D의 recipe 대조군4,312블록 대비2.9% 감소다. 최초 cold 실행은89.252초로 더 오래 걸렸고, 모델 최적성이나 전반적인 속도 향상을 주장하지 않는다. 입력 어댑터, provenance, 실패 시 recipe fallback과 비교 조건은 [별도 기록](exact_global_candidates.md)에 있다. + +Exact 물리 후보의 내부 Cobble/dust 전력 count는 NBT에 저장되지 않으므로, persistent cache의 world 해시를 NBT 왕복으로 정규화했다(cache v13). geometry/포트/reservation/cost와 실제 블록 변조 검증은 유지한다. 실제 mixed-polarity SAT 후보의 캐시 재로딩 후 전이 검증도 통과했다. 이 확장 직후 전체 release workspace는 core447개/실패0/ignored56개와 나머지 검사를 통과했으며, 이후 cache 해시 보정은 별도 회귀로 검사했다. + +## 디코더 전체 monolithic Exact + +Verilog를20개 셀로 나누지 않는 `lower_flat_logic_graph()`와 `whole_decoder_exact` 실험을 추가했다. 입력4개·출력7개·NOR28개를 하나의 SAT 모델로 넣고 독립 mask16개를 먼저 확인한다. 자유핀3개 박스/단일worker30초,5×10×5/4worker60초,입력면고정·출력면제한/4worker60초까지 다섯 시도는 모두 UNKNOWN이었다. encoding은0.2~0.5초였고 simulator refinement는0회여서 최초 SAT witness 탐색이 병목이었다. 성공 NBT가 없으며, 불가능 증명으로 해석하지 않는다. [조건·측정·재현 명령](whole_decoder_exact.md)에 별도로 기록했다. + +이후8×12×6/4worker/120초 확대 실험도 UNKNOWN이었다(encoding1.279초, solve120.621초,390,656변수/2,925,936절). 박스 확대만으로 최초 해를 얻지는 못했으며 성공 NBT는 여전히 없다. + +## 전체 decoder 구성 후 압축 경로 + +기존 Exact `construct → compact`를 호출하는 별도 runner를 추가했다. 정형 셀/Global PnR 없이 같은 전체 논리를 사용하며, 초기 회로를 NBT 전이 검증한 다음 압축하도록 했다.4×높이6은2/28단계, 기본형2×높이10+단계별최소화는6/28단계,2입력OR체인+2×높이24는1/45단계에서 창별 탐색 시간 초과로 중단됐다. 완성 초기 NBT가 없어 compact는 실행하지 않았다. [실험 기록](whole_decoder_construct.md)에 prefix 크기와 완성 회로를 구분했다. + +추가 내장 재시작1회 시험은 총232.462초였다. 첫 시도6단계 뒤 막혔던 g103을 두 번째 시도에서 넘어7/28단계까지 진행했지만, seg_4에서 시도별120초 예산이 끝났다. 두 번째 시도의 창6은 실행하지 않았으며 완성 NBT/compact 결과는 여전히 없다. 바깥은 이전 배치를 대부분 고정하는 휴리스틱 순차 구성이고 각 창은 SAT이므로, 재시작에 의한 국소 선택 복구와 전체 최적성은 구분한다. + +이후 전체 초기해 생성에 성공했다. 일반 Verilog의 공유 논리를24-NOR로 정리하고, 상보 경계 대표·간격3의 repeater 포트·범용 NOR 템플릿의 SAT 부분집합 탐색을 연결했다. Global PnR 없이 **4,132블록, Minecraft220×7×33**이며 checkpoint 없는 cold 구성185.937초였다. NBT를 다시 읽어16개 정적 입력과256개 모든 전이를 통과했다. 템플릿 밖의 자유로운 동시 배치나 전역 최적성을 뜻하지 않는다. + +주요 엔진 병목은 export된 `Cut[Cell,Cell]`의 제곱 개수 materialization이었다. `@internal` 처리, hard Air/given 선택지 특수화, 경우별 순위의 희소 인덱스와 dust OR 전원 증명을 추가했다. soft proof-core와 기존 DFF standalone 모델의 CNF 호환성을 유지한다. 압축은 길이2칸을 줄인 후보에서 블록이 늘어 초기해를 유지한다. 재현 스크립트와 조건은 [전체 Exact 구성 기록](whole_decoder_construct.md)에 있다. diff --git a/docs/exact_global_candidates.md b/docs/exact_global_candidates.md new file mode 100644 index 00000000..f38bd9aa --- /dev/null +++ b/docs/exact_global_candidates.md @@ -0,0 +1,43 @@ +# Exact 후보를 사용하는 전역 PnR 압축 + +기준: `codex/8bit-alu-display`, draft PR #69. 2026-11-09. + +## 구현 + +`UnitCandidateConfig.exact: Option`를 켜면 각 작은 조합 leaf에 실제 `ExactLocalPlacer` SAT 탐색과 블록 최소화를 수행한다. 기본값은 `None`이며 기존 경로를 유지한다. 기본 탐색 상자는3×5×3, 입력 최대4개, 원본 logic node 최대16개, solver 제한은 셀당5초다. worker와 seed는 각각1이다. 시간 제한은 solver 시간이며 인코딩·어댑터·검증 시간까지 포함하는 총 실행 제한은 아니다. + +배치용 double-NOT 버퍼를 논리적으로 제거한 뒤 NOR로 내린다. Exact의 출력은 박스 밖을 구동할 수 있어야 한다. 원래 스위치가 강하게 구동하던 solid를 외부 입력 대상으로 보존하고, 입력마다 격리 diode를 붙인다. 원본 스위치 좌표의 단락만 보고 공유 support를 부당하게 거절하지 않고, 격리된 최종 경계에서 입력 독립성을 검사한다. + +완성된 후보는 NBT 재읽기 후 진리표, 작은 셀의 모든 입력 전이, 입력 역류, 출력 역구동과 `RestoredDirect` 강도를 독립 simulator로 검사한다. 실패·시간 초과·범위 초과는 기존 검증된 recipe를 사용한다. 후보끼리는 어댑터를 포함한 비용으로 경쟁한다. `exact_core`는 실제 채택된 SAT 후보의 출처, 박스, 스위치를 포함한 물리 원본 블록 수, 시간과 최적성 여부를 보존한다. 최종 `selected-candidates.json`과 `verification.json.exact_selected`로 SAT 실행과 최종 채택을 구분한다. + +`ExactPlacement.block_count`는 입력 스위치를 제외하므로, recipe의 전체 물리 블록 수와 직접 비교하지 않는다. 보고하는 최종 후보 비용에는 스위치 제거와 외부 인터페이스가 반영된다. 제한 시간 내 해를 찾았다는 사실은 최소 크기 증명이 아니다. + +## 실제 전체 decoder 측정 + +동일한 최소 cover RTL과 `blank=0` wrapper, 입출력 예약, PinAccess, A*20,000, 경로 길이256을 사용했다. 전부 최종 NBT의16개 입력, invalid BCD OFF, 모든256개 전이(연속512회 입력 적용), burnout 부재를 통과했다. + +| 설정 | NBT 크기(X×높이×Z) | 블록 | 경계 상자 부피 | Exact 채택 | +| --- | --- | ---: | ---: | ---: | +| 기존2층 recipe |95×16×95|5,638|144,400|0| +| 자유3D recipe 대조군 |41×64×51|4,312|133,824|0| +| 자유3D + Exact |36×64×56|4,187|129,024|6/20| + +기존2층 대비 블록25.7%, 바닥 면적77.7%, 부피10.6% 감소다. 같은 자유3D에서 Exact만 비교하면 블록2.9%, 부피3.6% 감소다. 높이는16→64로 증가하므로 모든 축이 짧아진 결과는 아니다. 더 이른 평면95×7×95 결과와 비교하면 총 경계 상자 부피는 증가한다. + +자유3D 대조군은20.023초, Exact는 cold89.252초(후보 준비70.949초, 전역 단계18.30초)였다. 후보 재사용과 한 번씩의 측정이며 반복 평균이나 보편적인 속도 향상을 주장하지 않는다. Exact를 켠 최초2층 실험은 셀이 작아졌는데도 전체5,823블록으로 늘었다. 따라서 내부 셀 크기만으로 전체 압축 효과를 판단하지 않는다. + +## 재현 + +```sh +EXACT_CANDIDATES=1 EXACT_BOX=3x5x3 EXACT_MS=5000 \ +BIT_LITERAL=1 NARY_STAGGER=1 PNR_PIN_ACCESS=1 PNR_FREE3D=1 \ +PNR_LAYOUT_COMBINATIONS=4 PNR_ASTAR_EXPANSIONS=20000 PNR_MAX_STEPS=256 \ + cargo run --release --example decoder_nbt -- \ + target/alu-display/decoder-exact-final \ + test/alu_display/bcd_to_7seg_minimized.v \ + test/alu_display/bcd_digit_to_7seg.v +``` + +`PNR_CANDIDATE_CACHE`로 후보 cache 경로를 따로 지정할 수 있다. 같은 출력 경로로 재실행해도 cache를 사용한다. `PNR_LAYERED=1 PNR_LAYER_SPACING=2`는 자유3D 대신 작은 층 간격을 실험하는 옵션이다. 대조군은 `EXACT_CANDIDATES`만 생략한다. + +검증된 최종 fixture는 `test/alu_display/nbt/alu-display-decoder-exact-verified.nbt`에 보존한다. 아직 blank 제어를 포함한 전체 decoder와 ALU 물리 완료를 뜻하지 않는다. diff --git a/docs/rsdsl/inverter_1x3x2.instance.rsdsl b/docs/rsdsl/inverter_1x3x2.instance.rsdsl index c87ef834..d81c5a92 100644 --- a/docs/rsdsl/inverter_1x3x2.instance.rsdsl +++ b/docs/rsdsl/inverter_1x3x2.instance.rsdsl @@ -12,6 +12,8 @@ domain Output = [out]; domain Tick = [0, 1, 2, 3, 4, 5, 6, 7, 8]; // 0..=stage_levels domain Sym = []; // no box mirrors domain Order = []; +domain RankedCell = []; // case_rank_levels=0: no per-case ranks +fact rank_cell = []; fact on = [(a, 1), (out, 0)]; fact unpowered = [off]; diff --git a/docs/solver_dsl_grammar.md b/docs/solver_dsl_grammar.md index 493c1be2..f2c072d4 100644 --- a/docs/solver_dsl_grammar.md +++ b/docs/solver_dsl_grammar.md @@ -517,6 +517,8 @@ rsdsl 2; instance inverter_1x3x2 of exact_placer; grid Cell = (1, 3, 2); domain Class = [off, a, out]; +domain RankedCell = []; // default case_rank_levels=0 +fact rank_cell = []; fact on = [(a, 1), (out, 0)]; fact switch_site = [(c, att, a) for c: Cell, att: Attach]; param rank_levels = 8; diff --git a/docs/whole_decoder_construct.md b/docs/whole_decoder_construct.md new file mode 100644 index 00000000..946f44ae --- /dev/null +++ b/docs/whole_decoder_construct.md @@ -0,0 +1,85 @@ +# 전체 decoder의 construct → compact 실험 + +## 현재 성공한 결과 + +일반 Verilog에서 시작해 전체 7seg decoder의 초기 NBT를 생성했다. **4,132블록, Minecraft 크기220×7×33**, 입력4개·출력7개다. `blank=0`인 decoder이며10~15는 모두 꺼진다. 새 출력 fixture는 `test/alu_display/nbt/alu-display-decoder-whole-exact.nbt`, 독립 검증 요약은 `test/alu_display/whole_decoder_exact_verification.json`이다. + +중간 checkpoint 없이 실행한 cold 구성은185.937초였고, 저장한 NBT를 다시 읽어16개 정적 입력·모든256개 순서쌍/512회 drive·burnout 부재를 통과했다. seed1/4 workers, 재시작0회다. 형제 실행/회귀 검사 부하가 있으므로 보편적인 속도 벤치마크로 해석하지 않는다. + +**성공 방식은 범용 NOR 템플릿으로 초기 후보의 기하를 제한한 Exact 구성이다.** 자유로운 단일 SAT 박스에서 전역 최적해를 찾은 결과가 아니다. Global PnR와 기존20개 물리 셀 분할은 사용하지 않는다. 이후 `compact`는 공간 창을 다시 자유롭게 푼다. 첫 압축은4,132블록을 유지했고, 별도 창 압축은 길이220→218을 얻었으나 블록4,594개로 늘어 초기해를 배포한다. 출력은 `anywhere-observable`이며 이전 monolithic 실험의 경계 driving 출력 계약과 다르다. + +재현은 저장된 NBT나 checkpoint 없이 다음 명령을 쓴다. + +```sh +sh tools/alu_display/whole_decoder_exact.sh target/alu-display/whole-decoder-new +``` + +## 성공 경로의 표현과 제약 + +- `flatten_positive_logic()`가 이중 부정과 양의 OR 중간 단계를 접어 원래28-NOR를24-NOR로 바꾼다. 전체16×7 진리값을 변환 뒤 다시 검증한다. +- 경계에서 같은 함수·반대 함수를 하나의 대표로 운반한다. 예를 들어 입력과 그 반전 네 쌍을 모두 들고 가던 경계를 줄이고, 필요한 반전은 다음 창에서 다시 만든다. 가능한 경우 원래 입력과 이미 운반 중인 극성을 유지한다. 완료 출력은 별도로 계속 관측한다. +- 살아 있는 기존 포트를 먼저 예약한 다음 새 결과의 포트를 배정한다. 그렇지 않으면 새 결과가 아직 필요한 옛 신호의 포트를 빼앗았다. +- 경계 포트는 바깥 방향 repeater와 지지 블록이다. 간격3으로 토치 약전원의 다른 rail 간섭을 막는다. 새 창에서는 같은 위치의 블록·repeater 열로 신호를 운반하고 블록에서 신호를 꺼낸다. +- `nor_scaffold.rs`는 입력 극성 버퍼, 상부 OR 버스, NOR 토치와 출력 하강 경로를 만드는 범용 후보 생성기다. OR 버스의 repeater가 신호 세기를 갱신하며, 별도 pass rail을 포함한 작은 전수 검증도 한다. Exact는 이 후보의 블록별 유지/삭제 부분집합을 탐색하고 전체 simulator 검증을 수행한다. +- 중간 OR의 함수도 vocabulary에 넣는다. `case_rank_levels > 0`은 입력 경우마다 전원이 실제 원천에서 오는 경로를 순위로 증명해, 서로 다른 신호가 합쳐지는 dust OR를 허용한다. 예전 모델이 거절하던 합법적인 작은 OR 배선을 새 모델이 받아들이는 회귀를 추가했다. + +상보 대표는 **논리 정보**를 보존하지만 물리적으로 같은 오른쪽 배치를 붙일 수 있다는 증명은 아니다. 템플릿·포트 격자·vocabulary 제한은 의도적인 합성 제한이며, 그 범위 밖의 모든 해를 보존하는 가지치기라고 주장하지 않는다. 기본 `place()`와 기존 construct의 선택적 설정은 유지한다. + +## 모델 생성 병목 + +`Cut[s,t]`는 Rust에서 읽지 않는 내부 판정식인데 export되어 모든 칸 쌍을 생성했다. 큰 prefix에서 약50GB를 쓰며 `Model::ground/get_def`에 머무는 스택을 확인했다. `@internal`로 바꿔 실제 참조하는 feed 쌍만 생성한다. 이는 물리 배치 조건을 바꾸지 않는다. + +hard Air·인증된 given 신호의 선택지를 미리 줄이고, Air의 전원 판정을 생략했다. soft/proof-core 셀은 이 제한에서 제외한다. 경우별 순위는 별도 정수 인덱스로 필요한 셀에만 할당한다. 예전 standalone 모델에 새 fact를 강제로 주입하지 않도록 모델 선언을 확인하며, 기존 DFF 모델의 CNF 동등성 회귀도 유지한다. + +긴 초기해에는 simulator 한도를4,096 cycles/2,000,000 events로 둔다. SAT 시간 제한은 모델 생성 시간을 포함하지 않아 전체 벽시계 제한과 같지 않다. 전역 삭제 최적화는 이 긴 초기해에서 모델 생성 비용이 컸으므로 재현 runner는 창별 slice 압축부터 진행하며, 블록 수가 늘어나는 압축 후보를 최종 결과로 선택하지 않는다. + +검증은 release core456개 통과/실패0/ignored56개, RSDsl9개, native WASM3개와 기존 integration을 포함한다. 오래된 DFF standalone 모델과 RSDsl reference instance의 새 domain 호환성도 확인했다. 기존 legacy 테스트가 NBT fixture를 덮어쓰므로 검증 후 원래 파일을 복원했다. + +## 앞선 실패 실험 + +`examples/whole_decoder_construct.rs`는 기존 최소 cover Verilog와 `blank=0` wrapper를 공유 NOT/OR 그래프로 내리고, 기존 `ExactLocalPlacer::construct()` 다음 `compact()`를 호출한다. 정형 물리 셀20개나 Global PnR를 사용하지 않는다. 전체 논리의16개 입력×7개 출력을 독립 segment mask와 비교한다. + +바깥은 gate order를 선택하고 이전 창 대부분을 고정하며 진행하는 휴리스틱 순차 구성이고, 각 창 내부의 배치·배선과 단계별 최소화는 SAT다. overlap/되돌리기/재시작은 잘못된 국소 선택을 복구하는 수단이다. 각 창의 SAT를 사용한다는 사실이 전체 회로의 전역 최적성이나 탐색 완전성을 뜻하지 않는다. + +완성된 초기 회로는 `constructed/`에 NBT로 저장해 다시 읽고,16개 fresh 입력·모든256개 순서쌍 전이·burnout 부재를 검사한다. 이를 통과한 후에만 compact를 실행하고 `compacted/`를 별도로 저장·검증한다. 압축 실패 시 검증된 초기 결과를 보존하도록 구성했다. + +아래 초기 세 시도와 추가 재시작 시험은 **모두 construct를 완료하지 못했다**. 이 초기 실험들에는 완성 NBT와 압축 결과가 없다. 위의 성공 경로와 구분한다. + +## 측정 + +base seed1, 자동 seed 재시작0회, given frozen signals/early outputs 사용. 각 창의 Unknown은 해당 시간 안에 해를 찾지 못했다는 뜻이며 decoder 전체의 불가능 증명이 아니다. + +| 설정 | 단계별 최소화 | 진행 | 중단 지점 | 시간 | +| --- | --- | --- | --- | ---: | +|width4/height6, MinLive, overlap1, 1 worker, 창당10초|없음|2/28|g99=NOR(g5,g6), 창3~6 Unknown|45.234초| +|width2/height10, SmallestConeFirst, overlap1, 4 workers, 창당15초|5초|6/28|g103=NOR(g6,g11), 창3~6 Unknown|106.655초| +|width2/height24, SmallestConeFirst, overlap2, 4 workers, 창당15초, chain2|5초|1/45|g11=NOT(digit_1), 창3~6 Unknown|64.946초| +|width2/height10, SmallestConeFirst, overlap1, 4 workers, 창당15초, 내장 재시작1회|5초|첫 시도6/28 → 재시작7/28|재시작 후 g103 통과, seg_4에서 시도별120초 예산 소진|232.462초| + +마지막 시험만 `CONSTRUCT_RESTARTS=1 CONSTRUCT_BUDGET_SECONDS=120`을 사용했다. 이 예산은 전체 실행이 아닌 각 구성 시도에 적용된다. 첫 시도(seed1)는 g103의 창3~6에서 Unknown이었다. 두 번째(seed104730)는 g103을 통과했지만 seg_4의 창3/4에서 Unknown, 창5에서 남은13.359초를 사용해 예산이 끝났다. 창6은 시도하지 않았다. 일반 오류 문구의 창6 제한과 실제 예산 중단을 구분해 `restart-analysis.json`에 기록했다. 재시작이 한 국소 정체를 벗어나는 사례지만 전체 구성 성공이나 seed 변경만의 인과 효과를 입증하지 않는다. 4-worker 탐색과 시간 제한 최소화는 같은 seed에서도 완전히 결정적이지 않다. + +두 번째 실행의 성공 단계는 g5/g79/g19/g83/g6/g11이고 마지막 부분 배치가197블록이었다. 이는 전체 decoder 크기가 아니다. 첫 게이트가4블록으로 생성됐으나 첫 실험과 geometry/order/worker도 달라 최소화 옵션만의 A/B 비교는 아니다. + +원래28-NOR 표현은 최대 live 신호가 NetIndex13/SmallestConeFirst14/MinLive14였다. `chain_wide_gates(2)`는 NOR28개를 유지하고 OR17개를 추가해45단계로 바꾸며, SmallestConeFirst의 peak를13, continuing fan-in 최대를2로 낮췄다. 변환된 전체 netlist도16×7 출력을 독립 검증했다. 논리 동등성을 확인한 다른 물리 합성 표현이며, 물리 배치 탐색 공간까지 동일하다는 주장은 아니다. + +출력 계약은 `anywhere-observable`이다. 기존 monolithic 실험의 박스 경계 driving 출력7개와 구분한다. `CONSTRUCT_OUTPUT_MODE=max-y`로 출력7개를 MaxYFace에 둘 수 있으며 같은 계약을 compact에도 전달하지만 이번 측정에서는 사용하지 않았다. + +각 실행의 `attempt.json`, 설정 원문, `step-observations.json`과 solver 로그는 `target/alu-display/whole-decoder-construct-*` 및 임시 로그에 보존한다. simulator refinement는 실패한 창마다0회여서, 발견한 모델의 물리 검증 거절보다 최초 단계 배치 탐색에서 막혔다. + +## 재현 + +```sh +CONSTRUCT_WIDTH=2 CONSTRUCT_HEIGHT=10 CONSTRUCT_ORDER=smallest-cone \ +CONSTRUCT_STEP_OPT_MS=5000 CONSTRUCT_STEP_SECONDS=15 \ +CONSTRUCT_WORKERS=4 CONSTRUCT_BUDGET_SECONDS=180 \ + cargo run --release --example whole_decoder_construct -- \ + target/alu-display/whole-decoder-construct-new + +CONSTRUCT_WIDTH=2 CONSTRUCT_HEIGHT=24 CONSTRUCT_ORDER=smallest-cone \ +CONSTRUCT_OVERLAP=2 CONSTRUCT_MAX_FANIN=2 CONSTRUCT_STEP_OPT_MS=5000 \ +CONSTRUCT_STEP_SECONDS=15 CONSTRUCT_WORKERS=4 CONSTRUCT_BUDGET_SECONDS=360 \ + cargo run --release --example whole_decoder_construct -- \ + target/alu-display/whole-decoder-construct-chain-new +``` + +새 디렉터리를 사용한다. 초기 회로가 검증되면 compact는60초/창당5초의 탐색 예산으로 실행하며 모델 생성 때문에 실제 벽시계 시간은 더 길 수 있다. 기존4,187블록의 회로는 별도의 개별 Exact 후보+Global PnR 결과로 유지한다. diff --git a/docs/whole_decoder_exact.md b/docs/whole_decoder_exact.md new file mode 100644 index 00000000..23e9b98b --- /dev/null +++ b/docs/whole_decoder_exact.md @@ -0,0 +1,51 @@ +# Decoder 전체를 한 번의 Exact 모델로 푸는 실험 + +`examples/whole_decoder_exact.rs`는 기존 최소 cover Verilog와 `blank=0` wrapper에서 시작한다. `LogicalDesign::lower_flat_logic_graph()`가 계층·버스·상수 접기를 기존 bit mapper와 공유하면서 하나의 NOT/OR 그래프를 만든다. 물리 셀 분할, 정형 recipe, Global PnR, window construction은 사용하지 않는다. + +입력4개·출력7개를 동시에 모델링하며, 독립적인 segment mask로16개 입력의7개 출력을 먼저 전수 검증한다. 결과는 NOR28개/net32개/서로 다른 net 함수31개이며, 기존 Exact 모델의 함수와 보수 vocabulary는 zero를 포함해51종이다. 임의의 모든 Boolean 중간함수를 발명하는 무제한 회로 합성이나 전역 최적성 증명을 뜻하지 않는다. + +## 조건 + +- seed1, `optimize=false`, rank/stage24, 블록 수 추가 상한 없음 +- 모든 출력은 박스 바깥을 구동하는 관찰 지점이어야 함 +- 기본 실험은 입력·출력 위치를 자유롭게 선택 +- solver 제한은 encoding 이후부터 적용되므로 인코딩/탐색/전체 wall 시간을 별도로 기록 +- 외부 실행기는30초 solver 실험에90초,60초 실험에120초 wall 상한을 적용 +- 해를 찾으면 NBT 재읽기 후16개 fresh 입력·모든256개 순서쌍 전이·burnout 부재를 독립 검증 + +`UNKNOWN`은 시간 내 해도 불가능 증명도 얻지 못했다는 뜻이다. `INFEASIBLE`과 구분하며, 성공 NBT로 표시하지 않는다. 각 시도는 새 산출물 디렉터리를 사용한다. `attempt.json`은 상태·변수·절·시간·인터페이스 제약을 보존한다. + +## 초기 자유 배치 측정 + +| 박스 | worker | solver 제한 | 변수 | 절 | encoding | 결과 | +| --- | ---: | ---: | ---: | ---: | ---: | --- | +|3×8×4|1|30초|66,907|451,097|196ms|UNKNOWN, wall30.201초| +|4×8×4|1|30초|86,352|597,336|235ms|UNKNOWN, wall30.239초| +|4×10×5|1|30초|139,896|995,256|404ms|UNKNOWN, wall30.466초| +|5×10×5|4|60초|171,354|1,235,624|529ms|UNKNOWN, wall60.647초| + +모두 simulator refinement0회, last rejection 없음이었다. 즉 찾은 배치를 simulator가 거절한 것이 아니라, 제한 시간 내 최초 SAT 배치를 찾지 못했다. 이 결과로 박스가 반드시 작다거나, 전체 decoder의 monolithic 합성이 불가능하다고 판단하지 않는다. + +## 재현 + +```sh +cargo run --release --example whole_decoder_exact -- \ + target/alu-display/whole-decoder-exact-new 3x8x4 30 + +WHOLE_WORKERS=4 cargo run --release --example whole_decoder_exact -- \ + target/alu-display/whole-decoder-exact-portfolio-new 5x10x5 60 + +WHOLE_WORKERS=4 WHOLE_PIN_CONTRACT=faces \ + cargo run --release --example whole_decoder_exact -- \ + target/alu-display/whole-decoder-exact-faces-new 5x10x5 60 +``` + +`faces`는 입력4개를 `(1,0,1),(3,0,1),(1,0,3),(3,0,3)`에 North 방향 부착으로 고정하고, 출력7개는 모두 y-max 면에서 위치를 고른다. 내부 전체는 여전히 한 SAT 모델이다. 자유 배치보다 추가적인 인터페이스 제약이므로 서로 다른 문제로 명시한다. + +최종 `5×10×5`/4-worker/60초 `faces` 실험도 UNKNOWN이었다. 변수122,273개·절894,912개, encoding425ms·solve60.028초·wall60.457초, refinement0회/last rejection 없음이다. 자유 핀보다 변수·절은 줄었지만 최초 witness를 얻지는 못했다. 다섯 실험 모두 성공 NBT가 없다. + +## 더 큰 박스 + +추가 요청으로 자유 핀 박스를 `8×12×6`(576칸)으로 확대하고4-worker/SAT120초/외부wall180초로 한 번 더 실행했다. seed·논리·출력 구동·rank/stage 조건은 유지했다. 결과는 변수390,656개·절2,925,936개, encoding1.279초·solve120.621초·wall121.918초의 UNKNOWN이었다. refinement0회/last rejection 없음이며 새 NBT는 없다. 공간이 부족하다는 증명도, 더 큰 박스에 해가 없다는 증명도 아니다. 현재까지 총6회 모두 최초 SAT witness를 얻지 못했다. + +기존4,187블록 결과는 개별 Exact 후보와 Global PnR로 만든 별도 체크포인트이며, 이 전체 모델에서 찾은 결과가 아니다. diff --git a/examples/add_sub_nbt.rs b/examples/add_sub_nbt.rs new file mode 100644 index 00000000..8b27f227 --- /dev/null +++ b/examples/add_sub_nbt.rs @@ -0,0 +1,503 @@ +//! Bounded Verilog -> NBT arithmetic experiment, independent integer oracle. +//! cargo run --release --example add_sub_nbt -- 2 +//! Widths 1/2/4 are exhaustive statically; width 8 is explicitly sampled. +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use std::time::Instant; + +use eyre::{ensure, ContextCompat, WrapErr}; +use redstone_compiler::ir::LogicalDesign; +use redstone_compiler::nbt::{NBTRoot, ToNBT}; +use redstone_compiler::output::{OutputEndpoint, PlacedWorld}; +use redstone_compiler::transform::place_and_route::global_pnr::candidate::UnitCandidateConfig; +use redstone_compiler::transform::place_and_route::global_pnr::policy::GlobalPnrPreset; +use redstone_compiler::transform::place_and_route::global_pnr::router::{ + GlobalRoutingStrategy, NetOrderStrategy, +}; +use redstone_compiler::transform::place_and_route::global_pnr::{ + place_and_route_routable_design_with_visualization, GlobalPnrConfig, +}; +use redstone_compiler::transform::place_and_route::local_placer::{ + LocalPlacerConfig, PlacementSchedulePolicy, +}; +use redstone_compiler::transform::place_and_route::sampling::SamplingPolicy; +use redstone_compiler::world::position::DimSize; +use redstone_compiler::world::simulator::Simulator; +use redstone_compiler::world::{World, World3D}; +use tracing_subscriber::prelude::*; + +const MAX_CYCLES: usize = 4096; +const MAX_EVENTS: usize = 2_000_000; +const SEED: u64 = 0xadd8_2026_1108_0001; + +fn bit_index(name: &str, bus: &str, width: usize) -> Option { + if width == 1 && name == bus { + return Some(0); + } + name.strip_prefix(bus)? + .trim_matches(|c| c == '_' || c == '[' || c == ']') + .parse() + .ok() +} + +fn directed_cases(width: usize) -> Vec<(usize, usize)> { + let mask = (1 << width) - 1; + let mut cases = vec![ + (0, 0), + (mask, 0), + (0, mask), + (mask, 1), + (1, mask), + (mask, mask), + (mask >> 1, 1), + (0x55 & mask, 0xaa & mask), + (0xaa & mask, 0x55 & mask), + ]; + for bit in 1..=width { + let carry = (1 << bit) - 1; + cases.extend([(carry, 1), (1, carry), (carry, mask ^ carry)]); + } + cases +} + +fn operand_cases(width: usize) -> Vec<(usize, usize)> { + if width <= 4 { + return (0..1 << width) + .flat_map(|a| (0..1 << width).map(move |b| (a, b))) + .collect(); + } + let mut cases = directed_cases(width); + let mut state = SEED; + for _ in 0..256 { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + cases.push(((state & 255) as usize, ((state >> 8) & 255) as usize)); + } + cases +} + +fn static_cases(width: usize) -> Vec<(usize, usize, bool)> { + operand_cases(width) + .into_iter() + .flat_map(|(a, b)| [(a, b, false), (a, b, true)]) + .collect() +} + +fn validate_interface(placed: &PlacedWorld, width: usize) -> eyre::Result<()> { + ensure!( + placed.inputs.len() == 2 * width + 1 && placed.outputs.len() == width + 1, + "add/sub interface width mismatch" + ); + let mut inputs = HashSet::new(); + for input in &placed.inputs { + let endpoint = if input.name == "subtract" { + ("subtract", 0) + } else { + ["a", "b"] + .into_iter() + .find_map(|bus| bit_index(&input.name, bus, width).map(|bit| (bus, bit))) + .context("unknown operand input")? + }; + ensure!( + (endpoint.0 == "subtract" || endpoint.1 < width) && inputs.insert(endpoint), + "duplicate or invalid input" + ); + } + let mut outputs = HashSet::new(); + for output in &placed.outputs { + let bit = if output.name == "c" { + width + } else { + bit_index(&output.name, "y", width).context("unknown result output")? + }; + ensure!( + bit <= width && outputs.insert(bit), + "duplicate or invalid output" + ); + } + ensure!( + placed.outputs.iter().any(|output| output.name == "c"), + "missing carry/no-borrow output" + ); + Ok(()) +} + +fn drive_and_check( + sim: &mut Simulator, + placed: &PlacedWorld, + width: usize, + a: usize, + b: usize, + subtract: bool, +) -> eyre::Result<()> { + let inputs = placed + .inputs + .iter() + .map(|input| { + let value = if input.name == "subtract" { + subtract + } else if let Some(bit) = bit_index(&input.name, "a", width) { + a & (1 << bit) != 0 + } else { + let bit = bit_index(&input.name, "b", width).context("unknown operand input")?; + b & (1 << bit) != 0 + }; + Ok((input.position(), value)) + }) + .collect::>>()?; + sim.drive_inputs_with_limits(inputs, MAX_CYCLES, MAX_EVENTS)?; + let mask = (1usize << width) - 1; + let expected = if subtract { + a.wrapping_sub(b) & mask + } else { + (a + b) & mask + }; + let carry = if subtract { a >= b } else { a + b > mask }; + for output in &placed.outputs { + let expected_bit = if output.name == "c" { + carry + } else { + let bit = bit_index(&output.name, "y", width).context("unknown result output")?; + expected & (1 << bit) != 0 + }; + ensure!(sim.world()[output.position()].kind.is_powered()==expected_bit,"add/sub {width}-bit mismatch: a={a}, b={b}, subtract={subtract}, result={expected}, carry/no-borrow={carry}, output={}",output.name); + } + let burned = sim + .world() + .iter_block() + .iter() + .filter_map(|(pos, _)| sim.is_torch_burned_out(*pos).then_some(*pos)) + .collect::>(); + ensure!(burned.is_empty(), "add/sub torch burnout at {burned:?}"); + Ok(()) +} + +fn verify_static_world( + world: &World, + placed: &PlacedWorld, + width: usize, + cases: &[(usize, usize, bool)], +) -> eyre::Result<()> { + for &(a, b, subtract) in cases { + let mut sim = + Simulator::from_settled_with_limits_and_trace(world, MAX_CYCLES, MAX_EVENTS, 0)?; + drive_and_check(&mut sim, placed, width, a, b, subtract) + .wrap_err_with(|| format!("static case a={a}, b={b}, subtract={subtract}"))?; + } + Ok(()) +} + +fn candidate_verifier(placed: &PlacedWorld) -> eyre::Result<()> { + let width = (placed.inputs.len() - 1) / 2; + ensure!( + [1, 2, 4, 8].contains(&width), + "unsupported add/sub interface" + ); + validate_interface(placed, width)?; + // Reject bad assembled layouts inside PnR so it can try another layout. + // NBT roundtrip and sequential-transition checks remain independent. + let world = World::from(&placed.world); + let cases = static_cases(width); + verify_static_world(&world, placed, width, &cases)?; + let mut sim = Simulator::from_settled_with_limits_and_trace(&world, MAX_CYCLES, MAX_EVENTS, 0)?; + if width <= 2 { + for &(a, b, subtract) in &cases { + for &(next_a, next_b, next_subtract) in &cases { + sim.advance_idle_cycles(61)?; + drive_and_check(&mut sim, placed, width, a, b, subtract)?; + sim.advance_idle_cycles(61)?; + drive_and_check(&mut sim,placed,width,next_a,next_b,next_subtract).wrap_err_with(||format!("assembled ordered transition ({a},{b},{subtract})->({next_a},{next_b},{next_subtract})"))?; + } + } + } else { + for (index, &(a, b, subtract)) in cases.iter().enumerate() { + sim.advance_idle_cycles(61)?; + drive_and_check(&mut sim, placed, width, a, b, subtract) + .wrap_err_with(|| format!("assembled continuous walk case {}", index + 1))?; + } + } + Ok(()) +} + +fn run(width: usize, dir: &Path) -> eyre::Result<()> { + let started = Instant::now(); + std::fs::write( + dir.join("status.json"), + serde_json::to_vec_pretty(&serde_json::json!({"stage":"compile","bits":width}))?, + )?; + let source = std::fs::read_to_string( + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("test/alu_display/add_sub{width}.v")), + )?; + std::fs::write(dir.join("add_sub.v"), &source)?; + let logical = LogicalDesign::from_verilog_source_named(&source, "add_sub.v")?; + std::fs::write(dir.join("logical.rcir"), logical.to_string())?; + let routable = logical.lower_to_routable_with_associative_cells(8)?; + std::fs::write(dir.join("routable.rcir"), routable.to_string())?; + let dim_text = std::env::var("ADD_SUB_DIM").unwrap_or("10x10x5".into()); + let dims = dim_text + .split('x') + .map(str::parse::) + .collect::, _>>()?; + ensure!( + dims.len() == 3 && dims.iter().all(|dim| *dim > 0), + "ADD_SUB_DIM must be positive XxYxZ" + ); + let beam = std::env::var("ADD_SUB_BEAM") + .unwrap_or("256".into()) + .parse::()?; + ensure!(beam > 0, "ADD_SUB_BEAM must be positive"); + let cache_dir = std::env::var_os("ADD_SUB_CACHE_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| dir.join("candidate-cache")); + let cache_entries_before = std::fs::read_dir(&cache_dir) + .map(|entries| { + entries + .filter_map(Result::ok) + .filter(|entry| entry.path().join("index.json").is_file()) + .count() + }) + .unwrap_or(0); + let mut config = GlobalPnrConfig { + candidate: UnitCandidateConfig { + verified_nary_cells: true, + reserve_combinational_io_diodes: true, + dim: DimSize(dims[0], dims[1], dims[2]), + max_candidates: 4, + local_config: LocalPlacerConfig { + greedy_input_generation: true, + input_candidate_limit: Some(16), + schedule: PlacementSchedulePolicy::Auto, + placement_sampling_policy: LocalPlacerConfig::ranked_sampling(beam, 0, 0), + max_route_step: 4, + route_step_sampling_policy: SamplingPolicy::Random(32), + not_route_step_sampling_policy: SamplingPolicy::Random(8), + ..Default::default() + }, + ..Default::default() + } + .into(), + candidate_cache_dir: Some(cache_dir.clone()), + ..Default::default() + }; + config.search = GlobalPnrPreset::Fast.search_config(); + config.search.budget.max_layout_combinations = 1; + config.search.policies.net_order_strategies = vec![ + NetOrderStrategy::Criticality, + NetOrderStrategy::HighestFanoutFirst, + NetOrderStrategy::ReverseCriticality, + ]; + config.placement.max_attempts = 4; + config.placement.spacing = 4; + config.routing.strategy = GlobalRoutingStrategy::AStarBudgeted { + max_expansions: 20_000, + }; + config.verifier = Some(candidate_verifier); + std::fs::write( + dir.join("experiment.json"), + serde_json::to_vec_pretty( + &serde_json::json!({"cache_dir":cache_dir,"cache_entries_before":cache_entries_before,"candidate_dim":dims,"beam":beam,"bits":width,"operation":"shared_add_sub_with_no_borrow","candidate_verifier":"static_and_exhaustive_ordered_up_to_2bit_else_bounded_walk","mapping":"associative_cells_8","verified_nary_cells":true,"reserve_io_diodes":true,"astar_expansions":20000,"placement_attempts":4,"layout_combinations":1,"preset":"fast","sim_max_cycles":MAX_CYCLES,"sim_max_events":MAX_EVENTS,"seed":SEED,"random_operand_pairs":if width==8 {256}else{0},"random_cases":if width==8 {512}else{0}}), + )?, + )?; + let result = place_and_route_routable_design_with_visualization(&routable, &config)?; + let compile_ms = started.elapsed().as_millis(); + let placed = &result.placed_world; + validate_interface(placed, width)?; + placed.world.to_nbt().save(dir.join("add_sub.nbt")); + placed.metadata().save(dir.join("add_sub.outputs.json"))?; + std::fs::write( + dir.join("interface.json"), + serde_json::to_vec_pretty( + &serde_json::json!({"inputs":placed.inputs,"outputs":placed.outputs}), + )?, + )?; + verify_saved(width, dir, Some(compile_ms), false, None, started) +} + +fn fingerprint(bytes: &[u8]) -> String { + let value = bytes.iter().fold(0xcbf29ce484222325u64, |hash, byte| { + (hash ^ u64::from(*byte)).wrapping_mul(0x100000001b3) + }); + format!("fnv1a64:{value:016x}") +} + +#[derive(serde::Deserialize)] +struct Interface { + inputs: Vec, + outputs: Vec, +} + +fn archive_evidence(dir: &Path) -> eyre::Result> { + let names = ["verification.json", "failure.json", "status.json"]; + if !names.iter().any(|name| dir.join(name).is_file()) { + return Ok(None); + } + let stamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_nanos(); + let relative = format!("diagnostics/prior-{stamp}"); + let archive = dir.join(&relative); + std::fs::create_dir_all(&archive)?; + for name in names { + let path = dir.join(name); + if path.is_file() { + std::fs::copy(&path, archive.join(name))?; + } + } + Ok(Some(relative)) +} + +fn verify_saved( + width: usize, + dir: &Path, + compile_ms: Option, + recheck: bool, + prior_evidence: Option, + started: Instant, +) -> eyre::Result<()> { + let verification_started = Instant::now(); + let nbt_bytes = std::fs::read(dir.join("add_sub.nbt"))?; + let interface_bytes = std::fs::read(dir.join("interface.json"))?; + let world = NBTRoot::from_nbt_bytes(&nbt_bytes)?.to_world(); + let interface: Interface = serde_json::from_slice(&interface_bytes)?; + let placed = PlacedWorld { + world: World3D::from(&world), + inputs: interface.inputs, + outputs: interface.outputs, + }; + validate_interface(&placed, width)?; + std::fs::write( + dir.join("status.json"), + serde_json::to_vec_pretty( + &serde_json::json!({"stage":"verify_nbt","bits":width,"operation":"shared_add_sub_with_no_borrow","compile_ms":compile_ms}), + )?, + )?; + let cases = static_cases(width); + verify_static_world(&world, &placed, width, &cases)?; + let mut sim = Simulator::from_settled_with_limits_and_trace(&world, MAX_CYCLES, MAX_EVENTS, 0)?; + std::fs::write( + dir.join("status.json"), + serde_json::to_vec_pretty( + &serde_json::json!({"stage":"verify_transitions","bits":width,"static_cases_passed":cases.len(),"compile_ms":compile_ms}), + )?, + )?; + let mut transitions = 0; + if width <= 2 { + for &(a, b, subtract) in &cases { + for &(next_a, next_b, next_subtract) in &cases { + sim.advance_idle_cycles(61)?; + drive_and_check(&mut sim, &placed, width, a, b, subtract)?; + sim.advance_idle_cycles(61)?; + drive_and_check(&mut sim, &placed, width, next_a, next_b, next_subtract).wrap_err_with(|| { + format!("ordered transition ({a},{b},{subtract})->({next_a},{next_b},{next_subtract})") + })?; + transitions += 1; + } + } + } else { + for &(a, b, subtract) in &cases { + sim.advance_idle_cycles(61)?; + drive_and_check(&mut sim, &placed, width, a, b, subtract).wrap_err_with(|| { + format!( + "continuous walk case {}: a={a}, b={b}, subtract={subtract}", + transitions + 1 + ) + })?; + transitions += 1; + } + } + ensure!( + std::fs::read(dir.join("add_sub.nbt"))? == nbt_bytes, + "NBT changed during verification" + ); + let summary = serde_json::json!({"mode":if recheck {"verify-only"}else{"compile-and-verify"},"verification_after_simfix":recheck,"simulator_normalization":"fixpoint-on-off-notification","prior_evidence":prior_evidence,"nbt_fingerprint":fingerprint(&nbt_bytes),"interface_fingerprint":fingerprint(&interface_bytes),"nbt_unchanged":true,"verification_ms":verification_started.elapsed().as_millis(),"bits":width,"operation":"shared_add_sub_with_no_borrow","compile_ms":compile_ms,"total_ms":started.elapsed().as_millis(),"blocks":world.blocks.len(),"nbt_roundtrip":true,"static_cases":cases.len(),"static_exhaustive":width<=4,"ordered_transitions_exhaustive":width<=2,"ordered_transition_pairs":if width<=2 {transitions}else{0},"continuous_walk_cases":if width>2 {transitions}else{0},"random_seed":if width==8 {Some(SEED)}else{None},"random_operand_pairs":if width==8 {256}else{0},"random_cases":if width==8 {512}else{0},"eight_bit_scope":"directed carry/borrow boundaries plus 256 seeded random operand pairs, both operations; not exhaustive proof"}); + std::fs::write( + dir.join("verification.json"), + serde_json::to_vec_pretty(&summary)?, + )?; + std::fs::write(dir.join("status.json"), b"{\"stage\":\"verified\"}")?; + println!("{}", serde_json::to_string_pretty(&summary)?); + Ok(()) +} + +fn main() -> eyre::Result<()> { + let debug = std::env::var_os("PNR_DEBUG").is_some(); + let subscriber = tracing_subscriber::fmt() + .with_ansi(false) + .with_max_level(tracing::Level::DEBUG) + .finish() + .with(tracing_subscriber::filter::filter_fn(move |meta| { + *meta.level() <= tracing::Level::INFO + || (debug && meta.target() == "redstone_compiler::global_pnr") + })); + tracing::subscriber::set_global_default(subscriber).ok(); + let mut args = std::env::args().skip(1); + let first = args.next().unwrap_or("2".into()); + let recheck = first == "--verify-only"; + let width = if recheck { + args.next().context("--verify-only requires width")? + } else { + first + } + .parse::()?; + ensure!( + [1, 2, 4, 8].contains(&width), + "expected width 1, 2, 4, or 8" + ); + let dir = args + .next() + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(format!("target/alu-display/add-sub-{width}"))); + ensure!(args.next().is_none(), "unexpected extra argument"); + if recheck { + ensure!( + dir.join("add_sub.nbt").is_file() && dir.join("interface.json").is_file(), + "verify-only requires saved add_sub.nbt and interface.json" + ); + } else { + std::fs::create_dir_all(&dir)?; + } + let compile_ms = ["verification.json", "status.json"] + .into_iter() + .find_map(|name| { + std::fs::read(dir.join(name)) + .ok() + .and_then(|data| serde_json::from_slice::(&data).ok()) + .and_then(|value| { + value + .get("compile_ms") + .and_then(|value| value.as_u64()) + .map(u128::from) + }) + }); + let prior_evidence = archive_evidence(&dir)?; + for file in ["verification.json", "failure.json"] { + let path = dir.join(file); + if path.exists() { + std::fs::remove_file(path)?; + } + } + let started = Instant::now(); + let result = if recheck { + verify_saved( + width, + &dir, + compile_ms, + true, + prior_evidence.clone(), + started, + ) + } else { + run(width, &dir) + }; + if let Err(error) = result { + std::fs::write( + dir.join("failure.json"), + serde_json::to_vec_pretty( + &serde_json::json!({"bits":width,"mode":if recheck {"verify-only"}else{"compile-and-verify"},"prior_evidence":prior_evidence,"elapsed_ms":started.elapsed().as_millis(),"error":format!("{error:?}")}), + )?, + )?; + return Err(error); + } + Ok(()) +} diff --git a/examples/adder_nbt.rs b/examples/adder_nbt.rs new file mode 100644 index 00000000..48dde382 --- /dev/null +++ b/examples/adder_nbt.rs @@ -0,0 +1,456 @@ +//! Bounded Verilog -> NBT arithmetic experiment, independent integer oracle. +//! cargo run --release --example adder_nbt -- 2 +//! Widths 1/2/4 are exhaustive statically; width 8 is explicitly sampled. +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use std::time::Instant; + +use eyre::{ensure, ContextCompat, WrapErr}; +use redstone_compiler::ir::LogicalDesign; +use redstone_compiler::nbt::{NBTRoot, ToNBT}; +use redstone_compiler::output::{OutputEndpoint, PlacedWorld}; +use redstone_compiler::transform::place_and_route::global_pnr::candidate::UnitCandidateConfig; +use redstone_compiler::transform::place_and_route::global_pnr::policy::GlobalPnrPreset; +use redstone_compiler::transform::place_and_route::global_pnr::router::{ + GlobalRoutingStrategy, NetOrderStrategy, +}; +use redstone_compiler::transform::place_and_route::global_pnr::{ + place_and_route_routable_design_with_visualization, GlobalPnrConfig, +}; +use redstone_compiler::transform::place_and_route::local_placer::{ + LocalPlacerConfig, PlacementSchedulePolicy, +}; +use redstone_compiler::transform::place_and_route::sampling::SamplingPolicy; +use redstone_compiler::world::position::DimSize; +use redstone_compiler::world::simulator::Simulator; +use redstone_compiler::world::{World, World3D}; +use tracing_subscriber::prelude::*; + +const MAX_CYCLES: usize = 4096; +const MAX_EVENTS: usize = 2_000_000; +const SEED: u64 = 0xadd8_2026_1108_0001; + +fn bit_index(name: &str, bus: &str, width: usize) -> Option { + if width == 1 && name == bus { + return Some(0); + } + name.strip_prefix(bus)? + .trim_matches(|c| c == '_' || c == '[' || c == ']') + .parse() + .ok() +} + +fn directed_cases(width: usize) -> Vec<(usize, usize)> { + let mask = (1 << width) - 1; + let mut cases = vec![ + (0, 0), + (mask, 0), + (0, mask), + (mask, 1), + (1, mask), + (mask, mask), + (mask >> 1, 1), + (0x55 & mask, 0xaa & mask), + (0xaa & mask, 0x55 & mask), + ]; + for bit in 1..=width { + let carry = (1 << bit) - 1; + cases.extend([(carry, 1), (1, carry), (carry, mask ^ carry)]); + } + cases +} + +pub(crate) fn static_cases(width: usize) -> Vec<(usize, usize)> { + if width <= 4 { + return (0..1 << width) + .flat_map(|a| (0..1 << width).map(move |b| (a, b))) + .collect(); + } + let mut cases = directed_cases(width); + let mut state = SEED; + for _ in 0..256 { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + cases.push(((state & 255) as usize, ((state >> 8) & 255) as usize)); + } + cases +} + +fn validate_interface(placed: &PlacedWorld, width: usize) -> eyre::Result<()> { + ensure!( + placed.inputs.len() == 2 * width && placed.outputs.len() == width + 1, + "adder interface width mismatch" + ); + let mut inputs = HashSet::new(); + for input in &placed.inputs { + let endpoint = ["a", "b"] + .into_iter() + .find_map(|bus| bit_index(&input.name, bus, width).map(|bit| (bus, bit))) + .context("unrecognized adder input")?; + ensure!( + endpoint.1 < width && inputs.insert(endpoint), + "duplicate or out-of-range adder input {}", + input.name + ); + } + let mut outputs = HashSet::new(); + for output in &placed.outputs { + let bit = bit_index(&output.name, "sum", width + 1).context("unrecognized sum output")?; + ensure!( + bit <= width && outputs.insert(bit), + "duplicate or out-of-range sum output {}", + output.name + ); + } + Ok(()) +} + +fn drive_and_check( + sim: &mut Simulator, + placed: &PlacedWorld, + width: usize, + a: usize, + b: usize, +) -> eyre::Result<()> { + let inputs = placed + .inputs + .iter() + .map(|input| { + if let Some(bit) = bit_index(&input.name, "a", width) { + Ok((input.position(), a & (1 << bit) != 0)) + } else { + let bit = bit_index(&input.name, "b", width).context("unknown operand input")?; + Ok((input.position(), b & (1 << bit) != 0)) + } + }) + .collect::>>()?; + sim.drive_inputs_with_limits(inputs, MAX_CYCLES, MAX_EVENTS)?; + // The reference is integer arithmetic, independent of graph lowering. + let expected = a + b; + for output in &placed.outputs { + let bit = bit_index(&output.name, "sum", width + 1).context("unknown sum output")?; + ensure!( + sim.world()[output.position()].kind.is_powered() == (expected & (1 << bit) != 0), + "adder {width}-bit mismatch: {a}+{b}={expected}, output={}", + output.name + ); + } + ensure!( + !sim.world() + .iter_block() + .iter() + .any(|(pos, _)| sim.is_torch_burned_out(*pos)), + "adder {width}-bit torch burnout after {a}+{b}" + ); + Ok(()) +} + +fn verify_static_world( + world: &World, + placed: &PlacedWorld, + width: usize, + cases: &[(usize, usize)], +) -> eyre::Result<()> { + for &(a, b) in cases { + let mut sim = + Simulator::from_settled_with_limits_and_trace(world, MAX_CYCLES, MAX_EVENTS, 0)?; + drive_and_check(&mut sim, placed, width, a, b) + .wrap_err_with(|| format!("static case {a}+{b}"))?; + } + Ok(()) +} + +fn candidate_verifier(placed: &PlacedWorld) -> eyre::Result<()> { + let width = placed.inputs.len() / 2; + ensure!([1, 2, 4, 8].contains(&width), "unsupported adder interface"); + validate_interface(placed, width)?; + // Reject bad assembled layouts inside PnR so it can try another layout. + // NBT roundtrip and sequential-transition checks remain independent. + let world = World::from(&placed.world); + let cases = static_cases(width); + verify_static_world(&world, placed, width, &cases)?; + let mut sim = Simulator::from_settled_with_limits_and_trace(&world, MAX_CYCLES, MAX_EVENTS, 0)?; + for (index, &(a, b)) in cases.iter().enumerate() { + sim.advance_idle_cycles(61)?; + drive_and_check(&mut sim, placed, width, a, b) + .wrap_err_with(|| format!("assembled continuous walk case {}: {a}+{b}", index + 1))?; + } + Ok(()) +} + +fn run(width: usize, dir: &Path) -> eyre::Result<()> { + let started = Instant::now(); + std::fs::write( + dir.join("status.json"), + serde_json::to_vec_pretty(&serde_json::json!({"stage":"compile","bits":width}))?, + )?; + let source=format!("module adder(a,b,sum);\n input [{}:0] a,b;\n output [{width}:0] sum;\n assign sum={{1'b0,a}}+{{1'b0,b}};\nendmodule\n",width-1); + std::fs::write(dir.join("adder.v"), &source)?; + let logical = LogicalDesign::from_verilog_source_named(&source, "adder.v")?; + std::fs::write(dir.join("logical.rcir"), logical.to_string())?; + let routable = logical.lower_to_routable_with_associative_cells(8)?; + std::fs::write(dir.join("routable.rcir"), routable.to_string())?; + let dim_text = std::env::var("ADDER_DIM").unwrap_or("8x8x4".into()); + let dims = dim_text + .split('x') + .map(str::parse::) + .collect::, _>>()?; + ensure!( + dims.len() == 3 && dims.iter().all(|dim| *dim > 0), + "ADDER_DIM must be positive XxYxZ" + ); + let beam = std::env::var("ADDER_BEAM") + .unwrap_or("64".into()) + .parse::()?; + ensure!(beam > 0, "ADDER_BEAM must be positive"); + let cache_dir = std::env::var_os("ADDER_CACHE_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| dir.join("candidate-cache")); + let cache_entries_before = std::fs::read_dir(&cache_dir) + .map(|entries| { + entries + .filter_map(Result::ok) + .filter(|entry| entry.path().join("index.json").is_file()) + .count() + }) + .unwrap_or(0); + let mut config = GlobalPnrConfig { + candidate: UnitCandidateConfig { + verified_nary_cells: true, + reserve_combinational_io_diodes: true, + dim: DimSize(dims[0], dims[1], dims[2]), + max_candidates: 4, + local_config: LocalPlacerConfig { + greedy_input_generation: true, + input_candidate_limit: Some(16), + schedule: PlacementSchedulePolicy::Auto, + placement_sampling_policy: LocalPlacerConfig::ranked_sampling(beam, 0, 0), + max_route_step: 4, + route_step_sampling_policy: SamplingPolicy::Random(32), + not_route_step_sampling_policy: SamplingPolicy::Random(8), + ..Default::default() + }, + ..Default::default() + } + .into(), + candidate_cache_dir: Some(cache_dir.clone()), + ..Default::default() + }; + config.search = GlobalPnrPreset::Fast.search_config(); + config.search.budget.max_layout_combinations = 1; + config.search.policies.net_order_strategies = vec![ + NetOrderStrategy::Criticality, + NetOrderStrategy::HighestFanoutFirst, + NetOrderStrategy::ReverseCriticality, + ]; + config.placement.max_attempts = 4; + config.placement.spacing = 4; + config.routing.strategy = GlobalRoutingStrategy::AStarBudgeted { + max_expansions: 20_000, + }; + config.verifier = Some(candidate_verifier); + std::fs::write( + dir.join("experiment.json"), + serde_json::to_vec_pretty( + &serde_json::json!({"cache_dir":cache_dir,"cache_entries_before":cache_entries_before,"candidate_dim":dims,"beam":beam,"bits":width,"candidate_verifier":"static_cases_and_continuous_walk","mapping":"associative_cells_8","verified_nary_cells":true,"reserve_io_diodes":true,"astar_expansions":20000,"placement_attempts":4,"layout_combinations":1,"preset":"fast","sim_max_cycles":MAX_CYCLES,"sim_max_events":MAX_EVENTS,"seed":SEED,"random_cases":if width==8 {256}else{0}}), + )?, + )?; + let result = place_and_route_routable_design_with_visualization(&routable, &config)?; + let compile_ms = started.elapsed().as_millis(); + let placed = &result.placed_world; + validate_interface(placed, width)?; + placed.world.to_nbt().save(dir.join("adder.nbt")); + placed.metadata().save(dir.join("adder.outputs.json"))?; + std::fs::write( + dir.join("interface.json"), + serde_json::to_vec_pretty( + &serde_json::json!({"inputs":placed.inputs,"outputs":placed.outputs}), + )?, + )?; + verify_saved(width, dir, Some(compile_ms), false, None, started) +} + +fn fingerprint(bytes: &[u8]) -> String { + let value = bytes.iter().fold(0xcbf29ce484222325u64, |hash, byte| { + (hash ^ u64::from(*byte)).wrapping_mul(0x100000001b3) + }); + format!("fnv1a64:{value:016x}") +} + +#[derive(serde::Deserialize)] +struct Interface { + inputs: Vec, + outputs: Vec, +} + +fn archive_evidence(dir: &Path) -> eyre::Result> { + let names = ["verification.json", "failure.json", "status.json"]; + if !names.iter().any(|name| dir.join(name).is_file()) { + return Ok(None); + } + let stamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_nanos(); + let relative = format!("diagnostics/prior-{stamp}"); + let archive = dir.join(&relative); + std::fs::create_dir_all(&archive)?; + for name in names { + let path = dir.join(name); + if path.is_file() { + std::fs::copy(&path, archive.join(name))?; + } + } + Ok(Some(relative)) +} + +fn verify_saved( + width: usize, + dir: &Path, + compile_ms: Option, + recheck: bool, + prior_evidence: Option, + started: Instant, +) -> eyre::Result<()> { + let verification_started = Instant::now(); + let nbt_bytes = std::fs::read(dir.join("adder.nbt"))?; + let interface_bytes = std::fs::read(dir.join("interface.json"))?; + let world = NBTRoot::from_nbt_bytes(&nbt_bytes)?.to_world(); + let interface: Interface = serde_json::from_slice(&interface_bytes)?; + let placed = PlacedWorld { + world: World3D::from(&world), + inputs: interface.inputs, + outputs: interface.outputs, + }; + validate_interface(&placed, width)?; + std::fs::write( + dir.join("status.json"), + serde_json::to_vec_pretty( + &serde_json::json!({"stage":"verify_nbt","bits":width,"compile_ms":compile_ms}), + )?, + )?; + let cases = static_cases(width); + verify_static_world(&world, &placed, width, &cases)?; + let mut sim = Simulator::from_settled_with_limits_and_trace(&world, MAX_CYCLES, MAX_EVENTS, 0)?; + std::fs::write( + dir.join("status.json"), + serde_json::to_vec_pretty( + &serde_json::json!({"stage":"verify_transitions","bits":width,"static_cases_passed":cases.len(),"compile_ms":compile_ms}), + )?, + )?; + let mut transitions = 0; + if width <= 2 { + for &(a, b) in &cases { + for &(next_a, next_b) in &cases { + sim.advance_idle_cycles(61)?; + drive_and_check(&mut sim, &placed, width, a, b)?; + sim.advance_idle_cycles(61)?; + drive_and_check(&mut sim, &placed, width, next_a, next_b).wrap_err_with(|| { + format!("ordered transition ({a},{b})->({next_a},{next_b})") + })?; + transitions += 1; + } + } + } else { + for &(a, b) in &cases { + sim.advance_idle_cycles(61)?; + drive_and_check(&mut sim, &placed, width, a, b) + .wrap_err_with(|| format!("continuous walk case {}: {a}+{b}", transitions + 1))?; + transitions += 1; + } + } + ensure!( + std::fs::read(dir.join("adder.nbt"))? == nbt_bytes, + "NBT changed during verification" + ); + let summary = serde_json::json!({"mode":if recheck {"verify-only"}else{"compile-and-verify"},"verification_after_simfix":recheck,"simulator_normalization":"fixpoint-on-off-notification","prior_evidence":prior_evidence,"nbt_fingerprint":fingerprint(&nbt_bytes),"interface_fingerprint":fingerprint(&interface_bytes),"nbt_unchanged":true,"verification_ms":verification_started.elapsed().as_millis(),"bits":width,"compile_ms":compile_ms,"total_ms":started.elapsed().as_millis(),"blocks":world.blocks.len(),"nbt_roundtrip":true,"static_cases":cases.len(),"static_exhaustive":width<=4,"ordered_transitions_exhaustive":width<=2,"ordered_transition_pairs":if width<=2 {transitions}else{0},"continuous_walk_cases":if width>2 {transitions}else{0},"random_seed":if width==8 {Some(SEED)}else{None},"random_cases":if width==8 {256}else{0},"eight_bit_scope":"directed carry/boundary plus 256 seeded random cases; not exhaustive proof"}); + std::fs::write( + dir.join("verification.json"), + serde_json::to_vec_pretty(&summary)?, + )?; + std::fs::write(dir.join("status.json"), b"{\"stage\":\"verified\"}")?; + println!("{}", serde_json::to_string_pretty(&summary)?); + Ok(()) +} + +fn main() -> eyre::Result<()> { + let debug = std::env::var_os("PNR_DEBUG").is_some(); + let subscriber = tracing_subscriber::fmt() + .with_ansi(false) + .with_max_level(tracing::Level::DEBUG) + .finish() + .with(tracing_subscriber::filter::filter_fn(move |meta| { + *meta.level() <= tracing::Level::INFO + || (debug && meta.target() == "redstone_compiler::global_pnr") + })); + tracing::subscriber::set_global_default(subscriber).ok(); + let mut args = std::env::args().skip(1); + let first = args.next().unwrap_or("2".into()); + let recheck = first == "--verify-only"; + let width = if recheck { + args.next().context("--verify-only requires width")? + } else { + first + } + .parse::()?; + ensure!( + [1, 2, 4, 8].contains(&width), + "expected width 1, 2, 4, or 8" + ); + let dir = args + .next() + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(format!("target/alu-display/adder-{width}"))); + ensure!(args.next().is_none(), "unexpected extra argument"); + if recheck { + ensure!( + dir.join("adder.nbt").is_file() && dir.join("interface.json").is_file(), + "verify-only requires saved adder.nbt and interface.json" + ); + } else { + std::fs::create_dir_all(&dir)?; + } + let compile_ms = ["verification.json", "status.json"] + .into_iter() + .find_map(|name| { + std::fs::read(dir.join(name)) + .ok() + .and_then(|data| serde_json::from_slice::(&data).ok()) + .and_then(|value| { + value + .get("compile_ms") + .and_then(|value| value.as_u64()) + .map(u128::from) + }) + }); + let prior_evidence = archive_evidence(&dir)?; + for file in ["verification.json", "failure.json"] { + let path = dir.join(file); + if path.exists() { + std::fs::remove_file(path)?; + } + } + let started = Instant::now(); + let result = if recheck { + verify_saved( + width, + &dir, + compile_ms, + true, + prior_evidence.clone(), + started, + ) + } else { + run(width, &dir) + }; + if let Err(error) = result { + std::fs::write( + dir.join("failure.json"), + serde_json::to_vec_pretty( + &serde_json::json!({"bits":width,"mode":if recheck {"verify-only"}else{"compile-and-verify"},"prior_evidence":prior_evidence,"elapsed_ms":started.elapsed().as_millis(),"error":format!("{error:?}")}), + )?, + )?; + return Err(error); + } + Ok(()) +} diff --git a/examples/adder_replay.rs b/examples/adder_replay.rs new file mode 100644 index 00000000..3829b753 --- /dev/null +++ b/examples/adder_replay.rs @@ -0,0 +1,130 @@ +//! Replay saved NBT only: this never invokes compilation or placement. +//! cargo run --release --example adder_replay -- target/alu-display/adder-8 +#[allow(dead_code)] +#[path = "adder_nbt.rs"] +mod original; + +use std::path::PathBuf; +use std::time::Instant; + +use redstone_compiler::nbt::NBTRoot; +use redstone_compiler::output::OutputEndpoint; +use redstone_compiler::world::simulator::Simulator; +use redstone_compiler::world::World; +use serde::Deserialize; +use serde_json::json; + +#[derive(Deserialize)] +struct Interface { + inputs: Vec, + outputs: Vec, +} + +fn drive(sim: &mut Simulator, interface: &Interface, a: usize, b: usize) -> eyre::Result<()> { + sim.advance_idle_cycles(61)?; + let states = interface + .inputs + .iter() + .map(|p| { + let (bus, bit) = p.name.rsplit_once('_').expect("scalar bus pin"); + let bit = bit.parse::().expect("bit index"); + ( + p.position(), + (if bus == "a" { a } else { b }) & (1 << bit) != 0, + ) + }) + .collect(); + sim.drive_inputs_with_limits(states, 4096, 2_000_000) +} + +fn observed(sim: &Simulator, interface: &Interface) -> usize { + interface.outputs.iter().fold(0, |sum, p| { + let bit = p.name.rsplit_once('_').unwrap().1.parse::().unwrap(); + sum | (usize::from(sim.world()[p.position()].kind.is_powered()) << bit) + }) +} + +fn burned(sim: &Simulator) -> Vec<[usize; 3]> { + sim.world() + .iter_block() + .into_iter() + .filter(|(p, _)| sim.is_torch_burned_out(*p)) + .map(|(p, _)| [p.0, p.1, p.2]) + .collect() +} + +fn new_sim(world: &World) -> eyre::Result { + Ok(Simulator::from_settled_with_limits_and_trace( + world, 4096, 2_000_000, 0, + )?) +} + +fn main() -> eyre::Result<()> { + let root = PathBuf::from( + std::env::args() + .nth(1) + .unwrap_or("target/alu-display/adder-8".into()), + ); + let world = NBTRoot::from_nbt_bytes(&std::fs::read(root.join("adder.nbt"))?)?.to_world(); + let interface: Interface = + serde_json::from_slice(&std::fs::read(root.join("interface.json"))?)?; + let width = interface.inputs.len() / 2; + let cases = original::static_cases(width); + let started = Instant::now(); + for &(a, b) in &cases { + let mut fresh = new_sim(&world)?; + drive(&mut fresh, &interface, a, b)?; + eyre::ensure!( + observed(&fresh, &interface) == a + b && burned(&fresh).is_empty(), + "static case ({a},{b}) failed" + ); + } + let mut sim = new_sim(&world)?; + let mut previous = None; + for (index, &(a, b)) in cases.iter().enumerate() { + // Keep only the failing step's trace, with bounded event storage. + sim.set_trace_limit(if index >= 45 { 20_000 } else { 0 }); + sim.clear_trace(); + drive(&mut sim, &interface, a, b)?; + let actual = observed(&sim, &interface); + let burnt = burned(&sim); + if actual != a + b || !burnt.is_empty() { + let mut fresh = new_sim(&world)?; + drive(&mut fresh, &interface, a, b)?; + let mut pair = new_sim(&world)?; + if let Some((pa, pb)) = previous { + drive(&mut pair, &interface, pa, pb)?; + } + drive(&mut pair, &interface, a, b)?; + let differences=sim.world().iter_block().into_iter().filter(|(p,block)| block.kind.is_powered()!=fresh.world()[*p].kind.is_powered()).map(|(p,block)| json!({"pos":[p.0,p.1,p.2],"bad":format!("{:?}",block),"fresh":format!("{:?}",fresh.world()[p]),"burned":sim.is_torch_burned_out(p),"power_sources":sim.diagnostic_power_sources(p)})).collect::>(); + let report = json!({"case":index+1,"previous":previous,"inputs":[a,b],"expected":a+b,"actual":actual,"fresh_actual":observed(&fresh,&interface),"pair_actual":observed(&pair,&interface),"burned":burnt,"pair_burned":burned(&pair),"fresh_burned":burned(&fresh),"powered_differences":differences,"trace":sim.trace(),"elapsed_ms":started.elapsed().as_millis()}); + std::fs::write( + root.join("replay-failure-trace.json"), + serde_json::to_vec_pretty(&report)?, + )?; + // A long idle followed by a real settle distinguishes retained + // burnout/queue history from a stable geometric feedback loop. + sim.set_trace_limit(0); + sim.advance_idle_cycles(1000)?; + sim.run_with_max_cycles(4096)?; + let after_settle = observed(&sim, &interface); + drive(&mut sim, &interface, a, b)?; + let after_repeat = observed(&sim, &interface); + let summary = json!({"case":index+1,"previous":previous,"inputs":[a,b],"expected":a+b,"actual":actual,"fresh_actual":observed(&fresh,&interface),"pair_actual":observed(&pair,&interface),"burned":burnt,"pair_burned":burned(&pair),"after_1000_idle_and_settle":after_settle,"after_same_input_again":after_repeat,"remaining_burned":burned(&sim),"powered_difference_count":differences.len(),"elapsed_ms":started.elapsed().as_millis()}); + std::fs::write( + root.join("replay-summary.json"), + serde_json::to_vec_pretty(&summary)?, + )?; + println!("{}", serde_json::to_string_pretty(&summary)?); + return Ok(()); + } + previous = Some((a, b)); + } + let report = json!({"static_passed":cases.len(), "continuous_passed":cases.len(), "nbt_recompiled":false,"elapsed_ms":started.elapsed().as_millis()}); + std::fs::write( + root.join("replay-verification.json"), + serde_json::to_vec_pretty(&report)?, + )?; + println!("{}", serde_json::to_string_pretty(&report)?); + Ok(()) +} diff --git a/examples/decoder_cone_probe.rs b/examples/decoder_cone_probe.rs new file mode 100644 index 00000000..91d2efe2 --- /dev/null +++ b/examples/decoder_cone_probe.rs @@ -0,0 +1,165 @@ +//! Bounded local-candidate diagnostic for the decoder's three-input AND cone. +use redstone_compiler::ir::{ + LogicalDesign, RoutableModule, RoutableModuleBody, RoutableNode, RoutableNodeKind, +}; +use redstone_compiler::transform::place_and_route::global_pnr::candidate::{ + generate_routable_module_candidates_with_progress_label, UnitCandidateConfig, +}; +use redstone_compiler::transform::place_and_route::local_placer::{ + LocalPlacerConfig, PlacementSchedulePolicy, +}; +use redstone_compiler::transform::place_and_route::sampling::SamplingPolicy; +use redstone_compiler::world::position::DimSize; + +fn evaluate(module: &RoutableModule, mask: usize) -> bool { + let RoutableModuleBody::Leaf { nodes } = &module.body else { + panic!() + }; + fn value(id: usize, nodes: &[RoutableNode], mask: usize) -> bool { + let node = nodes.iter().find(|node| node.id == id).unwrap(); + match &node.kind { + RoutableNodeKind::Input { name } => { + mask & (1 << name.strip_prefix('a').unwrap().parse::().unwrap()) != 0 + } + RoutableNodeKind::Not => !value(node.inputs[0], nodes, mask), + RoutableNodeKind::Or => node.inputs.iter().any(|id| value(*id, nodes, mask)), + RoutableNodeKind::Output { .. } => value(node.inputs[0], nodes, mask), + _ => panic!(), + } + } + value( + nodes + .iter() + .find(|node| matches!(node.kind, RoutableNodeKind::Output { .. })) + .unwrap() + .id, + nodes, + mask, + ) +} +fn main() -> eyre::Result<()> { + tracing_subscriber::fmt::try_init().ok(); + let logical = + LogicalDesign::from_verilog_source(include_str!("../test/alu_display/bcd_to_7seg.v"))?; + let mapped = logical.lower_to_routable_with_bit_mapping_limits(4, 3)?; + let original = mapped.module("bcd_to_7seg_bit_19").unwrap().clone(); + let mut flat = original.clone(); + flat.name = "flat_and3".into(); + let mut nodes = Vec::new(); + for i in 0..3 { + nodes.push(RoutableNode { + id: i, + kind: RoutableNodeKind::Input { + name: format!("a{i}"), + }, + inputs: vec![], + tag: String::new(), + }); + } + for i in 0..3 { + nodes.push(RoutableNode { + id: i + 3, + kind: RoutableNodeKind::Not, + inputs: vec![i], + tag: String::new(), + }); + } + nodes.push(RoutableNode { + id: 6, + kind: RoutableNodeKind::Or, + inputs: vec![3, 4, 5], + tag: String::new(), + }); + nodes.push(RoutableNode { + id: 7, + kind: RoutableNodeKind::Not, + inputs: vec![6], + tag: String::new(), + }); + nodes.push(RoutableNode { + id: 8, + kind: RoutableNodeKind::Output { name: "y".into() }, + inputs: vec![7], + tag: String::new(), + }); + flat.body = RoutableModuleBody::Leaf { nodes }; + for mask in 0..8 { + eyre::ensure!( + evaluate(&original, mask) == evaluate(&flat, mask), + "parity mismatch" + ); + } + let config = UnitCandidateConfig { + dim: DimSize(8, 8, 4), + max_candidates: 4, + local_config: LocalPlacerConfig { + greedy_input_generation: true, + input_candidate_limit: Some(16), + schedule: PlacementSchedulePolicy::Auto, + placement_sampling_policy: LocalPlacerConfig::ranked_sampling(64, 0, 0), + max_route_step: 4, + route_step_sampling_policy: SamplingPolicy::Random(32), + not_route_step_sampling_policy: SamplingPolicy::Random(8), + ..Default::default() + }, + ..Default::default() + }; + let selection = std::env::args().nth(1).unwrap_or("twoinputs".into()); + if selection == "twoinputs" { + let path = std::env::args() + .nth(2) + .unwrap_or("test/alu_display/bcd_to_7seg.v".into()); + let logical = LogicalDesign::from_verilog_source(&std::fs::read_to_string(path)?)?; + let mapped = logical.lower_to_routable_with_bit_mapping_limits(4, 2)?; + std::fs::create_dir_all("target/alu-display")?; + std::fs::write( + "target/alu-display/decoder-twoinputs.rcir", + mapped.to_string(), + )?; + if let RoutableModuleBody::Composite { instances, .. } = + &mapped.module(&mapped.top).unwrap().body + { + println!( + "instances={} definitions={}", + instances.len(), + mapped.modules.len() - 1 + ); + } + for module in &mapped.modules { + if let RoutableModuleBody::Leaf { nodes } = &module.body { + let started = std::time::Instant::now(); + let result = generate_routable_module_candidates_with_progress_label( + module, + &config, + Some(&module.name), + ); + println!( + "name={} nodes={} elapsed={:?} result={:?}", + module.name, + nodes.len(), + started.elapsed(), + result.as_ref().map(|v| v.len()) + ); + } + } + return Ok(()); + } + eyre::ensure!( + selection == "original", + "flat AND3 passes all eight Boolean cases, but its 3-input OR is unsupported by the binary local placer; use original or twoinputs" + ); + let candidate = original; + let started = std::time::Instant::now(); + let result = generate_routable_module_candidates_with_progress_label( + &candidate, + &config, + Some(&candidate.name), + ); + println!( + "name={} elapsed={:?} result={:?}", + candidate.name, + started.elapsed(), + result.as_ref().map(|v| v.len()) + ); + Ok(()) +} diff --git a/examples/decoder_nbt.rs b/examples/decoder_nbt.rs new file mode 100644 index 00000000..8a255fbe --- /dev/null +++ b/examples/decoder_nbt.rs @@ -0,0 +1,432 @@ +//! End-to-end decoder experiment. Artifacts stay outside tracked fixtures. +//! cargo run --release --example decoder_nbt -- target/alu-display/decoder +use std::path::PathBuf; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::OnceLock; +use std::time::Instant; + +use eyre::{ensure, ContextCompat}; +use redstone_compiler::ir::LogicalDesign; +use redstone_compiler::nbt::{NBTRoot, ToNBT}; +use redstone_compiler::transform::place_and_route::global_pnr::candidate::UnitCandidateConfig; +use redstone_compiler::transform::place_and_route::global_pnr::{ + place_and_route_routable_design_with_visualization, GlobalPnrConfig, +}; +use redstone_compiler::transform::place_and_route::local_placer::{ + LocalPlacerConfig, PlacementSchedulePolicy, +}; +use redstone_compiler::transform::place_and_route::sampling::SamplingPolicy; +use redstone_compiler::world::position::DimSize; +use redstone_compiler::world::simulator::Simulator; +use tracing_subscriber::prelude::*; + +const MASKS: [u8; 16] = [ + 0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0, 0, 0, 0, 0, 0, +]; +static DIAGNOSTIC_DIR: OnceLock = OnceLock::new(); +static FAILURE_INDEX: AtomicUsize = AtomicUsize::new(0); + +fn bit_index(name: &str, bus: &str) -> Option { + name.strip_prefix(bus)? + .trim_matches(|c| c == '_' || c == '[' || c == ']') + .parse() + .ok() +} + +fn verify_static(placed: &redstone_compiler::output::PlacedWorld) -> eyre::Result<()> { + use redstone_compiler::world::World; + ensure!( + (placed.inputs.len() == 4 || placed.inputs.len() == 5) && placed.outputs.len() == 7, + "decoder interface mismatch" + ); + let world = World::from(&placed.world); + for value in 0usize..(1usize << placed.inputs.len()) { + let mut sim = Simulator::from_settled_with_limits_and_trace(&world, 4096, 2_000_000, 0)?; + let inputs = placed + .inputs + .iter() + .map(|input| { + let enabled = if input.name == "blank" { + value & 16 != 0 + } else { + let bit = bit_index(&input.name, "digit").context("unknown input")?; + ensure!(bit < 4, "digit index outside decoder"); + value & (1 << bit) != 0 + }; + Ok((input.position(), enabled)) + }) + .collect::>>()?; + sim.drive_inputs_with_limits(inputs, 4096, 2_000_000)?; + let expected = if value & 16 != 0 { + 0 + } else { + MASKS[value & 15] + }; + for output in &placed.outputs { + let bit = bit_index(&output.name, "seg").context("unknown output")?; + if bit < 7 + && sim.world()[output.position()].kind.is_powered() != (expected & (1 << bit) != 0) + { + if let Some(dir) = DIAGNOSTIC_DIR.get() { + let dir = dir.join(format!( + "failed-layout-{}", + FAILURE_INDEX.fetch_add(1, Ordering::Relaxed) + )); + std::fs::create_dir_all(&dir)?; + placed.world.to_nbt().save(dir.join("decoder.nbt")); + placed.metadata().save(dir.join("decoder.outputs.json"))?; + std::fs::write( + dir.join("interface.json"), + serde_json::to_string_pretty( + &serde_json::json!({"inputs":placed.inputs,"outputs":placed.outputs}), + )?, + )?; + std::fs::write( + dir.join("failure.json"), + serde_json::to_string_pretty( + &serde_json::json!({"input":value,"output":output.name,"expected_mask":expected,"observed":placed.outputs.iter().map(|o|(&o.name,sim.world()[o.position()].kind.is_powered())).collect::>()}), + )?, + )?; + } + } + ensure!( + bit < 7 + && sim.world()[output.position()].kind.is_powered() + == (expected & (1 << bit) != 0), + "static decoder mismatch input={value}, output={}", + output.name + ); + } + } + Ok(()) +} + +fn verify_candidate(placed: &redstone_compiler::output::PlacedWorld) -> eyre::Result<()> { + verify_static(placed)?; + let world = redstone_compiler::world::World::from(&placed.world); + let mut sim = Simulator::from_settled_with_limits_and_trace(&world, 4096, 2_000_000, 0)?; + let states = 1usize << placed.inputs.len(); + // Reject a layout with transition hazards before selecting it. The exported + // NBT is tested again independently after serialization below. + for from in 0..states { + for to in 0..states { + for value in [from, to] { + sim.advance_idle_cycles(61)?; + let inputs = placed + .inputs + .iter() + .map(|input| { + let bit = if input.name == "blank" { + 4 + } else { + bit_index(&input.name, "digit").context("unknown input")? + }; + Ok((input.position(), value & (1 << bit) != 0)) + }) + .collect::>>()?; + sim.drive_inputs_with_limits(inputs, 4096, 2_000_000)?; + let expected = if value & 16 != 0 { + 0 + } else { + MASKS[value & 15] + }; + let correct = placed.outputs.iter().all(|output| { + bit_index(&output.name, "seg").is_some_and(|bit| { + bit < 7 + && sim.world()[output.position()].kind.is_powered() + == (expected & (1 << bit) != 0) + }) + }); + let burned = sim + .world() + .iter_block() + .iter() + .any(|(p, _)| sim.is_torch_burned_out(*p)); + if !correct || burned { + if let Some(dir) = DIAGNOSTIC_DIR.get() { + let dir = dir.join(format!( + "failed-layout-{}", + FAILURE_INDEX.fetch_add(1, Ordering::Relaxed) + )); + std::fs::create_dir_all(&dir)?; + placed.world.to_nbt().save(dir.join("decoder.nbt")); + std::fs::write( + dir.join("interface.json"), + serde_json::to_string_pretty( + &serde_json::json!({"inputs":placed.inputs,"outputs":placed.outputs}), + )?, + )?; + std::fs::write( + dir.join("failure.json"), + serde_json::to_string_pretty( + &serde_json::json!({"from":from,"to":to,"driving":value,"correct":correct,"burned_out":burned}), + )?, + )?; + } + eyre::bail!("decoder transition {from}->{to} while driving {value}: correct={correct}, burnout={burned}"); + } + } + } + } + Ok(()) +} + +fn main() -> eyre::Result<()> { + let debug = std::env::var_os("PNR_DEBUG").is_some(); + let subscriber = tracing_subscriber::fmt() + .with_ansi(false) + .with_max_level(tracing::Level::DEBUG) + .finish() + .with(tracing_subscriber::filter::filter_fn(move |meta| { + *meta.level() <= tracing::Level::INFO + || (debug && meta.target() == "redstone_compiler::global_pnr") + })); + tracing::subscriber::set_global_default(subscriber).ok(); + let dir = PathBuf::from( + std::env::args() + .nth(1) + .unwrap_or("target/alu-display/decoder".into()), + ); + std::fs::create_dir_all(&dir)?; + let _ = DIAGNOSTIC_DIR.set(dir.join("diagnostics")); + let _ = std::fs::remove_file(dir.join("verification.json")); + let _ = std::fs::remove_file(dir.join("failure.json")); + let source_path = std::env::args() + .nth(2) + .unwrap_or("test/alu_display/bcd_to_7seg.v".into()); + let mut source = std::fs::read_to_string(&source_path)?; + for extra in std::env::args().skip(3) { + source.push('\n'); + source.push_str(&std::fs::read_to_string(extra)?); + } + std::fs::write(dir.join("source.v"), &source)?; + let logical = LogicalDesign::from_verilog_source_named(&source, "bcd_to_7seg.v")?; + std::fs::write(dir.join("logical.rcir"), logical.to_string())?; + let gates = std::env::var("BIT_GATES").unwrap_or("1".into()).parse()?; + let boundary_inputs = std::env::var("BIT_INPUTS").unwrap_or("2".into()).parse()?; + let routable = if std::env::var_os("BIT_LITERAL").is_some() { + logical.lower_to_routable_with_literal_associative_cells(8)? + } else if std::env::var_os("BIT_EXPANDED").is_some() { + logical.lower_to_routable_with_expanded_associative_cells(8)? + } else if std::env::var_os("BIT_NARY").is_some() { + logical.lower_to_routable_with_associative_cells(8)? + } else if std::env::var_os("BIT_BALANCED").is_some() { + logical.lower_to_routable_with_balanced_associative_trees(gates, boundary_inputs)? + } else { + logical.lower_to_routable_with_bit_mapping_limits(gates, boundary_inputs)? + }; + std::fs::write(dir.join("routable.rcir"), routable.to_string())?; + let exact = if std::env::var_os("EXACT_CANDIDATES").is_some() { + let dims = std::env::var("EXACT_BOX") + .unwrap_or("3x5x3".into()) + .split('x') + .map(str::parse::) + .collect::, _>>()?; + ensure!( + dims.len() == 3 && dims.iter().all(|&d| d > 0), + "EXACT_BOX must be positive XxYxZ" + ); + Some(redstone_compiler::transform::place_and_route::global_pnr::candidate::ExactCandidateConfig { + search_box:[dims[0],dims[1],dims[2]], + time_limit_ms:std::env::var("EXACT_MS").unwrap_or("5000".into()).parse()?, + ..Default::default() + }) + } else { + None + }; + let mut config = GlobalPnrConfig { + candidate: UnitCandidateConfig { + exact, + nary_stagger_inputs: std::env::var_os("NARY_STAGGER").is_some(), + verified_nary_cells: std::env::var_os("BIT_NARY").is_some() + || std::env::var_os("BIT_EXPANDED").is_some() + || std::env::var_os("BIT_LITERAL").is_some(), + reserve_combinational_io_diodes: true, + dim: DimSize(8, 8, 4), + max_candidates: 4, + local_config: LocalPlacerConfig { + greedy_input_generation: true, + input_candidate_limit: Some(16), + schedule: PlacementSchedulePolicy::Auto, + placement_sampling_policy: LocalPlacerConfig::ranked_sampling(64, 0, 0), + max_route_step: 4, + route_step_sampling_policy: SamplingPolicy::Random(32), + not_route_step_sampling_policy: SamplingPolicy::Random(8), + ..Default::default() + }, + ..Default::default() + } + .into(), + candidate_cache_dir: Some( + std::env::var_os("PNR_CANDIDATE_CACHE") + .map(PathBuf::from) + .unwrap_or_else(|| dir.join("candidate-cache")), + ), + ..Default::default() + }; + config.search = + redstone_compiler::transform::place_and_route::global_pnr::policy::GlobalPnrPreset::Fast + .search_config(); + config.placement.max_attempts = std::env::var("PNR_ATTEMPTS") + .unwrap_or("4".into()) + .parse()?; + config.placement.spacing = std::env::var("PNR_SPACING").unwrap_or("4".into()).parse()?; + if std::env::var_os("PNR_LAYERED").is_some() { + use redstone_compiler::transform::place_and_route::global_pnr::policy::{ + LayerAssignmentStrategy, LayeredPlacementConfig, PlacementHeuristic, + }; + config.search.policies.placement_heuristics = + vec![PlacementHeuristic::Layered3D(LayeredPlacementConfig { + layers: 2, + layer_spacing: std::env::var("PNR_LAYER_SPACING") + .unwrap_or("6".into()) + .parse()?, + assignment: LayerAssignmentStrategy::NetAware, + })]; + } + if std::env::var_os("PNR_FREE3D").is_some() { + use redstone_compiler::transform::place_and_route::global_pnr::policy::{ + Free3DPlacementConfig, PlacementHeuristic, + }; + config.search.policies.placement_heuristics = + vec![PlacementHeuristic::Free3D(Free3DPlacementConfig::default())]; + } + config.search.policies.net_order_strategies = vec![ + redstone_compiler::transform::place_and_route::global_pnr::router::NetOrderStrategy::Criticality, + redstone_compiler::transform::place_and_route::global_pnr::router::NetOrderStrategy::HighestFanoutFirst, + redstone_compiler::transform::place_and_route::global_pnr::router::NetOrderStrategy::ReverseCriticality, + ]; + if std::env::var_os("PNR_PIN_ACCESS").is_some() { + config.search.policies.net_order_strategies = vec![redstone_compiler::transform::place_and_route::global_pnr::router::NetOrderStrategy::PinAccess]; + } + config.search.budget.max_layout_combinations = std::env::var("PNR_LAYOUT_COMBINATIONS") + .unwrap_or("1".into()) + .parse()?; + config.verifier = Some(verify_candidate); + if let Ok(value) = std::env::var("PNR_MAX_STEPS") { + config.routing.max_route_steps = value.parse()?; + ensure!( + config.routing.max_route_steps > 0, + "route step budget must be positive" + ); + } + if let Ok(value) = std::env::var("PNR_ASTAR_EXPANSIONS") { + let max_expansions = value.parse()?; + ensure!(max_expansions > 0, "A* expansion budget must be positive"); + config.routing.strategy = redstone_compiler::transform::place_and_route::global_pnr::router::GlobalRoutingStrategy::AStarBudgeted { max_expansions }; + } + if std::env::var_os("PNR_GREEDY").is_some() { + config.routing.strategy = redstone_compiler::transform::place_and_route::global_pnr::router::GlobalRoutingStrategy::GreedyBeam { beam_width: 16, max_expansions: 2000, variant_seed: 0 }; + } + if std::env::var_os("PNR_SHELVES").is_some() { + config.search.policies.placement_heuristics = vec![redstone_compiler::transform::place_and_route::global_pnr::policy::PlacementHeuristic::Shelf]; + } + let start = Instant::now(); + let result = place_and_route_routable_design_with_visualization(&routable, &config)?; + let elapsed = start.elapsed(); + std::fs::write(dir.join("selected-candidates.json"), serde_json::to_string_pretty(&result.selected_candidates.iter().map(|c| serde_json::json!({ + "module":c.module_name,"blocks":c.cost.block_count,"bbox_volume":c.cost.bbox_volume,"footprint":c.cost.bbox_footprint,"height":c.cost.bbox_height,"exact_core":c.exact_core + })).collect::>())?)?; + std::fs::write( + dir.join("pnr-config.json"), + serde_json::to_string_pretty(&result.config_snapshot)?, + )?; + let placed = &result.placed_world; + placed.world.to_nbt().save(dir.join("decoder.nbt")); + placed.metadata().save(dir.join("decoder.outputs.json"))?; + std::fs::write( + dir.join("interface.json"), + serde_json::to_string_pretty( + &serde_json::json!({"inputs":placed.inputs,"outputs":placed.outputs}), + )?, + )?; + // Read back the exported NBT, rather than testing only the pre-export world. + let world = NBTRoot::from_nbt_bytes(&std::fs::read(dir.join("decoder.nbt"))?)?.to_world(); + ensure!( + (placed.inputs.len() == 4 || placed.inputs.len() == 5) && placed.outputs.len() == 7, + "unexpected interface" + ); + let mut seen = [false; 7]; + for output in &placed.outputs { + let bit = bit_index(&output.name, "seg").context("unknown segment endpoint")?; + ensure!(bit < 7 && !seen[bit], "duplicate/invalid segment endpoint"); + seen[bit] = true; + } + let mut simulator = Simulator::from_settled_with_limits_and_trace(&world, 4096, 2_000_000, 0)?; + let mut cases = 0; + // All 32 initial assignments, followed by all 1024 ordered transitions. + let states = 1usize << placed.inputs.len(); + for from in 0..states { + for to in 0..states { + for value in [from, to] { + simulator.advance_idle_cycles(61)?; + let inputs = placed + .inputs + .iter() + .map(|input| { + let powered = if input.name == "blank" { + value & 16 != 0 + } else { + let bit = bit_index(&input.name, "digit") + .context("unknown input endpoint")?; + ensure!(bit < 4, "invalid digit endpoint"); + value & (1 << bit) != 0 + }; + Ok((input.position(), powered)) + }) + .collect::>>()?; + simulator.drive_inputs_with_limits(inputs, 4096, 2_000_000)?; + let expected = if value & 16 != 0 { + 0 + } else { + MASKS[value & 15] + }; + for output in &placed.outputs { + let bit = bit_index(&output.name, "seg").unwrap(); + if simulator.world()[output.position()].kind.is_powered() + != (expected & (1 << bit) != 0) + { + std::fs::write( + dir.join("failure.json"), + serde_json::to_string_pretty(&serde_json::json!({ + "from":from,"to":to,"driving":value,"driven_case":cases, + "output":output.name,"expected_mask":expected, + "observed":placed.outputs.iter().map(|o|(&o.name,simulator.world()[o.position()].kind.is_powered())).collect::>(), + "burned_out":simulator.world().iter_block().iter().filter_map(|(p,_)|simulator.is_torch_burned_out(*p).then_some([p.0,p.1,p.2])).collect::>() + }))?, + )?; + } + ensure!( + simulator.world()[output.position()].kind.is_powered() + == (expected & (1 << bit) != 0), + "decoder mismatch: input={value}, output={}", + output.name + ); + } + ensure!( + !simulator + .world() + .iter_block() + .iter() + .any(|(pos, _)| simulator.is_torch_burned_out(*pos)), + "torch burnout" + ); + cases += 1; + } + } + } + let summary = serde_json::json!({ + "exact_selected":result.selected_candidates.iter().filter(|c|c.exact_core.is_some()).count(), + "compile_ms":elapsed.as_millis(),"blocks":world.blocks.len(), + "static_cases":states,"driven_cases":cases, + "ordered_transitions":states * states,"nbt_roundtrip":true, + "blank_input":placed.inputs.iter().any(|p|p.name=="blank"), + "invalid_bcd_off":true,"torch_burnout_checked":true + }); + std::fs::write( + dir.join("verification.json"), + serde_json::to_string_pretty(&summary)?, + )?; + println!("{summary}"); + Ok(()) +} diff --git a/examples/decoder_replay.rs b/examples/decoder_replay.rs new file mode 100644 index 00000000..c0f4b80c --- /dev/null +++ b/examples/decoder_replay.rs @@ -0,0 +1,208 @@ +//! Replay an existing decoder NBT without compilation or placement. +//! cargo run --release --example decoder_replay -- DIR [x,y,z ...] +use std::path::PathBuf; + +use redstone_compiler::nbt::NBTRoot; +use redstone_compiler::output::OutputEndpoint; +use redstone_compiler::world::position::Position; +use redstone_compiler::world::simulator::Simulator; +use redstone_compiler::world::World; +use serde::Deserialize; +use serde_json::json; +#[path = "../test/alu_display/oracle.rs"] +mod oracle; +#[derive(Deserialize)] +struct Interface { + inputs: Vec, + outputs: Vec, +} +fn bit(name: &str) -> usize { + if name == "blank" { + 4 + } else { + name.rsplit_once('_') + .expect("bus pin") + .1 + .parse() + .expect("pin index") + } +} +fn fresh(world: &World) -> eyre::Result { + Ok(Simulator::from_settled_with_limits_and_trace( + world, 4096, 2_000_000, 0, + )?) +} +fn drive(sim: &mut Simulator, io: &Interface, value: usize) -> eyre::Result<()> { + sim.advance_idle_cycles(61)?; + sim.drive_inputs_with_limits( + io.inputs + .iter() + .map(|p| (p.position(), value & (1 << bit(&p.name)) != 0)) + .collect(), + 4096, + 2_000_000, + ) +} +fn actual(sim: &Simulator, io: &Interface) -> usize { + io.outputs.iter().fold(0, |m, p| { + m | (usize::from(sim.world()[p.position()].kind.is_powered()) << bit(&p.name)) + }) +} +fn inspect(sim: &Simulator, p: Position) -> serde_json::Value { + json!({"position":[p.0,p.1,p.2],"block":format!("{:?}",sim.world()[p]),"powered":sim.world()[p].kind.is_powered(),"burned":sim.is_torch_burned_out(p),"sources":sim.diagnostic_power_sources(p)}) +} +fn main() -> eyre::Result<()> { + let mut args = std::env::args().skip(1); + let root = PathBuf::from( + args.next() + .expect("directory containing decoder.nbt and interface.json"), + ); + let queries = args + .map(|s| { + let xyz = s + .split(',') + .map(str::parse::) + .collect::, _>>()?; + eyre::ensure!(xyz.len() == 3, "query must be x,y,z"); + Ok(Position(xyz[0], xyz[1], xyz[2])) + }) + .collect::>>()?; + let world = NBTRoot::from_nbt_bytes(&std::fs::read(root.join("decoder.nbt"))?)?.to_world(); + let io: Interface = serde_json::from_slice(&std::fs::read(root.join("interface.json"))?)?; + eyre::ensure!( + [4, 5].contains(&io.inputs.len()) && io.outputs.len() == 7, + "unexpected interface" + ); + for &p in &queries { + eyre::ensure!(world.size.bound_on(p), "query outside world"); + } + let count = 1 << io.inputs.len(); + let mut rows = vec![]; + let mut walking = fresh(&world)?; + let mut first_failure = false; + for value in 0..count { + let expected = oracle::segment_mask(value & 15, value & 16 != 0); + let mut sim = fresh(&world)?; + sim.set_trace_limit(30_000); + drive(&mut sim, &io, value)?; + let observed = actual(&sim, &io); + let mut from_zero = fresh(&world)?; + drive(&mut from_zero, &io, 0)?; + drive(&mut from_zero, &io, value)?; + drive(&mut walking, &io, value)?; + let row = json!({"input":value,"expected":expected,"fresh":observed,"from_zero":actual(&from_zero,&io),"walk":actual(&walking,&io),"queries":queries.iter().map(|&p|inspect(&sim,p)).collect::>()}); + println!("{row}"); + rows.push(row); + if observed != expected && !first_failure { + first_failure = true; + let states = sim + .world() + .iter_block() + .iter() + .map(|(p, _)| inspect(&sim, *p)) + .collect::>(); + std::fs::write( + root.join("decoder-replay-first-failure.json"), + serde_json::to_vec_pretty( + &json!({"input":value,"expected":expected,"actual":observed,"blocks":states,"trace":sim.trace()}), + )?, + )?; + } + } + let mut transition_failures = vec![]; + let mut continuous_failures = vec![]; + let mut continuous = fresh(&world)?; + for from in 0..count { + for to in 0..count { + for value in [from, to] { + drive(&mut continuous, &io, value)?; + let expected = oracle::segment_mask(value & 15, value & 16 != 0); + let observed = actual(&continuous, &io); + if observed != expected { + continuous_failures.push(json!({"from":from,"to":to,"input":value,"actual":observed,"expected":expected})); + } + } + let mut sim = fresh(&world)?; + drive(&mut sim, &io, from)?; + drive(&mut sim, &io, to)?; + let observed = actual(&sim, &io); + let expected = oracle::segment_mask(to & 15, to & 16 != 0); + if observed != expected { + if transition_failures.is_empty() { + let mut traced = fresh(&world)?; + drive(&mut traced, &io, from)?; + traced.set_trace_limit(30_000); + drive(&mut traced, &io, to)?; + let mut good = fresh(&world)?; + drive(&mut good, &io, to)?; + let differences = traced + .world() + .iter_block() + .iter() + .filter(|(p, b)| b.kind.is_powered() != good.world()[*p].kind.is_powered()) + .map(|(p, _)| json!({"bad":inspect(&traced,*p),"fresh":inspect(&good,*p)})) + .collect::>(); + let burned = traced + .world() + .iter_block() + .iter() + .filter(|(p, _)| traced.is_torch_burned_out(*p)) + .map(|(p, _)| inspect(&traced, *p)) + .collect::>(); + let inputs = io + .inputs + .iter() + .map(|p| json!({"name":p.name,"state":inspect(&traced,p.position())})) + .collect::>(); + let trace = traced.trace().to_vec(); + let states = traced + .world() + .iter_block() + .iter() + .map(|(p, _)| inspect(&traced, *p)) + .collect::>(); + traced.advance_idle_cycles(1000)?; + traced.run_with_max_cycles(4096)?; + let after_idle = actual(&traced, &io); + drive(&mut traced, &io, to)?; + std::fs::write( + root.join("decoder-replay-transition-failure.json"), + serde_json::to_vec_pretty( + &json!({"from":from,"to":to,"expected":expected,"actual":observed,"after_idle":after_idle,"after_same_input":actual(&traced,&io),"inputs":inputs,"burned":burned,"differences":differences,"blocks":states,"trace":trace}), + )?, + )?; + } + transition_failures + .push(json!({"from":from,"to":to,"actual":observed,"expected":expected})); + } + } + } + let static_failures = rows + .iter() + .filter(|row| { + row["fresh"] != row["expected"] + || row["from_zero"] != row["expected"] + || row["walk"] != row["expected"] + }) + .count(); + let report = json!({"static":rows,"static_failures":static_failures,"transitions_checked":count*count,"transition_failures":transition_failures,"continuous_failures":continuous_failures,"nbt_recompiled":false}); + std::fs::write( + root.join("decoder-replay-report.json"), + serde_json::to_vec_pretty(&report)?, + )?; + println!( + "transition_failures={}/{}", + transition_failures.len(), + count * count + ); + println!( + "continuous_failures={}/{}", + continuous_failures.len(), + 2 * count * count + ); + eyre::ensure!( + static_failures == 0 && transition_failures.is_empty() && continuous_failures.is_empty(), + "saved decoder NBT replay failed; see decoder-replay-report.json" + ); + Ok(()) +} diff --git a/examples/exact_cell_probe.rs b/examples/exact_cell_probe.rs new file mode 100644 index 00000000..d09b866e --- /dev/null +++ b/examples/exact_cell_probe.rs @@ -0,0 +1,57 @@ +//! Bounded direct exact synthesis versus verified literal recipes. +//! cargo run --release --example exact_cell_probe -- 5 +use std::time::{Duration, Instant}; + +use redstone_compiler::graph::logic::LogicGraph; +use redstone_compiler::transform::place_and_route::global_pnr::candidate::nary::build_verified_candidate; +use redstone_compiler::transform::place_and_route::local_placer::exact::{ + ExactLocalPlacer, ExactOutcome, ExactPlacerConfig, NetDriver, +}; +use redstone_compiler::world::position::DimSize; +fn main() -> eyre::Result<()> { + let seconds = std::env::args() + .nth(1) + .unwrap_or("5".into()) + .parse::()?; + for (name, expression) in [ + ("or2", "a|b"), + ("mixed_or2", "a|~b"), + ("or3", "a|b|c"), + ("mixed_or3", "a|~b|c"), + ("or4", "a|b|c|d"), + ("mixed_and4", "~(a|~b|c|~d)"), + ] { + // Keep OR chains: exact folds them into NOR inputs. prepare_place() + // inserts OR-to-OR buffers intended for the route-based placer. + let graph = LogicGraph::from_stmt(expression, "y")?; + let recipe = build_verified_candidate(&graph, name)?.unwrap(); + let placer = ExactLocalPlacer::new(&graph)?; + let gates = placer + .netlist() + .nets + .iter() + .filter(|n| n.driver == NetDriver::Gate) + .count(); + let mut config = ExactPlacerConfig::new(DimSize(3, 5, 3)); + config.time_limit = Some(Duration::from_secs(seconds)); + config.driving_outputs.insert("y".into()); + config.workers = 1; + config.optimize = std::env::var_os("EXACT_OPTIMIZE").is_some(); + let start = Instant::now(); + let (outcome, stats) = placer.place(&config)?; + let result = match outcome { + ExactOutcome::Placed(p) => format!( + "placed logic_blocks_excluding_switches={} world_blocks={} dim={:?}", + p.block_count, + p.placed.world.iter_block().len(), + p.placed.world.size + ), + ExactOutcome::Infeasible => "infeasible".into(), + ExactOutcome::Unknown { last_rejection } => { + format!("unknown rejection={last_rejection:?}") + } + }; + println!("{name}: NOR={gates} recipe_blocks={} recipe_dim={:?} exact={result} wall={:.3}s encode={:.3}s solve={:.3}s clauses={}", recipe.world.iter_block().len(), recipe.world.size, start.elapsed().as_secs_f64(), stats.encode_time.as_secs_f64(), stats.solve_time.as_secs_f64(), stats.clauses); + } + Ok(()) +} diff --git a/examples/minterm_fanout_probe.rs b/examples/minterm_fanout_probe.rs new file mode 100644 index 00000000..bb90089a --- /dev/null +++ b/examples/minterm_fanout_probe.rs @@ -0,0 +1,216 @@ +//! Isolate shared input routing into 2, 4, or 10 four-input minterms. +//! cargo run --release --example minterm_fanout_probe -- target/alu-display/minterm-2 2 +use std::path::PathBuf; +use std::time::Instant; + +use eyre::{ensure, ContextCompat}; +use redstone_compiler::ir::LogicalDesign; +use redstone_compiler::nbt::{NBTRoot, ToNBT}; +use redstone_compiler::transform::place_and_route::global_pnr::candidate::UnitCandidateConfig; +use redstone_compiler::transform::place_and_route::global_pnr::{ + place_and_route_routable_design_with_visualization, GlobalPnrConfig, +}; +use redstone_compiler::transform::place_and_route::local_placer::{ + LocalPlacerConfig, PlacementSchedulePolicy, +}; +use redstone_compiler::transform::place_and_route::sampling::SamplingPolicy; +use redstone_compiler::world::position::DimSize; +use redstone_compiler::world::simulator::Simulator; +use tracing_subscriber::prelude::*; + +fn bit_index(name: &str, bus: &str) -> Option { + name.strip_prefix(bus)? + .trim_matches(|c| c == '_' || c == '[' || c == ']') + .parse() + .ok() +} + +fn verify_static(placed: &redstone_compiler::output::PlacedWorld) -> eyre::Result<()> { + use redstone_compiler::world::World; + ensure!(placed.inputs.len() == 4, "expected 4 digit inputs"); + ensure!( + [2, 4, 10].contains(&placed.outputs.len()) + || (std::env::var_os("MINTERM_OR").is_some() && placed.outputs.len() == 3), + "unexpected minterm output count" + ); + let mut seen = vec![false; placed.outputs.len()]; + for output in &placed.outputs { + let index = bit_index(&output.name, "out").context("unknown output")?; + ensure!( + index < seen.len() && !seen[index], + "invalid or duplicate minterm output" + ); + seen[index] = true; + } + let world = World::from(&placed.world); + for value in 0usize..16 { + let mut sim = Simulator::from_settled_with_limits_and_trace(&world, 4096, 2_000_000, 0)?; + let inputs = placed + .inputs + .iter() + .map(|p| { + let bit = bit_index(&p.name, "digit").context("unknown input")?; + ensure!(bit < 4, "invalid input"); + Ok((p.position(), value & (1 << bit) != 0)) + }) + .collect::>>()?; + sim.drive_inputs_with_limits(inputs, 4096, 2_000_000)?; + for p in &placed.outputs { + let index = bit_index(&p.name, "out").context("unknown output")?; + ensure!( + sim.world()[p.position()].kind.is_powered() + == (if std::env::var_os("MINTERM_OR").is_some() && index == 2 { + value < 2 + } else { + value == index + }), + "minterm mismatch value={value} output={}", + p.name + ); + } + } + Ok(()) +} + +fn main() -> eyre::Result<()> { + let debug = std::env::var_os("PNR_DEBUG").is_some(); + let subscriber = tracing_subscriber::fmt() + .with_ansi(false) + .with_max_level(tracing::Level::DEBUG) + .finish() + .with(tracing_subscriber::filter::filter_fn(move |meta| { + *meta.level() <= tracing::Level::INFO + || (debug && meta.target() == "redstone_compiler::global_pnr") + })); + tracing::subscriber::set_global_default(subscriber).ok(); + let dir = PathBuf::from( + std::env::args() + .nth(1) + .unwrap_or("target/alu-display/minterms".into()), + ); + std::fs::create_dir_all(&dir)?; + let _ = std::fs::remove_file(dir.join("verification.json")); + let count: usize = std::env::args().nth(2).unwrap_or("2".into()).parse()?; + ensure!([2, 4, 10].contains(&count), "count must be 2, 4, or 10"); + let mut source = format!( + "module minterms(digit, out);\ninput [3:0] digit;\noutput [{}:0] out;\n", + count - 1 + ); + for index in 0..count { + source.push_str(&format!( + "wire d{index};\nassign d{index} = digit == 4'd{index};\n" + )); + } + source.push_str(&format!( + "assign out = {{{}}};\n", + (0..count) + .rev() + .map(|i| format!("d{i}")) + .collect::>() + .join(",") + )); + if std::env::var_os("MINTERM_OR").is_some() { + ensure!(count == 2, "MINTERM_OR requires count 2"); + source = source + .replace("output [1:0] out;", "output [2:0] out;") + .replace("assign out = {d1,d0};", "assign out = {d0 | d1,d1,d0};"); + } + source.push_str("endmodule\n"); + std::fs::write(dir.join("source.v"), &source)?; + let logical = LogicalDesign::from_verilog_source_named(&source, "minterms.v")?; + std::fs::write(dir.join("logical.rcir"), logical.to_string())?; + let routable = logical.lower_to_routable_with_literal_associative_cells(8)?; + std::fs::write(dir.join("routable.rcir"), routable.to_string())?; + let mut config = GlobalPnrConfig { + candidate: UnitCandidateConfig { + verified_nary_cells: true, + nary_stagger_inputs: std::env::var_os("NARY_STAGGER").is_some(), + reserve_combinational_io_diodes: true, + dim: DimSize(8, 8, 4), + max_candidates: 4, + local_config: LocalPlacerConfig { + greedy_input_generation: true, + input_candidate_limit: Some(16), + schedule: PlacementSchedulePolicy::Auto, + placement_sampling_policy: LocalPlacerConfig::ranked_sampling(64, 0, 0), + max_route_step: 4, + route_step_sampling_policy: SamplingPolicy::Random(32), + not_route_step_sampling_policy: SamplingPolicy::Random(8), + ..Default::default() + }, + ..Default::default() + } + .into(), + candidate_cache_dir: Some(dir.join("candidate-cache")), + ..Default::default() + }; + config.search = + redstone_compiler::transform::place_and_route::global_pnr::policy::GlobalPnrPreset::Fast + .search_config(); + config.placement.max_attempts = 1; + config.routing.max_route_steps = 256; + config.routing.strategy = redstone_compiler::transform::place_and_route::global_pnr::router::GlobalRoutingStrategy::AStarBudgeted { max_expansions: 20_000 }; + config.placement.spacing = std::env::var("PNR_SPACING").unwrap_or("4".into()).parse()?; + if std::env::var_os("PNR_LAYERED").is_some() { + use redstone_compiler::transform::place_and_route::global_pnr::policy::{ + LayerAssignmentStrategy, LayeredPlacementConfig, PlacementHeuristic, + }; + config.search.policies.placement_heuristics = + vec![PlacementHeuristic::Layered3D(LayeredPlacementConfig { + layers: 2, + layer_spacing: 6, + assignment: LayerAssignmentStrategy::NetAware, + })]; + } + config.search.policies.net_order_strategies = vec![redstone_compiler::transform::place_and_route::global_pnr::router::NetOrderStrategy::Criticality]; + if std::env::var_os("PIN_ACCESS").is_some() { + config.search.policies.net_order_strategies = vec![redstone_compiler::transform::place_and_route::global_pnr::router::NetOrderStrategy::PinAccess]; + } + config.search.budget.max_layout_combinations = 1; + config.verifier = Some(verify_static); + if let Ok(value) = std::env::var("PNR_MAX_STEPS") { + config.routing.max_route_steps = value.parse()?; + ensure!( + config.routing.max_route_steps > 0, + "route step budget must be positive" + ); + } + if let Ok(value) = std::env::var("PNR_ASTAR_EXPANSIONS") { + let max_expansions = value.parse()?; + ensure!(max_expansions > 0, "A* expansion budget must be positive"); + config.routing.strategy = redstone_compiler::transform::place_and_route::global_pnr::router::GlobalRoutingStrategy::AStarBudgeted { max_expansions }; + } + if std::env::var_os("PNR_GREEDY").is_some() { + config.routing.strategy = redstone_compiler::transform::place_and_route::global_pnr::router::GlobalRoutingStrategy::GreedyBeam { beam_width: 16, max_expansions: 2000, variant_seed: 0 }; + } + if std::env::var_os("PNR_SHELVES").is_some() { + config.search.policies.placement_heuristics = vec![redstone_compiler::transform::place_and_route::global_pnr::policy::PlacementHeuristic::Shelf]; + } + let start = Instant::now(); + let result = place_and_route_routable_design_with_visualization(&routable, &config)?; + let elapsed = start.elapsed(); + std::fs::write( + dir.join("pnr-config.json"), + serde_json::to_string_pretty(&result.config_snapshot)?, + )?; + let placed = &result.placed_world; + placed.world.to_nbt().save(dir.join("minterms.nbt")); + placed.metadata().save(dir.join("minterms.outputs.json"))?; + std::fs::write( + dir.join("interface.json"), + serde_json::to_string_pretty( + &serde_json::json!({"inputs":placed.inputs,"outputs":placed.outputs}), + )?, + )?; + let world = NBTRoot::from_nbt_bytes(&std::fs::read(dir.join("minterms.nbt"))?)?.to_world(); + let mut reloaded = placed.clone(); + reloaded.world = redstone_compiler::world::World3D::from(&world); + verify_static(&reloaded)?; + let summary = serde_json::json!({"outputs":placed.outputs.len(),"compile_ms":elapsed.as_millis(),"static_states":16,"nbt_roundtrip":true}); + std::fs::write( + dir.join("verification.json"), + serde_json::to_string_pretty(&summary)?, + )?; + println!("{summary}"); + Ok(()) +} diff --git a/examples/nary_cell_lab.rs b/examples/nary_cell_lab.rs new file mode 100644 index 00000000..dc5301df --- /dev/null +++ b/examples/nary_cell_lab.rs @@ -0,0 +1,56 @@ +//! Export and verify the same n-ary recipes used by the opt-in production dispatcher. +//! cargo run --release --example nary_cell_lab +use std::path::Path; +use std::time::Instant; + +use eyre::ContextCompat; +use redstone_compiler::graph::logic::LogicGraph; +use redstone_compiler::nbt::ToNBT; +use redstone_compiler::output::OutputEndpoint; +use redstone_compiler::transform::place_and_route::global_pnr::candidate::nary::build_verified_candidate; +use redstone_compiler::transform::place_and_route::global_pnr::ir::PhysicalPortDirection; +fn main() -> eyre::Result<()> { + let directory = Path::new("target/nary-cell-lab"); + std::fs::create_dir_all(directory)?; + let start = Instant::now(); + for count in 2..=8 { + let names = (0..count).map(|i| format!("a{i}")).collect::>(); + for (function, expression) in [ + ("or", names.join("|")), + ("nor", format!("~({})", names.join("|"))), + ("and", names.join("&")), + ] { + let candidate = + build_verified_candidate(&LogicGraph::from_stmt(&expression, "y")?, "lab")? + .context("unrecognized signature")?; + let stem = format!("{function}-{count}"); + std::fs::write( + directory.join(format!("{stem}.nbt")), + fastnbt::to_bytes(&candidate.world.to_nbt())?, + )?; + let endpoints = |direction| { + candidate + .ports + .iter() + .filter(|p| p.direction == direction) + .map(|p| OutputEndpoint::new(p.name.clone(), p.position)) + .collect::>() + }; + std::fs::write( + directory.join(format!("{stem}.interface.json")), + serde_json::to_vec_pretty( + &serde_json::json!({"format":"redstone-compiler.outputs.v1","inputs":endpoints(PhysicalPortDirection::Input),"outputs":endpoints(PhysicalPortDirection::Output)}), + )?, + )?; + println!( + "PASS {function}{count}: static={} transitions={} blocks={} size={:?}", + 1 << count, + 68 + 3 * count, + candidate.world.iter_block().len(), + candidate.world.size + ); + } + } + println!("completed in {:.3}s", start.elapsed().as_secs_f64()); + Ok(()) +} diff --git a/examples/port_route_probe.rs b/examples/port_route_probe.rs new file mode 100644 index 00000000..afc09759 --- /dev/null +++ b/examples/port_route_probe.rs @@ -0,0 +1,231 @@ +//! Minimal global input-routing probe; no NBT editing or export. +//! cargo run --release --example port_route_probe -- single +//! cargo run --release --example port_route_probe -- fanout +use std::path::PathBuf; +use std::time::Instant; + +use redstone_compiler::ir::LogicalDesign; +use redstone_compiler::transform::place_and_route::global_pnr::candidate::UnitCandidateConfig; +use redstone_compiler::transform::place_and_route::global_pnr::{ + place_and_route_routable_design_with_visualization, GlobalPnrConfig, +}; +use redstone_compiler::transform::place_and_route::local_placer::{ + LocalPlacerConfig, PlacementSchedulePolicy, +}; +use redstone_compiler::transform::place_and_route::sampling::SamplingPolicy; +use redstone_compiler::world::position::DimSize; +use tracing_subscriber::prelude::*; +fn main() -> eyre::Result<()> { + let subscriber = tracing_subscriber::fmt() + .with_ansi(false) + .with_max_level(tracing::Level::DEBUG) + .finish() + .with(tracing_subscriber::filter::filter_fn(|meta| { + *meta.level() <= tracing::Level::INFO + || meta.target() == "redstone_compiler::global_pnr" + })); + tracing::subscriber::set_global_default(subscriber).ok(); + let mode = std::env::args().nth(1).unwrap_or_else(|| "single".into()); + let source = match mode.as_str() { + "single" => "module probe(a,y); input a; output y; assign y=~a; endmodule", + "fanout" => { + "module probe(a,b,y,z); input a,b; output y,z; assign y=~a; assign z=a&b; endmodule" + } + "chain" => { + "module probe(a,b,y); input a,b; output y; wire n; assign n=~a; assign y=n|b; endmodule" + } + _ => eyre::bail!("expected single, fanout, or chain"), + }; + let logical = LogicalDesign::from_verilog_source(source)?; + let routable = logical.lower_to_routable_with_bit_mapping_limits(1, 2)?; + let cache = PathBuf::from( + std::env::args() + .nth(2) + .unwrap_or_else(|| "target/alu-display/port-route-cache".into()), + ); + let mut config = GlobalPnrConfig { + candidate: UnitCandidateConfig { + dim: DimSize(8, 8, 4), + max_candidates: 4, + local_config: LocalPlacerConfig { + greedy_input_generation: true, + input_candidate_limit: Some(16), + schedule: PlacementSchedulePolicy::Auto, + placement_sampling_policy: LocalPlacerConfig::ranked_sampling(64, 0, 0), + max_route_step: 4, + route_step_sampling_policy: SamplingPolicy::Random(32), + not_route_step_sampling_policy: SamplingPolicy::Random(8), + ..Default::default() + }, + ..Default::default() + } + .into(), + candidate_cache_dir: Some(cache), + ..Default::default() + }; + config.search = + redstone_compiler::transform::place_and_route::global_pnr::policy::GlobalPnrPreset::Fast + .search_config(); + config.placement.max_attempts = 4; + let start = Instant::now(); + match place_and_route_routable_design_with_visualization(&routable, &config) { + Ok(result) => println!( + "probe={mode} success elapsed={:?} inputs={} outputs={}", + start.elapsed(), + result.placed_world.inputs.len(), + result.placed_world.outputs.len() + ), + Err(error) => { + eprintln!( + "probe={mode} FAILED elapsed={:?}: {error:?}", + start.elapsed() + ); + return Err(error); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use redstone_compiler::transform::place_and_route::detailed_router; + use redstone_compiler::transform::place_and_route::placed_node::PlacedNode; + use redstone_compiler::world::block::{Block, BlockKind, Direction}; + use redstone_compiler::world::position::Position; + use redstone_compiler::world::World3D; + + use super::*; + + #[test] + fn overhead_wire_can_cage_an_unrouted_dust_port() { + use redstone_compiler::transform::place_and_route::global_pnr::router::{ + route_point_to_point_with_strategy, GlobalRoutingStrategy, + }; + let mut world = World3D::new(DimSize(7, 5, 5)); + for x in 0..7 { + for y in 0..5 { + let p = Position(x, y, 0); + world[p] = PlacedNode::new_cobble(p).block; + } + } + let source = Position(2, 2, 1); + let sink = Position(4, 2, 2); + for p in [ + Position(2, 1, 1), + Position(2, 1, 2), + Position(2, 3, 1), + Position(2, 3, 2), + Position(3, 2, 1), + Position(4, 2, 1), + ] { + world[p] = PlacedNode::new_cobble(p).block; + } + world[Position(1, 2, 1)] = + PlacedNode::new_repeater(Position(1, 2, 1), Direction::West).block; + world[source] = PlacedNode::new_redstone(source).block; + world[sink] = PlacedNode::new_redstone(sink).block; + world.initialize_redstone_states(); + assert!( + route_point_to_point_with_strategy(&world, source, sink, GlobalRoutingStrategy::AStar) + .is_ok(), + "unroofed port must have an uphill exit" + ); + let roof = source.up(); + let support = detailed_router::try_generate_cobble_node(&world, roof, &[]) + .expect("existing rules permit roofing isolated dust"); + detailed_router::place_node(&mut world, support); + let overhead = PlacedNode::new_redstone(roof.up()); + assert!(!overhead.has_conflict(&world, &Default::default())); + assert!(!overhead.has_short(&world, &Default::default())); + detailed_router::place_node(&mut world, overhead); + assert!( + route_point_to_point_with_strategy(&world, source, sink, GlobalRoutingStrategy::AStar) + .is_err(), + "unrelated overhead wire consumes the future port escape" + ); + } + + #[test] + fn recorded_compact_output_needs_first_hop_repeater() { + // Failed source (43,4,2) neighborhood, translated by -40 on X. + let mut world = World3D::new(DimSize(12, 12, 6)); + for (x, y, z) in [ + (2, 4, 0), + (2, 3, 1), + (4, 3, 1), + (3, 4, 1), + (5, 4, 1), + (1, 3, 2), + (3, 3, 2), + (2, 4, 2), + (6, 4, 2), + (1, 5, 2), + (2, 5, 2), + ] { + world[Position(x, y, z)] = Block { + kind: BlockKind::Cobble { + on_count: 0, + on_base_count: 0, + }, + direction: Direction::None, + }; + } + for (x, y, z, state) in [ + (1, 4, 1, 3), + (3, 6, 1, 3), + (2, 3, 2, 11), + (4, 3, 2, 3), + (5, 3, 2, 10), + (5, 4, 2, 5), + (3, 3, 3, 3), + (2, 4, 3, 12), + (2, 5, 3, 6), + ] { + world[Position(x, y, z)] = Block { + kind: BlockKind::Redstone { + on_count: 0, + state, + strength: 0, + }, + direction: Direction::None, + }; + } + world[Position(2, 4, 1)] = + PlacedNode::new_repeater(Position(2, 4, 1), Direction::West).block; + let source = Position(3, 4, 2); + let sink = Position(8, 4, 2); + world[source] = Block { + kind: BlockKind::Torch { is_on: false }, + direction: Direction::Bottom, + }; + let mut dust = Vec::new(); + let mut repeaters = Vec::new(); + for bound in PlacedNode::new(source, world[source]).propagation_bound(Some(&world)) { + if let detailed_router::PlaceRedstoneResult::Placed(_, node) = + detailed_router::place_redstone_with_cobble(&world, bound, source, sink) + { + dust.push(node.position); + } + for direction in [ + Direction::West, + Direction::East, + Direction::North, + Direction::South, + ] { + if let detailed_router::PlaceRepeaterResult::Placed(_, node) = + detailed_router::place_repeater_with_cobble( + &world, bound, source, sink, direction, None, + ) + { + repeaters.push((node.position, direction)); + } + } + } + eprintln!("dust={dust:?}, repeaters={repeaters:?}"); + assert!(dust.is_empty()); + assert!( + !repeaters.is_empty(), + "a legal directional escape exists before signal attenuation" + ); + } +} diff --git a/examples/whole_decoder_construct.rs b/examples/whole_decoder_construct.rs new file mode 100644 index 00000000..12fee4e5 --- /dev/null +++ b/examples/whole_decoder_construct.rs @@ -0,0 +1,479 @@ +//! Whole decoder construction followed by separately saved/verified compaction. +//! Uses one flattened RTL graph; no cell mapper or global router. +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant}; + +use eyre::{ensure, ContextCompat}; +use redstone_compiler::ir::LogicalDesign; +use redstone_compiler::nbt::{NBTRoot, ToNBT}; +use redstone_compiler::output::PlacedWorld; +use redstone_compiler::transform::place_and_route::local_placer::exact::{ + CompactionConfig, ConstructionConfig, ExactLocalPlacer, GateOrder, +}; +use redstone_compiler::world::simulator::Simulator; +const MASKS: [u8; 16] = [ + 0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0, 0, 0, 0, 0, 0, +]; +fn index(name: &str, prefix: &str) -> eyre::Result { + Ok(name + .strip_prefix(prefix) + .and_then(|s| s.strip_prefix('_')) + .context("unexpected endpoint name")? + .parse()?) +} +fn check(sim: &mut Simulator, placed: &PlacedWorld, value: usize) -> eyre::Result<()> { + sim.advance_idle_cycles(61)?; + let inputs = placed + .inputs + .iter() + .map(|p| Ok((p.position(), value & (1 << index(&p.name, "digit")?) != 0))) + .collect::>>()?; + sim.drive_inputs_with_limits(inputs, 4096, 2_000_000)?; + for output in &placed.outputs { + let bit = index(&output.name, "seg")?; + ensure!( + sim.world()[output.position()].kind.is_powered() == (MASKS[value] & (1 << bit) != 0), + "input {value}, output {} mismatch", + output.name + ); + } + ensure!( + !sim.world() + .iter_block() + .iter() + .any(|(p, _)| sim.is_torch_burned_out(*p)), + "torch burnout" + ); + Ok(()) +} + +fn save_verify(directory: &Path, placed: &PlacedWorld) -> eyre::Result { + std::fs::create_dir_all(directory)?; + placed.world.to_nbt().save(directory.join("decoder.nbt")); + std::fs::write( + directory.join("interface.json"), + serde_json::to_vec_pretty( + &serde_json::json!({"inputs":placed.inputs,"outputs":placed.outputs}), + )?, + )?; + let world = NBTRoot::from_nbt_bytes(&std::fs::read(directory.join("decoder.nbt"))?)?.to_world(); + ensure!( + placed.inputs.len() == 4 && placed.outputs.len() == 7, + "unexpected physical interface" + ); + ensure!( + placed + .inputs + .iter() + .map(|p| p.name.clone()) + .collect::>() + == (0..4).map(|i| format!("digit_{i}")).collect() + && placed + .outputs + .iter() + .map(|p| p.name.clone()) + .collect::>() + == (0..7).map(|i| format!("seg_{i}")).collect(), + "physical endpoint names must cover every input and segment exactly once" + ); + for value in 0..16 { + let mut sim = Simulator::from_settled_with_limits_and_trace(&world, 4096, 2_000_000, 0)?; + check(&mut sim, placed, value)?; + } + let mut sim = Simulator::from_settled_with_limits_and_trace(&world, 4096, 2_000_000, 0)?; + for from in 0..16 { + for to in 0..16 { + check(&mut sim, placed, from)?; + check(&mut sim, placed, to)?; + } + } + let size = placed.world.size; + let result = serde_json::json!({"status":"verified","static_cases":16,"ordered_transitions":256,"transition_drives":512,"nbt_roundtrip":true,"blocks":placed.world.iter_block().len(),"dim":[size.0,size.1,size.2]}); + std::fs::write( + directory.join("verification.json"), + serde_json::to_vec_pretty(&result)?, + )?; + Ok(result) +} +fn main() -> eyre::Result<()> { + tracing_subscriber::fmt() + .with_ansi(false) + .with_max_level(tracing::Level::INFO) + .try_init() + .ok(); + let directory = PathBuf::from( + std::env::args() + .nth(1) + .unwrap_or("target/alu-display/whole-decoder-construct-4x6".into()), + ); + std::fs::create_dir_all(&directory)?; + ensure!( + !directory.join("attempt.json").exists(), + "use a fresh directory" + ); + let source = format!( + "{}\n{}", + include_str!("../test/alu_display/bcd_to_7seg_minimized.v"), + include_str!("../test/alu_display/bcd_digit_to_7seg.v") + ); + let logical = LogicalDesign::from_verilog_source(&source)?; + let graph = logical.lower_flat_logic_graph()?; + let truth = graph.truth_table()?; + ensure!( + truth.input_names == ["digit_0", "digit_1", "digit_2", "digit_3"] + && truth.output_tables.len() == 7, + "unexpected graph interface" + ); + for bit in 0..7 { + for value in 0..16 { + ensure!( + truth.output_tables[&format!("seg_{bit}")][value] + == (MASKS[value] & (1 << bit) != 0), + "logical oracle mismatch" + ); + } + } + let original = ExactLocalPlacer::new(&graph)?; + let original = if let Some(path) = std::env::var_os("CONSTRUCT_NETLIST") { + ExactLocalPlacer::from_netlist(redstone_compiler::transform::place_and_route::local_placer::exact::NorNetlist::from_text(&std::fs::read_to_string(path)?)?) + } else { + original + }; + let original = if std::env::var("CONSTRUCT_FLATTEN_POSITIVE").as_deref() == Ok("1") { + ExactLocalPlacer::from_netlist(original.netlist().flatten_positive_logic()?) + } else { + original + }; + let isolate = std::env::var("CONSTRUCT_ISOLATE_OUTPUTS").as_deref() == Ok("1"); + let original = if isolate { + ExactLocalPlacer::from_netlist(original.netlist().isolate_output_cones()?) + } else { + original + }; + let max_fanin = std::env::var("CONSTRUCT_MAX_FANIN") + .unwrap_or("0".into()) + .parse::()?; + let placer = if max_fanin == 0 { + original + } else { + ExactLocalPlacer::from_netlist(original.netlist().chain_wide_gates(max_fanin)?) + }; + let net_values = placer.netlist().net_values(); + let input_names = placer.netlist().input_names(); + ensure!( + input_names.len() == 4 && placer.netlist().outputs.len() == 7, + "unexpected chained interface" + ); + ensure!( + input_names == truth.input_names + && placer + .netlist() + .outputs + .iter() + .map(|(name, _)| name.clone()) + .collect::>() + == (0..7).map(|i| format!("seg_{i}")).collect(), + "netlist must expose every decoder endpoint exactly once" + ); + for mask in 0..16 { + let mut digit = 0; + for (i, name) in input_names.iter().enumerate() { + if mask & (1 << i) != 0 { + digit |= 1 << index(name, "digit")?; + } + } + for (name, net) in &placer.netlist().outputs { + ensure!( + net_values[*net][mask] == (MASKS[digit] & (1 << index(name, "seg")?) != 0), + "chained netlist oracle mismatch" + ); + } + } + let nor_gates = placer + .netlist() + .nets + .iter() + .filter(|n| { + matches!( + n.driver, + redstone_compiler::transform::place_and_route::local_placer::exact::NetDriver::Gate + ) + }) + .count(); + let or_nets = placer + .netlist() + .nets + .iter() + .filter(|n| { + matches!( + n.driver, + redstone_compiler::transform::place_and_route::local_placer::exact::NetDriver::Or + ) + }) + .count(); + + std::fs::write(directory.join("source.v"), source)?; + std::fs::write(directory.join("logical.rcir"), logical.to_string())?; + std::fs::write(directory.join("nor.txt"), placer.netlist().to_text())?; + let mut config = ConstructionConfig { + width: 4, + height: 6, + gate_order: GateOrder::MinLive, + early_outputs: true, + step_time_limit: Duration::from_secs(10), + restart_after: Some(Duration::from_secs(120)), + max_restarts: 0, + workers: 1, + seed: 1, + step_optimize: None, + progress: Some(directory.join("construction-progress")), + checkpoint: Some(directory.join("checkpoint.json")), + resume: std::env::var_os("CONSTRUCT_RESUME").map(PathBuf::from), + ..Default::default() + }; + let number = |name: &str, fallback: u64| -> eyre::Result { + Ok(std::env::var(name) + .ok() + .map(|s| s.parse()) + .transpose()? + .unwrap_or(fallback)) + }; + config.max_restarts = number("CONSTRUCT_RESTARTS", 0)? as usize; + config.overlap = number("CONSTRUCT_OVERLAP", 1)? as usize; + config.max_overlap = config.overlap; + config.max_overlap = number("CONSTRUCT_MAX_OVERLAP", config.overlap as u64)? as usize; + config.window = number("CONSTRUCT_WINDOW", 3)? as usize; + config.max_window = number("CONSTRUCT_MAX_WINDOW", 6)? as usize; + config.max_backtracks = number("CONSTRUCT_BACKTRACKS", 0)? as usize; + config.step_vocabulary = number("CONSTRUCT_STEP_VOCABULARY", 0)? != 0; + config.partial_or_vocabulary = number("CONSTRUCT_PARTIAL_ORS", 0)? != 0; + config.driving_seams = number("CONSTRUCT_DRIVING_SEAMS", 0)? != 0; + config.compress_complements = number("CONSTRUCT_COMPRESS_COMPLEMENTS", 0)? != 0; + config.scaffold_seams = number("CONSTRUCT_SCAFFOLD_SEAMS", 0)? != 0; + config.seam_pitch = number("CONSTRUCT_SEAM_PITCH", 0)? as usize; + config.seam_primary_stride = number("CONSTRUCT_PRIMARY_STRIDE", 1)? as usize; + config.gate_scaffold = number("CONSTRUCT_GATE_SCAFFOLD", 0)? != 0; + config.gate_batch = number("CONSTRUCT_GATE_BATCH", 1)? as usize; + config.rank_levels = number("CONSTRUCT_RANK_LEVELS", 24)? as usize; + config.stage_levels = number("CONSTRUCT_STAGE_LEVELS", 24)? as usize; + config.tuning.sim_max_cycles = number("CONSTRUCT_SIM_CYCLES", 4096)? as usize; + config.tuning.sim_max_events = number("CONSTRUCT_SIM_EVENTS", 2_000_000)? as usize; + for (env, parameter) in [ + ("CONSTRUCT_CANONICAL_RANKS", "canonical_source_ranks"), + ("CONSTRUCT_SYMMETRY", "symmetry_breaking"), + ("CONSTRUCT_REPEATER_SEAMS", "repeater_driving_outputs"), + ] { + if let Ok(value) = std::env::var(env) { + config + .model_params + .insert(parameter.into(), rsdsl::IValue::Bool(value == "1")); + } + } + if let Ok(value) = std::env::var("CONSTRUCT_CASE_RANK_LEVELS") { + config.model_params.insert( + "case_rank_levels".into(), + rsdsl::IValue::Int(value.parse()?), + ); + } + config.given_frozen_signals = number("CONSTRUCT_GIVEN_SIGNALS", 1)? != 0; + config.width = number("CONSTRUCT_WIDTH", 4)? as usize; + config.height = number("CONSTRUCT_HEIGHT", 6)? as usize; + config.workers = number("CONSTRUCT_WORKERS", 1)? as usize; + ensure!( + config.width > 0 && config.height > 0 && config.workers > 0, + "invalid dimensions/workers" + ); + config.step_time_limit = Duration::from_secs(number("CONSTRUCT_STEP_SECONDS", 10)?); + config.restart_after = Some(Duration::from_secs(number( + "CONSTRUCT_BUDGET_SECONDS", + 120, + )?)); + let optimize_ms = number("CONSTRUCT_STEP_OPT_MS", 0)?; + config.step_optimize = (optimize_ms > 0).then(|| Duration::from_millis(optimize_ms)); + let delete_ms = number("CONSTRUCT_DELETE_MS", 0)?; + config.step_delete = (delete_ms > 0).then(|| Duration::from_millis(delete_ms)); + config.gate_order = match std::env::var("CONSTRUCT_ORDER") + .as_deref() + .unwrap_or("min-live") + { + "min-live" => GateOrder::MinLive, + "smallest-cone" => GateOrder::SmallestConeFirst, + "net-index" => GateOrder::NetIndex, + value => eyre::bail!("unknown construction order {value}"), + }; + let output_mode = std::env::var("CONSTRUCT_OUTPUT_MODE").unwrap_or("anywhere".into()); + ensure!( + output_mode == "anywhere" || output_mode == "max-y", + "unknown output mode" + ); + if output_mode == "max-y" { + for name in truth.output_tables.keys() { + config.output_policies.insert(name.clone(), redstone_compiler::transform::place_and_route::local_placer::exact::OutputPolicy::MaxYFace); + } + } + std::fs::write( + directory.join("construction-config.txt"), + format!("{config:#?}"), + )?; + let mut report = serde_json::json!({"mode":"whole-decoder-construct-compact","output_contract":if output_mode == "anywhere" {"anywhere-observable"} else {"max-y-driving"},"status":"constructing","logical_static_cases":16,"inputs":4,"outputs":7,"nor_gates":nor_gates,"or_nets":or_nets,"construction_steps":nor_gates+or_nets,"max_fanin":max_fanin,"netlist_static_cases":16,"overlap":config.overlap,"width":config.width,"height":config.height,"window":config.window,"max_window":config.max_window,"gate_order":format!("{:?}",config.gate_order),"early_outputs":true,"workers":config.workers,"seed":1,"step_seconds":config.step_time_limit.as_secs(),"construction_budget_seconds":config.restart_after.unwrap().as_secs(),"max_restarts":config.max_restarts,"restart_seed_stride":config.restart_seed_stride,"step_optimize_ms":optimize_ms}); + report["strategy"] = serde_json::json!({"isolate_outputs":isolate,"flatten_positive":std::env::var("CONSTRUCT_FLATTEN_POSITIVE").as_deref()==Ok("1"),"netlist_override":std::env::var("CONSTRUCT_NETLIST").ok(),"resume":config.resume.as_ref().map(|p|p.display().to_string()),"gate_batch":config.gate_batch,"step_vocabulary":config.step_vocabulary,"partial_ors":config.partial_or_vocabulary,"compress_complements":config.compress_complements,"driving_seams":config.driving_seams,"scaffold_seams":config.scaffold_seams,"given_frozen_signals":config.given_frozen_signals,"delete_ms":delete_ms,"rank_levels":config.rank_levels,"stage_levels":config.stage_levels,"model_params":format!("{:?}",config.model_params)}); + report["strategy"]["gate_scaffold"] = config.gate_scaffold.into(); + report["strategy"]["seam_pitch"] = config.seam_pitch.into(); + report["strategy"]["primary_stride"] = config.seam_primary_stride.into(); + report["strategy"]["sim_max_cycles"] = config.tuning.sim_max_cycles.into(); + let write_report = |report: &serde_json::Value| -> eyre::Result<()> { + std::fs::write( + directory.join("attempt.json"), + serde_json::to_vec_pretty(report)?, + )?; + Ok(()) + }; + write_report(&report)?; + let started = Instant::now(); + let (layout, baseline) = if let Some(path) = std::env::var_os("CONSTRUCT_COMPACT_ONLY") { + let source = PathBuf::from(path); + let checkpoint: serde_json::Value = + serde_json::from_slice(&std::fs::read(source.join("checkpoint.json"))?)?; + ensure!( + checkpoint["netlist"].as_str() == Some(placer.netlist().to_text().as_str()), + "compact checkpoint netlist mismatch" + ); + let document: redstone_compiler::physical_cell::PhysicalCellDocument = checkpoint["rcell"] + .as_str() + .context("checkpoint RCELL missing")? + .parse()?; + let mut layout=redstone_compiler::transform::place_and_route::local_placer::exact::ExactLayout::from_rcell(&document)?; + let signals: Vec<([usize; 3], u64)> = + serde_json::from_value(checkpoint["signals"].clone())?; + layout.signals = signals + .into_iter() + .map(|([x, y, z], f)| (redstone_compiler::world::position::Position(x, y, z), f)) + .collect(); + let interface: serde_json::Value = + serde_json::from_slice(&std::fs::read(source.join("constructed/interface.json"))?)?; + let world = + NBTRoot::from_nbt_bytes(&std::fs::read(source.join("constructed/decoder.nbt"))?)? + .to_world(); + ensure!( + world.blocks.iter().all(|(p, _)| layout.dim.bound_on(*p)), + "NBT block outside checkpoint bounds" + ); + let mut restored = redstone_compiler::world::World3D::new(layout.dim); + for (p, block) in &world.blocks { + restored[*p] = *block; + } + let placed = PlacedWorld { + world: restored, + inputs: serde_json::from_value(interface["inputs"].clone())?, + outputs: serde_json::from_value(interface["outputs"].clone())?, + }; + ensure!( + placed.world.size == layout.dim, + "checkpoint/NBT dimensions differ" + ); + let baseline = + redstone_compiler::transform::place_and_route::local_placer::exact::ExactPlacement { + block_count: layout.cells.len().saturating_sub(placed.inputs.len()), + placed, + cells: layout.cells.iter().map(|(p, k)| (*p, *k)).collect(), + signals: layout.signals.iter().map(|(p, f)| (*p, *f)).collect(), + delays: Default::default(), + rcell: document, + }; + report["compact_source"] = source.display().to_string().into(); + (layout, baseline) + } else { + let (layout, baseline, construction) = match placer.construct(&config) { + Ok(value) => value, + Err(error) => { + report["status"] = "construction-failed".into(); + report["error"] = format!("{error:#}").into(); + report["construct_ms"] = (started.elapsed().as_millis() as u64).into(); + write_report(&report)?; + println!("RESULT {report}"); + return Ok(()); + } + }; + report["construct_ms"] = (started.elapsed().as_millis() as u64).into(); + std::fs::write( + directory.join("construction-report.txt"), + format!("{construction:#?}"), + )?; + (layout, baseline) + }; + match save_verify(&directory.join("constructed"), &baseline.placed) { + Ok(result) => report["constructed"] = result, + Err(error) => { + report["status"] = "construction-verification-failed".into(); + report["error"] = format!("{error:#}").into(); + write_report(&report)?; + return Ok(()); + } + } + report["status"] = "compacting".into(); + write_report(&report)?; + let compact = CompactionConfig { + minimize_blocks: false, + repair_optimize: None, + tuning: config.tuning.clone(), + rank_levels: config.rank_levels, + stage_levels: config.stage_levels, + model_params: config.model_params.clone(), + extra_signal_functions: layout.signals.values().copied().collect(), + output_policies: config.output_policies.clone(), + time_limit: Some(Duration::from_secs(60)), + attempt_time_limit: Duration::from_secs(5), + workers: config.workers, + seed: 1, + progress: Some(directory.join("compaction-progress")), + ..Default::default() + }; + std::fs::write( + directory.join("compaction-config.txt"), + format!("{compact:#?}"), + )?; + let start = Instant::now(); + match placer.compact(layout, &compact) { + Ok((_layout, best, details)) => { + std::fs::write( + directory.join("compaction-report.txt"), + format!("{details:#?}"), + )?; + let improved = best.as_ref().filter(|candidate| { + candidate.cells.len() < baseline.cells.len() + || (candidate.cells.len() == baseline.cells.len() + && candidate.placed.world.size.0 + * candidate.placed.world.size.1 + * candidate.placed.world.size.2 + < baseline.placed.world.size.0 + * baseline.placed.world.size.1 + * baseline.placed.world.size.2) + }); + if let Some(candidate) = &best { + let s = candidate.placed.world.size; + report["compaction_candidate"] = + serde_json::json!({"blocks":candidate.cells.len(),"dim":[s.0,s.1,s.2]}); + } + let selected = improved.unwrap_or(&baseline); + match save_verify(&directory.join("compacted"), &selected.placed) { + Ok(result) => { + report["compacted"] = result; + report["status"] = "verified".into(); + report["compaction_produced_new_placement"] = improved.is_some().into(); + } + Err(error) => { + report["status"] = "compaction-verification-failed".into(); + report["error"] = format!("{error:#}").into(); + } + } + } + Err(error) => { + report["status"] = "compaction-failed-baseline-preserved".into(); + report["error"] = format!("{error:#}").into(); + } + } + report["compact_ms"] = (start.elapsed().as_millis() as u64).into(); + write_report(&report)?; + println!("RESULT {report}"); + Ok(()) +} diff --git a/examples/whole_decoder_exact.rs b/examples/whole_decoder_exact.rs new file mode 100644 index 00000000..801fcaa4 --- /dev/null +++ b/examples/whole_decoder_exact.rs @@ -0,0 +1,241 @@ +//! One simultaneous exact SAT model for the whole four-input seven-output decoder. +//! No physical cells, global routing, or window decomposition. +//! Usage: whole_decoder_exact FRESH_DIRECTORY XxYxZ SOLVER_SECONDS +//! WHOLE_WORKERS defaults to 1; WHOLE_PIN_CONTRACT=faces restricts input/output faces. +//! Solver seconds exclude encoding; orchestrators should enforce a wall timeout. +use std::path::PathBuf; +use std::time::{Duration, Instant}; + +use eyre::{ensure, ContextCompat}; +use redstone_compiler::ir::LogicalDesign; +use redstone_compiler::nbt::{NBTRoot, ToNBT}; +use redstone_compiler::output::PlacedWorld; +use redstone_compiler::transform::place_and_route::local_placer::exact::{ + ExactLocalPlacer, ExactOutcome, ExactPlacerConfig, NetDriver, +}; +use redstone_compiler::world::position::DimSize; +use redstone_compiler::world::simulator::Simulator; +const MASKS: [u8; 16] = [ + 0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0, 0, 0, 0, 0, 0, +]; +fn index(name: &str, prefix: &str) -> eyre::Result { + Ok(name + .strip_prefix(prefix) + .and_then(|s| s.strip_prefix('_')) + .context("unexpected endpoint name")? + .parse()?) +} +fn check(sim: &mut Simulator, placed: &PlacedWorld, value: usize) -> eyre::Result<()> { + sim.advance_idle_cycles(61)?; + let inputs = placed + .inputs + .iter() + .map(|p| Ok((p.position(), value & (1 << index(&p.name, "digit")?) != 0))) + .collect::>>()?; + sim.drive_inputs_with_limits(inputs, 4096, 2_000_000)?; + for output in &placed.outputs { + let bit = index(&output.name, "seg")?; + ensure!( + sim.world()[output.position()].kind.is_powered() == (MASKS[value] & (1 << bit) != 0), + "input {value}, output {} mismatch", + output.name + ); + } + ensure!( + !sim.world() + .iter_block() + .iter() + .any(|(p, _)| sim.is_torch_burned_out(*p)), + "torch burnout" + ); + Ok(()) +} +fn main() -> eyre::Result<()> { + let args = std::env::args().collect::>(); + let directory = PathBuf::from( + args.get(1) + .map(String::as_str) + .unwrap_or("target/alu-display/whole-decoder-exact"), + ); + let dims = args + .get(2) + .map(String::as_str) + .unwrap_or("3x8x4") + .split('x') + .map(str::parse::) + .collect::, _>>()?; + ensure!(dims.len() == 3, "expected XxYxZ"); + let seconds = args + .get(3) + .map(String::as_str) + .unwrap_or("30") + .parse::()?; + std::fs::create_dir_all(&directory)?; + ensure!( + !directory.join("attempt.json").exists(), + "use a fresh attempt directory" + ); + let source = format!( + "{}\n{}", + include_str!("../test/alu_display/bcd_to_7seg_minimized.v"), + include_str!("../test/alu_display/bcd_digit_to_7seg.v") + ); + let logical = LogicalDesign::from_verilog_source(&source)?; + std::fs::write(directory.join("source.v"), source)?; + std::fs::write(directory.join("logical.rcir"), logical.to_string())?; + let graph = logical.lower_flat_logic_graph()?; + let truth = graph.truth_table()?; + ensure!( + truth.input_names.len() == 4 && truth.output_tables.len() == 7, + "expected four inputs/seven outputs" + ); + for mask in 0..16 { + let mut digit = 0; + for (i, name) in truth.input_names.iter().enumerate() { + if mask & (1 << i) != 0 { + digit |= 1 << index(name, "digit")?; + } + } + for (name, values) in &truth.output_tables { + ensure!( + values[mask] == (MASKS[digit] & (1 << index(name, "seg")?) != 0), + "flattened graph truth mismatch" + ); + } + } + let placer = ExactLocalPlacer::new(&graph)?; + std::fs::write(directory.join("graph.txt"), format!("{graph:#?}"))?; + std::fs::write( + directory.join("nor.txt"), + format!("{:#?}", placer.netlist()), + )?; + let mut config = ExactPlacerConfig::new(DimSize(dims[0], dims[1], dims[2])); + config.time_limit = Some(Duration::from_secs(seconds)); + config.workers = std::env::var("WHOLE_WORKERS") + .unwrap_or("1".into()) + .parse()?; + ensure!(config.workers > 0, "workers must be positive"); + config.seed = 1; + config.optimize = false; + config.driving_outputs = truth.output_tables.keys().cloned().collect(); + let pin_contract = std::env::var("WHOLE_PIN_CONTRACT").unwrap_or("free".into()); + ensure!( + pin_contract == "free" || pin_contract == "faces", + "expected free or faces pin contract" + ); + if pin_contract == "faces" { + use redstone_compiler::world::block::Direction; + use redstone_compiler::world::position::Position; + ensure!( + dims[0] >= 5 && dims[2] >= 5, + "faces contract needs X,Z >= 5" + ); + for (bit, (x, z)) in [(1, 1), (3, 1), (1, 3), (3, 3)].into_iter().enumerate() { + config.input_sites.insert( + format!("digit_{bit}"), + vec![(Position(x, 0, z), Direction::North)], + ); + } + let output_y = dims[1] - 1; + let sites = (0..dims[0]) + .flat_map(|x| (0..dims[2]).map(move |z| Position(x, output_y, z))) + .collect::>(); + for name in truth.output_tables.keys() { + config.output_sites.insert(name.clone(), sites.clone()); + } + } + + let functions = placer.netlist().net_values(); + let unique_functions = functions + .iter() + .cloned() + .collect::>() + .len(); + let start = Instant::now(); + let mut report = serde_json::json!({"mode":"whole-model-exact","pin_contract":pin_contract,"inputs":4,"outputs":7,"logical_static_cases":16,"net_functions":functions,"unique_net_functions":unique_functions,"nor_nets":placer.netlist().nets.len(),"nor_gates":placer.netlist().nets.iter().filter(|n|n.driver==NetDriver::Gate).count(),"dim":dims,"time_limit_seconds":seconds,"workers":config.workers,"seed":1,"optimize":false,"status":"running"}); + report["input_sites"] = serde_json::to_value(config.input_sites.iter().map(|(name, sites)| (name, sites.iter().map(|(p,d)| serde_json::json!({"position":[p.0,p.1,p.2],"attach":format!("{d:?}")})).collect::>())).collect::>())?; + report["output_sites"] = serde_json::to_value( + config + .output_sites + .iter() + .map(|(name, sites)| { + ( + name, + sites.iter().map(|p| [p.0, p.1, p.2]).collect::>(), + ) + }) + .collect::>(), + )?; + std::fs::write( + directory.join("attempt.json"), + serde_json::to_vec_pretty(&report)?, + )?; + println!("START dim={dims:?} inputs=4 outputs=7 NOR={} unique_functions={unique_functions} solver_limit={seconds}s", placer.netlist().gates().count()); + let (outcome, stats) = placer.place(&config)?; + report["wall_ms"] = (start.elapsed().as_millis() as u64).into(); + report["encode_ms"] = (stats.encode_time.as_millis() as u64).into(); + report["solve_ms"] = (stats.solve_time.as_millis() as u64).into(); + report["variables"] = stats.variables.into(); + report["clauses"] = stats.clauses.into(); + report["refinements"] = stats.refinements.into(); + match outcome { + ExactOutcome::Unknown { last_rejection } => { + report["status"] = "unknown".into(); + report["last_rejection"] = last_rejection.into(); + } + ExactOutcome::Infeasible => report["status"] = "infeasible".into(), + ExactOutcome::Placed(placement) => { + let placed = &placement.placed; + placed.world.to_nbt().save(directory.join("decoder.nbt")); + std::fs::write( + directory.join("interface.json"), + serde_json::to_vec_pretty( + &serde_json::json!({"inputs":placed.inputs,"outputs":placed.outputs}), + )?, + )?; + let world = + NBTRoot::from_nbt_bytes(&std::fs::read(directory.join("decoder.nbt"))?)?.to_world(); + let verified = (|| -> eyre::Result<()> { + ensure!( + placed.inputs.len() == 4 && placed.outputs.len() == 7, + "unexpected physical endpoints" + ); + for value in 0..16 { + let mut sim = + Simulator::from_settled_with_limits_and_trace(&world, 4096, 2_000_000, 0)?; + check(&mut sim, placed, value)?; + } + let mut sim = + Simulator::from_settled_with_limits_and_trace(&world, 4096, 2_000_000, 0)?; + for from in 0..16 { + for to in 0..16 { + check(&mut sim, placed, from)?; + check(&mut sim, placed, to)?; + } + } + Ok(()) + })(); + report["blocks"] = placed.world.iter_block().len().into(); + match verified { + Ok(()) => { + report["status"] = "verified".into(); + report["static_cases"] = 16.into(); + report["ordered_transitions"] = 256.into(); + report["nbt_roundtrip"] = true.into(); + } + Err(err) => { + report["status"] = "verification-failed".into(); + report["error"] = format!("{err:#}").into(); + } + } + } + } + std::fs::write( + directory.join("attempt.json"), + serde_json::to_vec_pretty(&report)?, + )?; + let mut concise = report.clone(); + concise.as_object_mut().unwrap().remove("net_functions"); + println!("RESULT {concise}"); + Ok(()) +} diff --git a/examples/whole_decoder_resynthesize.rs b/examples/whole_decoder_resynthesize.rs new file mode 100644 index 00000000..e955e367 --- /dev/null +++ b/examples/whole_decoder_resynthesize.rs @@ -0,0 +1,143 @@ +//! Synthesize each RTL decoder output with bounded frontier/2-input NOR/OR, +//! then share only the four primary inputs between output cones. +use std::collections::BTreeMap; +use std::path::PathBuf; +use std::time::Duration; + +use eyre::ensure; +use redstone_compiler::ir::LogicalDesign; +use redstone_compiler::transform::place_and_route::local_placer::exact::{ + synthesize, Net, NetDriver, NorNetlist, SynthesisOptions, SynthesisOutcome, +}; + +fn main() -> eyre::Result<()> { + let directory = PathBuf::from( + std::env::args() + .nth(1) + .unwrap_or("target/alu-display/decoder-resynthesis".into()), + ); + std::fs::create_dir_all(&directory)?; + let source = format!( + "{}\n{}", + include_str!("../test/alu_display/bcd_to_7seg_minimized.v"), + include_str!("../test/alu_display/bcd_digit_to_7seg.v") + ); + let graph = LogicalDesign::from_verilog_source(&source)?.lower_flat_logic_graph()?; + let truth = graph.truth_table()?; + let inputs = truth + .input_names + .iter() + .map(String::as_str) + .collect::>(); + let mut combined = NorNetlist { + nets: inputs + .iter() + .enumerate() + .map(|(i, name)| Net { + name: name.to_string(), + node_id: i, + driver: NetDriver::Input(name.to_string()), + gate_inputs: Vec::new(), + }) + .collect(), + outputs: Vec::new(), + state: Vec::new(), + }; + let mut reports = Vec::new(); + for (name, table) in &truth.output_tables { + let expression = table + .iter() + .enumerate() + .filter(|(_, on)| **on) + .map(|(case, _)| { + format!( + "({})", + inputs + .iter() + .enumerate() + .map(|(i, n)| format!("{}{n}", if case & (1 << i) == 0 { "~" } else { "" })) + .collect::>() + .join("&") + ) + }) + .collect::>() + .join("|"); + let cache = directory.join(format!("{name}.nor")); + let netlist = if cache.exists() { + NorNetlist::from_text(&std::fs::read_to_string(&cache)?)? + } else { + let mut found = None; + for steps in [5, 6, 7, 8, 10, 12, 14, 16, 18] { + let options = SynthesisOptions { + steps, + max_live: 4, + max_depth: None, + time_limit: Duration::from_secs(15), + }; + let started = std::time::Instant::now(); + let outcome = synthesize(&inputs, &[(name, &expression)], &options)?; + eprintln!( + "{name}: steps={steps} seconds={:.3} outcome={:?}", + started.elapsed().as_secs_f64(), + outcome + ); + if let SynthesisOutcome::Found(result) = outcome { + reports.push(serde_json::json!({"output":name,"steps":steps,"torches":result.torches,"proven":result.proven})); + std::fs::write(&cache, result.netlist.to_text())?; + found = Some(result.netlist); + break; + } + } + found.ok_or_else(|| eyre::eyre!("no bounded synthesis for {name}"))? + }; + let values = netlist.net_values(); + let target = netlist + .outputs + .iter() + .find(|(n, _)| n == name) + .ok_or_else(|| eyre::eyre!("missing output"))? + .1; + ensure!( + netlist.input_names() == truth.input_names && values[target] == *table, + "resynthesis oracle mismatch for {name}" + ); + let mut remap = BTreeMap::new(); + for (id, net) in netlist.nets.iter().enumerate() { + if let NetDriver::Input(input) = &net.driver { + remap.insert(id, inputs.iter().position(|n| *n == input).unwrap()); + } else { + let mut copied = net.clone(); + copied.name = if id == target { + name.clone() + } else { + format!("{name}_{}", net.name) + }; + copied.node_id = combined.nets.len(); + copied.gate_inputs = net.gate_inputs.iter().map(|input| remap[input]).collect(); + remap.insert(id, combined.nets.len()); + combined.nets.push(copied); + } + } + combined.outputs.push((name.clone(), remap[&target])); + } + let values = combined.net_values(); + for (name, net) in &combined.outputs { + ensure!( + values[*net] == truth.output_tables[name], + "combined oracle mismatch" + ); + } + let output = directory.join("combined.nor"); + std::fs::write(&output, combined.to_text())?; + std::fs::write( + directory.join("synthesis.json"), + serde_json::to_vec_pretty(&reports)?, + )?; + println!( + "{}: {} gates, {} nets", + output.display(), + combined.gates().count(), + combined.nets.len() + ); + Ok(()) +} diff --git a/examples/world_clone_benchmark.rs b/examples/world_clone_benchmark.rs new file mode 100644 index 00000000..86141772 --- /dev/null +++ b/examples/world_clone_benchmark.rs @@ -0,0 +1,258 @@ +//! Optional storage diagnostic, not a timing test. +//! cargo run --release --example world_clone_benchmark -- 1000 +use std::collections::BTreeMap; +use std::hint::black_box; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use redstone_compiler::world::block::{Block, BlockKind}; +use redstone_compiler::world::position::{DimSize, Position, PositionIndex}; +use redstone_compiler::world::{World, World3D}; + +// Previous conversion algorithm, using the same current COW storage so this +// comparison isolates sparse construction from the earlier clone optimization. +fn dense_lookup_constructor(source: &World) -> World3D { + let mut blocks = BTreeMap::new(); + for (position, block) in &source.blocks { + blocks.insert(position.index(&source.size), *block); + } + let map = (0..source.size.2) + .map(|z| { + Arc::new( + (0..source.size.1) + .map(|y| { + Arc::new( + (0..source.size.0) + .map(|x| { + blocks + .get(&PositionIndex( + x + y * source.size.0 + + z * source.size.0 * source.size.1, + )) + .copied() + .unwrap_or_default() + }) + .collect(), + ) + }) + .collect(), + ) + }) + .collect(); + World3D { + size: source.size, + map, + } +} + +fn sparse_constructor(source: &World) -> World3D { + World3D::from(source) +} + +fn construction_run( + source: &World, + count: usize, + constructor: fn(&World) -> World3D, +) -> (Duration, usize) { + let start = Instant::now(); + let mut checksum = 0; + for _ in 0..count { + let world = constructor(black_box(source)); + black_box(&world); + checksum += usize::from(world[Position(0, 0, 0)].kind.is_powered()); + } + (start.elapsed(), checksum) +} + +// The previous World3D representation, retained here only for the A/B baseline. +#[derive(Clone)] +struct DeepWorld { + map: Vec>>, +} + +fn mutations(iteration: usize) -> [(Position, Block); 2] { + [ + ( + Position(iteration % 64, (iteration / 64) % 64, iteration % 8), + Block { + kind: BlockKind::Switch { is_on: true }, + ..Default::default() + }, + ), + ( + Position( + (iteration + 7) % 64, + (iteration / 64 + 1) % 64, + (iteration + 1) % 8, + ), + Block { + kind: BlockKind::RedstoneBlock, + ..Default::default() + }, + ), + ] +} + +fn deep_run(source: &DeepWorld, count: usize) -> (Duration, usize) { + let start = Instant::now(); + let mut checksum = 0; + for iteration in 0..count { + let mut branch = black_box(source).clone(); + for (p, block) in mutations(iteration) { + branch.map[p.2][p.1][p.0] = block; + } + black_box(&branch); + for (p, _) in mutations(iteration) { + checksum += usize::from(branch.map[p.2][p.1][p.0].kind.is_powered()); + } + } + (start.elapsed(), checksum) +} + +// The immediately previous row-only COW representation. +#[derive(Clone)] +struct RowCowWorld { + map: Vec>>>, +} + +fn row_cow_run(source: &RowCowWorld, count: usize) -> (Duration, usize) { + let start = Instant::now(); + let mut checksum = 0; + for iteration in 0..count { + let mut branch = black_box(source).clone(); + for (p, block) in mutations(iteration) { + Arc::make_mut(&mut branch.map[p.2][p.1])[p.0] = block; + } + black_box(&branch); + for (p, _) in mutations(iteration) { + checksum += usize::from(branch.map[p.2][p.1][p.0].kind.is_powered()); + } + } + (start.elapsed(), checksum) +} + +fn cow_run(source: &World3D, count: usize) -> (Duration, usize) { + let start = Instant::now(); + let mut checksum = 0; + for iteration in 0..count { + let mut branch = black_box(source).clone(); + for (p, block) in mutations(iteration) { + branch[p] = block; + } + black_box(&branch); + for (p, _) in mutations(iteration) { + checksum += usize::from(branch[p].kind.is_powered()); + } + } + (start.elapsed(), checksum) +} + +fn main() -> eyre::Result<()> { + let count = std::env::args() + .nth(1) + .map(|arg| arg.parse()) + .transpose()? + .unwrap_or(1000); + eyre::ensure!(count > 0, "iteration count must be positive"); + let mut source = World3D::new(DimSize(64, 64, 8)); + for z in 0..8 { + for y in 0..64 { + source[Position((y + z) % 64, y, z)].kind = BlockKind::RedstoneBlock; + } + } + let deep = DeepWorld { + map: source + .map + .iter() + .map(|plane| plane.iter().map(|row| row.as_ref().clone()).collect()) + .collect(), + }; + let mut old_times = Vec::new(); + let mut new_times = Vec::new(); + for sample in 0..5 { + let (old, new) = if sample % 2 == 0 { + (deep_run(&deep, count), cow_run(&source, count)) + } else { + let new = cow_run(&source, count); + (deep_run(&deep, count), new) + }; + assert_eq!(old.1, count * 2); + assert_eq!(old.1, new.1); + old_times.push(old.0); + new_times.push(new.0); + println!( + "sample={sample} deep_ms={:.3} cow_ms={:.3} checksum={}", + old.0.as_secs_f64() * 1000.0, + new.0.as_secs_f64() * 1000.0, + new.1 + ); + } + old_times.sort(); + new_times.sort(); + println!("world=64x64x8 clones={count} edits_per_clone=2 samples=5 median_deep_ms={:.3} median_cow_ms={:.3} speedup={:.2}x", old_times[2].as_secs_f64()*1000.0, new_times[2].as_secs_f64()*1000.0, old_times[2].as_secs_f64()/new_times[2].as_secs_f64()); + let row_source = RowCowWorld { + map: source + .map + .iter() + .map(|plane| plane.as_ref().clone()) + .collect(), + }; + old_times.clear(); + new_times.clear(); + for sample in 0..5 { + let (old, new) = if sample % 2 == 0 { + (row_cow_run(&row_source, count), cow_run(&source, count)) + } else { + let new = cow_run(&source, count); + (row_cow_run(&row_source, count), new) + }; + assert_eq!(old.1, count * 2); + assert_eq!(old.1, new.1); + old_times.push(old.0); + new_times.push(new.0); + println!( + "plane_sample={sample} row_cow_ms={:.3} plane_row_cow_ms={:.3} checksum={}", + old.0.as_secs_f64() * 1000.0, + new.0.as_secs_f64() * 1000.0, + new.1 + ); + } + old_times.sort(); + new_times.sort(); + println!("world=64x64x8 clones={count} edits_per_clone=2 samples=5 median_row_cow_ms={:.3} median_plane_row_cow_ms={:.3} speedup={:.2}x",old_times[2].as_secs_f64()*1000.0,new_times[2].as_secs_f64()*1000.0,old_times[2].as_secs_f64()/new_times[2].as_secs_f64()); + let sparse = World::from(&source); + assert_eq!( + dense_lookup_constructor(&sparse).iter_block(), + World3D::from(&sparse).iter_block() + ); + old_times.clear(); + new_times.clear(); + for sample in 0..5 { + let (old, new) = if sample % 2 == 0 { + ( + construction_run(&sparse, count, dense_lookup_constructor), + construction_run(&sparse, count, sparse_constructor), + ) + } else { + let new = construction_run(&sparse, count, sparse_constructor); + ( + construction_run(&sparse, count, dense_lookup_constructor), + new, + ) + }; + assert_eq!(old.1, count); + assert_eq!(old.1, new.1); + old_times.push(old.0); + new_times.push(new.0); + println!( + "construction_sample={sample} dense_lookup_ms={:.3} sparse_write_ms={:.3} checksum={}", + old.0.as_secs_f64() * 1000.0, + new.0.as_secs_f64() * 1000.0, + new.1 + ); + } + old_times.sort(); + new_times.sort(); + println!("world=64x64x8 blocks={} constructions={count} samples=5 median_dense_lookup_ms={:.3} median_sparse_write_ms={:.3} speedup={:.2}x",sparse.blocks.len(),old_times[2].as_secs_f64()*1000.0,new_times[2].as_secs_f64()*1000.0,old_times[2].as_secs_f64()/new_times[2].as_secs_f64()); + Ok(()) +} diff --git a/src/ir/debug.rs b/src/ir/debug.rs index aa1bc01b..661d558d 100644 --- a/src/ir/debug.rs +++ b/src/ir/debug.rs @@ -545,7 +545,7 @@ mod tests { #[test] fn counter_source_map_links_all_three_documents() -> eyre::Result<()> { - let source = include_str!("../../test/counter.snapshot/counter.v"); + let source = include_str!("../../test/fixtures/source_map/counter.v"); let logical = super::super::LogicalDesign::from_verilog_source_named(source, "counter.v")?; let routable = logical.lower_to_routable()?; let map = build_source_map( @@ -605,7 +605,7 @@ mod tests { #[test] fn hierarchical_leaf_nodes_keep_definition_level_provenance() -> eyre::Result<()> { - let source = include_str!("../../test/d-flip-flop.snapshot/d-flip-flop.v"); + let source = include_str!("../../test/fixtures/source_map/d-flip-flop.v"); let logical = super::super::LogicalDesign::from_verilog_source_named(source, "d-flip-flop.v")?; let routable = logical.lower_to_routable()?; @@ -643,7 +643,7 @@ mod tests { #[test] fn source_map_separates_instantiation_references_from_provenance() -> eyre::Result<()> { - let source = include_str!("../../test/d-flip-flop.snapshot/d-flip-flop.v"); + let source = include_str!("../../test/fixtures/source_map/d-flip-flop.v"); let logical = super::super::LogicalDesign::from_verilog_source_named(source, "d-flip-flop.v")?; let routable = logical.lower_to_routable()?; diff --git a/src/ir/logical.rs b/src/ir/logical.rs index e3442b95..5be2209d 100644 --- a/src/ir/logical.rs +++ b/src/ir/logical.rs @@ -85,6 +85,12 @@ pub enum LogicalCellKind { Add, Inc, Mux, + Equal, + GreaterEqual, + Sub, + Select { msb: usize, lsb: usize }, + Concat, + Resize { width: usize }, DLatch { width: usize }, Dff { edge: ClockEdge }, Register { width: usize, edge: ClockEdge }, @@ -400,10 +406,31 @@ impl LogicalCell { LogicalCellKind::And | LogicalCellKind::Or | LogicalCellKind::Xor - | LogicalCellKind::Add => { + | LogicalCellKind::Add + | LogicalCellKind::Sub => { ensure_data_width(self.input_value("lhs")?, output_width, nets)?; ensure_data_width(self.input_value("rhs")?, output_width, nets)?; } + LogicalCellKind::Equal | LogicalCellKind::GreaterEqual => { + ensure_declared_width(1, output_width, &self.name)?; + let width = value_width(self.input_value("lhs")?, nets)?; + ensure_exact_width(self.input_value("rhs")?, width, nets, "comparison operand")?; + } + LogicalCellKind::Select { msb, lsb } => { + let width = value_width(self.input_value("value")?, nets)?; + if lsb > msb || *msb >= width { + eyre::bail!("invalid logical select {msb}:{lsb} for width {width}"); + } + ensure_declared_width(msb - lsb + 1, output_width, &self.name)?; + } + LogicalCellKind::Concat => { + let width = value_width(self.input_value("lhs")?, nets)? + + value_width(self.input_value("rhs")?, nets)?; + ensure_declared_width(width, output_width, &self.name)?; + } + LogicalCellKind::Resize { width } => { + ensure_declared_width(*width, output_width, &self.name)? + } LogicalCellKind::Mux => { ensure_exact_width(self.input_value("select")?, 1, nets, "mux select")?; ensure_data_width(self.input_value("when_true")?, output_width, nets)?; @@ -463,8 +490,17 @@ impl LogicalCellKind { fn pin_names(&self) -> (&'static [&'static str], &'static [&'static str]) { match self { - Self::Buffer | Self::Not => (&["value"], &["result"]), - Self::And | Self::Or | Self::Xor | Self::Add => (&["lhs", "rhs"], &["result"]), + Self::Buffer | Self::Not | Self::Select { .. } | Self::Resize { .. } => { + (&["value"], &["result"]) + } + Self::And + | Self::Or + | Self::Xor + | Self::Add + | Self::Equal + | Self::GreaterEqual + | Self::Sub + | Self::Concat => (&["lhs", "rhs"], &["result"]), Self::Inc => (&["value"], &["result"]), Self::Mux => (&["select", "when_true", "when_false"], &["result"]), Self::DLatch { .. } => (&["d", "enable"], &["q"]), diff --git a/src/ir/logical_adapter.rs b/src/ir/logical_adapter.rs index c5816d20..6effebd3 100644 --- a/src/ir/logical_adapter.rs +++ b/src/ir/logical_adapter.rs @@ -36,7 +36,7 @@ impl LogicalDesign { top: top.name.clone(), modules: modules .iter() - .map(logical_module_from_verilog) + .map(|module| logical_module_from_verilog(module, modules)) .collect::>>()?, debug: Default::default(), }; @@ -48,6 +48,65 @@ impl LogicalDesign { super::logical_lowering::lower_logical_to_routable(self) } + /// Maps a combinational design into bounded, fanout-free scalar cones. + /// The gate budget counts bit-level Boolean operators before NOR mapping. + /// `max_gates=1, max_inputs=2` provides the primitive-leaf baseline. + pub fn lower_to_routable_with_bit_mapping_limits( + &self, + max_gates: usize, + max_inputs: usize, + ) -> eyre::Result { + super::logical_lowering::lower_with_bit_mapping_limits(self, max_gates, max_inputs) + } + + /// Experimental AND/OR reassociation before bounded primitive mapping. + /// Shared subexpressions are preserved; gate count and every output's + /// Boolean depth may not increase. This does not predict physical timing. + pub fn lower_to_routable_with_balanced_associative_trees( + &self, + max_gates: usize, + max_inputs: usize, + ) -> eyre::Result { + super::logical_lowering::lower_with_balanced_bit_mapping(self, max_gates, max_inputs, true) + } + + /// Experimental pure AND/OR cells with 2..=8 boundary inputs. Only + /// same-operation fanout-free cones merge; NOT/XOR remain primitive. + /// Leaf bodies keep binary NOR mapping for existing Routable consumers. + pub fn lower_to_routable_with_associative_cells( + &self, + max_arity: usize, + ) -> eyre::Result { + super::logical_lowering::lower_with_associative_cells(self, max_arity) + } + + /// Experimental recipe-oriented mapping. Shared AND/OR prefixes may be + /// duplicated within bounded same-operation cells only when this reduces + /// physical instance count; otherwise the shared-cell mapping is returned. + pub fn lower_to_routable_with_expanded_associative_cells( + &self, + max_arity: usize, + ) -> eyre::Result { + super::logical_lowering::lower_with_expanded_associative_cells(self, max_arity) + } + + /// Experimental literal cube/clause cells: absorb one boundary NOT layer + /// into bounded AND/OR recipes, including shared literal inversions. + /// Select only when fewer instances than expanded positive cells result. + pub fn lower_to_routable_with_literal_associative_cells( + &self, + max_arity: usize, + ) -> eyre::Result { + super::logical_lowering::lower_with_literal_associative_cells(self, max_arity) + } + + /// Flatten a combinational design into one shared NOT/OR logic graph. + /// No physical partitioning, local candidates, or global routing occurs. + /// Sequential cells and outputs remaining constant after folding are rejected. + pub fn lower_flat_logic_graph(&self) -> eyre::Result { + super::logical_lowering::lower_flat_logic_graph(self) + } + pub fn to_verilog_modules(&self) -> eyre::Result> { self.validate()?; self.modules.iter().map(logical_module_to_verilog).collect() @@ -246,18 +305,12 @@ fn is_word_char(ch: char) -> bool { ch == '_' || ch == '$' || ch.is_ascii_alphanumeric() } -fn logical_module_from_verilog(module: &VerilogModule) -> eyre::Result { +fn logical_module_from_verilog( + module: &VerilogModule, + definitions: &[VerilogModule], +) -> eyre::Result { let rtl = lower_rtl_module(module)?; let synth = synthesize_module(&rtl)?; - if !module.instances.is_empty() - && (!rtl.continuous_assigns.is_empty() || !synth.cells.is_empty()) - { - eyre::bail!( - "logical RCIR does not support mixing instances and local cells in module `{}`", - module.name - ); - } - let nets = rtl .signals .iter() @@ -292,25 +345,59 @@ fn logical_module_from_verilog(module: &VerilogModule) -> eyre::Result>>()?; - let instances = module - .instances - .iter() - .map(|instance| LogicalInstance { + let mut builder = LogicalModuleBuilder::new(&rtl, nets); + let mut instances = Vec::new(); + for instance in &module.instances { + let definition = definitions + .iter() + .find(|child| child.name == instance.module_name) + .with_context(|| format!("unknown module `{}`", instance.module_name))?; + let child = lower_rtl_module(definition)?; + let mut bindings = Vec::new(); + for (port, expr) in &instance.connections { + let target = child + .ports + .iter() + .find(|p| &p.name == port) + .with_context(|| { + format!("unknown instance port `{}.{port}`", instance.instance_name) + })?; + let signal = &child.signals[target.signal.signal.0]; + let net = match signal.kind { + RtlSignalKind::Input => { + let elaborated = rtl.elaborate_expr(expr, signal.width)?; + if let RtlExpr::Signal(reference) = elaborated { + rtl.signal_name(reference)?.to_owned() + } else { + let net = builder.fresh_net( + &format!("{}_{}", instance.instance_name, port), + signal.width, + ); + builder.emit_expr_to(&elaborated, &net, signal.width, "port_binding")?; + net + } + } + RtlSignalKind::Output | RtlSignalKind::RegOutput => match expr { + Expr::Ident(net) => net.clone(), + _ => eyre::bail!( + "output port `{}.{port}` requires a named wire", + instance.instance_name + ), + }, + _ => eyre::bail!("instance port `{port}` is not an input or output"), + }; + bindings.push(LogicalBinding { + port: port.clone(), + net, + }); + } + instances.push(LogicalInstance { name: instance.instance_name.clone(), module: instance.module_name.clone(), - bindings: instance - .connections - .iter() - .map(|(port, net)| LogicalBinding { - port: port.clone(), - net: net.clone(), - }) - .collect(), + bindings, origin: Some(format!("verilog.instance.{}", instance.instance_name)), - }) - .collect(); - - let mut builder = LogicalModuleBuilder::new(&rtl, nets); + }); + } for assignment in &rtl.continuous_assigns { let output = rtl.signal_name(assignment.output)?.to_owned(); let width = rtl.signal_width(assignment.output)?; @@ -444,6 +531,61 @@ impl<'a> LogicalModuleBuilder<'a> { }, )], ), + RtlExpr::Select { value, msb, lsb } => ( + LogicalCellKind::Select { + msb: *msb, + lsb: *lsb, + }, + vec![input( + "value", + self.emit_expr_value(value, self.rtl.expr_width(value)?, role)?, + )], + ), + RtlExpr::Resize { value, width } => ( + LogicalCellKind::Resize { width: *width }, + vec![input( + "value", + self.emit_expr_value(value, self.rtl.expr_width(value)?, role)?, + )], + ), + RtlExpr::Sub(left, right) => ( + LogicalCellKind::Sub, + self.binary_inputs(left, right, expected_width, role)?, + ), + RtlExpr::GreaterEqual(left, right) => ( + LogicalCellKind::GreaterEqual, + self.binary_inputs(left, right, self.rtl.expr_width(left)?, role)?, + ), + RtlExpr::Equal(left, right) => ( + LogicalCellKind::Equal, + self.binary_inputs(left, right, self.rtl.expr_width(left)?, role)?, + ), + RtlExpr::Concat(values) => { + let (last, prefix) = values + .split_last() + .ok_or_else(|| eyre::eyre!("empty concatenation"))?; + if prefix.is_empty() { + return self.emit_expr_to(last, output_net, expected_width, role); + } + let upper = if prefix.len() == 1 { + prefix[0].clone() + } else { + RtlExpr::Concat(prefix.to_vec()) + }; + ( + LogicalCellKind::Concat, + vec![ + input( + "lhs", + self.emit_expr_value(&upper, self.rtl.expr_width(&upper)?, role)?, + ), + input( + "rhs", + self.emit_expr_value(last, self.rtl.expr_width(last)?, role)?, + ), + ], + ) + } RtlExpr::Not(value) => ( LogicalCellKind::Not, vec![input( @@ -642,7 +784,6 @@ fn logical_module_to_verilog(module: &LogicalModule) -> eyre::Result eyre::Result eyre::Result { LogicalCellKind::Xor => binary(BinaryOp::Xor, value("lhs")?, value("rhs")?), LogicalCellKind::Add => binary(BinaryOp::Add, value("lhs")?, value("rhs")?), LogicalCellKind::Inc => binary(BinaryOp::Add, value("value")?, Expr::Number(1)), - LogicalCellKind::Mux => eyre::bail!("mux is not an immediate Verilog expression"), + LogicalCellKind::Mux => Expr::Mux { + select: Box::new(value("select")?), + when_true: Box::new(value("when_true")?), + when_false: Box::new(value("when_false")?), + }, + LogicalCellKind::Equal => binary(BinaryOp::Equal, value("lhs")?, value("rhs")?), + LogicalCellKind::GreaterEqual => { + binary(BinaryOp::GreaterEqual, value("lhs")?, value("rhs")?) + } + LogicalCellKind::Sub => binary(BinaryOp::Sub, value("lhs")?, value("rhs")?), + LogicalCellKind::Select { msb, lsb } => Expr::Select { + value: Box::new(value("value")?), + msb, + lsb, + }, + LogicalCellKind::Concat => Expr::Concat(vec![value("lhs")?, value("rhs")?]), + LogicalCellKind::Resize { .. } => value("value")?, LogicalCellKind::DLatch { .. } | LogicalCellKind::Dff { .. } | LogicalCellKind::Register { .. } => { @@ -808,9 +965,37 @@ fn expand_value( expand("rhs", active)?, ), LogicalCellKind::Inc => binary(BinaryOp::Add, expand("value", active)?, Expr::Number(1)), - LogicalCellKind::Mux => { - eyre::bail!("generic mux expression cannot be represented by the current Verilog AST") + LogicalCellKind::Mux => Expr::Mux { + select: Box::new(expand("select", active)?), + when_true: Box::new(expand("when_true", active)?), + when_false: Box::new(expand("when_false", active)?), + }, + LogicalCellKind::Equal => binary( + BinaryOp::Equal, + expand("lhs", active)?, + expand("rhs", active)?, + ), + LogicalCellKind::GreaterEqual => binary( + BinaryOp::GreaterEqual, + expand("lhs", active)?, + expand("rhs", active)?, + ), + LogicalCellKind::Sub => binary( + BinaryOp::Sub, + expand("lhs", active)?, + expand("rhs", active)?, + ), + LogicalCellKind::Select { msb, lsb } => Expr::Select { + value: Box::new(expand("value", active)?), + msb, + lsb, + }, + LogicalCellKind::Concat => { + Expr::Concat(vec![expand("lhs", active)?, expand("rhs", active)?]) } + // Keep the declared-width wire: expanding through it would erase + // truncation/extension when embedded in a wider parent expression. + LogicalCellKind::Resize { .. } => Expr::Ident(net.clone()), _ => eyre::bail!("sequential cell cannot drive a combinational expression"), }; active.pop(); @@ -820,7 +1005,10 @@ fn expand_value( fn value_expr(value: &LogicalValue) -> Expr { match value { LogicalValue::Net { net } => Expr::Ident(net.clone()), - LogicalValue::Constant { value, .. } => Expr::Number(*value as usize), + LogicalValue::Constant { value, width } => Expr::SizedNumber { + value: *value, + width: *width, + }, } } diff --git a/src/ir/logical_lowering.rs b/src/ir/logical_lowering.rs index d02bb126..1424c16f 100644 --- a/src/ir/logical_lowering.rs +++ b/src/ir/logical_lowering.rs @@ -1,3 +1,9 @@ +mod bit_mapping; + +pub(crate) fn lower_flat_logic_graph(design: &LogicalDesign) -> eyre::Result { + bit_mapping::lower_flat_logic_graph(design) +} + use std::collections::{BTreeMap, HashMap, HashSet}; use eyre::{ContextCompat, WrapErr}; @@ -27,6 +33,88 @@ pub(crate) fn lower_logical_to_routable(design: &LogicalDesign) -> eyre::Result< Ok(routable) } +pub(crate) fn lower_with_bit_mapping_limits( + design: &LogicalDesign, + max_gates: usize, + max_inputs: usize, +) -> eyre::Result { + lower_with_balanced_bit_mapping(design, max_gates, max_inputs, false) +} + +pub(crate) fn lower_with_balanced_bit_mapping( + design: &LogicalDesign, + max_gates: usize, + max_inputs: usize, + balance: bool, +) -> eyre::Result { + design.validate()?; + eyre::ensure!( + !design + .modules + .iter() + .any(|module| module.cells.iter().any(|cell| cell.kind.is_sequential())), + "bounded bit mapping only supports combinational designs" + ); + let mut routable = bit_mapping::lower_with_balancing(design, max_gates, max_inputs, balance)?; + routable.debug = design.debug.clone(); + attach_routable_debug_locations(design, &mut routable); + Ok(routable) +} + +pub(crate) fn lower_with_associative_cells( + design: &LogicalDesign, + max_arity: usize, +) -> eyre::Result { + design.validate()?; + eyre::ensure!( + !design + .modules + .iter() + .any(|module| module.cells.iter().any(|cell| cell.kind.is_sequential())), + "associative cell mapping only supports combinational designs" + ); + let mut routable = bit_mapping::lower_with_associative_cells(design, max_arity)?; + routable.debug = design.debug.clone(); + attach_routable_debug_locations(design, &mut routable); + Ok(routable) +} + +pub(crate) fn lower_with_expanded_associative_cells( + design: &LogicalDesign, + max_arity: usize, +) -> eyre::Result { + design.validate()?; + eyre::ensure!( + !design + .modules + .iter() + .any(|module| module.cells.iter().any(|cell| cell.kind.is_sequential())), + "associative cell mapping only supports combinational designs" + ); + let mut routable = bit_mapping::lower_with_expanded_associative_cells(design, max_arity)?; + routable.debug = design.debug.clone(); + attach_routable_debug_locations(design, &mut routable); + Ok(routable) +} + +pub(crate) fn lower_with_literal_associative_cells( + design: &LogicalDesign, + max_arity: usize, +) -> eyre::Result { + design.validate()?; + eyre::ensure!( + !design + .modules + .iter() + .any(|module| module.cells.iter().any(|cell| cell.kind.is_sequential())), + "literal associative cell mapping only supports combinational designs" + ); + let mut routable = bit_mapping::lower_with_literal_associative_cells(design, max_arity)?; + routable.debug = design.debug.clone(); + attach_routable_debug_locations(design, &mut routable); + Ok(routable) +} + fn attach_routable_debug_locations(design: &LogicalDesign, routable: &mut RoutableDesign) { use super::debug::{logical_entity, routable_entity, DebugRelationKind}; use super::routable::RoutableModuleBody; @@ -282,6 +370,41 @@ fn lower_logical_design(design: &LogicalDesign) -> eyre::Result .copied() .with_context(|| format!("unknown logical top module `{}`", design.top))?; + let needs_bit_mapping = design.modules.iter().any(|module| { + module.nets.iter().any(|net| net.width != 1) + || (!module.instances.is_empty() && !module.cells.is_empty()) + || module.instances.iter().any(|instance| { + design + .module(&instance.module) + .is_some_and(|child| !child.instances.is_empty()) + }) + || module.cells.iter().any(|cell| { + cell.inputs + .iter() + .any(|input| matches!(input.value, LogicalValue::Constant { .. })) + }) + || module.cells.iter().any(|cell| { + matches!( + cell.kind, + LogicalCellKind::Equal + | LogicalCellKind::GreaterEqual + | LogicalCellKind::Sub + | LogicalCellKind::Select { .. } + | LogicalCellKind::Concat + | LogicalCellKind::Resize { .. } + | LogicalCellKind::Mux + | LogicalCellKind::Add + ) + }) + }); + if needs_bit_mapping + && !design + .modules + .iter() + .any(|module| module.cells.iter().any(|cell| cell.kind.is_sequential())) + { + return bit_mapping::lower(design); + } if top.instances.is_empty() { if let Some(state_design) = lower_state_design(top)? { return Ok(state_design); @@ -694,7 +817,13 @@ fn emit_graph_cell( )), &["d", "enable"], ), - LogicalCellKind::Add + LogicalCellKind::Equal + | LogicalCellKind::GreaterEqual + | LogicalCellKind::Sub + | LogicalCellKind::Select { .. } + | LogicalCellKind::Concat + | LogicalCellKind::Resize { .. } + | LogicalCellKind::Add | LogicalCellKind::Inc | LogicalCellKind::Mux | LogicalCellKind::Dff { .. } diff --git a/src/ir/logical_lowering/bit_mapping.rs b/src/ir/logical_lowering/bit_mapping.rs new file mode 100644 index 00000000..e7526607 --- /dev/null +++ b/src/ir/logical_lowering/bit_mapping.rs @@ -0,0 +1,1730 @@ +//! Unsigned bit mapping with structural sharing and bounded primitive leaves. +//! Select/concat/resize are wiring; constants fold before physical generation. +use super::*; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +enum Bit { + Constant(bool), + Input(String), + Not(usize), + And(usize, usize), + Or(usize, usize), + Xor(usize, usize), +} + +#[derive(Clone, Default)] +struct Builder { + bits: Vec, + intern: HashMap, +} + +fn bit_inputs(bit: &Bit) -> Vec { + match *bit { + Bit::Not(a) => vec![a], + Bit::And(a, b) | Bit::Or(a, b) | Bit::Xor(a, b) => vec![a, b], + _ => vec![], + } +} + +fn bit_depth(id: usize, bits: &[Bit], cache: &mut HashMap) -> usize { + if let Some(&depth) = cache.get(&id) { + return depth; + } + let inputs = bit_inputs(&bits[id]); + let depth = if inputs.is_empty() { + 0 + } else { + 1 + inputs + .into_iter() + .map(|input| bit_depth(input, bits, cache)) + .max() + .unwrap() + }; + cache.insert(id, depth); + depth +} + +fn live_bits(bits: &[Bit], outputs: &[(String, usize)]) -> HashSet { + let mut live = HashSet::new(); + let mut stack = outputs.iter().map(|(_, id)| *id).collect::>(); + while let Some(id) = stack.pop() { + if live.insert(id) { + stack.extend(bit_inputs(&bits[id])); + } + } + live +} + +impl Builder { + fn bit(&mut self, bit: Bit) -> usize { + if let Some(&id) = self.intern.get(&bit) { + return id; + } + let id = self.bits.len(); + self.bits.push(bit.clone()); + self.intern.insert(bit, id); + id + } + fn constant(&mut self, value: bool) -> usize { + self.bit(Bit::Constant(value)) + } + fn not(&mut self, a: usize) -> usize { + match self.bits[a] { + Bit::Constant(v) => self.constant(!v), + Bit::Not(v) => v, + _ => self.bit(Bit::Not(a)), + } + } + fn binary(&mut self, op: &str, mut a: usize, mut b: usize) -> usize { + if a > b { + std::mem::swap(&mut a, &mut b); + } + if a == b { + return if op == "xor" { self.constant(false) } else { a }; + } + if matches!(self.bits[a], Bit::Not(v) if v == b) + || matches!(self.bits[b], Bit::Not(v) if v == a) + { + return self.constant(op != "and"); + } + for (x, y) in [(a, b), (b, a)] { + if let Bit::Constant(v) = self.bits[x] { + return match (op, v) { + ("and", false) | ("or", true) => x, + ("and", true) | ("or", false) | ("xor", false) => y, + ("xor", true) => self.not(y), + _ => unreachable!(), + }; + } + } + self.bit(match op { + "and" => Bit::And(a, b), + "or" => Bit::Or(a, b), + "xor" => Bit::Xor(a, b), + _ => unreachable!(), + }) + } + /// Reassociate only fanout-free AND/OR cones. Shared intermediate signals + /// remain boundaries, so balancing cannot duplicate their logic. + fn balanced(&self, outputs: &[(String, usize)]) -> (Self, Vec<(String, usize)>) { + let live = live_bits(&self.bits, outputs); + let mut fanout = vec![0; self.bits.len()]; + for &id in &live { + for input in bit_inputs(&self.bits[id]) { + fanout[input] += 1; + } + } + for (_, id) in outputs { + fanout[*id] += 1; + } + let mut rebuilt = Self::default(); + // Preserve unused interface inputs as possible constant-source seeds. + for bit in &self.bits { + if matches!(bit, Bit::Input(_)) { + rebuilt.bit(bit.clone()); + } + } + fn rebuild( + id: usize, + old: &Builder, + fanout: &[usize], + new: &mut Builder, + memo: &mut HashMap, + depths: &mut HashMap, + ) -> usize { + if let Some(&mapped) = memo.get(&id) { + return mapped; + } + let mapped = match old.bits[id].clone() { + Bit::And(_, _) | Bit::Or(_, _) => { + let is_and = matches!(old.bits[id], Bit::And(..)); + fn collect( + id: usize, + root: bool, + is_and: bool, + old: &Builder, + fanout: &[usize], + operands: &mut Vec, + ) { + let same = if is_and { + matches!(old.bits[id], Bit::And(..)) + } else { + matches!(old.bits[id], Bit::Or(..)) + }; + if same && (root || fanout[id] == 1) { + for input in bit_inputs(&old.bits[id]) { + collect(input, false, is_and, old, fanout, operands); + } + } else { + operands.push(id); + } + } + let mut operands = Vec::new(); + collect(id, true, is_and, old, fanout, &mut operands); + let mut queue = std::collections::BinaryHeap::new(); + for input in operands { + let input = rebuild(input, old, fanout, new, memo, depths); + queue.push(std::cmp::Reverse(( + bit_depth(input, &new.bits, depths), + input, + ))); + } + while queue.len() > 1 { + let std::cmp::Reverse((_, a)) = queue.pop().unwrap(); + let std::cmp::Reverse((_, b)) = queue.pop().unwrap(); + let combined = new.binary(if is_and { "and" } else { "or" }, a, b); + queue.push(std::cmp::Reverse(( + bit_depth(combined, &new.bits, depths), + combined, + ))); + } + queue.pop().unwrap().0 .1 + } + Bit::Not(a) => { + let a = rebuild(a, old, fanout, new, memo, depths); + new.not(a) + } + Bit::Xor(a, b) => { + let a = rebuild(a, old, fanout, new, memo, depths); + let b = rebuild(b, old, fanout, new, memo, depths); + new.binary("xor", a, b) + } + other => new.bit(other), + }; + memo.insert(id, mapped); + mapped + } + let mut memo = HashMap::new(); + let mut depths = HashMap::new(); + let mapped = outputs + .iter() + .map(|(name, id)| { + ( + name.clone(), + rebuild(*id, self, &fanout, &mut rebuilt, &mut memo, &mut depths), + ) + }) + .collect::>(); + let gate_count = |bits: &[Bit], live: &HashSet| { + live.iter() + .filter(|&&id| !bit_inputs(&bits[id]).is_empty()) + .count() + }; + let mut original_depths = HashMap::new(); + let no_deeper = outputs.iter().zip(&mapped).all(|((_, old), (_, new))| { + bit_depth(*new, &rebuilt.bits, &mut depths) + <= bit_depth(*old, &self.bits, &mut original_depths) + }); + if no_deeper + && gate_count(&rebuilt.bits, &live_bits(&rebuilt.bits, &mapped)) + <= gate_count(&self.bits, &live) + { + (rebuilt, mapped) + } else { + (self.clone(), outputs.to_vec()) + } + } + + fn value( + &mut self, + value: &LogicalValue, + nets: &HashMap>, + ) -> Option> { + match value { + LogicalValue::Net { net } => nets.get(net).cloned(), + LogicalValue::Constant { value, width } => Some( + (0..*width) + .map(|i| self.constant(i < 128 && (value >> i) & 1 != 0)) + .collect(), + ), + } + } + fn resize(&mut self, mut bits: Vec, width: usize) -> Vec { + bits.resize(width, self.constant(false)); + bits.truncate(width); + bits + } + fn cell( + &mut self, + cell: &LogicalCell, + inputs: &[Vec], + width: usize, + ) -> eyre::Result> { + let get = |pin: &str| { + inputs[cell + .inputs + .iter() + .position(|input| input.pin == pin) + .unwrap()] + .clone() + }; + Ok(match &cell.kind { + LogicalCellKind::Select { msb, lsb } => get("value")[*lsb..=*msb].to_vec(), + LogicalCellKind::Concat => { + let mut result = get("rhs"); + result.extend(get("lhs")); + result + } + LogicalCellKind::Resize { .. } | LogicalCellKind::Buffer => { + self.resize(get("value"), width) + } + LogicalCellKind::Not => { + let value = self.resize(get("value"), width); + value.into_iter().map(|a| self.not(a)).collect() + } + LogicalCellKind::And + | LogicalCellKind::Or + | LogicalCellKind::Xor + | LogicalCellKind::Add + | LogicalCellKind::Sub + | LogicalCellKind::Inc => { + let a = self.resize( + get(if cell.kind == LogicalCellKind::Inc { + "value" + } else { + "lhs" + }), + width, + ); + let b = if cell.kind == LogicalCellKind::Inc { + let mut bits = vec![self.constant(false); width]; + bits[0] = self.constant(true); + bits + } else { + self.resize(get("rhs"), width) + }; + let subtract = cell.kind == LogicalCellKind::Sub; + let b = if subtract { + b.into_iter().map(|bit| self.not(bit)).collect() + } else { + b + }; + let mut carry = self.constant(subtract); + a.into_iter() + .zip(b) + .map(|(a, b)| match cell.kind { + LogicalCellKind::And => self.binary("and", a, b), + LogicalCellKind::Or => self.binary("or", a, b), + LogicalCellKind::Xor => self.binary("xor", a, b), + _ => { + let p = self.binary("xor", a, b); + let sum = self.binary("xor", p, carry); + let generate = self.binary("and", a, b); + let propagate = self.binary("and", p, carry); + carry = self.binary("or", generate, propagate); + sum + } + }) + .collect() + } + LogicalCellKind::GreaterEqual => { + let mut greater_equal = self.constant(true); + for (a, b) in get("lhs").into_iter().zip(get("rhs")) { + let not_b = self.not(b); + let greater = self.binary("and", a, not_b); + let different = self.binary("xor", a, b); + let equal = self.not(different); + let prior = self.binary("and", equal, greater_equal); + greater_equal = self.binary("or", greater, prior); + } + vec![greater_equal] + } + LogicalCellKind::Equal => { + let mut same = self.constant(true); + for (a, b) in get("lhs").into_iter().zip(get("rhs")) { + let different = self.binary("xor", a, b); + let equal = self.not(different); + same = self.binary("and", same, equal); + } + vec![same] + } + LogicalCellKind::Mux => { + let select = get("select")[0]; + let yes = self.resize(get("when_true"), width); + let no = self.resize(get("when_false"), width); + let inverse = self.not(select); + yes.into_iter() + .zip(no) + .map(|(yes, no)| { + if yes == no { + return yes; + } + let a = self.binary("and", select, yes); + let b = self.binary("and", inverse, no); + self.binary("or", a, b) + }) + .collect() + } + _ => eyre::bail!( + "generic bit mapping does not support sequential cell `{}`", + cell.name + ), + }) + } + fn module( + &mut self, + design: &LogicalDesign, + module: &LogicalModule, + mut nets: HashMap>, + ) -> eyre::Result>> { + // Flatten hierarchy before scheduling cells. Requiring every child + // input first invents dependencies between independent child outputs. + fn flatten( + design: &LogicalDesign, + module: &LogicalModule, + names: &HashMap, + used: &mut HashSet, + serial: &mut usize, + cells: &mut Vec<(LogicalCell, usize)>, + ) { + for cell in &module.cells { + let mut renamed = cell.clone(); + for input in &mut renamed.inputs { + if let LogicalValue::Net { net } = &mut input.value { + *net = names[net].clone(); + } + } + for output in &mut renamed.outputs { + output.net = names[&output.net].clone(); + } + let width = module + .nets + .iter() + .find(|net| net.name == cell.outputs[0].net) + .unwrap() + .width; + cells.push((renamed, width)); + } + for instance in &module.instances { + let child = design.module(&instance.module).unwrap(); + let mut child_names = HashMap::new(); + for binding in &instance.bindings { + let port = child + .ports + .iter() + .find(|port| port.name == binding.port) + .unwrap(); + child_names.insert(port.net.clone(), names[&binding.net].clone()); + } + for net in &child.nets { + if !child_names.contains_key(&net.name) { + let name = loop { + let name = format!("__flat_{}", *serial); + *serial += 1; + if used.insert(name.clone()) { + break name; + } + }; + child_names.insert(net.name.clone(), name); + } + } + flatten(design, child, &child_names, used, serial, cells); + } + } + let names = module + .nets + .iter() + .map(|net| (net.name.clone(), net.name.clone())) + .collect::>(); + let mut used = names.values().cloned().collect::>(); + let mut cells = Vec::new(); + flatten(design, module, &names, &mut used, &mut 0, &mut cells); + while !cells.is_empty() { + let before = cells.len(); + let mut remaining = Vec::new(); + for (cell, width) in cells { + let Some(inputs) = cell + .inputs + .iter() + .map(|input| self.value(&input.value, &nets)) + .collect::>>() + else { + remaining.push((cell, width)); + continue; + }; + let output = cell.output("result")?; + let value = self.cell(&cell, &inputs, width)?; + nets.insert(output.clone(), value); + } + cells = remaining; + if cells.len() == before { + eyre::bail!( + "combinational cycle or unavailable driver in `{}`", + module.name + ); + } + } + Ok(nets) + } +} + +pub(super) fn lower(design: &LogicalDesign) -> eyre::Result { + lower_with_limits(design, 1, 2) +} + +pub(super) fn lower_with_limits( + design: &LogicalDesign, + max_gates: usize, + max_inputs: usize, +) -> eyre::Result { + lower_with_balancing(design, max_gates, max_inputs, false) +} + +pub(super) fn lower_with_balancing( + design: &LogicalDesign, + max_gates: usize, + max_inputs: usize, + balance: bool, +) -> eyre::Result { + lower_options(design, max_gates, max_inputs, balance, None, false, false) +} + +pub(super) fn lower_with_associative_cells( + design: &LogicalDesign, + max_arity: usize, +) -> eyre::Result { + eyre::ensure!( + (2..=8).contains(&max_arity), + "associative cells support arity 2 through 8" + ); + lower_options(design, 1, 2, false, Some(max_arity), false, false) +} + +pub(super) fn lower_with_expanded_associative_cells( + design: &LogicalDesign, + max_arity: usize, +) -> eyre::Result { + let baseline = lower_with_associative_cells(design, max_arity)?; + let expanded = lower_options(design, 1, 2, false, Some(max_arity), true, false)?; + let count = |design: &RoutableDesign| match &design.module(&design.top).unwrap().body { + RoutableModuleBody::Composite { instances, .. } => instances.len(), + _ => 0, + }; + // This is a cell-count tradeoff, not a raw Boolean-gate optimization. + // Shared logic may be duplicated inside recipes only if fewer physical + // instances are required. The ordinary shared mapping remains the fallback. + Ok(if count(&expanded) < count(&baseline) { + expanded + } else { + baseline + }) +} + +pub(super) fn lower_with_literal_associative_cells( + design: &LogicalDesign, + max_arity: usize, +) -> eyre::Result { + let baseline = lower_with_expanded_associative_cells(design, max_arity)?; + let literal = lower_options(design, 1, 2, false, Some(max_arity), true, true)?; + let count = |design: &RoutableDesign| match &design.module(&design.top).unwrap().body { + RoutableModuleBody::Composite { instances, .. } => instances.len(), + _ => 0, + }; + Ok(if count(&literal) < count(&baseline) { + literal + } else { + baseline + }) +} + +fn build_flat_bits( + design: &LogicalDesign, +) -> eyre::Result<(Builder, Vec<(String, usize)>, Vec)> { + let top = design.module(&design.top).unwrap(); + // Preserve ordinary bus_0 naming, but reserve all declared port names + // first. Escaped generated names cannot alias a scalar input or output. + let mut used_names = top + .ports + .iter() + .map(|port| port.name.clone()) + .collect::>(); + let mut port_names = HashMap::>::new(); + for port in &top.ports { + let width = top + .nets + .iter() + .find(|net| net.name == port.net) + .unwrap() + .width; + let names = if width == 1 { + vec![port.name.clone()] + } else { + (0..width) + .map(|bit| { + let ordinary = bit_signal_name(&port.name, bit); + if used_names.insert(ordinary.clone()) { + return ordinary; + } + let mut escaped = format!("{}[{bit}]", port.name); + while !used_names.insert(escaped.clone()) { + escaped.insert(0, '$'); + } + escaped + }) + .collect() + }; + port_names.insert(port.name.clone(), names); + } + let mut builder = Builder::default(); + let mut inputs = HashMap::new(); + for port in top + .ports + .iter() + .filter(|p| p.direction == LogicalPortDirection::Input) + { + let width = top.nets.iter().find(|n| n.name == port.net).unwrap().width; + let bits = (0..width) + .map(|i| builder.bit(Bit::Input(port_names[&port.name][i].clone()))) + .collect(); + inputs.insert(port.net.clone(), bits); + } + let values = builder.module(design, top, inputs)?; + let ports: Vec = top + .ports + .iter() + .flat_map(|port| { + port_names[&port.name].iter().map(|name| RoutablePort { + name: name.clone(), + direction: match port.direction { + LogicalPortDirection::Input => RoutablePortDirection::Input, + LogicalPortDirection::Output => RoutablePortDirection::Output, + }, + }) + }) + .collect(); + let mut outputs = Vec::new(); + for port in top + .ports + .iter() + .filter(|p| p.direction == LogicalPortDirection::Output) + { + let bits = &values[&port.net]; + for (i, &bit) in bits.iter().enumerate() { + outputs.push((port_names[&port.name][i].clone(), bit)); + } + } + Ok((builder, outputs, ports)) +} + +pub(super) fn lower_flat_logic_graph(design: &LogicalDesign) -> eyre::Result { + design.validate()?; + eyre::ensure!( + !design + .modules + .iter() + .any(|m| m.cells.iter().any(|c| c.kind.is_sequential())), + "flat logic graph only supports combinational designs" + ); + let (builder, outputs, _) = build_flat_bits(design)?; + let live = live_bits(&builder.bits, &outputs); + let mut nodes = Vec::new(); + for (id, bit) in builder.bits.iter().enumerate() { + if !live.contains(&id) && !matches!(bit, Bit::Input(_)) { + continue; + } + let kind = match bit { + Bit::Input(name) => GraphNodeKind::Input(name.clone()), + Bit::Not(_) => GraphNodeKind::Logic(Logic { + logic_type: LogicType::Not, + }), + Bit::And(..) => GraphNodeKind::Logic(Logic { + logic_type: LogicType::And, + }), + Bit::Or(..) => GraphNodeKind::Logic(Logic { + logic_type: LogicType::Or, + }), + Bit::Xor(..) => GraphNodeKind::Logic(Logic { + logic_type: LogicType::Xor, + }), + Bit::Constant(_) => { + eyre::bail!("flat logic graph cannot represent a live constant output") + } + }; + nodes.push(( + id, + GraphNode { + kind, + inputs: bit_inputs(bit), + ..Default::default() + }, + )); + } + for (index, (name, input)) in outputs.into_iter().enumerate() { + nodes.push(( + builder.bits.len() + index, + GraphNode { + kind: GraphNodeKind::Output(name), + inputs: vec![input], + ..Default::default() + }, + )); + } + let mut graph = Graph::from_nodes_with_ids(nodes); + graph.build_outputs(); + graph.build_producers(); + graph.build_consumers(); + graph.verify()?; + let mut transform = crate::transform::logic::LogicGraphTransformer::new(LogicGraph { graph }); + transform.decompose_xor()?; + transform.decompose_and()?; + transform.remove_double_neg_expression(); + transform.optimize_cse()?; + Ok(transform.finish()) +} + +fn lower_options( + design: &LogicalDesign, + max_gates: usize, + max_inputs: usize, + balance: bool, + associative_arity: Option, + expand_shared: bool, + absorb_literals: bool, +) -> eyre::Result { + eyre::ensure!( + max_gates >= 1 && max_inputs >= 2, + "bit mapping needs at least one gate and two input ports" + ); + let (mut builder, mut outputs, ports) = build_flat_bits(design)?; + let top = design.module(&design.top).unwrap(); + if balance { + (builder, outputs) = builder.balanced(&outputs); + } + let mut live = HashSet::new(); + fn visit(id: usize, bits: &[Bit], live: &mut HashSet) { + if !live.insert(id) { + return; + } + match bits[id] { + Bit::Not(a) => visit(a, bits, live), + Bit::And(a, b) | Bit::Or(a, b) | Bit::Xor(a, b) => { + visit(a, bits, live); + visit(b, bits, live); + } + _ => {} + } + } + for (_, bit) in &outputs { + visit(*bit, &builder.bits, &mut live); + } + fn dependencies(bit: &Bit) -> Vec { + match *bit { + Bit::Not(a) => vec![a], + Bit::And(a, b) | Bit::Or(a, b) | Bit::Xor(a, b) => vec![a, b], + _ => vec![], + } + } + let mut fanout = vec![0; builder.bits.len()]; + for &id in &live { + for input in dependencies(&builder.bits[id]) { + fanout[input] += 1; + } + } + for (_, id) in &outputs { + fanout[*id] += 1; + } + let mut absorbed = HashSet::new(); + let mut groups = HashMap::new(); + // Fanout-free cones only: a shared decode signal remains one routed driver. + // Every merge checks both gate and unique boundary-input budgets. + for root in (0..builder.bits.len()).rev() { + if !live.contains(&root) + || (!expand_shared && absorbed.contains(&root)) + || matches!(builder.bits[root], Bit::Input(_) | Bit::Constant(_)) + { + continue; + } + let mut included = HashSet::from([root]); + let gate_limit = associative_arity.map_or(max_gates, |arity| { + if matches!(builder.bits[root], Bit::And(..) | Bit::Or(..)) { + arity - 1 + } else { + 1 + } + }); + let input_limit = associative_arity.unwrap_or(max_inputs); + let boundary = |included: &HashSet| { + included + .iter() + .flat_map(|id| dependencies(&builder.bits[*id])) + .filter(|id| !included.contains(id)) + .collect::>() + }; + loop { + let mut merged = false; + for candidate in boundary(&included).into_iter().rev() { + if (!expand_shared && fanout[candidate] != 1) + || matches!(builder.bits[candidate], Bit::Input(_) | Bit::Constant(_)) + { + continue; + } + // A NOT's operand is an opaque literal terminal. Never + // absorb an AND/OR hidden underneath that inversion. + if absorb_literals + && included.iter().any( + |id| matches!(builder.bits[*id],Bit::Not(operand) if operand==candidate), + ) + { + continue; + } + let same_operation = matches!( + (&builder.bits[root], &builder.bits[candidate]), + (Bit::And(..), Bit::And(..)) | (Bit::Or(..), Bit::Or(..)) + ); + let literal_not = absorb_literals + && matches!(builder.bits[root], Bit::And(..) | Bit::Or(..)) + && matches!(builder.bits[candidate], Bit::Not(..)); + if associative_arity.is_some() && !same_operation && !literal_not { + continue; + } + let mut proposed = included.clone(); + proposed.insert(candidate); + let operations = proposed + .iter() + .filter(|id| { + !absorb_literals + || !matches!(builder.bits[**id], Bit::Not(..)) + || **id == root + }) + .count(); + if operations > gate_limit { + continue; + } + // Contradictory literals would become a constant function, + // not the single-exception cube/clause the recipe promises. + fn collect_literal_polarities( + id: usize, + bits: &[Bit], + included: &HashSet, + negative: bool, + polarities: &mut HashMap, + ) -> bool { + if !included.contains(&id) { + return polarities + .insert(id, negative) + .is_none_or(|prior| prior == negative); + } + match bits[id] { + Bit::And(a, b) | Bit::Or(a, b) => { + collect_literal_polarities(a, bits, included, negative, polarities) + && collect_literal_polarities( + b, bits, included, negative, polarities, + ) + } + Bit::Not(a) => { + collect_literal_polarities(a, bits, included, !negative, polarities) + } + _ => false, + } + } + if absorb_literals + && matches!(builder.bits[root], Bit::And(..) | Bit::Or(..)) + && !collect_literal_polarities( + root, + &builder.bits, + &proposed, + false, + &mut HashMap::new(), + ) + { + continue; + } + if boundary(&proposed).len() <= input_limit { + included = proposed; + if !expand_shared { + absorbed.insert(candidate); + } + merged = true; + break; + } + } + if !merged { + break; + } + } + groups.insert(root, included); + } + if expand_shared { + // Select complete cells from the outputs backwards. A shared prefix + // expanded into every consumer must not remain as an unused instance. + let mut needed = HashSet::new(); + let mut pending = outputs.iter().map(|(_, id)| *id).collect::>(); + while let Some(root) = pending.pop() { + if !needed.insert(root) { + continue; + } + if let Some(included) = groups.get(&root) { + pending.extend( + included + .iter() + .flat_map(|id| dependencies(&builder.bits[*id])) + .filter(|id| !included.contains(id)), + ); + } + } + live = needed; + } + let mut modules = Vec::new(); + let mut instances = Vec::new(); + let mut connections = NetConnections::default(); + let mut endpoints = HashMap::new(); + for (id, bit) in builder.bits.iter().enumerate() { + if !live.contains(&id) || absorbed.contains(&id) { + continue; + } + if let Bit::Input(name) = bit { + endpoints.insert(id, self_port(name)); + continue; + } + let name = format!("{}_bit_{id}", top.name); + let (expr, args): (String, Vec<(String, usize)>) = if let Bit::Constant(value) = bit { + let seed = builder + .bits + .iter() + .position(|bit| matches!(bit, Bit::Input(_))) + .ok_or_else(|| { + eyre::eyre!("input-free constant design requires a constant-source target cell") + })?; + if let Bit::Input(input) = &builder.bits[seed] { + endpoints.insert(seed, self_port(input)); + } + ( + (if *value { "a|~a" } else { "a&~a" }).to_owned(), + vec![("a".to_owned(), seed)], + ) + } else { + let included = &groups[&id]; + let boundary = included + .iter() + .flat_map(|id| dependencies(&builder.bits[*id])) + .filter(|id| !included.contains(id)) + .collect::>(); + let args = boundary + .into_iter() + .enumerate() + .map(|(index, id)| (format!("a{index}"), id)) + .collect::>(); + fn expression( + id: usize, + bits: &[Bit], + included: &HashSet, + args: &[(String, usize)], + ) -> String { + if !included.contains(&id) { + return args + .iter() + .find(|(_, input)| *input == id) + .unwrap() + .0 + .clone(); + } + let expr = |id| expression(id, bits, included, args); + match bits[id] { + Bit::Not(a) => format!("~({})", expr(a)), + Bit::And(a, b) => format!("({}&{})", expr(a), expr(b)), + Bit::Or(a, b) => format!("({}|{})", expr(a), expr(b)), + Bit::Xor(a, b) => format!("({}^{})", expr(a), expr(b)), + _ => unreachable!(), + } + } + (expression(id, &builder.bits, included, &args), args) + }; + modules.push(combinational_output_module(&name, &expr, "y")?); + for (pin, source) in args { + connections.connect( + &format!("bit_{source}"), + NetClass::Data, + endpoints[&source].clone(), + instance_port(&name, &pin), + ); + } + endpoints.insert(id, instance_port(&name, "y")); + instances.push(name); + } + // Materialize pass-through outputs as a buffer leaf: the physical output + // collector observes instance outputs, not top-input aliases. + for (index, (name, id)) in outputs.into_iter().enumerate() { + let mut driver = endpoints[&id].clone(); + if matches!(driver, Endpoint::SelfPort { .. }) { + let instance = format!("{}_output_{index}", top.name); + modules.push(RoutableModule { + name: instance.clone(), + ports: vec![ + RoutablePort { + name: "a".into(), + direction: RoutablePortDirection::Input, + }, + RoutablePort { + name: "y".into(), + direction: RoutablePortDirection::Output, + }, + ], + body: RoutableModuleBody::Leaf { + nodes: vec![ + RoutableNode { + id: 0, + kind: RoutableNodeKind::Input { name: "a".into() }, + inputs: vec![], + tag: String::new(), + }, + RoutableNode { + id: 1, + kind: RoutableNodeKind::Not, + inputs: vec![0], + tag: String::new(), + }, + RoutableNode { + id: 2, + kind: RoutableNodeKind::Not, + inputs: vec![1], + tag: String::new(), + }, + RoutableNode { + id: 3, + kind: RoutableNodeKind::Output { name: "y".into() }, + inputs: vec![2], + tag: String::new(), + }, + ], + }, + }); + connections.connect( + &format!("bit_{id}"), + NetClass::Io, + driver, + instance_port(&instance, "a"), + ); + driver = instance_port(&instance, "y"); + instances.push(instance); + } + connections.connect(&name, NetClass::Io, driver, self_port(&name)); + } + modules.push(composite_module( + &top.name, + ports, + instances, + connections.finish(), + )); + finish_design(&top.name, modules) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn whole_decoder_flat_graph_preserves_all_seven_outputs() -> eyre::Result<()> { + let source = format!( + "{}\n{}", + include_str!("../../../test/alu_display/bcd_to_7seg_minimized.v"), + include_str!("../../../test/alu_display/bcd_digit_to_7seg.v") + ); + let design = LogicalDesign::from_verilog_source(&source)?; + let graph = design.lower_flat_logic_graph()?; + let truth = graph.truth_table()?; + assert_eq!( + truth.input_names, + vec!["digit_0", "digit_1", "digit_2", "digit_3"] + ); + assert_eq!(truth.output_tables.len(), 7); + let masks = [ + 0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0, 0, 0, 0, 0, 0, + ]; + for bit in 0..7 { + for value in 0..16 { + assert_eq!( + truth.output_tables[&format!("seg_{bit}")][value], + masks[value] & (1 << bit) != 0 + ); + } + } + assert!(graph.nodes.iter().all(|node| matches!(&node.kind, GraphNodeKind::Input(_) | GraphNodeKind::Output(_)) || matches!(&node.kind, GraphNodeKind::Logic(logic) if matches!(logic.logic_type, LogicType::Not | LogicType::Or)))); + Ok(()) + } + + fn simulate(design: &RoutableDesign, input: &HashMap) -> HashMap { + let top = design.module(&design.top).unwrap(); + let RoutableModuleBody::Composite { instances, nets } = &top.body else { + panic!("expected bit-mapped composite") + }; + let mut values = input + .iter() + .map(|(name, &value)| (self_port(name), value)) + .collect::>(); + let mut remaining = instances.iter().collect::>(); + while !remaining.is_empty() { + for net in nets { + if let Some(value) = values.get(&net.driver).copied() { + for sink in &net.sinks { + values.insert(sink.clone(), value); + } + } + } + let before = remaining.len(); + remaining.retain(|instance| { + let leaf = design.module(&instance.module).unwrap(); + let RoutableModuleBody::Leaf { nodes } = &leaf.body else { + panic!("nested physical module") + }; + if leaf + .ports + .iter() + .filter(|p| p.direction == RoutablePortDirection::Input) + .any(|p| !values.contains_key(&instance_port(&instance.name, &p.name))) + { + return true; + } + fn evaluate( + id: usize, + nodes: &[RoutableNode], + instance: &str, + values: &HashMap, + ) -> bool { + let node = nodes.iter().find(|node| node.id == id).unwrap(); + match &node.kind { + RoutableNodeKind::Input { name } => values[&instance_port(instance, name)], + RoutableNodeKind::Output { .. } => { + evaluate(node.inputs[0], nodes, instance, values) + } + RoutableNodeKind::Not => !evaluate(node.inputs[0], nodes, instance, values), + RoutableNodeKind::Or => node + .inputs + .iter() + .any(|id| evaluate(*id, nodes, instance, values)), + _ => panic!("unexpected sequential node"), + } + } + for node in nodes { + if let RoutableNodeKind::Output { name } = &node.kind { + let value = evaluate(node.id, nodes, &instance.name, &values); + values.insert(instance_port(&instance.name, name), value); + } + } + false + }); + assert!(remaining.len() < before, "stuck physical evaluator"); + } + for net in nets { + if let Some(value) = values.get(&net.driver).copied() { + for sink in &net.sinks { + values.insert(sink.clone(), value); + } + } + } + top.ports + .iter() + .filter(|p| p.direction == RoutablePortDirection::Output) + .map(|p| (p.name.clone(), values[&self_port(&p.name)])) + .collect() + } + + fn bits(input: &mut HashMap, name: &str, width: usize, value: usize) { + for i in 0..width { + input.insert( + if width == 1 { + name.to_owned() + } else { + format!("{name}_{i}") + }, + value & (1 << i) != 0, + ); + } + } + fn number(output: &HashMap, name: &str, width: usize) -> usize { + (0..width) + .map(|i| { + usize::from( + output[&if width == 1 { + name.to_owned() + } else { + format!("{name}_{i}") + }], + ) << i + }) + .sum() + } + fn compile(source: &str) -> eyre::Result { + let logical = LogicalDesign::from_verilog_source(source)?; + // Every new operation must survive the editable RCIR text boundary. + let reparsed: LogicalDesign = logical.to_string().parse()?; + assert_eq!(logical.to_string(), reparsed.to_string()); + reparsed.lower_to_routable() + } + + #[test] + fn decoder_vectors_survive_typed_and_bit_mapping() -> eyre::Result<()> { + let source = include_str!("../../../test/alu_display/bcd_to_7seg.v"); + let logical = LogicalDesign::from_verilog_source(source)?; + let design = logical.lower_to_routable_with_bit_mapping_limits(4, 3)?; + let baseline = lower_with_limits(&LogicalDesign::from_verilog_source(source)?, 1, 2)?; + let count = |design: &RoutableDesign| match &design.module(&design.top).unwrap().body { + RoutableModuleBody::Composite { instances, .. } => instances.len(), + _ => 0, + }; + assert!(count(&design) < count(&baseline)); + eprintln!( + "decoder instances: grouped={}, primitive={}", + count(&design), + count(&baseline) + ); + for module in &design.modules { + if let RoutableModuleBody::Leaf { .. } = &module.body { + assert!( + module + .ports + .iter() + .filter(|p| p.direction == RoutablePortDirection::Input) + .count() + <= 3 + ); + } + } + let masks = [0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f]; + for digit in 0..16 { + for blank in 0..2 { + let mut input = HashMap::new(); + bits(&mut input, "digit", 4, digit); + bits(&mut input, "blank", 1, blank); + let output = simulate(&design, &input); + assert_eq!(output, simulate(&baseline, &input)); + assert_eq!( + number(&output, "seg", 7), + if blank == 0 && digit < 10 { + masks[digit] + } else { + 0 + }, + "digit={digit},blank={blank}" + ); + } + } + Ok(()) + } + + fn primitive_stats(design: &RoutableDesign) -> (usize, usize) { + let RoutableModuleBody::Composite { instances, nets } = + &design.module(&design.top).unwrap().body + else { + panic!() + }; + fn depth(instance: &str, nets: &[RoutableNet], memo: &mut HashMap) -> usize { + if let Some(&depth) = memo.get(instance) { + return depth; + } + let parents=nets.iter().filter(|net| net.sinks.iter().any(|sink|matches!(sink,Endpoint::InstancePort{instance:target,..} if target==instance))).map(|net|match &net.driver {Endpoint::SelfPort{..}=>0,Endpoint::InstancePort{instance,..}=>depth(instance,nets,memo)}).max().unwrap_or(0); + memo.insert(instance.to_owned(), parents + 1); + parents + 1 + } + let mut memo = HashMap::new(); + ( + instances.len(), + instances + .iter() + .map(|instance| depth(&instance.name, nets, &mut memo)) + .max() + .unwrap_or(0), + ) + } + + #[test] + fn associative_balancing_preserves_decoder_vectors_and_sharing() -> eyre::Result<()> { + for (name, source) in [ + ( + "ternary", + include_str!("../../../test/alu_display/bcd_to_7seg.v"), + ), + ( + "sop", + include_str!("../../../test/alu_display/bcd_to_7seg_sop.v"), + ), + ] { + let logical = LogicalDesign::from_verilog_source(source)?; + let baseline = logical.lower_to_routable()?; + let balanced = logical.lower_to_routable_with_balanced_associative_trees(1, 2)?; + let before = primitive_stats(&baseline); + let after = primitive_stats(&balanced); + eprintln!( + "balanced {name}: instances {} -> {}, primitive stages {} -> {}", + before.0, after.0, before.1, after.1 + ); + assert!(after.0 <= before.0); + assert!(after.1 <= before.1); + for mask in 0..32 { + let mut input = HashMap::new(); + bits(&mut input, "digit", 4, mask & 15); + bits(&mut input, "blank", 1, mask >> 4); + assert_eq!( + simulate(&baseline, &input), + simulate(&balanced, &input), + "{name}, input={mask}" + ); + } + } + Ok(()) + } + + #[test] + fn associative_balancing_shortens_reductions_without_duplicating_shared_logic( + ) -> eyre::Result<()> { + for (name, source) in [ + ( + "reductions", + "module top(a,all,zero); input [7:0] a; output all,zero; assign all=a[0]&a[1]&a[2]&a[3]&a[4]&a[5]&a[6]&a[7]; assign zero=~(a[0]|a[1]|a[2]|a[3]|a[4]|a[5]|a[6]|a[7]); endmodule", + ), + ( + "shared", + "module top(a,y,z); input [7:0] a; output y,z; wire p; assign p=a[0]|a[1]|a[2]|a[3]; assign y=p|a[4]|a[5]|a[6]|a[7]; assign z=p^a[7]; endmodule", + ), + ] { + let logical = LogicalDesign::from_verilog_source(source)?; + let baseline = logical.lower_to_routable()?; + let balanced = logical.lower_to_routable_with_balanced_associative_trees(1, 2)?; + let before = primitive_stats(&baseline); + let after = primitive_stats(&balanced); + eprintln!( + "balanced {name}: instances {} -> {}, primitive stages {} -> {}", + before.0, after.0, before.1, after.1 + ); + assert!(after.0 <= before.0); + assert!(after.1 < before.1); + if name == "reductions" { + assert_eq!(before, (15, 8)); + assert_eq!(after, (15, 4)); + } + for mask in 0..256 { + let mut input = HashMap::new(); + bits(&mut input, "a", 8, mask); + let actual = simulate(&balanced, &input); + assert_eq!(actual, simulate(&baseline, &input)); + if name == "reductions" { + assert_eq!(actual["all"], mask == 255); + assert_eq!(actual["zero"], mask == 0); + } else { + assert_eq!(actual["y"], mask != 0); + assert_eq!(actual["z"], (mask & 15 != 0) ^ (mask & 128 != 0)); + } + } + } + Ok(()) + } + + #[test] + fn associative_cells_are_pure_bounded_and_or_and_preserve_decoder() -> eyre::Result<()> { + for (name, source) in [ + ( + "ternary", + include_str!("../../../test/alu_display/bcd_to_7seg.v"), + ), + ( + "sop", + include_str!("../../../test/alu_display/bcd_to_7seg_sop.v"), + ), + ] { + let logical = LogicalDesign::from_verilog_source(source)?; + let primitive = logical.lower_to_routable()?; + let mapped = logical.lower_to_routable_with_associative_cells(8)?; + eprintln!( + "associative cells {name}: {:?} -> {:?} (instances, stages)", + primitive_stats(&primitive), + primitive_stats(&mapped) + ); + for leaf in mapped + .modules + .iter() + .filter(|module| matches!(module.body, RoutableModuleBody::Leaf { .. })) + { + let table = LogicGraph { + graph: crate::ir::graph_from_routable_leaf(leaf)?, + } + .truth_table()?; + let n = table.input_names.len(); + assert!((1..=8).contains(&n)); + assert_eq!(table.output_tables.len(), 1); + let actual = table.output_tables.values().next().unwrap(); + let and = (0..1usize << n) + .map(|mask| mask == (1 << n) - 1) + .collect::>(); + let or = (0..1usize << n).map(|mask| mask != 0).collect::>(); + let not = n == 1 && actual == &vec![true, false]; + let xor = n == 2 && actual == &vec![false, true, true, false]; + assert!( + actual == &and || actual == &or || not || xor, + "mixed-function leaf {}", + leaf.name + ); + } + for mask in 0..32 { + let mut input = HashMap::new(); + bits(&mut input, "digit", 4, mask & 15); + bits(&mut input, "blank", 1, mask >> 4); + assert_eq!( + simulate(&mapped, &input), + simulate(&primitive, &input), + "{name} input={mask}" + ); + } + } + Ok(()) + } + + #[test] + fn associative_cells_group_eight_operands_but_not_not_or_xor() -> eyre::Result<()> { + let logical = LogicalDesign::from_verilog_source( + "module top(a,any,all,inverted,parity); input [7:0] a; output any,all,inverted,parity; assign any=a[0]|a[1]|a[2]|a[3]|a[4]|a[5]|a[6]|a[7]; assign all=a[0]&a[1]&a[2]&a[3]&a[4]&a[5]&a[6]&a[7]; assign inverted=~a[0]; assign parity=a[0]^a[1]; endmodule", + )?; + let mapped = logical.lower_to_routable_with_associative_cells(8)?; + assert_eq!(primitive_stats(&mapped).0, 4); + assert!(logical.lower_to_routable_with_associative_cells(9).is_err()); + for mask in 0..256 { + let mut input = HashMap::new(); + bits(&mut input, "a", 8, mask); + let actual = simulate(&mapped, &input); + assert_eq!(actual["any"], mask != 0); + assert_eq!(actual["all"], mask == 255); + assert_eq!(actual["inverted"], mask & 1 == 0); + assert_eq!(actual["parity"], ((mask & 1) ^ ((mask >> 1) & 1)) != 0); + } + Ok(()) + } + + #[test] + fn expanded_associative_cells_reduce_instances_with_bounded_pure_leaves() -> eyre::Result<()> { + for (name, source) in [ + ( + "ternary", + include_str!("../../../test/alu_display/bcd_to_7seg.v"), + ), + ( + "sop", + include_str!("../../../test/alu_display/bcd_to_7seg_sop.v"), + ), + ] { + let logical = LogicalDesign::from_verilog_source(source)?; + let shared = logical.lower_to_routable_with_associative_cells(8)?; + let expanded = logical.lower_to_routable_with_expanded_associative_cells(8)?; + let primitive = logical.lower_to_routable()?; + assert!(primitive_stats(&expanded).0 <= primitive_stats(&shared).0); + let raw_gates = |design: &RoutableDesign| { + let RoutableModuleBody::Composite { instances, .. } = + &design.module(&design.top).unwrap().body + else { + panic!() + }; + instances + .iter() + .map( + |instance| match &design.module(&instance.module).unwrap().body { + RoutableModuleBody::Leaf { nodes } => nodes + .iter() + .filter(|node| { + matches!( + node.kind, + RoutableNodeKind::Not | RoutableNodeKind::Or + ) + }) + .count(), + _ => 0, + }, + ) + .sum::() + }; + eprintln!( + "expanded {name}: shared {:?}, expanded {:?}, binary NOR nodes {} -> {}", + primitive_stats(&shared), + primitive_stats(&expanded), + raw_gates(&shared), + raw_gates(&expanded) + ); + for leaf in expanded + .modules + .iter() + .filter(|module| matches!(module.body, RoutableModuleBody::Leaf { .. })) + { + let table = LogicGraph { + graph: crate::ir::graph_from_routable_leaf(leaf)?, + } + .truth_table()?; + let n = table.input_names.len(); + assert!((1..=8).contains(&n)); + assert_eq!(table.output_tables.len(), 1); + let actual = table.output_tables.values().next().unwrap(); + let and = (0..1usize << n) + .map(|mask| mask == (1 << n) - 1) + .collect::>(); + let or = (0..1usize << n).map(|mask| mask != 0).collect::>(); + assert!( + actual == &and + || actual == &or + || (n == 1 && actual == &vec![true, false]) + || (n == 2 && actual == &vec![false, true, true, false]), + "unexpected mixed leaf {}", + leaf.name + ); + } + for mask in 0..32 { + let mut input = HashMap::new(); + bits(&mut input, "digit", 4, mask & 15); + bits(&mut input, "blank", 1, mask >> 4); + assert_eq!( + simulate(&expanded, &input), + simulate(&primitive, &input), + "{name} input={mask}" + ); + } + } + let logical=LogicalDesign::from_verilog_source("module top(a,y,z); input [7:0] a; output y,z; wire p; assign p=a[0]|a[1]|a[2]|a[3]; assign y=p|a[4]|a[5]|a[6]|a[7]; assign z=p^a[7]; endmodule")?; + let shared = logical.lower_to_routable_with_associative_cells(8)?; + let expanded = logical.lower_to_routable_with_expanded_associative_cells(8)?; + assert_eq!( + shared.to_string(), + expanded.to_string(), + "equal cell count must keep shared implementation" + ); + Ok(()) + } + + #[test] + fn literal_associative_cells_preserve_decoder_and_single_exception_signatures( + ) -> eyre::Result<()> { + let sop = include_str!("../../../test/alu_display/bcd_to_7seg_sop.v"); + let digit = format!( + "{}\n{}", + sop, + include_str!("../../../test/alu_display/bcd_digit_to_7seg.v") + ); + for (name, source) in [("full", sop), ("digit", digit.as_str())] { + let logical = LogicalDesign::from_verilog_source(source)?; + let baseline = logical.lower_to_routable_with_expanded_associative_cells(8)?; + let literal = logical.lower_to_routable_with_literal_associative_cells(8)?; + eprintln!( + "literal {name}: {:?} -> {:?} (instances, stages)", + primitive_stats(&baseline), + primitive_stats(&literal) + ); + assert!(primitive_stats(&literal).0 < primitive_stats(&baseline).0); + for leaf in literal + .modules + .iter() + .filter(|module| matches!(module.body, RoutableModuleBody::Leaf { .. })) + { + let table = LogicGraph { + graph: crate::ir::graph_from_routable_leaf(leaf)?, + } + .truth_table()?; + let n = table.input_names.len(); + assert!((1..=8).contains(&n)); + assert_eq!(table.output_tables.len(), 1); + let actual = table.output_tables.values().next().unwrap(); + let true_rows = actual.iter().filter(|&&value| value).count(); + assert!( + true_rows == 1 || true_rows == actual.len() - 1, + "not a literal cube/clause: {}", + leaf.name + ); + } + let masks = [ + 0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0, 0, 0, 0, 0, 0, + ]; + for mask in 0..if name == "full" { 32 } else { 16 } { + let mut input = HashMap::new(); + bits(&mut input, "digit", 4, mask & 15); + if name == "full" { + bits(&mut input, "blank", 1, mask >> 4); + } + let actual = simulate(&literal, &input); + assert_eq!(actual, simulate(&baseline, &input)); + assert_eq!( + number(&actual, "seg", 7), + if mask & 16 == 0 { masks[mask & 15] } else { 0 } + ); + } + } + Ok(()) + } + + #[test] + fn literal_associative_cells_keep_not_operands_opaque_and_handle_opposite_literals( + ) -> eyre::Result<()> { + let source="module top(a,y,z,c); input [3:0] a; output y,z,c; wire p; assign p=a[0]&a[1]; assign y=(~p)&a[2]&a[3]; assign z=p|a[3]; assign c=(a[0]&a[1])&~a[0]; endmodule"; + let logical = LogicalDesign::from_verilog_source(source)?; + let literal = logical.lower_to_routable_with_literal_associative_cells(8)?; + for mask in 0..16 { + let mut input = HashMap::new(); + bits(&mut input, "a", 4, mask); + let actual = simulate(&literal, &input); + let p = mask & 3 == 3; + assert_eq!(actual["y"], !p && mask & 12 == 12); + assert_eq!(actual["z"], p || mask & 8 != 0); + assert!(!actual["c"]); + } + Ok(()) + } + + #[test] + fn minimized_decoder_covers_preserve_all_values_and_reduce_routed_edges() -> eyre::Result<()> { + let masks = [ + 0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0, 0, 0, 0, 0, 0, + ]; + let old = include_str!("../../../test/alu_display/bcd_to_7seg_sop.v"); + let minimized = include_str!("../../../test/alu_display/bcd_to_7seg_minimized.v"); + let wrapper = include_str!("../../../test/alu_display/bcd_digit_to_7seg.v"); + for blank in [true, false] { + let make = |source: &str| { + let source = if blank { + source.to_owned() + } else { + format!("{source}\n{wrapper}") + }; + LogicalDesign::from_verilog_source(&source)? + .lower_to_routable_with_literal_associative_cells(8) + }; + let baseline = make(old)?; + let mapped = make(minimized)?; + let stats = |design: &RoutableDesign| { + let RoutableModuleBody::Composite { instances, nets } = + &design.module(&design.top).unwrap().body + else { + panic!() + }; + ( + instances.len(), + nets.iter() + .flat_map(|net| &net.sinks) + .filter(|sink| matches!(sink, Endpoint::InstancePort { .. })) + .count(), + nets.iter().map(|net| net.sinks.len()).max().unwrap_or(0), + ) + }; + let before = stats(&baseline); + let after = stats(&mapped); + eprintln!("minimized decoder blank={blank}: (cells, input edges, max fanout) {before:?} -> {after:?}"); + assert!(after.1 < before.1); + for mask in 0..if blank { 32 } else { 16 } { + let mut input = HashMap::new(); + bits(&mut input, "digit", 4, mask & 15); + if blank { + bits(&mut input, "blank", 1, mask >> 4); + } + let actual = simulate(&mapped, &input); + assert_eq!(actual, simulate(&baseline, &input)); + assert_eq!( + number(&actual, "seg", 7), + if mask & 16 == 0 { masks[mask & 15] } else { 0 } + ); + } + for leaf in mapped + .modules + .iter() + .filter(|module| matches!(module.body, RoutableModuleBody::Leaf { .. })) + { + let table = LogicGraph { + graph: crate::ir::graph_from_routable_leaf(leaf)?, + } + .truth_table()?; + assert!(table.input_names.len() <= 8); + let values = table.output_tables.values().next().unwrap(); + let count = values.iter().filter(|&&v| v).count(); + assert!(count == 1 || count == values.len() - 1); + } + } + Ok(()) + } + + #[test] + fn explicit_shared_add_sub_fixtures_match_integer_oracle() -> eyre::Result<()> { + for (width, source) in [ + (1, include_str!("../../../test/alu_display/add_sub1.v")), + (2, include_str!("../../../test/alu_display/add_sub2.v")), + (4, include_str!("../../../test/alu_display/add_sub4.v")), + ] { + let logical = LogicalDesign::from_verilog_source(source)?; + let design = logical.lower_to_routable_with_associative_cells(8)?; + let mask = (1usize << width) - 1; + eprintln!("shared add/sub {width}bit: {:?}", primitive_stats(&design)); + for a in 0..=mask { + for b in 0..=mask { + for subtract in 0..2 { + let mut input = HashMap::new(); + bits(&mut input, "a", width, a); + bits(&mut input, "b", width, b); + bits(&mut input, "subtract", 1, subtract); + let output = simulate(&design, &input); + assert_eq!( + number(&output, "y", width), + if subtract != 0 { + a.wrapping_sub(b) & mask + } else { + (a + b) & mask + } + ); + assert_eq!( + output["c"], + if subtract != 0 { a >= b } else { a + b > mask } + ); + } + } + } + } + Ok(()) + } + + #[test] + fn unsigned_arithmetic_select_concat_and_mux_are_bit_exact() -> eyre::Result<()> { + let design = compile( + "module arithmetic(a,b,s,sum,diff,ge,eq,y,rev); input [3:0] a,b; input s; output [4:0] sum; output [3:0] diff,y,rev; output ge,eq; assign sum={1'b0,a}+{1'b0,b}; assign diff=a-b; assign ge=a>=b; assign eq=a==b; assign y=s?a:b; assign rev={a[0],a[1],a[2],a[3]}; endmodule", + )?; + for a in 0..16 { + for b in 0..16 { + for s in 0..2 { + let mut input = HashMap::new(); + bits(&mut input, "a", 4, a); + bits(&mut input, "b", 4, b); + bits(&mut input, "s", 1, s); + let output = simulate(&design, &input); + assert_eq!(number(&output, "sum", 5), a + b); + assert_eq!(number(&output, "diff", 4), a.wrapping_sub(b) & 15); + assert_eq!(number(&output, "ge", 1), usize::from(a >= b)); + assert_eq!(number(&output, "eq", 1), usize::from(a == b)); + assert_eq!(number(&output, "y", 4), if s == 1 { a } else { b }); + assert_eq!( + number(&output, "rev", 4), + (a as u8).reverse_bits() as usize >> 4 + ); + } + } + } + Ok(()) + } + + #[test] + fn nested_hierarchy_and_expression_bindings_preserve_bits() -> eyre::Result<()> { + let design = compile( + "module invert(a,y); input [1:0] a; output [1:0] y; assign y=~a; endmodule module inner(a,y); input [3:0] a; output [1:0] y; invert u(.a({a[0],a[3]}),.y(y)); endmodule module outer(a,y,z); input [3:0] a; output [1:0] y; output z; wire [3:0] intermediate; assign intermediate=a^4'b1010; inner u(.a(intermediate),.y(y)); assign z=y[1]^y[0]; endmodule", + )?; + for a in 0..16 { + let mut input = HashMap::new(); + bits(&mut input, "a", 4, a); + let output = simulate(&design, &input); + let intermediate = a ^ 10; + let expected = (!(((intermediate & 1) << 1) | (intermediate >> 3))) & 3; + assert_eq!(number(&output, "y", 2), expected); + assert_eq!(number(&output, "z", 1), (expected >> 1) ^ (expected & 1)); + } + Ok(()) + } + + #[test] + fn constant_and_pass_through_outputs_have_physical_drivers() -> eyre::Result<()> { + let design = compile( + "module wiring(a,y,z); input [1:0] a; output [3:0] y; output z; assign y={2'b10,a}; assign z=a[1]; endmodule", + )?; + for a in 0..4 { + let mut input = HashMap::new(); + bits(&mut input, "a", 2, a); + let output = simulate(&design, &input); + assert_eq!(number(&output, "y", 4), 8 + a); + assert_eq!(number(&output, "z", 1), a >> 1); + } + Ok(()) + } +} diff --git a/src/ir/logical_text.rs b/src/ir/logical_text.rs index aa01be35..eed37b5d 100644 --- a/src/ir/logical_text.rs +++ b/src/ir/logical_text.rs @@ -150,6 +150,10 @@ fn write_module(output: &mut fmt::Formatter<'_>, module: &LogicalModule) -> fmt: } match &cell.kind { + LogicalCellKind::Select { msb, lsb } => { + writeln!(output, " msb = {msb};")?; + writeln!(output, " lsb = {lsb};")?; + } LogicalCellKind::Dff { edge } | LogicalCellKind::Register { edge, .. } => { writeln!(output, " edge = {};", edge_name(*edge))?; } @@ -181,11 +185,19 @@ fn cell_width(cell: &LogicalCell, net_widths: &std::collections::HashMap<&str, u fn pin_rank(kind: &LogicalCellKind, pin: &str) -> usize { let order: &[&str] = match kind { - LogicalCellKind::Buffer | LogicalCellKind::Not | LogicalCellKind::Inc => &["value"], + LogicalCellKind::Buffer + | LogicalCellKind::Not + | LogicalCellKind::Inc + | LogicalCellKind::Select { .. } + | LogicalCellKind::Resize { .. } => &["value"], LogicalCellKind::And | LogicalCellKind::Or | LogicalCellKind::Xor - | LogicalCellKind::Add => &["lhs", "rhs"], + | LogicalCellKind::Add + | LogicalCellKind::Equal + | LogicalCellKind::GreaterEqual + | LogicalCellKind::Sub + | LogicalCellKind::Concat => &["lhs", "rhs"], LogicalCellKind::Mux => &["select", "when_false", "when_true"], LogicalCellKind::DLatch { .. } => &["d", "enable"], LogicalCellKind::Dff { .. } | LogicalCellKind::Register { .. } => &["d", "clock"], @@ -254,6 +266,12 @@ fn operation_name(kind: &LogicalCellKind) -> &'static str { LogicalCellKind::Add => "add", LogicalCellKind::Inc => "inc", LogicalCellKind::Mux => "mux", + LogicalCellKind::Equal => "equal", + LogicalCellKind::GreaterEqual => "greater_equal", + LogicalCellKind::Sub => "sub", + LogicalCellKind::Concat => "concat", + LogicalCellKind::Select { .. } => "select", + LogicalCellKind::Resize { .. } => "resize", LogicalCellKind::DLatch { .. } => "d_latch", LogicalCellKind::Dff { .. } => "dff", LogicalCellKind::Register { .. } => "register", @@ -396,6 +414,8 @@ impl Parser { let mut inputs = Vec::new(); let mut outputs = Vec::new(); let mut edge = None; + let mut msb = None; + let mut lsb = None; while !self.consume_symbol('}') { if self.consume_keyword("in") { let pin = self.expect_name()?; @@ -409,6 +429,14 @@ impl Parser { let net = self.expect_name()?; self.expect_symbol(';')?; outputs.push(LogicalOutput { pin, net }); + } else if self.consume_keyword("msb") { + self.expect_symbol('=')?; + msb = Some(self.expect_usize()?); + self.expect_symbol(';')?; + } else if self.consume_keyword("lsb") { + self.expect_symbol('=')?; + lsb = Some(self.expect_usize()?); + self.expect_symbol(';')?; } else if self.consume_keyword("edge") { self.expect_symbol('=')?; edge = Some(self.parse_edge()?); @@ -427,6 +455,15 @@ impl Parser { "add" => LogicalCellKind::Add, "inc" => LogicalCellKind::Inc, "mux" => LogicalCellKind::Mux, + "equal" => LogicalCellKind::Equal, + "greater_equal" => LogicalCellKind::GreaterEqual, + "sub" => LogicalCellKind::Sub, + "concat" => LogicalCellKind::Concat, + "resize" => LogicalCellKind::Resize { width }, + "select" => LogicalCellKind::Select { + msb: msb.context("select requires msb")?, + lsb: lsb.context("select requires lsb")?, + }, "d_latch" => LogicalCellKind::DLatch { width }, "dff" => { if width != 1 { @@ -442,6 +479,9 @@ impl Parser { }, other => eyre::bail!("unknown logical operation `logical.{other}`"), }; + if (msb.is_some() || lsb.is_some()) && !matches!(kind, LogicalCellKind::Select { .. }) { + eyre::bail!("logical.{operation} does not accept select attributes"); + } if edge.is_some() && !matches!( kind, diff --git a/src/ir/mod.rs b/src/ir/mod.rs index e7a3ac8d..2bf1623c 100644 --- a/src/ir/mod.rs +++ b/src/ir/mod.rs @@ -20,13 +20,14 @@ pub use logical::{ LogicalValue, LOGICAL_IR_VERSION, }; pub use pnr::{ - CandidateSpec, CellFaceSpec, ClusteringSpec, CongestionSpec, Free3dSweepSpec, - InputPlacementSpec, LayerAssignmentSpec, LocalAdaptiveSpec, LocalCellContractSpec, - LocalObjectiveSpec, LocalPlacerSpec, NetOrderSpec, NotRouteSpec, ObjectiveSpec, - PhysicalConstraintSpec, PhysicalRegionSpec, PhysicalSpec, PlacementHeuristicSpec, - PlacementSamplingSpec, PlacementScheduleSpec, PlacementSpec, PnrSpec, PortAccessDirectionSpec, - PortAccessSpec, PortRef, PreferenceSpec, RoutableDocument, RouteStageSpec, RouteStrategySpec, - RouteValidationSpec, RoutingSpec, SamplingSpec, SearchSpec, TorchPlacementSpec, + CandidateSpec, CellFaceSpec, ClusteringSpec, CongestionSpec, ExactCandidateSpec, + Free3dSweepSpec, InputPlacementSpec, LayerAssignmentSpec, LocalAdaptiveSpec, + LocalCellContractSpec, LocalObjectiveSpec, LocalPlacerSpec, NetOrderSpec, NotRouteSpec, + ObjectiveSpec, PhysicalConstraintSpec, PhysicalRegionSpec, PhysicalSpec, + PlacementHeuristicSpec, PlacementSamplingSpec, PlacementScheduleSpec, PlacementSpec, PnrSpec, + PortAccessDirectionSpec, PortAccessSpec, PortRef, PreferenceSpec, RoutableDocument, + RouteStageSpec, RouteStrategySpec, RouteValidationSpec, RoutingSpec, SamplingSpec, SearchSpec, + TorchPlacementSpec, }; pub use routable::{ Endpoint, NetClass, RoutableDesign, RoutableInstance, RoutableModule, RoutableModuleBody, diff --git a/src/ir/pnr.rs b/src/ir/pnr.rs index b1582df0..1456caef 100644 --- a/src/ir/pnr.rs +++ b/src/ir/pnr.rs @@ -111,8 +111,31 @@ pub struct PnrSpec { pub search: SearchSpec, } +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ExactCandidateSpec { + pub optimize: bool, + pub search_box: [usize; 3], + pub time_limit_ms: u64, + pub max_inputs: usize, + pub max_logic_nodes: usize, +} + +impl Default for ExactCandidateSpec { + fn default() -> Self { + Self { + optimize: true, + search_box: [3, 5, 3], + time_limit_ms: 5_000, + max_inputs: 4, + max_logic_nodes: 16, + } + } +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CandidateSpec { + #[serde(default)] + pub exact: Option, pub search_box: [usize; 3], pub retain: usize, pub combinational_samples: Option, @@ -422,15 +445,24 @@ pub struct RoutingSpec { #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct RouteStageSpec { + #[serde(default = "default_route_max_steps")] + pub max_route_steps: usize, pub strategy: RouteStrategySpec, pub validation: RouteValidationSpec, } +fn default_route_max_steps() -> usize { + 128 +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum RouteStrategySpec { BreadthFirst, AStar, + AStarBudgeted { + max_expansions: usize, + }, DirectGreedy { max_steps: usize, }, @@ -454,6 +486,7 @@ pub enum NetOrderSpec { Criticality, HighestFanoutFirst, ReverseCriticality, + PinAccess, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] diff --git a/src/ir/routable.rs b/src/ir/routable.rs index c61815fa..23e58f20 100644 --- a/src/ir/routable.rs +++ b/src/ir/routable.rs @@ -338,8 +338,11 @@ impl RoutableModule { let endpoint = Endpoint::SelfPort { port: port.name.clone(), }; + // Bit selection and constant propagation can leave public input + // bits unused. They remain in the interface, but need no route. + // Every output must still have exactly one driver. let connected = match port.direction { - RoutablePortDirection::Input => driven_ports.contains(&endpoint), + RoutablePortDirection::Input => true, RoutablePortDirection::Output => consumed_ports.contains(&endpoint), }; if !connected { diff --git a/src/ir/text.rs b/src/ir/text.rs index d729d542..298e789e 100644 --- a/src/ir/text.rs +++ b/src/ir/text.rs @@ -8,13 +8,14 @@ use super::routable::{ }; use super::syntax::{tokenize, Token}; use super::{ - CandidateSpec, CellFaceSpec, ClusteringSpec, CongestionSpec, Free3dSweepSpec, - InputPlacementSpec, LayerAssignmentSpec, LocalAdaptiveSpec, LocalCellContractSpec, - LocalObjectiveSpec, LocalPlacerSpec, NetOrderSpec, NotRouteSpec, ObjectiveSpec, - PhysicalConstraintSpec, PhysicalRegionSpec, PhysicalSpec, PlacementHeuristicSpec, - PlacementSamplingSpec, PlacementScheduleSpec, PlacementSpec, PnrSpec, PortAccessDirectionSpec, - PortAccessSpec, PortRef, PreferenceSpec, RoutableDocument, RouteStageSpec, RouteStrategySpec, - RouteValidationSpec, RoutingSpec, SamplingSpec, SearchSpec, TorchPlacementSpec, + CandidateSpec, CellFaceSpec, ClusteringSpec, CongestionSpec, ExactCandidateSpec, + Free3dSweepSpec, InputPlacementSpec, LayerAssignmentSpec, LocalAdaptiveSpec, + LocalCellContractSpec, LocalObjectiveSpec, LocalPlacerSpec, NetOrderSpec, NotRouteSpec, + ObjectiveSpec, PhysicalConstraintSpec, PhysicalRegionSpec, PhysicalSpec, + PlacementHeuristicSpec, PlacementSamplingSpec, PlacementScheduleSpec, PlacementSpec, PnrSpec, + PortAccessDirectionSpec, PortAccessSpec, PortRef, PreferenceSpec, RoutableDocument, + RouteStageSpec, RouteStrategySpec, RouteValidationSpec, RoutingSpec, SamplingSpec, SearchSpec, + TorchPlacementSpec, }; impl fmt::Display for RoutableDesign { @@ -165,6 +166,7 @@ fn write_design_profile_body(output: &mut fmt::Formatter<'_>, pnr: &PnrSpec) -> NetOrderSpec::Criticality => "criticality", NetOrderSpec::HighestFanoutFirst => "highest-fanout-first", NetOrderSpec::ReverseCriticality => "reverse-criticality", + NetOrderSpec::PinAccess => "pin-access", } )?; } @@ -282,6 +284,9 @@ fn write_candidate_body( Some(value) => writeln!(output, "{indent}combinational-samples {value};")?, None => writeln!(output, "{indent}combinational-samples none;")?, } + if let Some(exact) = &candidate.exact { + writeln!(output, "{indent}exact {{ box [{}, {}, {}]; time-limit-ms {}; max-inputs {}; max-logic-nodes {}; optimize {}; }}", exact.search_box[0], exact.search_box[1], exact.search_box[2], exact.time_limit_ms, exact.max_inputs, exact.max_logic_nodes, exact.optimize)?; + } writeln!(output, "{indent}clustering {{")?; write_clustering_body(output, &candidate.clustering, &format!("{indent} "))?; writeln!(output, "{indent}}}")?; @@ -520,6 +525,9 @@ fn write_route_stage( match stage.strategy { RouteStrategySpec::BreadthFirst => writeln!(output, " strategy breadth-first;")?, RouteStrategySpec::AStar => writeln!(output, " strategy astar;")?, + RouteStrategySpec::AStarBudgeted { max_expansions } => { + writeln!(output, " strategy astar-budgeted {max_expansions};")? + } RouteStrategySpec::DirectGreedy { max_steps } => { writeln!(output, " strategy direct-greedy;")?; writeln!(output, " max-steps {max_steps};")?; @@ -543,6 +551,7 @@ fn write_route_stage( RouteValidationSpec::Deferred => "deferred", } )?; + writeln!(output, " max-steps {};", stage.max_route_steps)?; writeln!(output, " }}") } @@ -1182,6 +1191,41 @@ impl Parser { self.expect_keyword("combinational-samples")?; let combinational_samples = self.parse_optional_usize()?; self.expect_symbol(';')?; + let exact = if self.consume_keyword("exact") { + self.expect_symbol('{')?; + self.expect_keyword("box")?; + let search_box = self.parse_position()?; + self.expect_symbol(';')?; + self.expect_keyword("time-limit-ms")?; + let time_limit_ms = self.expect_u64()?; + self.expect_symbol(';')?; + self.expect_keyword("max-inputs")?; + let max_inputs = self.expect_usize()?; + self.expect_symbol(';')?; + self.expect_keyword("max-logic-nodes")?; + let max_logic_nodes = self.expect_usize()?; + self.expect_symbol(';')?; + self.expect_keyword("optimize")?; + let optimize = self.expect_bool()?; + self.expect_symbol(';')?; + self.expect_symbol('}')?; + eyre::ensure!( + search_box.iter().all(|v| *v > 0) + && time_limit_ms > 0 + && (1..=6).contains(&max_inputs) + && max_logic_nodes > 0, + "invalid exact candidate bounds" + ); + Some(ExactCandidateSpec { + search_box, + time_limit_ms, + max_inputs, + max_logic_nodes, + optimize, + }) + } else { + None + }; let clustering = if self.consume_keyword("clustering") { self.parse_clustering_spec()? } else { @@ -1191,6 +1235,7 @@ impl Parser { let local_placer = self.parse_local_placer_spec()?; self.expect_symbol('}')?; Ok(CandidateSpec { + exact, search_box, retain, combinational_samples, @@ -1600,6 +1645,7 @@ impl Parser { "criticality" => NetOrderSpec::Criticality, "highest-fanout-first" => NetOrderSpec::HighestFanoutFirst, "reverse-criticality" => NetOrderSpec::ReverseCriticality, + "pin-access" => NetOrderSpec::PinAccess, value => eyre::bail!("unknown net order `{value}`"), }); if self.consume_symbol(']') { @@ -1625,10 +1671,20 @@ impl Parser { self.expect_symbol('{')?; self.expect_keyword("strategy")?; let kind = self.expect_word()?; + let astar_budget = if kind == "astar-budgeted" { + let budget = self.expect_usize()?; + eyre::ensure!(budget > 0, "astar-budgeted max-expansions must be positive"); + Some(budget) + } else { + None + }; self.expect_symbol(';')?; let strategy = match kind.as_str() { "breadth-first" => RouteStrategySpec::BreadthFirst, "astar" => RouteStrategySpec::AStar, + "astar-budgeted" => RouteStrategySpec::AStarBudgeted { + max_expansions: astar_budget.expect("parsed budget"), + }, "direct-greedy" => { self.expect_keyword("max-steps")?; let max_steps = self.expect_usize()?; @@ -1653,6 +1709,12 @@ impl Parser { } _ => eyre::bail!("unknown route strategy `{kind}`"), }; + let mut max_route_steps = 128; + if self.peek_keyword("max-steps") { + self.expect_keyword("max-steps")?; + max_route_steps = self.expect_usize()?; + self.expect_symbol(';')?; + } self.expect_keyword("validation")?; let validation = match self.expect_word()?.as_str() { "incremental" => RouteValidationSpec::Incremental, @@ -1660,8 +1722,15 @@ impl Parser { value => eyre::bail!("unknown route validation mode `{value}`"), }; self.expect_symbol(';')?; + if self.peek_keyword("max-steps") { + self.expect_keyword("max-steps")?; + max_route_steps = self.expect_usize()?; + self.expect_symbol(';')?; + } + eyre::ensure!(max_route_steps > 0, "route max-steps must be positive"); self.expect_symbol('}')?; Ok(RouteStageSpec { + max_route_steps, strategy, validation, }) diff --git a/src/transform/place_and_route/detailed_router.rs b/src/transform/place_and_route/detailed_router.rs index 1c7eb851..49017ed5 100644 --- a/src/transform/place_and_route/detailed_router.rs +++ b/src/transform/place_and_route/detailed_router.rs @@ -40,11 +40,36 @@ pub fn place_node(world: &mut World3D, node: PlacedNode) { } } +/// Keep local candidate enumeration stable; global routing may reuse fixed floors. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum SupportReusePolicy { + #[default] + FreshOnly, + ReuseExisting, +} + pub fn try_generate_cobble_node( world: &World3D, cobble_pos: Position, except: &[Position], ) -> Option { + try_generate_cobble_node_with_policy(world, cobble_pos, except, SupportReusePolicy::FreshOnly) +} + +pub fn try_generate_cobble_node_with_policy( + world: &World3D, + cobble_pos: Position, + except: &[Position], + support_policy: SupportReusePolicy, +) -> Option { + if !world.size.bound_on(cobble_pos) { + return None; + } + if support_policy == SupportReusePolicy::ReuseExisting && world[cobble_pos].kind.is_cobble() { + // Reusing a support changes neither geometry nor its current power. + // New dust/repeater placement still checks conflicts and shorts below. + return Some(PlacedNode::new(cobble_pos, world[cobble_pos])); + } if cobble_would_stack_above_side_torch_support(world, cobble_pos) { return None; } @@ -84,6 +109,24 @@ pub fn place_redstone_with_cobble_and_allowed_shorts( prev: Position, to: Position, allowed_shorts: Option<&HashSet>, +) -> PlaceRedstoneResult { + place_redstone_with_cobble_and_policy( + world, + bound, + prev, + to, + allowed_shorts, + SupportReusePolicy::FreshOnly, + ) +} + +pub fn place_redstone_with_cobble_and_policy( + world: &World3D, + bound: PlaceBound, + prev: Position, + to: Position, + allowed_shorts: Option<&HashSet>, + support_policy: SupportReusePolicy, ) -> PlaceRedstoneResult { let Some(cobble_pos) = bound.position().walk(Direction::Bottom) else { return PlaceRedstoneResult::Rejected(RouteRejectReason::NoBottomForCobble); @@ -91,7 +134,9 @@ pub fn place_redstone_with_cobble_and_allowed_shorts( let cobble_except = (world[prev].kind.is_torch()) .then_some(vec![cobble_pos, prev]) .unwrap_or_default(); - let Some(cobble_node) = try_generate_cobble_node(world, cobble_pos, &cobble_except) else { + let Some(cobble_node) = + try_generate_cobble_node_with_policy(world, cobble_pos, &cobble_except, support_policy) + else { return PlaceRedstoneResult::Rejected(RouteRejectReason::CobbleConflict); }; let mut new_world = world.clone(); @@ -146,11 +191,33 @@ pub fn place_repeater_with_cobble( to: Position, direction: Direction, allowed_shorts: Option<&HashSet>, +) -> PlaceRepeaterResult { + place_repeater_with_cobble_and_policy( + world, + bound, + prev, + to, + direction, + allowed_shorts, + SupportReusePolicy::FreshOnly, + ) +} + +pub fn place_repeater_with_cobble_and_policy( + world: &World3D, + bound: PlaceBound, + prev: Position, + to: Position, + direction: Direction, + allowed_shorts: Option<&HashSet>, + support_policy: SupportReusePolicy, ) -> PlaceRepeaterResult { let Some(cobble_pos) = bound.position().walk(Direction::Bottom) else { return PlaceRepeaterResult::Rejected(RouteRejectReason::NoBottomForCobble); }; - let Some(cobble_node) = try_generate_cobble_node(world, cobble_pos, &[]) else { + let Some(cobble_node) = + try_generate_cobble_node_with_policy(world, cobble_pos, &[], support_policy) + else { return PlaceRepeaterResult::Rejected(RouteRejectReason::CobbleConflict); }; let mut new_world = world.clone(); @@ -167,6 +234,9 @@ pub fn place_repeater_with_cobble( return PlaceRepeaterResult::Rejected(RouteRejectReason::RedstoneConflict); } place_node(&mut new_world, repeater_node); + // A new repeater changes adjacent dust's connection shape just as a new + // dust piece does. Test connectivity against that resulting shape. + new_world.update_redstone_states(prev); if !target_powers_position(&new_world, prev, repeater_node.position) { return PlaceRepeaterResult::Rejected(RouteRejectReason::DisconnectedRoute); } @@ -207,3 +277,214 @@ pub fn target_powers_position(world: &World3D, target: Position, position: Posit bound.position() == position || propagates_to_position }) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::transform::place_and_route::global_pnr::router::{ + route_point_to_point_with_strategy, GlobalRoutingStrategy, + }; + use crate::transform::place_and_route::place_bound::PropagateType; + use crate::world::block::Block; + use crate::world::position::DimSize; + use crate::world::simulator::Simulator; + use crate::world::World; + + #[test] + fn reuse_does_not_apply_new_block_stack_rule_but_new_support_still_does() { + let mut world = World3D::new(DimSize(5, 5, 5)); + let below = Position(2, 2, 1); + let support = below.up(); + let torch = Position(3, 2, 1); + world[below] = PlacedNode::new_cobble(below).block; + world[support] = PlacedNode::new_cobble(support).block; + world[torch] = Block { + kind: BlockKind::Torch { is_on: true }, + direction: torch.diff(below), + }; + assert!(cobble_would_stack_above_side_torch_support(&world, support)); + assert!(try_generate_cobble_node_with_policy( + &world, + support, + &[], + SupportReusePolicy::ReuseExisting + ) + .is_some()); + world[support] = Block::default(); + assert!(world[support].kind.is_air()); + assert!(try_generate_cobble_node_with_policy( + &world, + support, + &[], + SupportReusePolicy::ReuseExisting + ) + .is_none()); + } + + #[test] + fn existing_support_is_reused_without_resetting_power() { + let mut world = World3D::new(DimSize(5, 5, 4)); + let support = Position(2, 2, 1); + world[support] = Block { + kind: BlockKind::Cobble { + on_count: 3, + on_base_count: 2, + }, + direction: Direction::None, + }; + let existing = world[support]; + assert!( + try_generate_cobble_node(&world, support, &[]).is_none(), + "local placement's default must keep its original support-enumeration policy" + ); + let node = try_generate_cobble_node_with_policy( + &world, + support, + &[], + SupportReusePolicy::ReuseExisting, + ) + .unwrap(); + assert_eq!(node.block, existing); + place_node(&mut world, node); + assert_eq!(world[support], existing); + world[support] = PlacedNode::new_redstone(support).block; + assert!(try_generate_cobble_node_with_policy( + &world, + support, + &[], + SupportReusePolicy::ReuseExisting + ) + .is_none()); + } + + #[test] + fn perpendicular_repeater_refreshes_predecessor_dust_shape() -> eyre::Result<()> { + let mut world = World3D::new(DimSize(5, 4, 3)); + for x in 0..5 { + for y in 0..4 { + let p = Position(x, y, 0); + world[p] = PlacedNode::new_cobble(p).block; + } + } + let switch = Position(1, 2, 1); + let source = Position(2, 2, 1); + let repeater = Position(2, 1, 1); + let sink = Position(2, 0, 1); + world[switch] = Block { + kind: BlockKind::Switch { is_on: false }, + direction: Direction::West, + }; + world[source] = PlacedNode::new_redstone(source).block; + world[sink] = PlacedNode::new_redstone(sink).block; + world.initialize_redstone_states(); + let old_shape = world[source]; + let routed = match place_repeater_with_cobble_and_policy( + &world, + PlaceBound(PropagateType::Soft, repeater, Direction::South), + source, + sink, + Direction::North, + None, + SupportReusePolicy::ReuseExisting, + ) { + PlaceRepeaterResult::Placed(world, _) => world, + PlaceRepeaterResult::Rejected(reason) => { + eyre::bail!("legal elbow rejected: {reason:?}") + } + }; + assert_ne!( + routed[source], old_shape, + "straight dust becomes an elbow toward the newly attached repeater" + ); + assert!(target_powers_position(&routed, source, repeater)); + let mut simulator = + Simulator::from_settled_with_limits_and_trace(&World::from(&routed), 128, 10_000, 0)?; + for on in [false, true, false, true, false] { + simulator.drive_inputs_with_limits(vec![(switch, on)], 128, 10_000)?; + assert_eq!(simulator.world()[sink].kind.is_powered(), on); + } + Ok(()) + } + + #[test] + fn routes_across_existing_cobble_floor_and_switches_on_and_off() -> eyre::Result<()> { + let mut world = World3D::new(DimSize(26, 5, 4)); + for x in 0..26 { + for y in 0..5 { + let p = Position(x, y, 0); + world[p] = PlacedNode::new_cobble(p).block; + } + } + let source = Position(0, 2, 1); + let sink = Position(22, 2, 1); + world[source] = Block { + kind: BlockKind::Switch { is_on: false }, + direction: Direction::West, + }; + world[sink] = PlacedNode::new_redstone(sink).block; + world.initialize_redstone_states(); + let (route, routed) = + route_point_to_point_with_strategy(&world, source, sink, GlobalRoutingStrategy::AStar) + .map_err(|error| eyre::eyre!("floor route failed: {error:?}"))?; + assert!(route.path.len() > 1); + assert!( + route + .blocks + .iter() + .any(|(_, block)| block.kind.is_repeater()), + "the long floor route must reuse supports for both dust and repeaters" + ); + for x in 0..26 { + for y in 0..5 { + let p = Position(x, y, 0); + assert_eq!(routed[p], world[p]); + } + } + let converted = World::from(&routed); + let mut simulator = + Simulator::from_settled_with_limits_and_trace(&converted, 256, 50_000, 0)?; + for powered in [true, false, true, false] { + simulator.drive_inputs_with_limits(vec![(source, powered)], 256, 50_000)?; + assert_eq!(simulator.world()[sink].kind.is_powered(), powered); + } + Ok(()) + } + + #[test] + fn reusing_support_does_not_allow_foreign_torch_backfeed() { + let mut world = World3D::new(DimSize(6, 5, 4)); + let source = Position(1, 2, 1); + let wire = Position(2, 2, 1); + let foreign_support = Position(3, 2, 1); + let foreign_torch = Position(3, 2, 2); + for p in [ + source.down().unwrap(), + wire.down().unwrap(), + foreign_support, + ] { + world[p] = PlacedNode::new_cobble(p).block; + } + world[source] = Block { + kind: BlockKind::Switch { is_on: false }, + direction: Direction::West, + }; + world[foreign_torch] = Block { + kind: BlockKind::Torch { is_on: true }, + direction: Direction::Bottom, + }; + let bound = PlaceBound(PropagateType::Soft, wire, wire.diff(source)); + assert!(matches!( + place_redstone_with_cobble_and_policy( + &world, + bound, + source, + Position(5, 2, 1), + None, + SupportReusePolicy::ReuseExisting + ), + PlaceRedstoneResult::Rejected( + RouteRejectReason::RedstoneConflict | RouteRejectReason::ShortCircuit + ) + )); + } +} diff --git a/src/transform/place_and_route/global_pnr/candidate.rs b/src/transform/place_and_route/global_pnr/candidate.rs index a3a33d5e..967f880a 100644 --- a/src/transform/place_and_route/global_pnr/candidate.rs +++ b/src/transform/place_and_route/global_pnr/candidate.rs @@ -31,11 +31,26 @@ use crate::world::simulator::Simulator; use crate::world::{World, World3D}; mod clustering; +mod exact; +mod exact_adapter; +mod exact_validation; +mod io_diodes; #[cfg(test)] mod local_diagnostic; +pub mod nary; +pub use crate::ir::ExactCandidateSpec as ExactCandidateConfig; #[derive(Clone, Debug, PartialEq, Eq)] pub struct UnitCandidateConfig { + /// Bounded SAT synthesis competes with existing verified recipes; disabled by default. + pub exact: Option, + /// Use verified truth-recognized recipes for 2..8-input single-minterm/maxterm cells. + pub verified_nary_cells: bool, + /// Stagger verified recipe inputs to expose separate approach corridors. + pub nary_stagger_inputs: bool, + /// Reserve isolated input and output drivers inside nonsequential external candidates. + /// Opt-in until broader composed-circuit measurements are available. + pub reserve_combinational_io_diodes: bool, pub dim: DimSize, pub local_config: LocalPlacerConfig, pub input_constraints: LocalPlacerInputConstraints, @@ -155,6 +170,10 @@ impl DerefMut for CandidatePolicySet { impl Default for UnitCandidateConfig { fn default() -> Self { Self { + exact: None, + verified_nary_cells: false, + nary_stagger_inputs: false, + reserve_combinational_io_diodes: false, dim: DimSize(16, 16, 6), local_config: LocalPlacerConfig::default(), input_constraints: LocalPlacerInputConstraints::default(), @@ -221,6 +240,42 @@ fn generate_routable_module_candidates( ) }) .collect(); + let mut exact_candidates = exact::generate(&module.name, &graph, &ports, config, input_mode)?; + if config.verified_nary_cells + && config.max_candidates > 0 + && input_mode == CandidateInputMode::ExternalPorts + && config.input_constraints == LocalPlacerInputConstraints::default() + && (2..=8).contains( + &ports + .iter() + .filter(|p| p.direction == PhysicalPortDirection::Input) + .count(), + ) + { + if let Some(mut candidate) = nary::build_verified_candidate_with_stagger( + &LogicGraph { + graph: graph.clone(), + }, + &module.name, + config.nary_stagger_inputs, + )? { + if candidate_ports_cover_module_ports(&ports, &candidate.ports) + && candidate_satisfies_local_cell_contract( + &mut candidate, + &config.local_cell_contract, + ) + { + tracing::info!(module=%module.name, blocks=candidate.cost.block_count, "using verified semantic n-ary cell"); + exact_candidates.push(candidate); + exact_candidates.sort_by_key(|c| (c.cost.block_count, c.cost.bbox_volume)); + exact_candidates.truncate(config.max_candidates); + return Ok(exact_candidates); + } + } + } + if !exact_candidates.is_empty() { + return Ok(exact_candidates); + } let clustered = clustering::try_generate_clustered_candidates( &module.name, &graph, @@ -476,6 +531,16 @@ fn generate_unit_candidates( } let mut candidate = LayoutCandidate::from_world(module_name.to_owned(), world, physical_ports)?; + if config.reserve_combinational_io_diodes + && !contains_sequential + && input_mode == CandidateInputMode::ExternalPorts + { + let Some(reserved) = io_diodes::reserve(candidate)? else { + port_rejections += 1; + continue; + }; + candidate = reserved; + } // The check above drove the local placer's input switches. // `candidate_layout` then replaces them with exposed ports, and a // switch site can become dust that a neighbouring strongly powered @@ -1408,6 +1473,10 @@ fn downstream_consumer_count(world: &World3D, source: Position) -> usize { pub fn d_latch_child_candidate_config(local_config: LocalPlacerConfig) -> UnitCandidateConfig { UnitCandidateConfig { + exact: None, + verified_nary_cells: false, + nary_stagger_inputs: false, + reserve_combinational_io_diodes: false, dim: DimSize(14, 10, 6), local_config, input_constraints: LocalPlacerInputConstraints::new() @@ -1427,6 +1496,180 @@ mod tests { use super::*; use crate::world::block::{BlockKind, Direction}; + #[test] + fn primitive_external_candidates_preserve_truth_tables() -> eyre::Result<()> { + for (expression, reserve) in ["~a", "a&b", "a|b"] + .into_iter() + .flat_map(|expression| [false, true].map(|reserve| (expression, reserve))) + { + let graph = LogicGraph::from_stmt(expression, "y")?.prepare_place()?; + let mut ports = graph + .truth_table()? + .input_names + .iter() + .map(|name| CandidatePort::new(name, name, PhysicalPortDirection::Input)) + .collect::>(); + ports.push(CandidatePort::new("y", "y", PhysicalPortDirection::Output)); + let config = UnitCandidateConfig { + reserve_combinational_io_diodes: reserve, + dim: DimSize(8, 8, 4), + max_candidates: 1, + local_config: LocalPlacerConfig { + input_candidate_limit: Some(16), + step_sampling_policy: SamplingPolicy::Take(64), + max_not_route_step: 4, + max_route_step: 4, + not_route_step_sampling_policy: SamplingPolicy::Take(32), + route_step_sampling_policy: SamplingPolicy::Take(32), + ..Default::default() + }, + adaptive_search: LocalAdaptiveSearchConfig { + max_retries: 0, + ..Default::default() + }, + ..Default::default() + }; + let candidates = generate_unit_candidates( + "primitive", + graph.graph.clone(), + ports, + &config, + None, + CandidateInputMode::ExternalPorts, + )?; + assert!( + !candidates.is_empty(), + "no external candidate for {expression}, reserved={reserve}" + ); + for candidate in candidates { + assert!( + clustering::candidate_matches_truth_table_with_switch_inputs( + &graph.graph, + &candidate + ) + ); + if reserve { + let truth = graph.truth_table()?; + let inputs = truth + .input_names + .iter() + .map(|name| { + candidate + .ports + .iter() + .find(|port| port.name == *name) + .unwrap() + }) + .collect::>(); + assert!(inputs + .iter() + .all(|port| port.connection == PortConnection::Direct + && candidate.world[port.position].kind.is_redstone())); + let output = candidate + .ports + .iter() + .find(|port| port.name == "y") + .unwrap() + .primary_route_position(); + assert_eq!( + candidate + .ports + .iter() + .find(|p| p.name == "y") + .unwrap() + .connection, + PortConnection::RestoredDirect + ); + assert!(candidate.world[output].kind.is_redstone()); + let output_repeaters = candidate + .world + .iter_block() + .into_iter() + .filter(|(p, b)| { + b.kind.is_repeater() + && detailed_router::target_powers_position( + &candidate.world, + *p, + output, + ) + }) + .map(|(p, _)| p) + .collect::>(); + assert_eq!( + output_repeaters.len(), + 1, + "reserved output must have one isolation repeater" + ); + // Externally powering the public output may not feed back + // into any input or change the output repeater itself. + let mut backfeed = Simulator::from_settled_with_limits_and_trace( + &World::from(&candidate.world), + 256, + 50_000, + 0, + )?; + backfeed.drive_inputs_with_limits( + inputs.iter().map(|p| (p.position, false)).collect(), + 256, + 50_000, + )?; + let upstream = backfeed.world()[output_repeaters[0]].kind.is_powered(); + backfeed.drive_inputs_with_limits(vec![(output, true)], 256, 50_000)?; + assert_eq!( + backfeed.world()[output_repeaters[0]].kind.is_powered(), + upstream + ); + assert!(inputs + .iter() + .all(|p| !backfeed.world()[p.position].kind.is_powered())); + let cases = 1usize << inputs.len(); + for from in 0..cases { + for to in 0..cases { + let mut sim = Simulator::from_settled_with_limits_and_trace( + &World::from(&candidate.world), + 256, + 50_000, + 0, + )?; + for mask in [from, to] { + sim.advance_idle_cycles( + crate::world::simulator::MANUAL_INPUT_IDLE_CYCLES, + )?; + sim.drive_inputs_with_limits( + inputs + .iter() + .enumerate() + .map(|(bit, port)| (port.position, mask & (1 << bit) != 0)) + .collect(), + 256, + 50_000, + )?; + for (bit, port) in inputs.iter().enumerate() { + assert_eq!( + sim.world()[port.position].kind.is_powered(), + mask & (1 << bit) != 0, + "input backfeed: {expression}, mask={mask}, bit={bit}" + ); + } + assert_eq!( + sim.world()[output].kind.is_powered(), + truth.output_tables["y"][mask], + "{expression}: {from}->{to}, mask={mask}" + ); + assert!(sim + .world() + .iter_block() + .iter() + .all(|(p, _)| !sim.is_torch_burned_out(*p))); + } + } + } + } + } + } + Ok(()) + } + fn contract_test_candidate(port_position: Position) -> LayoutCandidate { let mut world = World3D::new(DimSize(6, 6, 4)); world[Position(1, 1, 1)] = PlacedNode::new_cobble(Position(1, 1, 1)).block; diff --git a/src/transform/place_and_route/global_pnr/candidate/clustering.rs b/src/transform/place_and_route/global_pnr/candidate/clustering.rs index a3997891..502e56e9 100644 --- a/src/transform/place_and_route/global_pnr/candidate/clustering.rs +++ b/src/transform/place_and_route/global_pnr/candidate/clustering.rs @@ -1454,6 +1454,7 @@ fn compose_candidate( { routing_attempts += 1; let routing = GlobalRoutingConfig { + max_route_steps: 128, strategy, // Cluster boundary nets can be logically correlated after // cut refinement (for example x and OR(x, cin)). Validating @@ -2620,6 +2621,7 @@ mod tests { fn named_candidate(name: &str) -> LayoutCandidate { LayoutCandidate { + exact_core: None, module_name: name.to_owned(), world: World3D::new(DimSize(1, 1, 1)), bbox: BoundingBox { diff --git a/src/transform/place_and_route/global_pnr/candidate/exact.rs b/src/transform/place_and_route/global_pnr/candidate/exact.rs new file mode 100644 index 00000000..0fcb2570 --- /dev/null +++ b/src/transform/place_and_route/global_pnr/candidate/exact.rs @@ -0,0 +1,309 @@ +//! Bounded SAT synthesis followed by the same external-port adaptation as local search. +use std::time::{Duration, Instant}; + +use super::*; +use crate::transform::logic::LogicGraphTransformer; +use crate::transform::place_and_route::estimate::bounding_box; +use crate::transform::place_and_route::global_pnr::ir::ExactCandidateProvenance; +use crate::transform::place_and_route::local_placer::exact::{ + ExactLocalPlacer, ExactOutcome, ExactPlacerConfig, +}; + +pub(super) fn generate( + module_name: &str, + graph: &Graph, + ports: &[CandidatePort], + config: &UnitCandidateConfig, + input_mode: CandidateInputMode, +) -> eyre::Result> { + let Some(policy) = &config.exact else { + return Ok(Vec::new()); + }; + eyre::ensure!( + policy.search_box.iter().all(|v| *v > 0) + && policy.time_limit_ms > 0 + && (1..=6).contains(&policy.max_inputs) + && policy.max_logic_nodes > 0, + "invalid Exact candidate bounds" + ); + if config.max_candidates == 0 + || config.input_constraints != LocalPlacerInputConstraints::default() + || graph + .nodes + .iter() + .any(|n| matches!(n.kind, GraphNodeKind::Sequential(_))) + || ports + .iter() + .filter(|p| p.direction == PhysicalPortDirection::Input) + .count() + > policy.max_inputs + || graph + .nodes + .iter() + .filter(|n| matches!(n.kind, GraphNodeKind::Logic(_))) + .count() + > policy.max_logic_nodes + { + return Ok(Vec::new()); + } + let original = LogicGraph { + graph: graph.clone(), + }; + // Mapping inserts double-NOT physical buffers. They are identities, not + // required gates in exact synthesis; keep the truth contract as the oracle. + let mut transform = LogicGraphTransformer::new(original.clone()); + transform.decompose_binops(); + transform.decompose_xor()?; + transform.decompose_and()?; + transform.remove_double_neg_expression(); + transform.optimize_cse()?; + let normalized = transform.finish(); + let solver = match ExactLocalPlacer::new(&normalized) { + Ok(solver) => solver.with_name(module_name), + Err(error) => { + tracing::debug!(module=module_name, %error, "Exact unsupported; retaining normal candidates"); + return Ok(Vec::new()); + } + }; + let [x, y, z] = policy.search_box; + let mut exact = ExactPlacerConfig::new(DimSize(x, y, z)); + exact.time_limit = Some(Duration::from_millis(policy.time_limit_ms)); + exact.workers = 1; + exact.seed = 1; + exact.driving_outputs = ports + .iter() + .filter(|p| p.direction == PhysicalPortDirection::Output) + .map(|p| p.target.clone()) + .collect(); + let start = Instant::now(); + let (placement, optimal) = if policy.optimize { + let (placement, optimal, _) = solver.place_minimizing_blocks(&exact)?; + (placement, optimal) + } else { + let (outcome, stats) = solver.place(&exact)?; + ( + match outcome { + ExactOutcome::Placed(placement) => Some(*placement), + _ => None, + }, + stats.optimal, + ) + }; + let elapsed_ms = start.elapsed().as_millis() as u64; + let Some(placement) = placement else { + tracing::info!( + module = module_name, + elapsed_ms, + "Exact produced no candidate within bounds; using fallback" + ); + return Ok(Vec::new()); + }; + let bbox = bounding_box(&placement.placed.world).context("Exact returned empty placement")?; + let provenance = ExactCandidateProvenance { + search_box: policy.search_box, + core_bbox: [bbox.width(), bbox.depth(), bbox.height()], + core_blocks: placement.placed.world.iter_block().len(), + core_logic_blocks: placement.block_count, + elapsed_ms, + optimal, + }; + if !candidate_matches_truth_table(&original, &placement.placed)? { + tracing::info!( + module = module_name, + "Exact standalone verification rejected" + ); + return Ok(Vec::new()); + } + let placed = placement.placed; + let standalone = placed.clone(); + let (world, physical_ports) = candidate_layout( + ports, + false, + &config.input_constraints, + placed.world, + &placed.inputs, + &placed.outputs, + input_mode, + ); + if !missing_candidate_ports(ports, &physical_ports).is_empty() { + tracing::info!(module = module_name, "Exact endpoint conversion rejected"); + return Ok(Vec::new()); + } + let mut candidate = LayoutCandidate::from_world(module_name.to_owned(), world, physical_ports)?; + if input_mode == CandidateInputMode::ExternalPorts { + super::exact_adapter::restore_attached_input_targets(&mut candidate, &standalone, ports); + // External Exact candidates require a validated, independently drivable + // interface even if other recipe backends use their own port adapters. + let Some(reserved) = io_diodes::reserve(candidate)? else { + tracing::info!(module = module_name, "Exact IO diode adaptation rejected"); + return Ok(Vec::new()); + }; + candidate = reserved; + if !super::exact_validation::verify_external_candidate(&original, &candidate)? { + tracing::info!(module = module_name, "Exact external verification rejected"); + return Ok(Vec::new()); + } + } + if !candidate_ports_cover_module_ports(ports, &candidate.ports) { + tracing::info!( + module = module_name, + "Exact adapted inputs are missing or aliased" + ); + return Ok(Vec::new()); + } + if !candidate_satisfies_local_cell_contract(&mut candidate, &config.local_cell_contract) { + tracing::info!( + module = module_name, + "Exact adapted geometry violates local cell contract" + ); + return Ok(Vec::new()); + } + candidate.exact_core = Some(provenance); + tracing::info!( + module = module_name, + elapsed_ms, + core_blocks = candidate.exact_core.as_ref().unwrap().core_blocks, + adapted_blocks = candidate.cost.block_count, + adapted_volume = candidate.cost.bbox_volume, + "accepted SAT Exact candidate" + ); + Ok(vec![candidate]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sat_candidate_preserves_external_truth_and_provenance() -> eyre::Result<()> { + let graph = LogicGraph::from_stmt("a|b", "y")?; + let ports = vec![ + CandidatePort::new("a", "a", PhysicalPortDirection::Input), + CandidatePort::new("b", "b", PhysicalPortDirection::Input), + CandidatePort::new("y", "y", PhysicalPortDirection::Output), + ]; + let config = UnitCandidateConfig { + exact: Some(ExactCandidateConfig::default()), + ..Default::default() + }; + let candidates = generate( + "sat_or", + &graph.graph, + &ports, + &config, + CandidateInputMode::ExternalPorts, + )?; + assert!( + !candidates.is_empty(), + "bounded SAT OR must survive external adaptation" + ); + let candidate = &candidates[0]; + assert!(candidate.exact_core.is_some()); + assert!(candidate.exact_core.as_ref().unwrap().core_blocks < 20); + assert!(super::super::exact_validation::verify_external_candidate( + &graph, candidate + )?); + let cache_dir = std::env::temp_dir().join(format!( + "redstone-exact-cache-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_nanos() + )); + crate::transform::place_and_route::global_pnr::candidate_cache::store( + &cache_dir, + "exact", + &candidates, + )?; + let restored = crate::transform::place_and_route::global_pnr::candidate_cache::load( + &cache_dir, "exact", "sat_or", + )? + .unwrap(); + assert_eq!(restored[0].exact_core, candidate.exact_core); + assert!(super::super::exact_validation::verify_external_candidate( + &graph, + &restored[0] + )?); + std::fs::remove_dir_all(cache_dir)?; + + let mut disabled = config.clone(); + disabled.exact = None; + assert!(generate( + "off", + &graph.graph, + &ports, + &disabled, + CandidateInputMode::ExternalPorts + )? + .is_empty()); + Ok(()) + } + #[test] + fn sat_mixed_polarity_candidate_cache_preserves_truth_and_provenance() -> eyre::Result<()> { + let graph = LogicGraph::from_stmt("a|~b", "y")?; + let ports = vec![ + CandidatePort::new("a", "a", PhysicalPortDirection::Input), + CandidatePort::new("b", "b", PhysicalPortDirection::Input), + CandidatePort::new("y", "y", PhysicalPortDirection::Output), + ]; + let config = UnitCandidateConfig { + exact: Some(ExactCandidateConfig { + time_limit_ms: 10_000, + ..Default::default() + }), + ..Default::default() + }; + let candidates = generate( + "sat_mixed", + &graph.graph, + &ports, + &config, + CandidateInputMode::ExternalPorts, + )?; + assert!( + !candidates.is_empty(), + "bounded SAT OR must survive external adaptation" + ); + let candidate = &candidates[0]; + assert!(candidate.exact_core.is_some()); + assert!(super::super::exact_validation::verify_external_candidate( + &graph, candidate + )?); + let cache_dir = std::env::temp_dir().join(format!( + "redstone-exact-cache-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_nanos() + )); + crate::transform::place_and_route::global_pnr::candidate_cache::store( + &cache_dir, + "exact", + &candidates, + )?; + let restored = crate::transform::place_and_route::global_pnr::candidate_cache::load( + &cache_dir, + "exact", + "sat_mixed", + )? + .unwrap(); + assert_eq!(restored[0].exact_core, candidate.exact_core); + assert!(super::super::exact_validation::verify_external_candidate( + &graph, + &restored[0] + )?); + std::fs::remove_dir_all(cache_dir)?; + + let mut disabled = config.clone(); + disabled.exact = None; + assert!(generate( + "off", + &graph.graph, + &ports, + &disabled, + CandidateInputMode::ExternalPorts + )? + .is_empty()); + Ok(()) + } +} diff --git a/src/transform/place_and_route/global_pnr/candidate/exact_adapter.rs b/src/transform/place_and_route/global_pnr/candidate/exact_adapter.rs new file mode 100644 index 00000000..261da40b --- /dev/null +++ b/src/transform/place_and_route/global_pnr/candidate/exact_adapter.rs @@ -0,0 +1,93 @@ +//! Preserve the hard-powered attachment of exact solver input switches. +use super::*; + +/// A lever may power both its attachment block and adjacent dust. The generic +/// conversion sorts those contacts by coordinates; its first dust contact is +/// not an equivalent hard-power replacement for the attachment. Keep all +/// contacts for isolation checks, but ask the input diode to drive the solid. +/// Shared attachments are allowed here: final external drivers must still be +/// distinct and pass the complete post-adaptation truth/backfeed checks. +pub(super) fn restore_attached_input_targets( + candidate: &mut LayoutCandidate, + original: &PlacedWorld, + module_ports: &[CandidatePort], +) -> usize { + let mut changed = 0; + for port in candidate + .ports + .iter_mut() + .filter(|p| p.direction == PhysicalPortDirection::Input) + { + let Some(spec) = module_ports.iter().find(|p| p.name == port.name) else { + continue; + }; + let Some(input) = original.inputs.iter().find(|p| p.name == spec.target) else { + continue; + }; + let switch = input.position(); + let block = original.world[switch]; + if !block.kind.is_switch() { + continue; + } + let Some(target) = switch + .walk(block.direction) + .filter(|p| original.world.size.bound_on(*p) && original.world[*p].kind.is_cobble()) + else { + continue; + }; + changed += usize::from(port.position != target); + port.position = target; + port.route_position = Some(switch); + port.access_points.retain(|p| *p != target); + port.access_points.insert(0, target); + port.connection = PortConnection::InputDiode; + } + changed +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::world::block::Direction; + #[test] + fn coordinate_sorted_dust_does_not_replace_hard_powered_attachment() -> eyre::Result<()> { + let mut world = World3D::new(DimSize(6, 5, 3)); + let switch = Position(2, 2, 1); + let support = Position(3, 2, 1); + let dust = Position(1, 2, 1); + world[support] = PlacedNode::new_cobble(support).block; + world[dust.down().unwrap()] = PlacedNode::new_cobble(dust.down().unwrap()).block; + world[dust] = PlacedNode::new_redstone(dust).block; + world[switch] = Block { + kind: BlockKind::Switch { is_on: false }, + direction: Direction::East, + }; + let placed = PlacedWorld { + world: world.clone(), + inputs: vec![OutputEndpoint::new("solver_name".into(), switch)], + outputs: vec![], + }; + let ports = vec![PhysicalPort { + name: "public_name".into(), + direction: PhysicalPortDirection::Input, + position: dust, + route_position: Some(switch), + access_points: vec![dust, support], + connection: PortConnection::InputDiode, + }]; + let mut candidate = LayoutCandidate::from_world("test".into(), world, ports)?; + let module_ports = [CandidatePort::new( + "public_name", + "solver_name", + PhysicalPortDirection::Input, + )]; + assert_eq!( + restore_attached_input_targets(&mut candidate, &placed, &module_ports), + 1 + ); + assert_eq!(candidate.ports[0].position, support); + assert_eq!(candidate.ports[0].route_position, Some(switch)); + assert_eq!(candidate.ports[0].access_points, vec![support, dust]); + Ok(()) + } +} diff --git a/src/transform/place_and_route/global_pnr/candidate/exact_validation.rs b/src/transform/place_and_route/global_pnr/candidate/exact_validation.rs new file mode 100644 index 00000000..6d805b11 --- /dev/null +++ b/src/transform/place_and_route/global_pnr/candidate/exact_validation.rs @@ -0,0 +1,153 @@ +//! Independent simulation gate for compact exact cells after boundary conversion. +use super::*; +use crate::nbt::{NBTRoot, ToNBT}; +use crate::world::simulator::MANUAL_INPUT_IDLE_CYCLES; + +/// Validate the exported interface rather than the solver's switch-bearing body. +/// A rejected candidate may fall back to the existing verified recipe. +pub(super) fn verify_external_candidate( + graph: &LogicGraph, + candidate: &LayoutCandidate, +) -> eyre::Result { + let truth = graph.truth_table()?; + let count = truth.input_names.len(); + if count > 8 { + return Ok(false); + } + let inputs = truth + .input_names + .iter() + .map(|name| { + candidate + .ports + .iter() + .find(|p| p.name == *name && p.direction == PhysicalPortDirection::Input) + }) + .collect::>>(); + let Some(inputs) = inputs else { + return Ok(false); + }; + let outputs = truth + .output_tables + .iter() + .map(|(name, values)| { + candidate + .ports + .iter() + .find(|p| p.name == *name && p.direction == PhysicalPortDirection::Output) + .map(|port| (port, values)) + }) + .collect::>>(); + let Some(outputs) = outputs else { + return Ok(false); + }; + let world = NBTRoot::from_nbt_bytes(&fastnbt::to_bytes(&candidate.world.to_nbt())?)?.to_world(); + let fresh = || Simulator::from_settled_with_limits_and_trace(&world, 1024, 200_000, 0); + let drive = |sim: &mut Simulator, mask: usize| -> eyre::Result { + sim.advance_idle_cycles(MANUAL_INPUT_IDLE_CYCLES)?; + sim.drive_inputs_with_limits( + inputs + .iter() + .enumerate() + .map(|(bit, p)| (p.position, mask & (1 << bit) != 0)) + .collect(), + 1024, + 200_000, + )?; + for (bit, input) in inputs.iter().enumerate() { + if sim.world()[input.position].kind.is_powered() != (mask & (1 << bit) != 0) { + return Ok(false); + } + } + for (output, values) in &outputs { + let block = sim.world()[output.position].kind; + if block.is_powered() != values[mask] { + return Ok(false); + } + if values[mask] + && output.connection == PortConnection::RestoredDirect + && !matches!(block, BlockKind::Redstone { strength: 15, .. }) + { + return Ok(false); + } + } + Ok(sim + .world() + .iter_block() + .iter() + .all(|(p, _)| !sim.is_torch_burned_out(*p))) + }; + let states = 1 << count; + for mask in 0..states { + if !drive(&mut fresh()?, mask)? { + return Ok(false); + } + } + let mut sim = fresh()?; + if count <= 4 { + for from in 0..states { + for to in 0..states { + if !drive(&mut sim, from)? || !drive(&mut sim, to)? { + return Ok(false); + } + } + } + } else { + for mask in (0..states).chain((0..states).rev()) { + if !drive(&mut sim, mask)? { + return Ok(false); + } + } + } + // An unrelated route touching an output must not feed the input boundary. + for (output, _) in &outputs { + let mut sim = fresh()?; + if !drive(&mut sim, 0)? { + return Ok(false); + } + sim.drive_inputs_with_limits(vec![(output.position, true)], 1024, 200_000)?; + if inputs + .iter() + .any(|p| sim.world()[p.position].kind.is_powered()) + { + return Ok(false); + } + } + Ok(true) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn verified_recipe_export_passes_but_a_shorted_input_is_rejected() -> eyre::Result<()> { + let graph = LogicGraph::from_stmt("a&~b", "y")?; + let candidate = super::super::nary::build_verified_candidate(&graph, "arbitrary")?.unwrap(); + assert!(verify_external_candidate(&graph, &candidate)?); + // Moving a restored-dust label onto the preceding torch preserves + // Boolean values but violates the electrical strength/type contract. + let mut false_promise = candidate.clone(); + let out = false_promise + .ports + .iter_mut() + .find(|p| p.name == "y") + .unwrap(); + out.position = Position(out.position.0 - 2, out.position.1, out.position.2); + out.route_position = Some(out.position); + out.access_points = vec![out.position]; + assert!(!verify_external_candidate(&graph, &false_promise)?); + let mut shorted = candidate.clone(); + let a = shorted + .ports + .iter() + .find(|p| p.name == "a") + .unwrap() + .position; + let b = shorted.ports.iter_mut().find(|p| p.name == "b").unwrap(); + b.position = a; + b.route_position = Some(a); + b.access_points = vec![a]; + assert!(!verify_external_candidate(&graph, &shorted)?); + Ok(()) + } +} diff --git a/src/transform/place_and_route/global_pnr/candidate/io_diodes.rs b/src/transform/place_and_route/global_pnr/candidate/io_diodes.rs new file mode 100644 index 00000000..7b6af7b9 --- /dev/null +++ b/src/transform/place_and_route/global_pnr/candidate/io_diodes.rs @@ -0,0 +1,224 @@ +use super::*; +use crate::transform::place_and_route::global_pnr::router::{ + materialize_input_diode, materialize_output_diode, +}; + +/// Keep routing support blocks off the public dust drivers. This changes only +/// clearance metadata, and is called only for the opt-in prepared recipes. +pub(super) fn reserve_driver_headroom(candidate: &mut LayoutCandidate) { + for port in &candidate.ports { + if !matches!( + port.connection, + PortConnection::Direct | PortConnection::RestoredDirect + ) || !candidate.world[port.position].kind.is_redstone() + { + continue; + } + let above = port.position.up(); + if candidate.world.size.bound_on(above) && candidate.world[above].kind.is_air() { + candidate.blocked_cells.insert(above); + } + } + candidate.include_reserved_cells_in_bounds(); +} + +/// Reserve the approach geometry before unrelated nets can occupy it. +/// This is used only for the opt-in combinational external-port policy. +pub(super) fn reserve(candidate: LayoutCandidate) -> eyre::Result> { + let shift = |p: Position| Position(p.0 + 3, p.1 + 3, p.2 + 1); + let mut world = World3D::new(DimSize( + candidate.world.size.0 + 6, + candidate.world.size.1 + 6, + candidate.world.size.2 + 2, + )); + for (position, block) in candidate.world.iter_block() { + world[shift(position)] = block; + } + let mut ports = candidate.ports; + for port in &mut ports { + port.position = shift(port.position); + port.route_position = port.route_position.map(shift); + port.access_points = port.access_points.iter().copied().map(shift).collect(); + } + let unreserved_world = + tracing::enabled!(target: "redstone_compiler::global_pnr", tracing::Level::DEBUG) + .then(|| world.clone()); + // Outputs get first choice: their escape needs a free first hop from the + // internal source; later global nets must not be allowed to consume it. + for port in &mut ports { + if port.direction != PhysicalPortDirection::Output { + continue; + } + let Some((next_world, driver)) = + materialize_output_diode(&world, port.position, &port.routing_access_positions()) + else { + tracing::debug!(target: "redstone_compiler::global_pnr", module=%candidate.module_name, port=%port.name, position=?port.position, contacts=?port.routing_access_positions(), stage="output", "IO diode reservation rejected candidate"); + return Ok(None); + }; + world = next_world; + port.position = driver; + port.route_position = Some(driver); + port.access_points = vec![driver]; + port.connection = PortConnection::Direct; + } + let mut drivers = Vec::new(); + for port in &mut ports { + if port.direction != PhysicalPortDirection::Input || !port.requires_input_diode() { + continue; + } + let contacts = port.routing_access_positions(); + // The logical terminal is authoritative. route_position may be the + // former (now empty) switch site, useful only as preferred orientation. + let Some((next_world, driver)) = materialize_input_diode( + &world, + port.position, + port.route_position, + 1, + &contacts, + &drivers, + ) else { + let independently_materializable = unreserved_world.as_ref().map(|original| { + materialize_input_diode( + original, + port.position, + port.route_position, + 1, + &contacts, + &[], + ) + .is_some() + }); + tracing::debug!(target: "redstone_compiler::global_pnr", module=%candidate.module_name, port=%port.name, position=?port.position, preferred=?port.route_position, contacts=?contacts, previous_inputs=drivers.len(), ?independently_materializable, stage="input", "IO diode reservation rejected candidate"); + return Ok(None); + }; + world = next_world; + drivers.push(driver); + port.position = driver; + port.route_position = Some(driver); + port.access_points = vec![driver]; + port.connection = PortConnection::Direct; + } + world.initialize_redstone_states(); + let mut reserved = LayoutCandidate::from_world(candidate.module_name, world, ports)?; + reserved.blocked_cells = candidate.blocked_cells.into_iter().map(shift).collect(); + if certify_restored_outputs(&reserved).unwrap_or(false) { + for port in &mut reserved.ports { + if port.direction == PhysicalPortDirection::Output { + port.connection = PortConnection::RestoredDirect; + } + } + } + reserve_driver_headroom(&mut reserved); + Ok(Some(reserved)) +} + +/// Certification is bounded independently of local synthesis size. Larger +/// cells retain conservative Direct ports rather than an unverified promise. +fn certify_restored_outputs(candidate: &LayoutCandidate) -> eyre::Result { + let inputs = candidate + .ports + .iter() + .filter(|p| p.direction == PhysicalPortDirection::Input) + .collect::>(); + if inputs.len() > 8 { + return Ok(false); + } + let world = crate::world::World::from(&candidate.world); + for mask in 0..(1usize << inputs.len()) { + let mut sim = Simulator::from_settled_with_limits_and_trace(&world, 1024, 200_000, 0)?; + sim.drive_inputs_with_limits( + inputs + .iter() + .enumerate() + .map(|(bit, p)| (p.position, mask & (1 << bit) != 0)) + .collect(), + 1024, + 200_000, + )?; + for output in candidate + .ports + .iter() + .filter(|p| p.direction == PhysicalPortDirection::Output) + { + let block = sim.world()[output.position].kind; + if block.is_powered() && !matches!(block, BlockKind::Redstone { strength: 15, .. }) { + return Ok(false); + } + } + } + Ok(true) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn attenuated_output_cannot_claim_restored_drive() -> eyre::Result<()> { + let mut world = World3D::new(DimSize(5, 3, 3)); + for x in 1..=3 { + let p = Position(x, 1, 1); + world[p.down().unwrap()] = PlacedNode::new_cobble(p.down().unwrap()).block; + world[p] = PlacedNode::new_redstone(p).block; + } + world.initialize_redstone_states(); + let ports = [ + (PhysicalPortDirection::Input, Position(1, 1, 1)), + (PhysicalPortDirection::Output, Position(3, 1, 1)), + ] + .into_iter() + .enumerate() + .map(|(i, (direction, position))| PhysicalPort { + name: format!("pin{i}"), + direction, + position, + route_position: Some(position), + access_points: vec![position], + connection: PortConnection::Direct, + }) + .collect(); + let candidate = LayoutCandidate::from_world("attenuated".into(), world, ports)?; + assert!(!certify_restored_outputs(&candidate)?); + Ok(()) + } + #[test] + fn driver_headroom_is_air_metadata_and_preserves_prior_reservations() -> eyre::Result<()> { + let mut world = World3D::new(DimSize(5, 5, 4)); + let input = Position(2, 2, 1); + let output = Position(3, 2, 1); + for p in [input, output] { + world[p.down().unwrap()] = PlacedNode::new_cobble(p.down().unwrap()).block; + world[p] = PlacedNode::new_redstone(p).block; + } + world[output.up()] = PlacedNode::new_cobble(output.up()).block; + let before = world.iter_block(); + let ports = [ + (input, PhysicalPortDirection::Input), + (output, PhysicalPortDirection::Output), + ] + .into_iter() + .enumerate() + .map(|(i, (position, direction))| PhysicalPort { + name: format!("p{i}"), + direction, + position, + route_position: Some(position), + access_points: vec![position], + connection: PortConnection::Direct, + }) + .collect(); + let mut candidate = LayoutCandidate::from_world("test".into(), world, ports)?; + let prior = Position(0, 0, 3); + candidate.blocked_cells.insert(prior); + reserve_driver_headroom(&mut candidate); + assert_eq!(candidate.world.iter_block(), before); + assert_eq!( + candidate.blocked_cells, + [prior, input.up()].into_iter().collect() + ); + assert!(!candidate.occupied_cells.contains(&input.up())); + assert_eq!(candidate.bbox.max.2, 3); + assert_eq!(candidate.cost.bbox_height, 4); + assert_eq!(candidate.cost.block_count, before.len()); + Ok(()) + } +} diff --git a/src/transform/place_and_route/global_pnr/candidate/nary.rs b/src/transform/place_and_route/global_pnr/candidate/nary.rs new file mode 100644 index 00000000..8de2eebf --- /dev/null +++ b/src/transform/place_and_route/global_pnr/candidate/nary.rs @@ -0,0 +1,661 @@ +//! Truth-recognized, verified deterministic n-ary cells. No module-name dispatch. +use eyre::ensure; + +use super::{LayoutCandidate, PhysicalPort, PhysicalPortDirection, PortConnection}; +use crate::graph::logic::LogicGraph; +use crate::graph::GraphNodeKind; +use crate::nbt::{NBTRoot, ToNBT}; +use crate::output::{OutputEndpoint, PlacedWorld}; +use crate::transform::place_and_route::placed_node::PlacedNode; +use crate::world::block::{Block, BlockKind, Direction}; +use crate::world::position::{DimSize, Position}; +use crate::world::simulator::{Simulator, MANUAL_INPUT_IDLE_CYCLES}; +use crate::world::{World, World3D}; + +/// The one exceptional assignment and its output value. Each input whose +/// pattern bit is one is inverted before the OR bus. The bus is therefore +/// zero exactly at `pattern`; an output inverter implements equality. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct Function { + pattern: usize, + equal: bool, +} + +fn expected(function: Function, _count: usize, mask: usize) -> bool { + (mask == function.pattern) == function.equal +} + +// Recognition uses the entire truth signature, never a module/name hint. +fn recognize(graph: &LogicGraph) -> eyre::Result, String)>> { + // Reject unsupported sizes before truth-table construction can explode. + let count = graph + .nodes + .iter() + .filter(|node| matches!(node.kind, GraphNodeKind::Input(_))) + .count(); + if !(2..=8).contains(&count) + || graph + .nodes + .iter() + .any(|node| matches!(node.kind, GraphNodeKind::Sequential(_))) + { + return Ok(None); + } + let truth = graph.truth_table()?; + if !(2..=8).contains(&truth.input_names.len()) || truth.output_tables.len() != 1 { + return Ok(None); + } + let (output, values) = truth.output_tables.iter().next().unwrap(); + let true_patterns = values + .iter() + .enumerate() + .filter_map(|(mask, &v)| v.then_some(mask)) + .collect::>(); + let false_patterns = values + .iter() + .enumerate() + .filter_map(|(mask, &v)| (!v).then_some(mask)) + .collect::>(); + let function = if true_patterns.len() == 1 { + Some(Function { + pattern: true_patterns[0], + equal: true, + }) + } else if false_patterns.len() == 1 { + Some(Function { + pattern: false_patterns[0], + equal: false, + }) + } else { + None + }; + Ok(function.map(|function| (function, truth.input_names, output.clone()))) +} + +fn put(world: &mut World3D, node: PlacedNode) { + world[node.position.down().unwrap()] = + PlacedNode::new_cobble(node.position.down().unwrap()).block; + world[node.position] = node.block; +} + +fn recipe(function: Function, names: &[String], output: &str, pitch: usize) -> PlacedWorld { + debug_assert!(pitch >= 3 && pitch % 3 == 0); + let last = 2 + pitch * (names.len() - 1); + let mut world = World3D::new(DimSize(last + 7, 8, 3)); + let mut inputs = Vec::new(); + for (index, name) in names.iter().enumerate() { + let x = 2 + pitch * index; + let input = Position(x, 1, 1); + inputs.push(OutputEndpoint::new(name.clone(), input)); + put(&mut world, PlacedNode::new_redstone(input)); + put( + &mut world, + PlacedNode::new_repeater(Position(x, 2, 1), Direction::South), + ); + if function.pattern & (1 << index) != 0 { + put(&mut world, PlacedNode::new_cobble(Position(x, 3, 1))); + put( + &mut world, + PlacedNode::new( + Position(x, 4, 1), + Block { + kind: BlockKind::Torch { is_on: true }, + direction: Direction::South, + }, + ), + ); + put( + &mut world, + PlacedNode::new_repeater(Position(x, 5, 1), Direction::South), + ); + } else { + for y in 3..=5 { + put(&mut world, PlacedNode::new_redstone(Position(x, y, 1))); + } + } + } + // Each stage joins one input and forwards accumulated OR to the right. + // A repeater between stages prevents strength exhaustion and backfeed. + for x in 2..=last { + let p = Position(x, 6, 1); + if x > 2 && (x - 2) % 3 == 2 { + put(&mut world, PlacedNode::new_repeater(p, Direction::West)); + } else { + put(&mut world, PlacedNode::new_redstone(p)); + } + } + put( + &mut world, + PlacedNode::new_repeater(Position(last + 1, 6, 1), Direction::West), + ); + let endpoint = if !function.equal { + let p = Position(last + 2, 6, 1); + put(&mut world, PlacedNode::new_redstone(p)); + p + } else { + put(&mut world, PlacedNode::new_cobble(Position(last + 2, 6, 1))); + put( + &mut world, + PlacedNode::new( + Position(last + 3, 6, 1), + Block { + kind: BlockKind::Torch { is_on: true }, + direction: Direction::West, + }, + ), + ); + put( + &mut world, + PlacedNode::new_repeater(Position(last + 4, 6, 1), Direction::West), + ); + let p = Position(last + 5, 6, 1); + put(&mut world, PlacedNode::new_redstone(p)); + p + }; + world.initialize_redstone_states(); + PlacedWorld { + world, + inputs, + outputs: vec![OutputEndpoint::new(output.to_owned(), endpoint)], + } +} + +fn stagger_inputs(cell: PlacedWorld) -> PlacedWorld { + let padding = 3 * (cell.inputs.len() - 1); + let shift = |p: Position| Position(p.0, p.1 + padding, p.2); + let mut world = World3D::new(DimSize( + cell.world.size.0, + cell.world.size.1 + padding, + cell.world.size.2, + )); + for (position, block) in cell.world.iter_block() { + world[shift(position)] = block; + } + let inputs = cell + .inputs + .iter() + .enumerate() + .map(|(index, input)| { + let original = shift(input.position()); + let tip = Position(original.0, original.1 - 3 * index, original.2); + for y in tip.1..original.1 { + let p = Position(tip.0, y, tip.2); + put( + &mut world, + if (y - tip.1) % 2 == 1 { + PlacedNode::new_repeater(p, Direction::South) + } else { + PlacedNode::new_redstone(p) + }, + ); + } + OutputEndpoint::new(input.name.clone(), tip) + }) + .collect(); + let outputs = cell + .outputs + .iter() + .map(|p| OutputEndpoint::new(p.name.clone(), shift(p.position()))) + .collect(); + world.initialize_redstone_states(); + PlacedWorld { + world, + inputs, + outputs, + } +} + +fn drive( + sim: &mut Simulator, + cell: &PlacedWorld, + function: Function, + mask: usize, +) -> eyre::Result<()> { + sim.advance_idle_cycles(MANUAL_INPUT_IDLE_CYCLES)?; + sim.drive_inputs_with_limits( + cell.inputs + .iter() + .enumerate() + .map(|(bit, p)| (p.position(), mask & (1 << bit) != 0)) + .collect(), + 1024, + 200_000, + )?; + ensure!( + sim.world()[cell.outputs[0].position()].kind.is_powered() + == expected(function, cell.inputs.len(), mask), + "output mismatch {function:?}/{} mask={mask}", + cell.inputs.len() + ); + if expected(function, cell.inputs.len(), mask) { + ensure!( + matches!( + sim.world()[cell.outputs[0].position()].kind, + BlockKind::Redstone { strength: 15, .. } + ), + "restored output did not supply strength 15: {function:?} mask={mask}" + ); + } + for (bit, input) in cell.inputs.iter().enumerate() { + ensure!( + sim.world()[input.position()].kind.is_powered() == (mask & (1 << bit) != 0), + "input backfeed {function:?}/{} mask={mask} bit={bit}", + cell.inputs.len() + ); + } + ensure!( + sim.world() + .iter_block() + .iter() + .all(|(p, _)| !sim.is_torch_burned_out(*p)), + "torch burnout" + ); + Ok(()) +} + +/// Build a 2..8-input single-minterm/maxterm cell from its complete truth table. +/// Every returned +/// cell passed NBT roundtrip, all assignments and bounded transition checks. +pub fn build_verified_candidate( + graph: &LogicGraph, + module_name: &str, +) -> eyre::Result> { + build_verified_candidate_with_stagger(graph, module_name, false) +} + +/// Optional deterministic staggered input boundary; all verification is retained. +pub fn build_verified_candidate_with_stagger( + graph: &LogicGraph, + module_name: &str, + stagger: bool, +) -> eyre::Result> { + let Some((function, inputs, output)) = recognize(graph)? else { + return Ok(None); + }; + let count = inputs.len(); + // Wider staggered boundaries leave a three-block corridor between input + // columns. Restore bus strength every third block regardless of pitch. + let cell = recipe(function, &inputs, &output, if stagger { 6 } else { 3 }); + let cell = if stagger { stagger_inputs(cell) } else { cell }; + let bytes = fastnbt::to_bytes(&cell.world.to_nbt())?; + let world: World = NBTRoot::from_nbt_bytes(&bytes)?.to_world(); + for mask in 0..(1 << count) { + let mut sim = Simulator::from_settled_with_limits_and_trace(&world, 1024, 200_000, 0)?; + drive(&mut sim, &cell, function, mask)?; + } + let mut sim = Simulator::from_settled_with_limits_and_trace(&world, 1024, 200_000, 0)?; + let full = (1 << count) - 1; + let mut sequence = vec![0, full, 0, full, 0]; + for bit in 0..count { + sequence.extend([1 << bit, full ^ (1 << bit), 0]); + // Exercise both edges of the exceptional assignment, even when the + // bounded random walk never happens to reach this mixed-polarity mask. + sequence.extend([ + function.pattern, + function.pattern ^ (1 << bit), + function.pattern, + ]); + } + let mut seed = 42u32; + for _ in 0..64 { + seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); + sequence.push(seed as usize & full); + } + for mask in sequence { + drive(&mut sim, &cell, function, mask)?; + } + let ports = cell + .inputs + .iter() + .map(|p| (p, PhysicalPortDirection::Input)) + .chain( + cell.outputs + .iter() + .map(|p| (p, PhysicalPortDirection::Output)), + ) + .map(|(p, direction)| PhysicalPort { + name: p.name.clone(), + direction: direction.clone(), + position: p.position(), + route_position: Some(p.position()), + access_points: vec![p.position()], + connection: if direction == PhysicalPortDirection::Output { + PortConnection::RestoredDirect + } else { + PortConnection::Direct + }, + }) + .collect(); + let mut candidate = LayoutCandidate::from_world(module_name.to_owned(), cell.world, ports)?; + super::io_diodes::reserve_driver_headroom(&mut candidate); + Ok(Some(candidate)) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn staggered_patterns_preserve_truth_transitions_and_weak_inputs() -> eyre::Result<()> { + for (count, patterns) in [ + (4, (0..16).collect::>()), + (8, vec![0, 0x55, 0xaa, 0xff]), + ] { + for pattern in patterns { + let terms = (0..count) + .map(|bit| { + if pattern & (1 << bit) != 0 { + format!("port_{bit}") + } else { + format!("~port_{bit}") + } + }) + .collect::>(); + for equal in [true, false] { + let expr = terms.join("&"); + let expr = if equal { expr } else { format!("~({expr})") }; + let graph = LogicGraph::from_stmt(&expr, "result")?; + let candidate = + build_verified_candidate_with_stagger(&graph, "arbitrary", true)?.unwrap(); + let inputs = candidate + .ports + .iter() + .filter(|p| p.direction == PhysicalPortDirection::Input) + .collect::>(); + for (i, p) in inputs.iter().enumerate() { + assert_eq!(p.position.1, 1 + 3 * (count - 1 - i)); + assert_eq!(p.position.0, 2 + 6 * i); + assert!(candidate.blocked_cells.contains(&p.position.up())); + assert!(candidate.world[p.position.up()].kind.is_air()); + } + } + } + } + // Feed each tip through fourteen extra dust steps: it receives strength + // one, so its immediately following repeater must restore the signal. + let function = Function { + pattern: 5, + equal: true, + }; + let names = (0..4).map(|i| format!("weak_{i}")).collect::>(); + let cell = stagger_inputs(recipe(function, &names, "out", 6)); + let shift = |p: Position| Position(p.0, p.1 + 14, p.2); + let mut world = World3D::new(DimSize( + cell.world.size.0, + cell.world.size.1 + 14, + cell.world.size.2, + )); + for (p, b) in cell.world.iter_block() { + world[shift(p)] = b; + } + let tips = cell + .inputs + .iter() + .map(|p| shift(p.position())) + .collect::>(); + let inputs = cell + .inputs + .iter() + .map(|p| { + let tip = shift(p.position()); + let source = Position(tip.0, tip.1 - 14, tip.2); + for y in source.1..tip.1 { + put( + &mut world, + PlacedNode::new_redstone(Position(tip.0, y, tip.2)), + ); + } + OutputEndpoint::new(p.name.clone(), source) + }) + .collect(); + world.initialize_redstone_states(); + let weak = PlacedWorld { + world, + inputs, + outputs: cell + .outputs + .iter() + .map(|p| OutputEndpoint::new(p.name.clone(), shift(p.position()))) + .collect(), + }; + let mut sim = Simulator::from_settled_with_limits_and_trace( + &World::from(&weak.world), + 1024, + 200_000, + 0, + )?; + for mask in (0..16).chain((0..16).rev()) { + drive(&mut sim, &weak, function, mask)?; + for (bit, tip) in tips.iter().enumerate() { + let BlockKind::Redstone { strength, .. } = sim.world()[*tip].kind else { + panic!("tip not dust") + }; + assert_eq!(strength, usize::from(mask & (1 << bit) != 0)); + } + } + Ok(()) + } + + #[test] + fn literal_patterns_verify_all_four_input_masks_and_wide_examples() -> eyre::Result<()> { + for (count, patterns) in [ + (4, (0..16).collect::>()), + (8, vec![0, 1, 0x55, 0xaa, 0x81, 0xff]), + ] { + for pattern in patterns { + let terms = (0..count) + .map(|bit| { + let name = format!("arbitrary_operand_{bit}"); + if pattern & (1 << bit) != 0 { + name + } else { + format!("~{name}") + } + }) + .collect::>(); + for equal in [true, false] { + let conjunction = terms.join("&"); + let expression = if equal { + conjunction + } else { + format!("~({conjunction})") + }; + let graph = LogicGraph::from_stmt(&expression, "unrelated_output")?; + let recognized = recognize(&graph)?.unwrap(); + assert_eq!(recognized.0, Function { pattern, equal }); + // The factory checks every static input and bounded walking + // transitions after NBT roundtrip, including input isolation. + let candidate = build_verified_candidate(&graph, "no_name_hint")?.unwrap(); + assert_eq!(candidate.ports.len(), count + 1); + assert_eq!(candidate.module_name, "no_name_hint"); + } + } + } + Ok(()) + } + + #[test] + fn dispatcher_uses_recipe_with_named_ports_and_checks_bbox() -> eyre::Result<()> { + use super::super::{ + candidate_satisfies_local_cell_contract, + generate_routable_module_candidates_with_progress_label, UnitCandidateConfig, + }; + use crate::ir::{ + LocalCellContractSpec, LogicalDesign, RoutableModuleBody, RoutablePortDirection, + }; + let logical = LogicalDesign::from_verilog_source("module arbitrary(left,middle,right,result); input left,middle,right; output result; assign result=(left&middle)&right; endmodule")?; + let routable = logical.lower_to_routable_with_associative_cells(8)?; + let module = routable + .modules + .iter() + .find(|m| { + matches!(m.body, RoutableModuleBody::Leaf { .. }) + && m.ports + .iter() + .filter(|p| p.direction == RoutablePortDirection::Input) + .count() + == 3 + }) + .unwrap(); + let config = UnitCandidateConfig { + verified_nary_cells: true, + reserve_combinational_io_diodes: true, + ..Default::default() + }; + let candidates = + generate_routable_module_candidates_with_progress_label(module, &config, None)?; + assert_eq!(candidates.len(), 1); + let mut candidate = candidates[0].clone(); + // Recipe pins are already isolated; generic I/O reservation must not add another stage. + assert_eq!(candidate.world.iter_block().len(), 54); + assert!(candidate.ports.iter().all(|p| p.connection + == if p.direction == PhysicalPortDirection::Output { + PortConnection::RestoredDirect + } else { + PortConnection::Direct + })); + assert!(module + .ports + .iter() + .all(|port| candidate.ports.iter().any(|p| p.name == port.name))); + let contract = LocalCellContractSpec { + max_bbox: Some([1, 1, 1]), + ..Default::default() + }; + assert!(!candidate_satisfies_local_cell_contract( + &mut candidate, + &contract + )); + Ok(()) + } + + #[test] + fn dispatcher_two_input_recipes_cover_every_transition() -> eyre::Result<()> { + use super::super::{ + generate_routable_module_candidates_with_progress_label, UnitCandidateConfig, + }; + use crate::ir::{LogicalDesign, RoutableModuleBody, RoutablePortDirection}; + for (operator, function, block_count) in [ + ( + "&", + Function { + pattern: 3, + equal: true, + }, + 38, + ), + ( + "|", + Function { + pattern: 0, + equal: false, + }, + 32, + ), + ] { + let source = format!("module arbitrary(left,right,result); input left,right; output result; assign result=left{operator}right; endmodule"); + let routable = LogicalDesign::from_verilog_source(&source)? + .lower_to_routable_with_associative_cells(8)?; + let module = routable + .modules + .iter() + .find(|m| { + matches!(m.body, RoutableModuleBody::Leaf { .. }) + && m.ports + .iter() + .filter(|p| p.direction == RoutablePortDirection::Input) + .count() + == 2 + }) + .unwrap(); + let config = UnitCandidateConfig { + verified_nary_cells: true, + reserve_combinational_io_diodes: true, + ..Default::default() + }; + let candidates = + generate_routable_module_candidates_with_progress_label(module, &config, None)?; + assert_eq!(candidates.len(), 1); + let candidate = &candidates[0]; + assert_eq!(candidate.world.iter_block().len(), block_count); + let cell = PlacedWorld { + world: candidate.world.clone(), + inputs: candidate + .ports + .iter() + .filter(|p| p.direction == PhysicalPortDirection::Input) + .map(|p| OutputEndpoint::new(p.name.clone(), p.position)) + .collect(), + outputs: candidate + .ports + .iter() + .filter(|p| p.direction == PhysicalPortDirection::Output) + .map(|p| OutputEndpoint::new(p.name.clone(), p.position)) + .collect(), + }; + for from in 0..4 { + for to in 0..4 { + let mut sim = Simulator::from_settled_with_limits_and_trace( + &World::from(&cell.world), + 1024, + 200_000, + 0, + )?; + drive(&mut sim, &cell, function, from)?; + drive(&mut sim, &cell, function, to)?; + } + } + } + Ok(()) + } + + #[test] + fn recipes_verify_all_arities_functions_and_arbitrary_names() -> eyre::Result<()> { + for count in 2..=8 { + let names = (0..count) + .map(|i| format!("operand_{i}")) + .collect::>(); + for expression in [ + names.join("|"), + format!("~({})", names.join("|")), + names.join("&"), + ] { + let graph = LogicGraph::from_stmt(&expression, "renamed_result")?; + let candidate = build_verified_candidate(&graph, "arbitrary_definition")?.unwrap(); + assert_eq!(candidate.module_name, "arbitrary_definition"); + assert_eq!( + candidate + .ports + .iter() + .filter(|p| p.direction == PhysicalPortDirection::Input) + .count(), + count + ); + assert!(candidate.ports.iter().all(|p| p.connection + == if p.direction == PhysicalPortDirection::Output { + PortConnection::RestoredDirect + } else { + PortConnection::Direct + })); + for port in &candidate.ports { + assert!(candidate.blocked_cells.contains(&port.position.up())); + assert!(candidate.world[port.position.up()].kind.is_air()); + } + assert_eq!(candidate.cost.bbox_height, 3); + + assert!(candidate + .ports + .iter() + .any(|p| p.name == "renamed_result" + && p.direction == PhysicalPortDirection::Output)); + } + } + for unsupported in ["a^b", "(a&b)|(c&d)", "~a", "a|b|c|d|e|f|g|h|i"] { + assert!(build_verified_candidate( + &LogicGraph::from_stmt(unsupported, "y")?, + "not_magic" + )? + .is_none()); + } + Ok(()) + } +} diff --git a/src/transform/place_and_route/global_pnr/candidate_cache.rs b/src/transform/place_and_route/global_pnr/candidate_cache.rs index 741b9fb1..2ba434c1 100644 --- a/src/transform/place_and_route/global_pnr/candidate_cache.rs +++ b/src/transform/place_and_route/global_pnr/candidate_cache.rs @@ -10,7 +10,7 @@ use crate::nbt::{NBTRoot, ToNBT}; use crate::world::position::Position; use crate::world::World3D; -const CACHE_FORMAT: &str = "redstone-compiler.local-candidate-cache.v1"; +const CACHE_FORMAT: &str = "redstone-compiler.local-candidate-cache.v13"; #[derive(Serialize, Deserialize)] struct CacheManifest { @@ -27,6 +27,8 @@ struct CacheCandidate { #[derive(Serialize, Deserialize)] struct CandidateMetadata { + #[serde(default)] + exact_core: Option, ports: Vec, blocked_cells: Vec<[usize; 3]>, } @@ -78,11 +80,13 @@ pub(super) fn load( .collect(); let mut candidate = LayoutCandidate::from_world(module_name.to_owned(), world, ports) .context("cached candidate contains no blocks")?; + candidate.exact_core = metadata.exact_core; candidate.blocked_cells = metadata .blocked_cells .into_iter() .map(array_position) .collect::>(); + candidate.include_reserved_cells_in_bounds(); if candidate_parity_hash(&candidate) != entry.id { eyre::bail!("cached candidate `{}` failed its content hash", entry.id); } @@ -111,6 +115,7 @@ pub(super) fn store(root: &Path, key: &str, candidates: &[LayoutCandidate]) -> e .collect::>(); blocked_cells.sort(); let value = CandidateMetadata { + exact_core: candidate.exact_core.clone(), ports: candidate .ports .iter() @@ -163,6 +168,72 @@ mod tests { use crate::world::block::{Block, BlockKind}; use crate::world::position::DimSize; + #[test] + fn mixed_runtime_power_counters_roundtrip_without_weakening_content_check() -> eyre::Result<()> + { + use crate::world::block::{BlockKind, Direction}; + let mut world = World3D::new(DimSize(3, 2, 3)); + world[Position(0, 0, 0)] = Block { + kind: BlockKind::Cobble { + on_count: 2, + on_base_count: 1, + }, + direction: Direction::None, + }; + world[Position(0, 0, 1)] = Block { + kind: BlockKind::Redstone { + on_count: 4, + state: 3, + strength: 8, + }, + direction: Direction::None, + }; + world[Position(1, 0, 0)] = Block { + kind: BlockKind::Cobble { + on_count: 1, + on_base_count: 0, + }, + direction: Direction::None, + }; + world[Position(1, 0, 1)] = Block { + kind: BlockKind::Torch { is_on: true }, + direction: Direction::Bottom, + }; + let candidate = LayoutCandidate::from_world("mixed".into(), world, Vec::new())?; + let root = std::env::temp_dir().join(format!( + "mixed-power-cache-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_nanos() + )); + store(&root, "mixed", &[candidate.clone()])?; + let loaded = load(&root, "mixed", "mixed")?.unwrap(); + assert_ne!( + candidate.world[Position(0, 0, 0)], + loaded[0].world[Position(0, 0, 0)] + ); + assert_eq!( + candidate_parity_hash(&candidate), + candidate_parity_hash(&loaded[0]) + ); + let mut changed = loaded[0].world.clone(); + changed[Position(2, 0, 0)] = Block { + kind: BlockKind::RedstoneBlock, + direction: Direction::None, + }; + std::fs::write( + root.join("mixed/candidate-000.nbt"), + fastnbt::to_bytes(&changed.to_nbt())?, + )?; + assert!( + load(&root, "mixed", "mixed").is_err(), + "changed geometry must still fail the manifest hash" + ); + std::fs::remove_dir_all(root)?; + Ok(()) + } + #[test] fn persistent_candidate_cache_round_trips_and_relabels() -> eyre::Result<()> { let root = std::env::temp_dir().join(format!( @@ -189,16 +260,33 @@ mod tests { connection: PortConnection::Direct, }], )?; - candidate.blocked_cells.insert(Position(1, 0, 0)); + candidate.blocked_cells.insert(Position(1, 0, 1)); + candidate.include_reserved_cells_in_bounds(); store(&root, "shape", &[candidate.clone()])?; let loaded = load(&root, "shape", "second")?.context("cache miss")?; assert_eq!(loaded.len(), 1); assert_eq!(loaded[0].module_name, "second"); + assert_eq!(loaded[0].blocked_cells, candidate.blocked_cells); + assert_eq!(loaded[0].bbox.max, Position(1, 0, 1)); + assert_eq!(loaded[0].cost.bbox_height, 2); + assert!(loaded[0].world[Position(1, 0, 1)].kind.is_air()); assert_eq!( candidate_parity_hash(&loaded[0]), candidate_parity_hash(&candidate) ); + let restored = super::super::candidate::nary::build_verified_candidate( + &crate::graph::logic::LogicGraph::from_stmt("a&b", "y")?, + "restored", + )? + .unwrap(); + store(&root, "restored", &[restored])?; + let restored = load(&root, "restored", "renamed")?.context("restored cache miss")?; + assert!(restored[0] + .ports + .iter() + .filter(|p| p.direction == PhysicalPortDirection::Output) + .all(|p| p.connection == PortConnection::RestoredDirect)); std::fs::remove_dir_all(root)?; Ok(()) } diff --git a/src/transform/place_and_route/global_pnr/free_3d.rs b/src/transform/place_and_route/global_pnr/free_3d.rs index d860352d..cadc9814 100644 --- a/src/transform/place_and_route/global_pnr/free_3d.rs +++ b/src/transform/place_and_route/global_pnr/free_3d.rs @@ -294,6 +294,7 @@ mod tests { fn candidate(index: usize) -> LayoutCandidate { LayoutCandidate { + exact_core: None, module_name: format!("child_{index}"), world: World3D::new(DimSize(1, 1, 1)), bbox: BoundingBox { diff --git a/src/transform/place_and_route/global_pnr/ir.rs b/src/transform/place_and_route/global_pnr/ir.rs index ed2e43bb..4e7642f4 100644 --- a/src/transform/place_and_route/global_pnr/ir.rs +++ b/src/transform/place_and_route/global_pnr/ir.rs @@ -18,6 +18,8 @@ pub enum PhysicalPortDirection { #[serde(rename_all = "snake_case")] pub enum PortConnection { Direct, + /// Verified output dust has strength 15 whenever this output is active. + RestoredDirect, InputDiode, OutputDiode, } @@ -65,8 +67,22 @@ pub struct LayoutCandidateCost { pub port_access_points: usize, } +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ExactCandidateProvenance { + pub search_box: [usize; 3], + pub core_bbox: [usize; 3], + /// Physical occupied blocks, including local input switches. + pub core_blocks: usize, + /// SAT objective count, excluding local input switches. + #[serde(default)] + pub core_logic_blocks: usize, + pub elapsed_ms: u64, + pub optimal: bool, +} + #[derive(Clone, Debug)] pub struct LayoutCandidate { + pub exact_core: Option, pub module_name: String, pub world: World3D, pub bbox: BoundingBox, @@ -77,6 +93,21 @@ pub struct LayoutCandidate { } impl LayoutCandidate { + /// Air reservations are part of the placed package, not physical blocks. + pub(crate) fn include_reserved_cells_in_bounds(&mut self) { + for position in &self.blocked_cells { + self.bbox.min.0 = self.bbox.min.0.min(position.0); + self.bbox.min.1 = self.bbox.min.1.min(position.1); + self.bbox.min.2 = self.bbox.min.2.min(position.2); + self.bbox.max.0 = self.bbox.max.0.max(position.0); + self.bbox.max.1 = self.bbox.max.1.max(position.1); + self.bbox.max.2 = self.bbox.max.2.max(position.2); + } + self.cost.bbox_volume = self.bbox.volume(); + self.cost.bbox_footprint = self.bbox.width() * self.bbox.depth(); + self.cost.bbox_height = self.bbox.height(); + } + pub fn from_world( module_name: String, world: World3D, @@ -100,6 +131,7 @@ impl LayoutCandidate { }; Ok(Self { + exact_core: None, module_name, world, bbox, diff --git a/src/transform/place_and_route/global_pnr/mod.rs b/src/transform/place_and_route/global_pnr/mod.rs index 75e88d5e..9b9eb246 100644 --- a/src/transform/place_and_route/global_pnr/mod.rs +++ b/src/transform/place_and_route/global_pnr/mod.rs @@ -275,7 +275,12 @@ fn debug_parity_hash(value: &impl std::fmt::Debug) -> String { } fn candidate_parity_hash(candidate: &LayoutCandidate) -> String { - let mut blocks = candidate.world.iter_block(); + // Hash the persisted representation. NBT preserves physical block states, + // but not simulator-only counters or repeater lock-source bookkeeping. + // Applying the real roundtrip also canonicalizes dust shapes exactly as + // cache loading does; geometric/port/reservation validation remains intact. + let persisted_world = World3D::from(&candidate.world.to_nbt().to_world()); + let mut blocks = persisted_world.iter_block(); blocks.sort_by_key(|(position, _)| *position); let mut ports = candidate.ports.clone(); ports.sort_by(|left, right| { @@ -569,6 +574,25 @@ fn global_pnr_config_snapshot(config: &GlobalPnrConfig) -> Value { "show_progress": config.show_progress, "verifier_enabled": config.verifier.is_some(), "persistent_cache_enabled": config.candidate_cache_dir.is_some(), + // Experimental runtime policy is deliberately outside the RCIR grammar. + "verified_nary_cells": { + "default": config.candidate.default.verified_nary_cells, + "definition_overrides": config.candidate.definition_overrides.iter() + .map(|(name, policy)| (name.clone(), policy.verified_nary_cells)) + .collect::>(), + }, + "nary_stagger_inputs": { + "default": config.candidate.default.nary_stagger_inputs, + "definition_overrides": config.candidate.definition_overrides.iter() + .map(|(name, policy)| (name.clone(), policy.nary_stagger_inputs)) + .collect::>(), + }, + "reserve_combinational_io_diodes": { + "default": config.candidate.default.reserve_combinational_io_diodes, + "definition_overrides": config.candidate.definition_overrides.iter() + .map(|(name, policy)| (name.clone(), policy.reserve_combinational_io_diodes)) + .collect::>(), + }, "experimental_hooks": config.heuristic_hooks.names(), } }) @@ -1174,6 +1198,14 @@ fn route_first_successful_placement( semantic_feedback_nets: decision.semantic_feedback_nets.clone(), }); } + save_route_prefix_diagnostics( + topology, + attempt_serial, + candidates, + placed, + &failure.routed_nets, + &failure.error.to_string(), + ); save_failed_route_base_world(topology, attempt_serial, candidates, placed); progress.detail(format!( "placement attempt {attempt_serial} failed: {}", @@ -1383,6 +1415,72 @@ fn save_failed_verifier_world( let _ = std::fs::write(routes_path, routes); } +fn save_route_prefix_diagnostics( + topology: &ResolvedPnrTopology, + attempt: usize, + candidates: &[LayoutCandidate], + placed: &[PlacedModule], + routes: &[RoutedNet], + error: &str, +) { + let Some(directory) = std::env::var_os("PNR_ROUTE_DUMP_DIR") else { + return; + }; + let directory = std::path::PathBuf::from(directory); + if std::fs::create_dir_all(&directory).is_err() { + return; + } + for (label, prefix) in [("base", &[][..]), ("partial", routes)] { + let Ok(world) = placed_world_from_routing(topology, candidates, placed, prefix) else { + continue; + }; + world + .world + .to_nbt() + .save(directory.join(format!("attempt-{attempt}-{label}.nbt"))); + let blocks = world + .world + .iter_block() + .into_iter() + .map(|(position, block)| format!("{position:?} {block:?}\n")) + .collect::(); + let _ = std::fs::write( + directory.join(format!("attempt-{attempt}-{label}.txt")), + blocks, + ); + } + let mut ports = String::new(); + for module in placed { + let Some(candidate) = candidates.get(module.candidate_index) else { + continue; + }; + for port in &candidate.ports { + let position = crate::world::position::Position( + module.origin.0 + port.position.0 - candidate.bbox.min.0, + module.origin.1 + port.position.1 - candidate.bbox.min.1, + module.origin.2 + port.position.2 - candidate.bbox.min.2, + ); + ports.push_str(&format!( + "{}.{} {:?} {:?} {:?}\n", + module.module_name, port.name, port.direction, position, port.connection + )); + } + } + let _ = std::fs::write( + directory.join(format!("attempt-{attempt}-ports.txt")), + ports, + ); + let routes = routes.iter().enumerate().map(|(index,route)| format!("route={index} source={:?} sink={:?} source_label={:?} sink_label={:?} blocks={:?}\n",route.source,route.sink,route.source_label,route.sink_label,route.blocks)).collect::(); + let _ = std::fs::write( + directory.join(format!("attempt-{attempt}-routes.txt")), + routes, + ); + let _ = std::fs::write( + directory.join(format!("attempt-{attempt}-failure.txt")), + error, + ); +} + fn save_failed_route_base_world( topology: &ResolvedPnrTopology, attempt: usize, @@ -2136,6 +2234,7 @@ mod tests { ..Default::default() }, routing_probe: Some(GlobalRoutingConfig { + max_route_steps: 128, strategy: crate::transform::place_and_route::global_pnr::router::GlobalRoutingStrategy::DirectGreedy { max_steps: 128, }, @@ -2144,6 +2243,7 @@ mod tests { defer_feedback_cycles: false, }), routing: GlobalRoutingConfig { + max_route_steps: 128, strategy: crate::transform::place_and_route::global_pnr::router::GlobalRoutingStrategy::GreedyBeam { beam_width: 128, max_expansions: 4_096, @@ -2154,6 +2254,7 @@ mod tests { defer_feedback_cycles: false, }, routing_refinement: Some(GlobalRoutingConfig { + max_route_steps: 128, strategy: crate::transform::place_and_route::global_pnr::router::GlobalRoutingStrategy::GreedyBeam { beam_width: 128, max_expansions: 4_096, @@ -2284,6 +2385,7 @@ mod tests { ..Default::default() }, routing_probe: Some(GlobalRoutingConfig { + max_route_steps: 128, strategy: crate::transform::place_and_route::global_pnr::router::GlobalRoutingStrategy::DirectGreedy { max_steps: 128, }, @@ -2292,6 +2394,7 @@ mod tests { defer_feedback_cycles: false, }), routing: GlobalRoutingConfig { + max_route_steps: 128, strategy: crate::transform::place_and_route::global_pnr::router::GlobalRoutingStrategy::GreedyBeam { beam_width: 128, max_expansions: 4_096, @@ -2302,6 +2405,7 @@ mod tests { defer_feedback_cycles: false, }, routing_refinement: Some(GlobalRoutingConfig { + max_route_steps: 128, strategy: crate::transform::place_and_route::global_pnr::router::GlobalRoutingStrategy::GreedyBeam { beam_width: 128, max_expansions: 4_096, diff --git a/src/transform/place_and_route/global_pnr/placer.rs b/src/transform/place_and_route/global_pnr/placer.rs index 0b755645..ce16367c 100644 --- a/src/transform/place_and_route/global_pnr/placer.rs +++ b/src/transform/place_and_route/global_pnr/placer.rs @@ -291,6 +291,15 @@ fn placement_candidates_with_connectivity( ..*config }; if heuristics.contains(&PlacementHeuristic::Shelf) { + // Keep adjacent nodes close across row boundaries as well. + // A typewriter wrap otherwise sends a chain across the full + // shelf width even when the next cell is on the next row. + for order in [&original_order, &net_order] { + let mut snake = + place_candidates_on_shelves_in_order(candidates, order, &config); + snake_shelf_rows(&mut snake); + push_unique_placement(&mut placements, snake); + } push_unique_placement( &mut placements, place_candidates_on_shelves_in_order(candidates, &original_order, &config), @@ -338,6 +347,27 @@ fn placement_candidates_with_connectivity( placements } +fn snake_shelf_rows(placed: &mut [PlacedModule]) { + let mut rows = std::collections::BTreeMap::>::new(); + for (index, item) in placed.iter().enumerate() { + rows.entry(item.origin.1).or_default().push(index); + } + for (row, indices) in rows.values().enumerate() { + if row % 2 == 0 { + continue; + } + let min = indices.iter().map(|&i| placed[i].origin.0).min().unwrap(); + let max = indices + .iter() + .map(|&i| placed[i].origin.0 + placed[i].bbox.width()) + .max() + .unwrap(); + for &i in indices { + placed[i].origin.0 = min + max - placed[i].origin.0 - placed[i].bbox.width(); + } + } +} + pub fn placement_candidates_resolved( topology: &ResolvedPnrTopology, intent: Option<&ResolvedPhysicalIntent>, @@ -349,6 +379,11 @@ pub fn placement_candidates_resolved( let connectivity = PlacementConnectivity::from_resolved(topology)?; let mut placements = placement_candidates_with_connectivity(&connectivity, candidates, config, heuristics); + // A diode adapter beside a floor-level port needs a support block below + // it. Reserve that layer before applying explicit physical constraints. + for placed in &mut placements { + reserve_port_support_layer(candidates, placed); + } let context = PlacementHeuristicContext { topology, candidates, @@ -404,6 +439,21 @@ pub fn placement_candidates_resolved( Ok(constrained) } +fn reserve_port_support_layer(candidates: &[LayoutCandidate], placed: &mut [PlacedModule]) { + let floor_port = placed.iter().any(|item| { + let candidate = &candidates[item.candidate_index]; + candidate.ports.iter().any(|port| { + (port.requires_input_diode() || port.requires_output_diode()) + && item.origin.2 + port.position.2 - candidate.bbox.min.2 == 0 + }) + }); + if floor_port { + for item in placed { + item.origin.2 += 1; + } + } +} + fn apply_placement_intent( topology: &ResolvedPnrTopology, intent: &ResolvedPhysicalIntent, @@ -1506,8 +1556,51 @@ mod tests { use crate::world::position::DimSize; use crate::world::World3D; + #[test] + fn snake_shelves_reduce_chain_wrap_distance_without_overlapping_cells() { + let candidates = (0..4) + .map(|i| test_candidate(&format!("g{i}"), &[])) + .collect::>(); + let mut placed = place_candidates_on_shelves_in_order( + &candidates, + &[0, 1, 2, 3], + &GlobalPlacementConfig { + shelf_width: 20, + ..Default::default() + }, + ); + let before = placed[1].origin.manhattan_distance(&placed[2].origin); + let original = placed.clone(); + snake_shelf_rows(&mut placed); + assert!(placed[1].origin.manhattan_distance(&placed[2].origin) < before); + validate_no_placement_overlap(&placed).unwrap(); + snake_shelf_rows(&mut placed); + assert_eq!( + placed.iter().map(|p| p.origin).collect::>(), + original.iter().map(|p| p.origin).collect::>() + ); + } + + #[test] + fn diode_ports_reserve_support_below_floor_without_changing_relative_placement() { + let mut candidate = test_candidate("gate", &[("a", Position(0, 0, 0))]); + candidate.ports[0].direction = PhysicalPortDirection::Input; + candidate.ports[0].connection = PortConnection::InputDiode; + let mut placed = vec![PlacedModule { + module_name: "gate".into(), + candidate_index: 0, + origin: Position(4, 4, 0), + bbox: candidate.bbox, + }]; + reserve_port_support_layer(&[candidate.clone()], &mut placed); + assert_eq!(placed[0].origin, Position(4, 4, 1)); + reserve_port_support_layer(&[candidate], &mut placed); + assert_eq!(placed[0].origin, Position(4, 4, 1)); + } + fn test_candidate(module_name: &str, ports: &[(&str, Position)]) -> LayoutCandidate { LayoutCandidate { + exact_core: None, module_name: module_name.to_owned(), world: World3D::new(DimSize(1, 1, 1)), bbox: BoundingBox { diff --git a/src/transform/place_and_route/global_pnr/prepared_snapshot.rs b/src/transform/place_and_route/global_pnr/prepared_snapshot.rs index e55ff4e9..c941f37c 100644 --- a/src/transform/place_and_route/global_pnr/prepared_snapshot.rs +++ b/src/transform/place_and_route/global_pnr/prepared_snapshot.rs @@ -21,7 +21,7 @@ use crate::transform::place_and_route::global_pnr::{ use crate::world::position::Position; use crate::world::World3D; -const CANDIDATE_LIBRARY_FORMAT: &str = "redstone-compiler.candidate-library.v1"; +const CANDIDATE_LIBRARY_FORMAT: &str = "redstone-compiler.candidate-library.v2"; #[derive(Clone, Debug, Serialize, Deserialize)] struct CandidateLibraryManifest { @@ -54,6 +54,8 @@ struct InstanceBindingManifest { #[derive(Clone, Debug, Serialize, Deserialize)] struct CandidateMetadata { + #[serde(default)] + exact_core: Option, format: String, id: String, module_name: String, @@ -120,6 +122,7 @@ pub(super) fn emit_candidate_library(prepared: &PreparedPnrDesign) -> eyre::Resu emit_json( &metadata, CandidateMetadata { + exact_core: candidate.exact_core.clone(), format: "redstone-compiler.candidate.v1".to_owned(), id: id.clone(), module_name: candidate.module_name.clone(), @@ -256,11 +259,13 @@ pub fn load_prepared_pnr_snapshot( }) .collect(); let mut candidate = LayoutCandidate::from_world(metadata.module_name, world, ports)?; + candidate.exact_core = metadata.exact_core; candidate.blocked_cells = metadata .blocked_cells .into_iter() .map(array_position) .collect::>(); + candidate.include_reserved_cells_in_bounds(); let actual_id = candidate_parity_hash(&candidate); if actual_id != entry.id { eyre::bail!( @@ -439,9 +444,154 @@ impl SnapshotSource { mod tests { use super::*; + #[test] + fn prepared_snapshot_roundtrip_preserves_reserved_air_and_bbox() -> eyre::Result<()> { + use crate::graph::logic::LogicGraph; + use crate::ir::{graph_from_routable_leaf, LogicalDesign}; + let mut design = LogicalDesign::from_verilog_source( + "module top(a,b,y); input a,b; output y; assign y=a&b; endmodule", + )? + .lower_to_routable_with_associative_cells(8)?; + let module = design + .modules + .iter() + .find(|m| matches!(m.body, RoutableModuleBody::Leaf { .. }) && m.ports.len() == 3) + .unwrap() + .clone(); + design.top = module.name.clone(); + design.modules = vec![module.clone()]; + let candidate = super::super::candidate::nary::build_verified_candidate( + &LogicGraph { + graph: graph_from_routable_leaf(&module)?, + }, + &module.name, + )? + .unwrap(); + let document = + super::super::routable_document_from_config(&design, &GlobalPnrConfig::default())?; + let config = PnrPrepareConfig::from(&GlobalPnrConfig::default()); + let topology = ResolvedPnrTopology::from_routable(&design)?; + let id = candidate_parity_hash(&candidate); + let metadata = CandidateMetadata { + exact_core: Some(super::super::ir::ExactCandidateProvenance { + search_box: [3, 5, 3], + core_bbox: [2, 3, 2], + core_blocks: 5, + core_logic_blocks: 3, + elapsed_ms: 862, + optimal: true, + }), + format: "redstone-compiler.candidate.v1".into(), + id: id.clone(), + module_name: module.name.clone(), + bbox: CandidateBoundingBox { + min: position_array(candidate.bbox.min), + max: position_array(candidate.bbox.max), + size: [ + candidate.bbox.width(), + candidate.bbox.depth(), + candidate.bbox.height(), + ], + }, + cost: candidate.cost.clone(), + ports: candidate + .ports + .iter() + .map(|p| PhysicalPortDto { + name: p.name.clone(), + direction: p.direction.clone(), + position: position_array(p.position), + route_position: p.route_position.map(position_array), + access_points: p + .access_points + .iter() + .copied() + .map(position_array) + .collect(), + connection: p.connection, + }) + .collect(), + blocked_cells: candidate + .blocked_cells + .iter() + .copied() + .map(position_array) + .collect(), + }; + let manifest = CandidateLibraryManifest { + format: CANDIDATE_LIBRARY_FORMAT.into(), + prepare_config_fingerprint: prepare_config_fingerprint(&config, &topology), + summary: PnrPreparationSummary { + module: module.name.clone(), + instances: 0, + unique_candidate_sets: 1, + reused_candidate_sets: 0, + candidates: 1, + candidate_references: 1, + elapsed_ms: 0, + }, + candidate_sets: vec![CandidateSetManifest { + index: 0, + candidates: vec![CandidateManifestEntry { + id, + nbt: "candidates/cell.nbt".into(), + metadata: "candidates/cell.json".into(), + }], + }], + instance_bindings: vec![], + }; + let root = std::env::temp_dir().join(format!( + "redstone-headroom-snapshot-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_nanos() + )); + std::fs::create_dir_all(root.join("ir"))?; + std::fs::create_dir_all(root.join("candidates"))?; + std::fs::write(root.join("ir/routable.rcir"), document.to_string())?; + std::fs::write( + root.join("candidates/index.json"), + serde_json::to_vec(&manifest)?, + )?; + std::fs::write( + root.join("candidates/cell.json"), + serde_json::to_vec(&metadata)?, + )?; + std::fs::write( + root.join("candidates/cell.nbt"), + fastnbt::to_bytes(&candidate.world.to_nbt())?, + )?; + let loaded = load_prepared_pnr_snapshot(&root, &config)?; + let PreparedPnrBody::Leaf { candidates } = loaded.body else { + panic!("expected leaf"); + }; + assert_eq!(candidates.len(), 1); + assert_eq!(candidates[0].blocked_cells, candidate.blocked_cells); + assert_eq!(candidates[0].exact_core, metadata.exact_core); + assert!(candidates[0] + .ports + .iter() + .filter(|p| p.direction == super::super::ir::PhysicalPortDirection::Output) + .all(|p| p.connection == super::super::ir::PortConnection::RestoredDirect)); + assert_eq!(candidates[0].cost, candidate.cost); + assert_eq!(candidates[0].bbox.max, candidate.bbox.max); + assert_eq!( + candidate_parity_hash(&candidates[0]), + candidate_parity_hash(&candidate) + ); + assert!(candidates[0] + .blocked_cells + .iter() + .all(|p| candidates[0].world[*p].kind.is_air())); + std::fs::remove_dir_all(root)?; + Ok(()) + } + #[test] fn candidate_metadata_serializes_bbox_and_quality_metrics() { let metadata = CandidateMetadata { + exact_core: None, format: "redstone-compiler.candidate.v1".to_owned(), id: "candidate".to_owned(), module_name: "adder".to_owned(), diff --git a/src/transform/place_and_route/global_pnr/rcir.rs b/src/transform/place_and_route/global_pnr/rcir.rs index 25c11f4f..76128ebf 100644 --- a/src/transform/place_and_route/global_pnr/rcir.rs +++ b/src/transform/place_and_route/global_pnr/rcir.rs @@ -179,6 +179,15 @@ pub fn apply_routable_document( .candidate_profiles .get(profile_name) .with_context(|| format!("missing pnr.candidate profile `{profile_name}`"))?; + if let Some(exact) = &profile.exact { + eyre::ensure!( + exact.search_box.iter().all(|v| *v > 0) + && exact.time_limit_ms > 0 + && (1..=6).contains(&exact.max_inputs) + && exact.max_logic_nodes > 0, + "invalid Exact candidate bounds in `{profile_name}`" + ); + } validate_clustering_spec(profile_name, &profile.clustering)?; validate_local_search_spec(profile_name, &profile.local_placer)?; let mut policy = candidate_policy_from_spec(profile); @@ -339,6 +348,25 @@ pub fn pnr_spec_from_config(config: &GlobalPnrConfig) -> PnrSpec { } pub fn apply_pnr_spec(spec: &PnrSpec, config: &mut GlobalPnrConfig) -> eyre::Result<()> { + for stage in [ + Some(spec.routing.primary), + spec.routing.probe, + spec.routing.refinement, + ] + .into_iter() + .flatten() + { + eyre::ensure!( + stage.max_route_steps > 0, + "route max-steps must be positive" + ); + if let RouteStrategySpec::AStarBudgeted { max_expansions } = stage.strategy { + eyre::ensure!( + max_expansions > 0, + "astar-budgeted max-expansions must be positive" + ); + } + } config.placement.spacing = spec.placement.initial_spacing; config.placement.shelf_width = spec.placement.shelf_width; config.placement.max_attempts = spec.placement.max_attempts; @@ -374,6 +402,7 @@ pub fn apply_pnr_spec(spec: &PnrSpec, config: &mut GlobalPnrConfig) -> eyre::Res pub(super) fn candidate_spec_from_policy(policy: &UnitCandidateConfig) -> CandidateSpec { CandidateSpec { + exact: policy.exact.clone(), search_box: [policy.dim.0, policy.dim.1, policy.dim.2], retain: policy.max_candidates, combinational_samples: policy.combinational_sampling_limit, @@ -388,6 +417,10 @@ pub(super) fn candidate_spec_from_policy(policy: &UnitCandidateConfig) -> Candid fn candidate_policy_from_spec(spec: &CandidateSpec) -> UnitCandidateConfig { UnitCandidateConfig { + exact: spec.exact.clone(), + verified_nary_cells: false, + nary_stagger_inputs: false, + reserve_combinational_io_diodes: false, dim: DimSize(spec.search_box[0], spec.search_box[1], spec.search_box[2]), local_config: local_config(spec.local_placer.clone()), input_constraints: LocalPlacerInputConstraints::default(), @@ -744,9 +777,13 @@ fn placement_weights(w: ObjectiveSpec) -> PlacementCostWeights { fn route_stage_spec(config: GlobalRoutingConfig) -> RouteStageSpec { RouteStageSpec { + max_route_steps: config.max_route_steps, strategy: match config.strategy { GlobalRoutingStrategy::BreadthFirst => RouteStrategySpec::BreadthFirst, GlobalRoutingStrategy::AStar => RouteStrategySpec::AStar, + GlobalRoutingStrategy::AStarBudgeted { max_expansions } => { + RouteStrategySpec::AStarBudgeted { max_expansions } + } GlobalRoutingStrategy::DirectGreedy { max_steps } => { RouteStrategySpec::DirectGreedy { max_steps } } @@ -768,9 +805,13 @@ fn route_stage_spec(config: GlobalRoutingConfig) -> RouteStageSpec { } fn route_stage_config(spec: RouteStageSpec) -> GlobalRoutingConfig { GlobalRoutingConfig { + max_route_steps: spec.max_route_steps, strategy: match spec.strategy { RouteStrategySpec::BreadthFirst => GlobalRoutingStrategy::BreadthFirst, RouteStrategySpec::AStar => GlobalRoutingStrategy::AStar, + RouteStrategySpec::AStarBudgeted { max_expansions } => { + GlobalRoutingStrategy::AStarBudgeted { max_expansions } + } RouteStrategySpec::DirectGreedy { max_steps } => { GlobalRoutingStrategy::DirectGreedy { max_steps } } @@ -797,6 +838,7 @@ fn net_order_spec(value: NetOrderStrategy) -> NetOrderSpec { NetOrderStrategy::Criticality => NetOrderSpec::Criticality, NetOrderStrategy::HighestFanoutFirst => NetOrderSpec::HighestFanoutFirst, NetOrderStrategy::ReverseCriticality => NetOrderSpec::ReverseCriticality, + NetOrderStrategy::PinAccess => NetOrderSpec::PinAccess, } } fn net_order_strategy(value: NetOrderSpec) -> NetOrderStrategy { @@ -804,6 +846,7 @@ fn net_order_strategy(value: NetOrderSpec) -> NetOrderStrategy { NetOrderSpec::Criticality => NetOrderStrategy::Criticality, NetOrderSpec::HighestFanoutFirst => NetOrderStrategy::HighestFanoutFirst, NetOrderSpec::ReverseCriticality => NetOrderStrategy::ReverseCriticality, + NetOrderSpec::PinAccess => NetOrderStrategy::PinAccess, } } @@ -1067,6 +1110,12 @@ mod tests { }], }; let mut original = GlobalPnrConfig::default(); + original.candidate.default.exact = Some(crate::ir::ExactCandidateSpec::default()); + original.routing.strategy = GlobalRoutingStrategy::AStarBudgeted { + max_expansions: 20_000, + }; + original.routing.max_route_steps = 256; + original.search.policies.net_order_strategies = vec![NetOrderStrategy::PinAccess]; original.candidate = original .candidate @@ -1122,6 +1171,56 @@ mod tests { .collect(); let document = routable_document_from_config(&design, &original)?; let text = document.to_string(); + assert!(text.contains("strategy astar-budgeted 20000;")); + assert!(text.contains("max-steps 256;")); + assert!(text.contains("pin-access")); + assert!(text.contains("exact { box [3, 5, 3]; time-limit-ms 5000;")); + assert!(text + .replace("time-limit-ms 5000", "time-limit-ms 0") + .parse::() + .is_err()); + assert!(text + .replace("max-steps 256;", "max-steps 0;") + .parse::() + .is_err()); + let legacy_steps: RoutableDocument = text.replace(" max-steps 256;\n", "").parse()?; + let mut legacy_steps_config = GlobalPnrConfig::default(); + apply_routable_document(&legacy_steps, &mut legacy_steps_config)?; + assert_eq!(legacy_steps_config.routing.max_route_steps, 128); + + assert!(text + .replace("astar-budgeted 20000", "astar-budgeted 0") + .parse::() + .is_err()); + let legacy: RoutableDocument = text.replace("astar-budgeted 20000", "astar").parse()?; + let mut legacy_config = GlobalPnrConfig::default(); + apply_routable_document(&legacy, &mut legacy_config)?; + assert_eq!(legacy_config.routing.strategy, GlobalRoutingStrategy::AStar); + let mut invalid_budget = pnr_spec_from_config(&original); + invalid_budget.routing.primary.strategy = + RouteStrategySpec::AStarBudgeted { max_expansions: 0 }; + assert!(apply_pnr_spec(&invalid_budget, &mut GlobalPnrConfig::default()).is_err()); + let json = serde_json::to_string(&pnr_spec_from_config(&original))?; + let decoded: PnrSpec = serde_json::from_str(&json)?; + assert_eq!(decoded.routing.primary.max_route_steps, 256); + assert_eq!(decoded.routing.net_order, vec![NetOrderSpec::PinAccess]); + let mut legacy_json: serde_json::Value = serde_json::from_str(&json)?; + legacy_json["routing"]["primary"] + .as_object_mut() + .unwrap() + .remove("max_route_steps"); + let legacy_decoded: PnrSpec = serde_json::from_value(legacy_json)?; + assert_eq!(legacy_decoded.routing.primary.max_route_steps, 128); + let mut invalid_steps = decoded.clone(); + invalid_steps.routing.primary.max_route_steps = 0; + assert!(apply_pnr_spec(&invalid_steps, &mut GlobalPnrConfig::default()).is_err()); + + assert_eq!( + decoded.routing.primary.strategy, + RouteStrategySpec::AStarBudgeted { + max_expansions: 20_000 + } + ); assert!(text.contains("profile pnr.candidate \"leaf-cell-search\"")); assert!(text.contains("profile pnr.design \"leaf-design\"")); assert!(text.contains("@pnr.candidate(profile = \"leaf-cell-search\")")); diff --git a/src/transform/place_and_route/global_pnr/router.rs b/src/transform/place_and_route/global_pnr/router.rs index fcc8a5e7..7d620edc 100644 --- a/src/transform/place_and_route/global_pnr/router.rs +++ b/src/transform/place_and_route/global_pnr/router.rs @@ -110,6 +110,10 @@ const SIGNAL_CONTACT_SEARCH_RADIUS: usize = 3; pub enum GlobalRoutingStrategy { BreadthFirst, AStar, + /// Same A* ordering and visited-state policy, with an explicit expansion budget. + AStarBudgeted { + max_expansions: usize, + }, DirectGreedy { max_steps: usize, }, @@ -122,12 +126,80 @@ pub enum GlobalRoutingStrategy { }, } +/// Immutable construction resources belonging to the current global routing run. +/// These are planning keepouts, not additional Minecraft physics constraints. +#[derive(Clone, Copy, Debug)] +pub(super) struct RoutingSearchContext<'a> { + strategy: GlobalRoutingStrategy, + blocked_positions: &'a [Position], + access_reservations: &'a [(Position, Position)], + active_sink: Option, + max_route_steps: usize, +} + +impl<'a> From for RoutingSearchContext<'a> { + fn from(strategy: GlobalRoutingStrategy) -> Self { + Self { + strategy, + blocked_positions: &[], + access_reservations: &[], + active_sink: None, + max_route_steps: GLOBAL_ROUTE_MAX_STEPS, + } + } +} + +impl RoutingSearchContext<'_> { + fn allows_placement(self, position: Position) -> bool { + !self.blocked_positions.contains(&position) + && self + .access_reservations + .iter() + .all(|(reserved, owner)| *reserved != position || self.active_sink == Some(*owner)) + } + + fn for_sink(self, sink: Position) -> Self { + Self { + active_sink: Some(sink), + ..self + } + } + + fn preserves_reservations(self, before: &World3D, after: &World3D) -> bool { + self.blocked_positions + .iter() + .copied() + .chain( + self.access_reservations + .iter() + .filter_map(|(position, owner)| { + (self.active_sink != Some(*owner)).then_some(*position) + }), + ) + .all(|position| { + let old = before + .size + .bound_on(position) + .then(|| before[position]) + .unwrap_or_default(); + let new = after + .size + .bound_on(position) + .then(|| after[position]) + .unwrap_or_default(); + old == new + }) + } +} + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum NetOrderStrategy { #[default] Criticality, HighestFanoutFirst, ReverseCriticality, + /// Route deeper staggered input pins before outward pins on the same face. + PinAccess, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -144,6 +216,8 @@ pub enum RouteValidationMode { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct GlobalRoutingConfig { pub strategy: GlobalRoutingStrategy, + /// Maximum route hops, independent of the strategy expansion budget. + pub max_route_steps: usize, pub validation: RouteValidationMode, pub top_inputs_last: bool, pub defer_feedback_cycles: bool, @@ -153,6 +227,7 @@ impl Default for GlobalRoutingConfig { fn default() -> Self { Self { strategy: GlobalRoutingStrategy::AStar, + max_route_steps: GLOBAL_ROUTE_MAX_STEPS, validation: RouteValidationMode::Incremental, top_inputs_last: false, defer_feedback_cycles: false, @@ -571,14 +646,87 @@ fn route_module_variables_with_order_from_prefix_impl( progress: &GlobalPnrProgress, prefix: &[RoutedNet], ) -> Result, PartialRoutingFailure> { + if config.max_route_steps == 0 { + return Err(PartialRoutingFailure { + error: eyre::eyre!("route max-steps must be positive"), + routed_nets: Vec::new(), + }); + } let mut route_world = placed_candidate_world(candidates, placed_modules).map_err(|error| { PartialRoutingFailure { error, routed_nets: Vec::new(), } })?; + let mut blocked_positions = Vec::new(); + for placed in placed_modules { + let candidate = &candidates[placed.candidate_index]; + blocked_positions.extend( + candidate + .blocked_cells + .iter() + .map(|position| translate_candidate_position(*position, candidate, placed)), + ); + } + blocked_positions.sort_unstable(); + blocked_positions.dedup(); + let mut access_reservations = Vec::new(); + if order_strategy == NetOrderStrategy::PinAccess { + for placed in placed_modules { + let candidate = &candidates[placed.candidate_index]; + for port in &candidate.ports { + let Some((position, outward)) = input_port_outward_direction(candidate, port) + else { + continue; + }; + let owner = translate_candidate_position(position, candidate, placed); + let mut approach = owner; + for _ in 0..2 { + let Some(next) = approach.walk(outward) else { + break; + }; + approach = next; + for position in [approach, approach.up()] { + if route_world.size.bound_on(position) + && route_world[position].kind.is_air() + { + access_reservations.push((position, owner)); + } + } + } + } + } + } + let search_context = RoutingSearchContext { + strategy: config.strategy, + blocked_positions: &blocked_positions, + access_reservations: &access_reservations, + active_sink: None, + max_route_steps: config.max_route_steps, + }; for route in prefix { for &(position, block) in &route.blocks { + let prefix_owns_access = search_context + .for_sink(route.sink) + .allows_placement(position) + || route.source_label.as_ref().is_some_and(|label| { + prefix.iter().any(|branch| { + branch.source_label.as_ref() == Some(label) + && search_context + .for_sink(branch.sink) + .allows_placement(position) + }) + }); + if !prefix_owns_access + && (!route_world.size.bound_on(position) || route_world[position] != block) + { + return Err(PartialRoutingFailure { + error: eyre::eyre!( + "routed prefix occupies reserved construction cell {position:?}" + ), + routed_nets: Vec::new(), + }); + } if !route_world.size.bound_on(position) { return Err(PartialRoutingFailure { error: eyre::eyre!("routed prefix block {position:?} is outside route world"), @@ -614,6 +762,9 @@ fn route_module_variables_with_order_from_prefix_impl( std::cmp::Reverse(priority) }); } + if order_strategy == NetOrderStrategy::PinAccess { + order_connections_by_pin_access(&mut ordered_vars, candidates, placed_modules); + } let vars = ordered_vars .into_iter() .filter(|var| { @@ -660,12 +811,32 @@ fn route_module_variables_with_order_from_prefix_impl( .filter(|label| !label.contains('.')) .map(str::to_owned) .collect::>(); + let mut top_inputs = plan.top_inputs.clone(); + match order_strategy { + NetOrderStrategy::Criticality | NetOrderStrategy::PinAccess => {} + NetOrderStrategy::HighestFanoutFirst => { + top_inputs.sort_by_key(|port| std::cmp::Reverse(port.targets.len())) + } + NetOrderStrategy::ReverseCriticality => top_inputs.reverse(), + } + if order_strategy == NetOrderStrategy::PinAccess { + let targets = top_inputs + .iter() + .map(|input| input.targets.clone()) + .collect::>(); + let order = pin_access_group_order(&targets, candidates, placed_modules); + top_inputs = order + .into_iter() + .map(|index| top_inputs[index].clone()) + .collect(); + } if config.top_inputs_last { if let Err(error) = route_internal_module_nets( &vars, candidates, placed_modules, config, + search_context, progress, &mut route_world, &mut routes, @@ -676,10 +847,11 @@ fn route_module_variables_with_order_from_prefix_impl( }); } if let Err(error) = route_top_input_ports( - &plan.top_inputs, + &top_inputs, candidates, placed_modules, config, + search_context, progress, &completed_top_inputs, &mut route_world, @@ -692,10 +864,11 @@ fn route_module_variables_with_order_from_prefix_impl( } } else { if let Err(error) = route_top_input_ports( - &plan.top_inputs, + &top_inputs, candidates, placed_modules, config, + search_context, progress, &completed_top_inputs, &mut route_world, @@ -711,6 +884,7 @@ fn route_module_variables_with_order_from_prefix_impl( candidates, placed_modules, config, + search_context, progress, &mut route_world, &mut routes, @@ -738,13 +912,133 @@ fn route_module_variables_with_order_from_prefix_impl( Ok(routes) } +fn input_port_outward_direction( + candidate: &LayoutCandidate, + port: &PhysicalPort, +) -> Option<(Position, Direction)> { + if port.direction != PhysicalPortDirection::Input { + return None; + } + let position = port.route_position.unwrap_or(port.position); + if !candidate.world.size.bound_on(position) || !candidate.world[position].kind.is_redstone() { + return None; + } + let mut normals = position.cardinal().into_iter().filter_map(|neighbor| { + if !candidate.world.size.bound_on(neighbor) { + return None; + } + let block = candidate.world[neighbor]; + (block.kind.is_repeater() && neighbor.walk(block.direction) == Some(position)) + .then_some(block.direction) + }); + let normal = normals.next()?; + if normals.next().is_some() { + return None; + } + Some((position, normal)) +} + +/// Infer access ordering from physical input geometry, never signal names. +/// Each group is a single driver and all of its sinks; source grouping survives +/// the permutation. Cyclic geometric preferences fall back to the full original +/// ordering instead of dropping drivers or treating the circuit as unroutable. +fn pin_access_group_order( + targets: &[Vec<(String, String)>], + candidates: &[LayoutCandidate], + placed: &[PlacedModule], +) -> Vec { + let mut pins = Vec::new(); + for (group, sinks) in targets.iter().enumerate() { + for (instance, name) in sinks { + let Some(module) = placed.iter().find(|p| &p.module_name == instance) else { + continue; + }; + let candidate = &candidates[module.candidate_index]; + let Some(port) = candidate + .ports + .iter() + .find(|p| &p.name == name && p.direction == PhysicalPortDirection::Input) + else { + continue; + }; + let Some((position, normal)) = input_port_outward_direction(candidate, port) else { + continue; + }; + let depth = match normal { + Direction::East => position.0 as i128, + Direction::West => -(position.0 as i128), + Direction::North => position.1 as i128, + Direction::South => -(position.1 as i128), + _ => continue, + }; + pins.push((group, instance, normal, position.2, depth)); + } + } + let mut edges = HashSet::new(); + for a in &pins { + for b in &pins { + if a.0 != b.0 && a.1 == b.1 && a.2 == b.2 && a.3 == b.3 && a.4 < b.4 { + edges.insert((a.0, b.0)); + } + } + } + let mut indegree = vec![0; targets.len()]; + for &(_, target) in &edges { + indegree[target] += 1; + } + let mut selected = vec![false; targets.len()]; + let mut order = Vec::with_capacity(targets.len()); + while let Some(next) = (0..targets.len()).find(|&i| !selected[i] && indegree[i] == 0) { + selected[next] = true; + order.push(next); + for &(source, target) in &edges { + if source == next { + indegree[target] -= 1; + } + } + } + if order.len() != targets.len() { + return (0..targets.len()).collect(); + } + order +} + +fn order_connections_by_pin_access<'a>( + vars: &mut Vec<&'a RoutingConnection>, + candidates: &[LayoutCandidate], + placed: &[PlacedModule], +) { + let mut drivers = Vec::new(); + let mut groups: Vec> = Vec::new(); + for &var in vars.iter() { + let index = drivers + .iter() + .position(|driver| driver == &var.source) + .unwrap_or_else(|| { + drivers.push(var.source.clone()); + groups.push(Vec::new()); + drivers.len() - 1 + }); + groups[index].push(var); + } + let targets = groups + .iter() + .map(|group| group.iter().map(|var| var.target.clone()).collect()) + .collect::>(); + let order = pin_access_group_order(&targets, candidates, placed); + *vars = order + .into_iter() + .flat_map(|index| groups[index].iter().copied()) + .collect(); +} + pub(crate) fn ordered_module_variables( vars: &[RoutingConnection], strategy: NetOrderStrategy, ) -> Vec<&RoutingConnection> { let mut ordered = vars.iter().collect::>(); match strategy { - NetOrderStrategy::Criticality => { + NetOrderStrategy::Criticality | NetOrderStrategy::PinAccess => { ordered.sort_by_key(|var| route_variable_priority(var)); } NetOrderStrategy::ReverseCriticality => { @@ -772,6 +1066,7 @@ fn route_internal_module_nets( candidates: &[LayoutCandidate], placed_modules: &[PlacedModule], config: &GlobalRoutingConfig, + search_context: RoutingSearchContext<'_>, progress: &GlobalPnrProgress, route_world: &mut World3D, routes: &mut Vec, @@ -821,7 +1116,13 @@ fn route_internal_module_nets( .iter() .copied() .chain([logical_source]) - .filter_map(|position| powered_route_source(route_world, position)) + .filter_map(|position| { + powered_route_source(route_world, position).map(|mut source| { + source.strength = + port_source_strength(route_world, source_port, logical_source, position); + source + }) + }) .collect::>(); if route_sources.is_empty() { route_sources.push(PoweredRouteSource { @@ -868,15 +1169,18 @@ fn route_internal_module_nets( &source_positions, sink, &all_sinks, - config.strategy, + search_context, ) } else { - route_to_target_from_powered_network( + route_internal_fanout_with_source_fallback( route_world, &route_sources, + source_port, + logical_source, + &source_positions, sink, &all_sinks, - config.strategy, + search_context, ) }; let (route, next_world) = match route_result { @@ -957,7 +1261,17 @@ fn route_internal_module_nets( if covered_sinks == 0 { sink_targets.remove(sink_index); } - prune_powered_route_sources(&mut route_sources, &mut route_source_set, &all_sinks, 0); + // Completed sinks no longer need a launch point nearby. Ranking + // against all_sinks can evict the only tap near an unfinished branch. + let remaining_sinks = sink_targets + .iter() + .map(|(_, sink)| *sink) + .collect::>(); + prune_powered_route_sources( + &mut route_sources, + &mut route_source_set, + &remaining_sinks, + ); *route_world = next_world; routes.push(route); routed_sinks += covered_sinks.max(1); @@ -992,6 +1306,7 @@ fn route_top_input_ports( candidates: &[LayoutCandidate], placed_modules: &[PlacedModule], config: &GlobalRoutingConfig, + search_context: RoutingSearchContext<'_>, progress: &GlobalPnrProgress, completed_inputs: &HashSet, route_world: &mut World3D, @@ -1044,6 +1359,13 @@ fn route_top_input_ports( let mut routed = false; let mut last_error = None; for input_source in input_sources { + if !sinks.iter().any(|sink| { + search_context + .for_sink(sink.position) + .preserves_reservations(route_world, &input_source.world) + }) { + continue; + } let mut candidate_world = route_world.clone(); let mut candidate_routes = routes.clone(); match route_top_input_fanout( @@ -1053,6 +1375,7 @@ fn route_top_input_ports( input_source, sinks.clone(), config, + search_context, progress, &mut candidate_world, &mut candidate_routes, @@ -1090,6 +1413,7 @@ fn route_top_input_fanout( input_source: ExternalInputSource, mut sinks: Vec, config: &GlobalRoutingConfig, + search_context: RoutingSearchContext<'_>, progress: &GlobalPnrProgress, route_world: &mut World3D, routes: &mut Vec, @@ -1141,7 +1465,7 @@ fn route_top_input_fanout( &route_sources, sink, &all_sinks, - config.strategy, + search_context, ); let (mut route, next_world) = match route_result { Ok(route) => route, @@ -1201,7 +1525,7 @@ fn route_top_input_fanout( if covered_sinks == 0 { sinks.remove(sink_index); } - prune_powered_route_sources(&mut route_sources, &mut route_source_set, &sinks, 0); + prune_powered_route_sources(&mut route_sources, &mut route_source_set, &sinks); *route_world = next_world; routes.push(route.with_labels(port_name, format!("{port_name}.sink"))); routed_sinks += covered_sinks.max(1); @@ -1254,14 +1578,15 @@ fn sort_internal_sink_targets_by_current_tree( }); } -fn route_top_input_to_target_from_network( +fn route_top_input_to_target_from_network<'a>( world: &World3D, logical_source: Position, sources: &[PoweredRouteSource], sink: ResolvedPortTarget, same_net_sinks: &[ResolvedPortTarget], - strategy: GlobalRoutingStrategy, + strategy: impl Into>, ) -> Result<(RoutedNet, World3D), RouteFailure> { + let strategy = strategy.into().for_sink(sink.position); let mut sources = sources.to_vec(); sources.sort_by_key(|source| source.position.manhattan_distance(&sink.position)); if !sink.requires_input_diode @@ -1333,17 +1658,18 @@ fn active_route_powers_sink(before: &World3D, after: &World3D, route: &RoutedNet required_positions_powered } -fn route_candidate_powers_sink( +fn route_candidate_powers_sink<'a>( before: &World3D, after: &World3D, route: &RoutedNet, - strategy: GlobalRoutingStrategy, + strategy: impl Into>, ) -> bool { + let strategy = strategy.into(); // DirectGreedy is the cheap global probe. Its complete routed world is // dynamically validated by global PnR, so simulating every tentative tap, // adapter, and source alternative here only multiplies rejection cost. matches!( - strategy, + strategy.strategy, GlobalRoutingStrategy::DirectGreedy { .. } | GlobalRoutingStrategy::GreedyBeam { .. } ) || active_route_powers_sink(before, after, route) } @@ -1570,15 +1896,16 @@ fn register_module_bit(module: &str, suffix: &str) -> Option { .and_then(|(_, bit)| bit.parse::().ok()) } -fn route_source_to_target_position( +fn route_source_to_target_position<'a>( world: &World3D, source_port: &PhysicalPort, logical_source: Position, route_source: Position, sink: ResolvedPortTarget, same_net_sinks: &[ResolvedPortTarget], - strategy: GlobalRoutingStrategy, + strategy: impl Into>, ) -> Result<(RoutedNet, World3D), RouteFailure> { + let strategy = strategy.into(); if source_port.direction == PhysicalPortDirection::Output && source_port.requires_output_diode() { return route_isolated_output_to_target_position( @@ -1596,18 +1923,26 @@ fn route_source_to_target_position( return route_to_target_from_network(world, &sources, sink, same_net_sinks, strategy); } - route_to_target_position(world, route_source, sink, same_net_sinks, strategy) + route_to_target_position_with_strength( + world, + route_source, + sink, + same_net_sinks, + strategy, + port_source_strength(world, source_port, logical_source, route_source), + ) } -fn route_source_to_target_from_access_points( +fn route_source_to_target_from_access_points<'a>( world: &World3D, source_port: &PhysicalPort, logical_source: Position, route_sources: &[Position], sink: ResolvedPortTarget, same_net_sinks: &[ResolvedPortTarget], - strategy: GlobalRoutingStrategy, + strategy: impl Into>, ) -> Result<(RoutedNet, World3D), RouteFailure> { + let strategy = strategy.into().for_sink(sink.position); let mut route_sources = route_sources.to_vec(); route_sources.push(logical_source); route_sources.sort_by_key(|source| source.manhattan_distance(&sink.position)); @@ -1637,7 +1972,7 @@ fn route_source_to_target_from_access_points( } else { vec![route_source, sink.position] }; - let strength = initial_signal_strength(world, route_source); + let strength = port_source_strength(world, source_port, logical_source, route_source); let route = RoutedNet::new(logical_source, sink.position, Vec::new(), path) .with_powered_taps(vec![PoweredRouteSource { position: sink.position, @@ -1671,17 +2006,38 @@ fn route_source_to_target_from_access_points( }) } -fn route_to_target_position( +fn route_to_target_position<'a>( world: &World3D, source: Position, sink: ResolvedPortTarget, same_net_sinks: &[ResolvedPortTarget], - strategy: GlobalRoutingStrategy, + strategy: impl Into>, ) -> Result<(RoutedNet, World3D), RouteFailure> { + route_to_target_position_with_strength( + world, + source, + sink, + same_net_sinks, + strategy, + initial_signal_strength(world, source), + ) +} + +fn route_to_target_position_with_strength<'a>( + world: &World3D, + source: Position, + sink: ResolvedPortTarget, + same_net_sinks: &[ResolvedPortTarget], + strategy: impl Into>, + source_strength: usize, +) -> Result<(RoutedNet, World3D), RouteFailure> { + let strategy = strategy.into(); if sink.requires_input_diode { - return route_to_redstone_input_through_repeater( + return route_to_redstone_input_through_repeater_from_route_source( world, source, + source, + source_strength, sink.position, sink.input_repeater_delay, sink.preferred_input_repeater, @@ -1698,13 +2054,16 @@ fn route_to_target_position( allowed_contacts.sort_unstable(); allowed_contacts.dedup(); for route_target in route_targets { - let Ok((mut route, routed_world)) = route_point_to_point_with_strategy_and_allowed_contacts( - world, - source, - route_target, - strategy, - allowed_contacts.clone(), - ) else { + let Ok((mut route, routed_world)) = + route_point_to_point_with_strategy_and_allowed_contacts_and_initial_strength( + world, + source, + route_target, + strategy, + allowed_contacts.clone(), + source_strength, + ) + else { continue; }; route.sink = logical_sink; @@ -1730,15 +2089,16 @@ fn sink_route_access_positions(world: &World3D, sink: Position) -> Vec positions } -fn route_logical_source_to_target_position_with_strength( +fn route_logical_source_to_target_position_with_strength<'a>( world: &World3D, logical_source: Position, route_source: Position, route_source_strength: usize, sink: ResolvedPortTarget, same_net_sinks: &[ResolvedPortTarget], - strategy: GlobalRoutingStrategy, + strategy: impl Into>, ) -> Result<(RoutedNet, World3D), RouteFailure> { + let strategy = strategy.into(); if sink.requires_input_diode { return route_to_redstone_input_through_repeater_from_route_source( world, @@ -1775,13 +2135,14 @@ fn route_logical_source_to_target_position_with_strength( }) } -fn route_to_target_from_network( +fn route_to_target_from_network<'a>( world: &World3D, sources: &[Position], sink: ResolvedPortTarget, same_net_sinks: &[ResolvedPortTarget], - strategy: GlobalRoutingStrategy, + strategy: impl Into>, ) -> Result<(RoutedNet, World3D), RouteFailure> { + let strategy = strategy.into(); let mut sources = sources.to_vec(); sources.sort_by_key(|source| source.manhattan_distance(&sink.position)); let fallback_source = sources.first().copied().unwrap_or(sink.position); @@ -1805,13 +2166,43 @@ fn route_to_target_from_network( }) } -fn route_to_target_from_powered_network( +/// Pruned taps are a fast path, not a replacement for the physical driver's +/// access contract. A later branch may need to start again at the original +/// output, including its isolation adapter, even after nearby taps are caged. +fn route_internal_fanout_with_source_fallback<'a>( world: &World3D, sources: &[PoweredRouteSource], + source_port: &PhysicalPort, + logical_source: Position, + access_points: &[Position], sink: ResolvedPortTarget, same_net_sinks: &[ResolvedPortTarget], - strategy: GlobalRoutingStrategy, + strategy: impl Into>, +) -> Result<(RoutedNet, World3D), RouteFailure> { + let strategy = strategy.into(); + route_to_target_from_powered_network(world, sources, sink, same_net_sinks, strategy).or_else( + |_| { + route_source_to_target_from_access_points( + world, + source_port, + logical_source, + access_points, + sink, + same_net_sinks, + strategy, + ) + }, + ) +} + +fn route_to_target_from_powered_network<'a>( + world: &World3D, + sources: &[PoweredRouteSource], + sink: ResolvedPortTarget, + same_net_sinks: &[ResolvedPortTarget], + strategy: impl Into>, ) -> Result<(RoutedNet, World3D), RouteFailure> { + let strategy = strategy.into().for_sink(sink.position); let mut sources = sources.to_vec(); sources.sort_by_key(|source| source.position.manhattan_distance(&sink.position)); let fallback_source = sources @@ -1903,7 +2294,6 @@ fn prune_powered_route_sources( route_sources: &mut Vec, route_source_set: &mut HashSet, sinks: &[ResolvedPortTarget], - next_sink_index: usize, ) { if route_sources.len() <= FANOUT_ROUTE_TERMINAL_LIMIT { return; @@ -1912,7 +2302,6 @@ fn prune_powered_route_sources( route_sources.sort_by_key(|source| { let nearest_remaining_sink = sinks .iter() - .skip(next_sink_index) .map(|sink| source.position.manhattan_distance(&sink.position)) .min() .unwrap_or(0); @@ -1930,14 +2319,15 @@ fn prune_powered_route_sources( route_source_set.extend(route_sources.iter().map(|source| source.position)); } -fn route_isolated_output_to_target_position( +fn route_isolated_output_to_target_position<'a>( world: &World3D, logical_source: Position, route_source: Position, sink: ResolvedPortTarget, same_net_sinks: &[ResolvedPortTarget], - strategy: GlobalRoutingStrategy, + strategy: impl Into>, ) -> Result<(RoutedNet, World3D), RouteFailure> { + let strategy = strategy.into(); let same_net_contacts = same_net_contact_positions(same_net_sinks); if sink.requires_input_diode { for adapter in redstone_input_repeater_adapters( @@ -1947,6 +2337,9 @@ fn route_isolated_output_to_target_position( sink.preferred_input_repeater, &same_net_contacts, ) { + if !strategy.preserves_reservations(world, &adapter.world) { + continue; + } let Ok((route, routed_world)) = route_direct_output_to_point( &adapter.world, logical_source, @@ -1972,11 +2365,13 @@ fn route_isolated_output_to_target_position( adapter.target )); let powered_taps = route.powered_route_sources(); + let mut path = route.path; + path.extend([adapter.repeater, adapter.target]); let route = RoutedNet::new( logical_source, adapter.target, added_route_blocks(world, &routed_world), - route.path, + path, ) .with_required_powered_positions(vec![adapter.driver, adapter.repeater, adapter.target]) .with_required_released_positions(vec![adapter.driver, adapter.repeater]) @@ -1997,29 +2392,7 @@ fn route_isolated_output_to_target_position( ) } -fn route_to_redstone_input_through_repeater( - world: &World3D, - source: Position, - sink: Position, - input_repeater_delay: usize, - preferred_input_repeater: Option, - same_net_sinks: &[ResolvedPortTarget], - strategy: GlobalRoutingStrategy, -) -> Result<(RoutedNet, World3D), RouteFailure> { - route_to_redstone_input_through_repeater_from_route_source( - world, - source, - source, - initial_signal_strength(world, source), - sink, - input_repeater_delay, - preferred_input_repeater, - same_net_sinks, - strategy, - ) -} - -fn route_to_redstone_input_through_repeater_from_route_source( +fn route_to_redstone_input_through_repeater_from_route_source<'a>( world: &World3D, logical_source: Position, route_source: Position, @@ -2028,16 +2401,25 @@ fn route_to_redstone_input_through_repeater_from_route_source( input_repeater_delay: usize, preferred_input_repeater: Option, same_net_sinks: &[ResolvedPortTarget], - strategy: GlobalRoutingStrategy, + strategy: impl Into>, ) -> Result<(RoutedNet, World3D), RouteFailure> { + let strategy = strategy.into(); let same_net_contacts = same_net_contact_positions(same_net_sinks); - for adapter in redstone_input_repeater_adapters( + let adapters = redstone_input_repeater_adapters( world, sink, input_repeater_delay, preferred_input_repeater, &same_net_contacts, - ) { + ); + let adapter_count = adapters.len(); + let mut path_failures = 0; + let mut driver_failures = 0; + let mut terminal_failures = 0; + for adapter in adapters { + if !strategy.preserves_reservations(world, &adapter.world) { + continue; + } let Ok((route, routed_world)) = route_point_to_point_with_strategy_and_allowed_contacts_and_initial_strength( &adapter.world, @@ -2048,6 +2430,7 @@ fn route_to_redstone_input_through_repeater_from_route_source( route_source_strength, ) else { + path_failures += 1; continue; }; let driver_route = RoutedNet::new( @@ -2057,6 +2440,7 @@ fn route_to_redstone_input_through_repeater_from_route_source( route.path.clone(), ); if !route_candidate_powers_sink(world, &routed_world, &driver_route, strategy) { + driver_failures += 1; continue; } debug_assert!(detailed_router::target_powers_position( @@ -2075,15 +2459,16 @@ fn route_to_redstone_input_through_repeater_from_route_source( }); powered_taps.sort_by_key(|tap| tap.position); powered_taps.dedup_by_key(|tap| tap.position); - let mut required_powered_positions = vec![adapter.driver, adapter.repeater, adapter.target]; - required_powered_positions.extend(same_net_sinks.iter().map(|sink| sink.position)); - required_powered_positions.sort_unstable(); - required_powered_positions.dedup(); + // Other sinks are legal contacts for this net, but may not have been + // routed yet. Only this branch's terminals are power obligations. + let required_powered_positions = vec![adapter.driver, adapter.repeater, adapter.target]; + let mut path = route.path; + path.extend([adapter.repeater, adapter.target]); let route = RoutedNet::new( logical_source, adapter.target, added_route_blocks(world, &routed_world), - route.path, + path, ) .with_required_powered_positions(required_powered_positions) .with_required_released_positions(vec![adapter.driver, adapter.repeater]) @@ -2091,22 +2476,26 @@ fn route_to_redstone_input_through_repeater_from_route_source( if route_candidate_powers_sink(world, &routed_world, &route, strategy) { return Ok((route, routed_world)); } + terminal_failures += 1; } + tracing::debug!(target: "redstone_compiler::global_pnr", ?logical_source, ?route_source, ?sink, adapter_count, path_failures, driver_failures, terminal_failures, "input diode routing exhausted"); + Err(RouteFailure::Unreachable { source: logical_source, sink, }) } -fn route_direct_output_to_point( +fn route_direct_output_to_point<'a>( world: &World3D, logical_source: Position, route_source: Position, sink: Position, - strategy: GlobalRoutingStrategy, + strategy: impl Into>, additional_allowed_contacts: Vec, ) -> Result<(RoutedNet, World3D), RouteFailure> { + let strategy = strategy.into(); let source_node = PlacedNode::new(route_source, world[route_source]); let initial = RouteSearchState { world: world.clone(), @@ -2223,6 +2612,14 @@ fn input_repeater_adapter_world( input_repeater_delay: usize, additional_allowed_contacts: &[Position], ) -> Option { + // Powering a solid input intentionally changes the torches attached to + // it. Those consumers belong to this terminal, rather than a foreign net. + let mut terminal_contacts = additional_allowed_contacts.to_vec(); + terminal_contacts.extend(sink.forwards().into_iter().filter(|position| { + world.size.bound_on(*position) + && world[*position].kind.is_torch() + && position.walk(world[*position].direction) == Some(sink) + })); if !world.size.bound_on(repeater_position) || !world[repeater_position].kind.is_air() { return None; } @@ -2254,7 +2651,7 @@ fn input_repeater_adapter_world( world, &adapter_world, repeater_position, - &adapter_allowed_contacts(additional_allowed_contacts, &[sink, repeater_position]), + &adapter_allowed_contacts(&terminal_contacts, &[sink, repeater_position]), ) { return None; } @@ -2268,7 +2665,7 @@ fn input_repeater_adapter_world( &adapter_world, driver_position, &adapter_allowed_contacts( - additional_allowed_contacts, + &terminal_contacts, &[sink, repeater_position, driver_position], ), ) { @@ -2465,8 +2862,12 @@ fn external_input_sources( preferred .into_iter() - .chain(embedded) - .chain(compact) + .take(8) + .chain(embedded.into_iter().take(4)) + .chain(compact.into_iter().take(4)) + // Keep an independent source away from the cells. Preferred former + // switch sites can occupy a sink's only available diode adapter. + .chain(build_external_input_source(world, position)) .chain( selected .into_iter() @@ -3039,13 +3440,14 @@ pub fn route_point_to_point_with_strategy( ) } -pub(super) fn route_point_to_point_with_strategy_and_allowed_contacts( +pub(super) fn route_point_to_point_with_strategy_and_allowed_contacts<'a>( world: &World3D, source: Position, sink: Position, - strategy: GlobalRoutingStrategy, + strategy: impl Into>, additional_allowed_contacts: Vec, ) -> Result<(RoutedNet, World3D), RouteFailure> { + let strategy = strategy.into(); route_point_to_point_with_strategy_and_allowed_contacts_and_initial_strength( world, source, @@ -3056,14 +3458,15 @@ pub(super) fn route_point_to_point_with_strategy_and_allowed_contacts( ) } -fn route_point_to_point_with_strategy_and_allowed_contacts_and_initial_strength( +fn route_point_to_point_with_strategy_and_allowed_contacts_and_initial_strength<'a>( world: &World3D, source: Position, sink: Position, - strategy: GlobalRoutingStrategy, + strategy: impl Into>, additional_allowed_contacts: Vec, initial_strength: usize, ) -> Result<(RoutedNet, World3D), RouteFailure> { + let strategy = strategy.into(); let goal = RouteGoal::for_sink(world, sink); route_point_to_point_with_bounds_and_initial_strength( world, @@ -3091,20 +3494,22 @@ fn route_point_to_point_with_strategy_and_allowed_contacts_and_initial_strength( }) } -fn route_isolated_output_to_point( +fn route_isolated_output_to_point<'a>( world: &World3D, logical_source: Position, route_source: Position, sink: Position, - strategy: GlobalRoutingStrategy, + strategy: impl Into>, additional_allowed_contacts: Vec, ) -> Result<(RoutedNet, World3D), RouteFailure> { + let strategy = strategy.into(); let initial_states = isolated_output_repeater_initial_states( world, logical_source, route_source, sink, &additional_allowed_contacts, + strategy, ); for initial in initial_states { let Ok((route, routed_world)) = route_point_to_point_from_initial_state( @@ -3240,6 +3645,7 @@ fn isolated_output_repeater_initial_states( route_source: Position, sink: Position, additional_allowed_contacts: &[Position], + context: RoutingSearchContext<'_>, ) -> Vec { let mut seeds = vec![(route_source, initial_signal_strength(world, route_source))]; seeds.extend(routeable_output_taps(world, route_source, sink)); @@ -3269,6 +3675,9 @@ fn isolated_output_repeater_initial_states( for (adapter_world, driver_position) in output_repeater_adapters(world, seed, &output_allowed_contacts) { + if !context.preserves_reservations(world, &adapter_world) { + continue; + } states.push(RouteSearchState { world: adapter_world, terminal: driver_position, @@ -3287,6 +3696,7 @@ fn isolated_output_repeater_initial_states( seed, sink, &output_allowed_contacts, + context, )); } @@ -3298,6 +3708,7 @@ fn escaped_output_repeater_initial_states( seed: Position, sink: Position, output_allowed_contacts: &[Position], + context: RoutingSearchContext<'_>, ) -> Vec { let goal = RouteGoal::for_sink(world, sink); let forbidden_signal_contacts = @@ -3326,6 +3737,9 @@ fn escaped_output_repeater_initial_states( for (adapter_world, driver_position) in output_repeater_adapters(&state.world, state.terminal, output_allowed_contacts) { + if !context.preserves_reservations(&state.world, &adapter_world) { + continue; + } let mut route = state.route.clone(); route.push(driver_position); let mut powered_taps = state.powered_taps.clone(); @@ -3352,17 +3766,26 @@ fn escaped_output_repeater_initial_states( for bound in route_bounds_for_mode(BoundSearchMode::Propagation, &state.world, &terminal_node) { + if !context.allows_placement(bound.position()) + || bound + .position() + .down() + .is_some_and(|p| !context.allows_placement(p)) + { + continue; + } if !bound.is_bound_on(&state.world) || goal.rejects_bound_position(bound.position()) { continue; } let PlaceRedstoneResult::Placed(next_world, redstone_node) = - detailed_router::place_redstone_with_cobble_and_allowed_shorts( + detailed_router::place_redstone_with_cobble_and_policy( &state.world, bound, state.terminal, sink, Some(&allowed_shorts), + detailed_router::SupportReusePolicy::ReuseExisting, ) else { continue; @@ -3400,6 +3823,28 @@ fn escaped_output_repeater_initial_states( states } +/// Reserve a cell's output escape before global routes occupy its first hop. +pub(super) fn materialize_output_diode( + world: &World3D, + source: Position, + output_contacts: &[Position], +) -> Option<(World3D, Position)> { + let mut taps = vec![source]; + taps.extend(output_contacts.iter().copied()); + taps.extend( + routeable_output_taps(world, source, source) + .into_iter() + .map(|(p, _)| p), + ); + let mut seen = HashSet::new(); + taps.retain(|tap| seen.insert(*tap)); + taps.into_iter().find_map(|tap| { + output_repeater_adapters(world, tap, output_contacts) + .into_iter() + .next() + }) +} + fn output_repeater_adapters( world: &World3D, source: Position, @@ -3538,17 +3983,18 @@ enum BoundSearchMode { Nearby, } -fn route_point_to_point_with_bounds_and_initial_strength( +fn route_point_to_point_with_bounds_and_initial_strength<'a>( world: &World3D, source: Position, sink: Position, goal: RouteGoal, mode: BoundSearchMode, min_z: Option, - strategy: GlobalRoutingStrategy, + strategy: impl Into>, additional_allowed_contacts: &[Position], initial_strength: usize, ) -> Result<(RoutedNet, World3D), RouteFailure> { + let strategy = strategy.into(); route_point_to_point_with_initial_queue( world, source, @@ -3572,14 +4018,15 @@ fn route_point_to_point_with_bounds_and_initial_strength( ) } -fn route_point_to_point_from_initial_state( +fn route_point_to_point_from_initial_state<'a>( world: &World3D, source: Position, sink: Position, initial_state: RouteSearchState, - strategy: GlobalRoutingStrategy, + strategy: impl Into>, additional_allowed_contacts: &[Position], ) -> Result<(RoutedNet, World3D), RouteFailure> { + let strategy = strategy.into(); let goal = RouteGoal::for_sink(world, sink); route_point_to_point_with_initial_queue( world, @@ -3607,7 +4054,7 @@ fn route_point_to_point_from_initial_state( }) } -fn route_point_to_point_with_initial_queue( +fn route_point_to_point_with_initial_queue<'a>( original_world: &World3D, source: Position, sink: Position, @@ -3615,14 +4062,25 @@ fn route_point_to_point_with_initial_queue( mode: BoundSearchMode, min_z: Option, initial_states: Vec, - strategy: GlobalRoutingStrategy, + strategy: impl Into>, additional_allowed_contacts: &[Position], ) -> Result<(RoutedNet, World3D), RouteFailure> { + let strategy = strategy.into(); + let diagnostics = std::env::var_os("PNR_ROUTE_DIAGNOSTICS").is_some(); + let initial_states = initial_states + .into_iter() + .filter(|state| strategy.preserves_reservations(original_world, &state.world)) + .collect::>(); + let initial_count = initial_states.len(); + let mut rejected = std::collections::BTreeMap::::new(); + let mut enqueued = 0usize; + let mut nearest_distance = usize::MAX; + let mut limit_hit = false; let initial_visited = initial_states .iter() - .map(|state| route_visited_key(strategy, state)); + .map(|state| route_visited_key(strategy.strategy, state)); let mut visited = initial_visited.collect::>(); - let mut queue = RouteSearchQueue::new(strategy, sink, initial_states); + let mut queue = RouteSearchQueue::new(strategy.strategy, sink, initial_states); let forbidden_signal_contacts = route_forbidden_signal_contact_positions( original_world, source, @@ -3633,9 +4091,11 @@ fn route_point_to_point_with_initial_queue( while let Some(state) = queue.pop() { expansions += 1; - if route_expansion_limit(strategy).is_some_and(|limit| expansions > limit) { + if route_expansion_limit(strategy.strategy).is_some_and(|limit| expansions > limit) { + limit_hit = true; break; } + nearest_distance = nearest_distance.min(state.terminal.manhattan_distance(&sink)); if !is_route_terminal(&state.world, state.terminal) { continue; @@ -3649,7 +4109,7 @@ fn route_point_to_point_with_initial_queue( )); } - if state.route.len() > GLOBAL_ROUTE_MAX_STEPS { + if state.route.len() > strategy.max_route_steps { continue; } @@ -3668,6 +4128,19 @@ fn route_point_to_point_with_initial_queue( ) }); for bound in bounds { + if !strategy.allows_placement(bound.position()) + || bound + .position() + .down() + .is_some_and(|support| !strategy.allows_placement(support)) + { + if diagnostics { + *rejected + .entry("reserved_construction_cell".into()) + .or_default() += 1; + } + continue; + } if !bound.is_bound_on(&state.world) || goal.rejects_bound_position(bound.position()) { continue; } @@ -3678,12 +4151,13 @@ fn route_point_to_point_with_initial_queue( } if state.signal_strength > 1 { - match detailed_router::place_redstone_with_cobble_and_allowed_shorts( + match detailed_router::place_redstone_with_cobble_and_policy( &state.world, bound, state.terminal, goal.placement_target(), allowed_shorts.as_ref(), + detailed_router::SupportReusePolicy::ReuseExisting, ) { PlaceRedstoneResult::Placed(next_world, redstone_node) => { if route_touches_forbidden_existing_signal( @@ -3691,12 +4165,17 @@ fn route_point_to_point_with_initial_queue( redstone_node.position, &forbidden_signal_contacts, ) { + if diagnostics { + *rejected + .entry("dust:forbidden_signal_contact".into()) + .or_default() += 1; + } continue; } let next_strength = state.signal_strength - 1; if visited.insert(( redstone_node.position, - route_visited_depth(strategy, state.route.len()), + route_visited_depth(strategy.strategy, state.route.len()), next_strength, )) { let mut next_route = state.route.clone(); @@ -3706,6 +4185,7 @@ fn route_point_to_point_with_initial_queue( position: redstone_node.position, strength: next_strength, }); + enqueued += 1; queue.push(RouteSearchState { world: next_world, terminal: redstone_node.position, @@ -3716,22 +4196,30 @@ fn route_point_to_point_with_initial_queue( }); } } - PlaceRedstoneResult::Rejected(_) => {} + PlaceRedstoneResult::Rejected(reason) => { + if diagnostics { + *rejected.entry(format!("dust:{reason:?}")).or_default() += 1; + } + } } } - if state.signal_strength <= 2 { + // A producer may need directional isolation immediately: dust + // beside its output can backfeed the compact cell's input nets. + // Signal restoration is not the only reason to use a repeater. + if state.signal_strength <= 2 || state.route.len() == 1 { for direction in Direction::iter_direction_without_top() .into_iter() .filter(|direction| direction.is_cardinal()) { - match detailed_router::place_repeater_with_cobble( + match detailed_router::place_repeater_with_cobble_and_policy( &state.world, bound, state.terminal, goal.placement_target(), direction, allowed_shorts.as_ref(), + detailed_router::SupportReusePolicy::ReuseExisting, ) { PlaceRepeaterResult::Placed(next_world, repeater_node) => { if route_touches_forbidden_existing_signal( @@ -3739,11 +4227,16 @@ fn route_point_to_point_with_initial_queue( repeater_node.position, &forbidden_signal_contacts, ) { + if diagnostics { + *rejected + .entry("repeater:forbidden_signal_contact".into()) + .or_default() += 1; + } continue; } if visited.insert(( repeater_node.position, - route_visited_depth(strategy, state.route.len()), + route_visited_depth(strategy.strategy, state.route.len()), MAX_REDSTONE_STRENGTH, )) { let mut next_route = state.route.clone(); @@ -3753,6 +4246,7 @@ fn route_point_to_point_with_initial_queue( position: repeater_node.position, strength: MAX_REDSTONE_STRENGTH, }); + enqueued += 1; queue.push(RouteSearchState { world: next_world, terminal: repeater_node.position, @@ -3763,13 +4257,31 @@ fn route_point_to_point_with_initial_queue( }); } } - PlaceRepeaterResult::Rejected(_) => {} + PlaceRepeaterResult::Rejected(reason) => { + if diagnostics { + *rejected.entry(format!("repeater:{reason:?}")).or_default() += 1; + } + } } } } } } + if diagnostics { + if expansions == 1 && enqueued == 0 && source.manhattan_distance(&sink) <= 8 { + let source_neighborhood = original_world + .iter_block() + .into_iter() + .filter(|(position, _)| position.manhattan_distance(&source) <= 3) + .collect::>(); + tracing::debug!(target: "redstone_compiler::global_pnr", ?source, ?sink, ?source_neighborhood, + "detailed route source has no legal extension"); + } + tracing::debug!(target: "redstone_compiler::global_pnr", ?source, ?sink, ?mode, strategy=?strategy.strategy, + initial_count, expansions, enqueued, nearest_distance, limit_hit, ?rejected, + "detailed route search exhausted"); + } Err(RouteFailure::Unreachable { source, sink }) } @@ -3861,6 +4373,7 @@ fn route_expansion_limit(strategy: GlobalRoutingStrategy) -> Option { match strategy { GlobalRoutingStrategy::BreadthFirst => None, GlobalRoutingStrategy::AStar => Some(GLOBAL_ROUTE_ASTAR_MAX_EXPANSIONS), + GlobalRoutingStrategy::AStarBudgeted { max_expansions } => Some(max_expansions), GlobalRoutingStrategy::DirectGreedy { max_steps } => Some(max_steps), GlobalRoutingStrategy::GreedyBeam { max_expansions, .. } => Some(max_expansions), } @@ -3891,6 +4404,7 @@ fn route_visited_depth(strategy: GlobalRoutingStrategy, route_depth: usize) -> u match strategy { GlobalRoutingStrategy::BreadthFirst => route_depth, GlobalRoutingStrategy::AStar + | GlobalRoutingStrategy::AStarBudgeted { .. } | GlobalRoutingStrategy::DirectGreedy { .. } | GlobalRoutingStrategy::GreedyBeam { .. } => 0, } @@ -3925,7 +4439,7 @@ impl RouteSearchQueue { ) -> Self { match strategy { GlobalRoutingStrategy::BreadthFirst => Self::BreadthFirst(initial_states.into()), - GlobalRoutingStrategy::AStar => { + GlobalRoutingStrategy::AStar | GlobalRoutingStrategy::AStarBudgeted { .. } => { let mut queue = Self::AStar { heap: BinaryHeap::new(), next_sequence: 0, @@ -4116,6 +4630,23 @@ impl AStarPriority { } } +fn port_source_strength( + world: &World3D, + port: &PhysicalPort, + logical_source: Position, + source: Position, +) -> usize { + if source == logical_source + && port.direction == PhysicalPortDirection::Output + && port.connection + == crate::transform::place_and_route::global_pnr::ir::PortConnection::RestoredDirect + { + MAX_REDSTONE_STRENGTH + } else { + initial_signal_strength(world, source) + } +} + fn initial_signal_strength(world: &World3D, source: Position) -> usize { match world[source].kind { BlockKind::Redstone { strength, .. } if strength > 0 => strength, @@ -4181,7 +4712,10 @@ impl RouteGoal { fn accepts(self, world: &World3D, redstone: Position) -> bool { match self { Self::ConnectPosition { target } => { - detailed_router::target_powers_position(world, target, redstone) + // The routed terminal drives the sink. Repeater connections + // are directional, so testing the reverse edge rejects valid + // final hops and can accept a connection facing away. + detailed_router::target_powers_position(world, redstone, target) } Self::PowerCobble { cobble } => { detailed_router::redstone_powers_cobble(world, redstone, cobble) @@ -4294,8 +4828,496 @@ fn added_route_blocks(before: &World3D, after: &World3D) -> Vec<(Position, Block #[cfg(test)] mod tests { use super::*; - use crate::ir::{NetClass, RoutablePortDirection}; - use crate::transform::place_and_route::global_pnr::ir::{ + + #[test] + fn reserved_port_headroom_keeps_two_independent_routes_functional() -> eyre::Result<()> { + let mut world = World3D::new(DimSize(8, 6, 6)); + for x in 0..8 { + for y in 0..6 { + world[Position(x, y, 0)] = cobble_block(); + } + } + let lower = Position(2, 2, 1); + let lower_sink = Position(4, 2, 2); + let upper = Position(2, 1, 3); + let upper_sink = Position(2, 3, 3); + let lower_switch = Position(0, 2, 1); + let upper_switch = Position(2, 0, 3); + for p in [ + Position(2, 1, 1), + Position(2, 1, 2), + Position(2, 3, 1), + Position(2, 3, 2), + Position(3, 2, 1), + Position(4, 2, 1), + Position(3, 1, 3), + Position(3, 3, 3), + ] { + world[p] = cobble_block(); + } + world[Position(1, 2, 1)] = + PlacedNode::new_repeater(Position(1, 2, 1), Direction::West).block; + for p in [lower, lower_sink, upper, upper_sink] { + world[p] = redstone_block(); + } + world[lower_switch] = Block { + kind: BlockKind::Switch { is_on: false }, + direction: Direction::West, + }; + world[upper_switch] = Block { + kind: BlockKind::Switch { is_on: false }, + direction: Direction::North, + }; + world.initialize_redstone_states(); + let headroom = lower.up(); + let (_, unreserved) = route_point_to_point_with_strategy( + &world, + upper, + upper_sink, + GlobalRoutingStrategy::AStar, + ) + .unwrap(); + assert!( + unreserved[headroom].kind.is_cobble(), + "unreserved shortest route roofs the future port" + ); + assert!(route_point_to_point_with_strategy( + &unreserved, + lower, + lower_sink, + GlobalRoutingStrategy::AStar + ) + .is_err()); + let blocked = [headroom]; + let context = RoutingSearchContext { + strategy: GlobalRoutingStrategy::AStar, + blocked_positions: &blocked, + access_reservations: &[], + active_sink: None, + max_route_steps: GLOBAL_ROUTE_MAX_STEPS, + }; + let (_, upper_world) = route_point_to_point_with_strategy_and_allowed_contacts( + &world, + upper, + upper_sink, + context, + Vec::new(), + ) + .unwrap(); + assert!(upper_world[headroom].kind.is_air()); + let (_, complete) = route_point_to_point_with_strategy_and_allowed_contacts( + &upper_world, + lower, + lower_sink, + context, + Vec::new(), + ) + .unwrap(); + assert!(complete[headroom].kind.is_air()); + let mut simulator = + Simulator::from_settled_with_limits_and_trace(&World::from(&complete), 256, 50_000, 0)?; + for value in [0, 1, 3, 2, 0, 3, 0] { + simulator.drive_inputs_with_limits( + vec![ + (lower_switch, value & 1 != 0), + (upper_switch, value & 2 != 0), + ], + 256, + 50_000, + )?; + assert_eq!( + simulator.world()[lower_sink].kind.is_powered(), + value & 1 != 0, + "lower, value={value}" + ); + assert_eq!( + simulator.world()[upper_sink].kind.is_powered(), + value & 2 != 0, + "upper, value={value}" + ); + } + Ok(()) + } + + #[test] + fn owned_input_approach_preserves_future_route_and_allows_its_owner() -> eyre::Result<()> { + let mut world = World3D::new(DimSize(8, 6, 6)); + for x in 0..8 { + for y in 0..6 { + world[Position(x, y, 0)] = cobble_block(); + } + } + let lower = Position(2, 2, 1); + let lower_sink = Position(4, 2, 2); + let upper = Position(2, 1, 3); + let upper_sink = Position(2, 3, 3); + let lower_switch = Position(0, 2, 1); + let upper_switch = Position(2, 0, 3); + for p in [ + Position(2, 1, 1), + Position(2, 1, 2), + Position(2, 3, 1), + Position(2, 3, 2), + Position(3, 2, 1), + Position(4, 2, 1), + Position(3, 1, 3), + Position(3, 3, 3), + ] { + world[p] = cobble_block(); + } + world[Position(1, 2, 1)] = + PlacedNode::new_repeater(Position(1, 2, 1), Direction::West).block; + for p in [lower, lower_sink, upper, upper_sink] { + world[p] = redstone_block(); + } + world[lower_switch] = Block { + kind: BlockKind::Switch { is_on: false }, + direction: Direction::West, + }; + world[upper_switch] = Block { + kind: BlockKind::Switch { is_on: false }, + direction: Direction::North, + }; + // This future input reads westward into a consumer repeater. + world[Position(5, 2, 1)] = cobble_block(); + world[Position(5, 2, 2)] = + PlacedNode::new_repeater(Position(5, 2, 2), Direction::West).block; + world.initialize_redstone_states(); + let headroom = lower.up(); + let (_, unreserved) = route_point_to_point_with_strategy( + &world, + upper, + upper_sink, + GlobalRoutingStrategy::AStar, + ) + .unwrap(); + assert!( + unreserved[headroom].kind.is_cobble(), + "unreserved shortest route roofs the future port" + ); + assert!(route_point_to_point_with_strategy( + &unreserved, + lower, + lower_sink, + GlobalRoutingStrategy::AStar + ) + .is_err()); + let approach = Position(3, 2, 2); + let reservations = [ + (headroom, lower_sink), + (headroom.up(), lower_sink), + (approach, lower_sink), + (approach.up(), lower_sink), + ]; + let context = RoutingSearchContext { + strategy: GlobalRoutingStrategy::AStar, + blocked_positions: &[], + access_reservations: &reservations, + active_sink: None, + max_route_steps: GLOBAL_ROUTE_MAX_STEPS, + }; + let (_, upper_world) = route_point_to_point_with_strategy_and_allowed_contacts( + &world, + upper, + upper_sink, + context.for_sink(upper_sink), + Vec::new(), + ) + .unwrap(); + assert!(upper_world[headroom].kind.is_air()); + let (_, complete) = route_point_to_point_with_strategy_and_allowed_contacts( + &upper_world, + lower, + lower_sink, + context.for_sink(lower_sink), + Vec::new(), + ) + .unwrap(); + assert!(complete[headroom].kind.is_air()); + assert!( + !complete[approach].kind.is_air(), + "own route must be allowed to build in its reserved approach" + ); + let mut simulator = + Simulator::from_settled_with_limits_and_trace(&World::from(&complete), 256, 50_000, 0)?; + for value in [0, 1, 3, 2, 0, 3, 0] { + simulator.drive_inputs_with_limits( + vec![ + (lower_switch, value & 1 != 0), + (upper_switch, value & 2 != 0), + ], + 256, + 50_000, + )?; + assert_eq!( + simulator.world()[lower_sink].kind.is_powered(), + value & 1 != 0, + "lower, value={value}" + ); + assert_eq!( + simulator.world()[upper_sink].kind.is_powered(), + value & 2 != 0, + "upper, value={value}" + ); + } + Ok(()) + } + + #[test] + fn explicit_astar_budget_controls_search_without_changing_default_ordering() { + assert_eq!( + route_expansion_limit(GlobalRoutingStrategy::AStar), + Some(2_000) + ); + assert_eq!( + route_expansion_limit(GlobalRoutingStrategy::AStarBudgeted { + max_expansions: 20_000 + }), + Some(20_000) + ); + let source = Position(0, 1, 1); + let sink = Position(6, 1, 1); + let world = route_test_world_with_switch_source(source, sink, DimSize(10, 4, 3)); + assert!(route_point_to_point_with_strategy( + &world, + source, + sink, + GlobalRoutingStrategy::AStarBudgeted { max_expansions: 1 } + ) + .is_err()); + let (budgeted, budgeted_world) = route_point_to_point_with_strategy( + &world, + source, + sink, + GlobalRoutingStrategy::AStarBudgeted { + max_expansions: 2_000, + }, + ) + .unwrap(); + let (default, _) = + route_point_to_point_with_strategy(&world, source, sink, GlobalRoutingStrategy::AStar) + .unwrap(); + assert_eq!(budgeted.path, default.path); + assert_eq!(budgeted.blocks, default.blocks); + assert!(active_route_powers_sink(&world, &budgeted_world, &budgeted)); + } + + #[test] + fn connect_goal_accepts_repeater_driving_sink_in_forward_direction() { + let terminal = Position(3, 2, 1); + let sink = Position(4, 2, 1); + let mut world = World3D::new(DimSize(8, 5, 3)); + world[terminal.down().unwrap()] = cobble_block(); + world[sink.down().unwrap()] = cobble_block(); + world[sink] = redstone_block(); + let direction = [ + Direction::West, + Direction::East, + Direction::North, + Direction::South, + ] + .into_iter() + .find(|direction| { + world[terminal] = PlacedNode::new_repeater(terminal, *direction).block; + detailed_router::target_powers_position(&world, terminal, sink) + }) + .expect("one repeater orientation must drive the adjacent sink"); + world[terminal] = PlacedNode::new_repeater(terminal, direction).block; + assert!(detailed_router::target_powers_position( + &world, terminal, sink + )); + assert!(!detailed_router::target_powers_position( + &world, sink, terminal + )); + assert!( + RouteGoal::for_sink(&world, sink).accepts(&world, terminal), + "route goals must test terminal-to-sink power, not sink-to-terminal power" + ); + world[terminal] = PlacedNode::new_repeater(terminal, direction.inverse()).block; + assert!( + !RouteGoal::for_sink(&world, sink).accepts(&world, terminal), + "a repeater facing away from the sink must not complete the route" + ); + } + + #[test] + fn primitive_candidates_have_independently_routable_input_adapters() -> eyre::Result<()> { + use crate::graph::logic::LogicGraph; + use crate::graph::GraphNodeKind; + use crate::ir::{ + RoutableModule, RoutableModuleBody, RoutableNode, RoutableNodeKind, RoutablePort, + }; + use crate::logic::LogicType; + use crate::transform::place_and_route::global_pnr::candidate::{ + generate_routable_module_candidates_with_progress_label, LocalAdaptiveSearchConfig, + UnitCandidateConfig, + }; + use crate::transform::place_and_route::local_placer::LocalPlacerConfig; + use crate::transform::place_and_route::sampling::SamplingPolicy; + for expression in ["~a", "a&b", "a|b"] { + let graph = LogicGraph::from_stmt(expression, "y")?.prepare_place()?; + let mut ports = graph + .truth_table()? + .input_names + .into_iter() + .map(|name| RoutablePort { + name, + direction: RoutablePortDirection::Input, + }) + .collect::>(); + ports.push(RoutablePort { + name: "y".into(), + direction: RoutablePortDirection::Output, + }); + let nodes = graph + .nodes + .iter() + .map(|node| RoutableNode { + id: node.id, + inputs: node.inputs.clone(), + tag: String::new(), + kind: match &node.kind { + GraphNodeKind::Input(name) => { + RoutableNodeKind::Input { name: name.clone() } + } + GraphNodeKind::Output(name) => { + RoutableNodeKind::Output { name: name.clone() } + } + GraphNodeKind::Logic(logic) => match logic.logic_type { + LogicType::Not => RoutableNodeKind::Not, + LogicType::Or => RoutableNodeKind::Or, + other => panic!("unexpected prepared primitive {other:?}"), + }, + other => panic!("unexpected primitive node {other:?}"), + }, + }) + .collect(); + let module = RoutableModule { + name: "primitive-access".into(), + ports, + body: RoutableModuleBody::Leaf { nodes }, + }; + let config = UnitCandidateConfig { + dim: DimSize(8, 8, 4), + max_candidates: 8, + local_config: LocalPlacerConfig { + input_candidate_limit: Some(16), + step_sampling_policy: SamplingPolicy::Take(64), + max_not_route_step: 4, + max_route_step: 4, + not_route_step_sampling_policy: SamplingPolicy::Take(32), + route_step_sampling_policy: SamplingPolicy::Take(32), + ..Default::default() + }, + adaptive_search: LocalAdaptiveSearchConfig { + max_retries: 0, + ..Default::default() + }, + ..Default::default() + }; + let candidates = + generate_routable_module_candidates_with_progress_label(&module, &config, None)?; + assert!(!candidates.is_empty()); + let mut fully_accessible = 0; + for (index, candidate) in candidates.iter().enumerate() { + let offset = |p: Position| Position(p.0 + 4, p.1 + 4, p.2 + 1); + let mut world = World3D::new(DimSize(16, 16, 6)); + for (p, block) in candidate.world.iter_block() { + world[offset(p)] = block; + } + world.initialize_redstone_states(); + let mut summary = Vec::new(); + let mut all_accessible = true; + for port in candidate + .ports + .iter() + .filter(|p| p.direction == PhysicalPortDirection::Input) + { + let contacts = port + .routing_access_positions() + .into_iter() + .map(offset) + .collect::>(); + let preferred = port.route_position.map(offset); + let counts = contacts + .iter() + .map(|sink| { + redstone_input_repeater_adapters(&world, *sink, 1, preferred, &contacts) + .len() + }) + .collect::>(); + all_accessible &= counts.iter().all(|count| *count > 0); + summary.push(format!("{}:{:?}={counts:?}", port.name, contacts)); + } + fully_accessible += usize::from(all_accessible); + assert!( + all_accessible, + "primitive={expression} candidate={index}: {}", + summary.join(" ") + ); + } + assert_eq!(fully_accessible, candidates.len(), "primitive={expression}"); + } + Ok(()) + } + #[test] + fn input_adapter_drives_attached_not_torch_without_shorting_foreign_torch() -> eyre::Result<()> + { + let sink = Position(3, 3, 1); + let attached = Position(3, 3, 2); + let mut world = World3D::new(DimSize(8, 8, 4)); + world[sink] = cobble_block(); + world[attached] = Block { + kind: BlockKind::Torch { is_on: true }, + direction: Direction::Bottom, + }; + let adapters = redstone_input_repeater_adapters(&world, sink, 1, None, &[sink]); + assert_eq!( + adapters.len(), + 4, + "isolated NOT permits all four approaches" + ); + for adapter in adapters { + let mut simulator = Simulator::from(&World::from(&adapter.world))?; + for powered in [false, true, false] { + simulator.advance_idle_cycles(crate::world::simulator::MANUAL_INPUT_IDLE_CYCLES)?; + simulator.drive_inputs_with_limits(vec![(adapter.driver, powered)], 256, 50_000)?; + assert_eq!(simulator.world()[sink].kind.is_powered(), powered); + assert_eq!(simulator.world()[attached].kind.is_powered(), !powered); + assert!(!simulator.is_torch_burned_out(attached)); + } + } + let repeater = Position(2, 3, 1); + let direction = repeater.diff(sink).inverse(); + assert!( + input_repeater_adapter_world(&world, sink, repeater, direction, 1, &[sink]).is_some() + ); + // This torch has its own support and feeds the adapter's driver. + // It must not inherit the intended attached-torch exception. + let foreign = Position(1, 4, 1); + world[foreign.down().unwrap()] = cobble_block(); + world[foreign] = Block { + kind: BlockKind::Torch { is_on: true }, + direction: Direction::Bottom, + }; + assert!( + input_repeater_adapter_world(&world, sink, repeater, direction, 1, &[sink]).is_none() + ); + world[foreign] = Block::default(); + let remote = Position(6, 6, 1); + world[remote.down().unwrap()] = cobble_block(); + world[remote] = Block { + kind: BlockKind::Torch { is_on: true }, + direction: Direction::Bottom, + }; + assert!( + input_repeater_adapter_world(&world, sink, repeater, direction, 1, &[sink]).is_some() + ); + Ok(()) + } + + use crate::ir::{NetClass, RoutablePortDirection}; + use crate::transform::place_and_route::global_pnr::ir::{ LayoutCandidate, PhysicalPort, PhysicalPortDirection, PortConnection, }; use crate::transform::place_and_route::global_pnr::placer::{ @@ -4334,6 +5356,157 @@ mod tests { .unwrap() } + fn staggered_test_candidate() -> LayoutCandidate { + let mut world = World3D::new(DimSize(6, 7, 2)); + let ports = [ + ("z_deep", Position(1, 4, 1)), + ("a_outer", Position(4, 1, 1)), + ] + .into_iter() + .map(|(name, position)| { + world[position.down().unwrap()] = cobble_block(); + world[position] = redstone_block(); + let repeater = Position(position.0, position.1 + 1, position.2); + world[repeater.down().unwrap()] = cobble_block(); + world[repeater] = PlacedNode::new_repeater(repeater, Direction::South).block; + PhysicalPort { + name: name.to_owned(), + direction: PhysicalPortDirection::Input, + position, + route_position: None, + access_points: Vec::new(), + connection: PortConnection::Direct, + } + }) + .collect(); + LayoutCandidate::from_world("cell".into(), world, ports).unwrap() + } + + #[test] + fn pin_access_uses_geometry_despite_source_and_pin_name_order() { + let cell = staggered_test_candidate(); + let placed = vec![PlacedModule { + module_name: "cell".into(), + candidate_index: 0, + origin: Position(20, 30, 0), + bbox: cell.bbox, + }]; + for (outer_name, deep_name) in [("aaa", "zzz"), ("zzz", "aaa")] { + let vars = vec![ + RoutingConnection { + source: (outer_name.into(), "y".into()), + target: ("cell".into(), "a_outer".into()), + }, + RoutingConnection { + source: (deep_name.into(), "y".into()), + target: ("cell".into(), "z_deep".into()), + }, + RoutingConnection { + source: (deep_name.into(), "y".into()), + target: ("unknown".into(), "other_sink".into()), + }, + ]; + let mut ordered = vars.iter().collect(); + order_connections_by_pin_access(&mut ordered, &[cell.clone()], &placed); + assert_eq!(ordered.len(), 3); + assert_eq!( + ordered + .iter() + .map(|v| v.source.0.as_str()) + .collect::>(), + [deep_name, deep_name, outer_name] + ); + // The same group permutation is used by top-level input routing. + let targets = vars[..2] + .iter() + .map(|v| vec![v.target.clone()]) + .collect::>(); + assert_eq!( + pin_access_group_order(&targets, &[cell.clone()], &placed), + [1, 0] + ); + } + } + + #[test] + fn pin_access_cycle_preserves_all_groups_in_baseline_order() { + let cell = staggered_test_candidate(); + let placed = ["left", "right"] + .into_iter() + .map(|name| PlacedModule { + module_name: name.into(), + candidate_index: 0, + origin: Position(0, 0, 0), + bbox: cell.bbox, + }) + .collect::>(); + let targets = vec![ + vec![ + ("left".into(), "z_deep".into()), + ("right".into(), "a_outer".into()), + ], + vec![ + ("left".into(), "a_outer".into()), + ("right".into(), "z_deep".into()), + ], + vec![("unknown".into(), "unconstrained".into())], + ]; + assert_eq!( + pin_access_group_order(&targets, &[cell], &placed), + [0, 1, 2] + ); + } + + #[test] + fn routed_prefix_cannot_fill_translated_candidate_reservations() { + let mut cell = candidate( + "cell", + Position(0, 0, 1), + "in", + PhysicalPortDirection::Input, + ); + cell.blocked_cells.insert(Position(0, 0, 2)); + cell.include_reserved_cells_in_bounds(); + let placed = vec![PlacedModule { + module_name: "cell".to_owned(), + candidate_index: 0, + origin: Position(10, 20, 0), + bbox: cell.bbox, + }]; + let headroom = translate_candidate_position(Position(0, 0, 2), &cell, &placed[0]); + assert_eq!(headroom, Position(10, 20, 2)); + let prefix = vec![RoutedNet::new( + headroom, + headroom, + vec![(headroom, cobble_block())], + vec![headroom], + )]; + for top_inputs_last in [false, true] { + let config = GlobalRoutingConfig { + top_inputs_last, + ..Default::default() + }; + let failure = route_module_variables_with_order_from_prefix_impl( + None, + None, + &GlobalHeuristicHooks::default(), + &RoutingPlan::default(), + &[cell.clone()], + &placed, + &config, + NetOrderStrategy::Criticality, + &silent_progress(), + &prefix, + ) + .unwrap_err(); + assert!(failure + .error + .to_string() + .contains("reserved construction cell")); + assert!(failure.routed_nets.is_empty()); + } + } + #[test] fn resolve_port_targets_uses_exposed_route_position() { let mut candidates = vec![candidate( @@ -4477,6 +5650,74 @@ mod tests { .all(|(_, strength)| *strength <= MAX_REDSTONE_STRENGTH)); } + #[test] + fn input_diode_fanout_checks_only_connected_branch_until_other_sink_is_routed() { + let source = Position(0, 1, 1); + let first = Position(6, 1, 1); + let second = Position(6, 7, 1); + let mut world = route_test_world_with_switch_source(source, first, DimSize(10, 10, 3)); + world[second.down().unwrap()] = cobble_block(); + world[second] = redstone_block(); + world.initialize_redstone_states(); + let target = |position| ResolvedPortTarget { + position, + requires_input_diode: true, + input_repeater_delay: 1, + preferred_input_repeater: None, + }; + let sinks = [target(first), target(second)]; + let (first_route, first_world) = route_to_target_position( + &world, + source, + sinks[0], + &sinks, + GlobalRoutingStrategy::AStar, + ) + .expect("the first branch must not require the still-unrouted second sink to be powered"); + assert!(!first_route.required_powered_positions.contains(&second)); + assert!(active_route_powers_sink(&world, &first_world, &first_route)); + assert!( + route_required_positions_keep_route_contact(&first_world, &first_route), + "input diode's powered target must retain a contact witness in the recorded route path" + ); + let other = RoutedNet::new(source, second, Vec::new(), Vec::new()); + assert_eq!( + active_route_power_after_settle(&first_world, &other), + Some((true, false)) + ); + + let mut taps = first_route.powered_route_sources(); + taps.push(PoweredRouteSource { + position: source, + strength: MAX_REDSTONE_STRENGTH, + }); + let (second_route, complete_world) = route_top_input_to_target_from_network( + &first_world, + source, + &taps, + sinks[1], + &sinks, + GlobalRoutingStrategy::AStar, + ) + .expect("the remaining fanout branch must connect from the existing route tree"); + assert!(active_route_powers_sink( + &first_world, + &complete_world, + &second_route + )); + let complete = RoutedNet::new(source, second, Vec::new(), Vec::new()) + .with_required_powered_positions(vec![first, second]) + .with_required_released_positions(vec![first, second]); + assert_eq!( + active_route_power_after_settle(&complete_world, &complete), + Some((true, true)) + ); + assert!(switch_route_releases_required_positions_when_off( + &complete_world, + &complete + )); + } + #[test] fn input_diode_route_preserves_powered_taps_for_later_fanout() { let source = Position(0, 1, 1); @@ -4972,13 +6213,14 @@ mod tests { world[prev] = redstone_block(); world.initialize_redstone_states(); - let result = detailed_router::place_repeater_with_cobble( + let result = detailed_router::place_repeater_with_cobble_and_policy( &world, PlaceBound(PropagateType::Soft, repeater, Direction::East), prev, Position(19, 7, 5), Direction::East, None, + detailed_router::SupportReusePolicy::ReuseExisting, ); assert!(matches!( @@ -5002,13 +6244,14 @@ mod tests { direction: Direction::None, }; - let result = detailed_router::place_repeater_with_cobble( + let result = detailed_router::place_repeater_with_cobble_and_policy( &world, PlaceBound(PropagateType::Soft, repeater, Direction::South), prev, Position(2, 3, 1), Direction::East, None, + detailed_router::SupportReusePolicy::ReuseExisting, ); assert!(matches!( @@ -5520,6 +6763,227 @@ mod tests { Ok(()) } + #[test] + fn tap_budget_prioritizes_unfinished_sinks_over_completed_branch() { + let target = |position| ResolvedPortTarget { + position, + requires_input_diode: false, + input_repeater_delay: 1, + preferred_input_repeater: None, + }; + let completed = target(Position(0, 0, 1)); + let pending = target(Position(100, 0, 1)); + let useful = PoweredRouteSource { + position: Position(75, 0, 1), + strength: 7, + }; + let mut sources = (0..FANOUT_ROUTE_TERMINAL_LIMIT) + .map(|x| PoweredRouteSource { + position: Position(x, 0, 1), + strength: 15, + }) + .collect::>(); + sources.push(useful); + let mut stale = sources.clone(); + let mut stale_set = stale.iter().map(|s| s.position).collect(); + prune_powered_route_sources(&mut stale, &mut stale_set, &[completed, pending]); + assert!( + !stale_set.contains(&useful.position), + "completed-branch taps exhaust the old budget" + ); + let mut source_set = sources.iter().map(|s| s.position).collect(); + prune_powered_route_sources(&mut sources, &mut source_set, &[pending]); + assert_eq!(sources.len(), FANOUT_ROUTE_TERMINAL_LIMIT); + assert_eq!(source_set.len(), sources.len()); + assert_eq!( + sources[0], useful, + "retain the best launch point for the unfinished branch, with its original strength" + ); + assert!(sources + .iter() + .all(|source| source_set.contains(&source.position))); + } + + #[test] + fn restored_output_contract_is_scoped_and_reaches_initial_route_taps() { + let source = Position(1, 1, 1); + let sink = Position(20, 1, 1); + let world = route_test_world_with_size(source, sink, DimSize(24, 4, 3)); + let mut port = PhysicalPort { + name: "y".into(), + direction: PhysicalPortDirection::Output, + position: source, + route_position: None, + access_points: vec![source], + connection: PortConnection::Direct, + }; + assert_eq!(port_source_strength(&world, &port, source, source), 2); + let target = ResolvedPortTarget { + position: sink, + requires_input_diode: false, + input_repeater_delay: 1, + preferred_input_repeater: None, + }; + for (connection, expected) in [ + (PortConnection::Direct, 2), + (PortConnection::RestoredDirect, 15), + ] { + port.connection = connection; + let (route, _) = route_source_to_target_from_access_points( + &world, + &port, + source, + &[source], + target, + &[target], + GlobalRoutingStrategy::AStar, + ) + .unwrap(); + assert!( + route.powered_taps.contains(&(source, expected)), + "initial tap must carry only the declared source strength" + ); + } + assert_eq!( + port_source_strength(&world, &port, source, sink), + 2, + "access points do not inherit the output contract" + ); + port.direction = PhysicalPortDirection::Input; + assert_eq!( + port_source_strength(&world, &port, source, source), + 2, + "input ports cannot claim a restored output" + ); + assert_eq!( + serde_json::from_str::( + &serde_json::to_string(&PortConnection::RestoredDirect).unwrap() + ) + .unwrap(), + PortConnection::RestoredDirect + ); + } + + #[test] + fn internal_fanout_retains_original_driver_after_tap_pruning() -> eyre::Result<()> { + let mut world = World3D::new(DimSize(24, 7, 3)); + for x in 0..24 { + for y in 0..7 { + world[Position(x, y, 0)] = cobble_block(); + } + } + let switch = Position(0, 2, 1); + let original = Position(1, 2, 1); + let first_sink = Position(21, 2, 1); + let next_sink = Position(1, 5, 1); + world[switch] = Block { + kind: BlockKind::Switch { is_on: false }, + direction: Direction::West, + }; + for x in 1..=21 { + let p = Position(x, 2, 1); + world[p] = if x % 8 == 0 { + PlacedNode::new_repeater(p, Direction::West).block + } else { + redstone_block() + }; + } + world[next_sink] = redstone_block(); + world.initialize_redstone_states(); + // The retained tip is a genuine branch of the same switch, but cannot + // reach the second sink within this hop budget. The original driver can. + let taps = [PoweredRouteSource { + position: first_sink, + strength: 10, + }]; + let target = ResolvedPortTarget { + position: next_sink, + requires_input_diode: false, + input_repeater_delay: 1, + preferred_input_repeater: None, + }; + let context = RoutingSearchContext { + strategy: GlobalRoutingStrategy::AStar, + blocked_positions: &[], + access_reservations: &[], + active_sink: None, + max_route_steps: 8, + }; + assert!( + route_to_target_from_powered_network(&world, &taps, target, &[target], context) + .is_err() + ); + let port = PhysicalPort { + name: "y".into(), + direction: PhysicalPortDirection::Output, + position: original, + route_position: None, + access_points: Vec::new(), + connection: PortConnection::Direct, + }; + let (_, routed) = route_internal_fanout_with_source_fallback( + &world, + &taps, + &port, + original, + &[original], + target, + &[target], + context, + ) + .unwrap(); + let mut simulator = + Simulator::from_settled_with_limits_and_trace(&World::from(&routed), 256, 50_000, 0)?; + for on in [false, true, false, true, false] { + simulator.drive_inputs_with_limits(vec![(switch, on)], 256, 50_000)?; + assert_eq!(simulator.world()[first_sink].kind.is_powered(), on); + assert_eq!(simulator.world()[next_sink].kind.is_powered(), on); + } + Ok(()) + } + + #[test] + fn explicit_route_step_budget_connects_long_corridor_and_releases() -> eyre::Result<()> { + let source = Position(0, 0, 1); + let sink = Position(140, 0, 1); + let mut world = route_test_world_with_switch_source(source, sink, DimSize(141, 1, 2)); + for x in 0..141 { + world[Position(x, 0, 0)] = cobble_block(); + } + world.initialize_redstone_states(); + assert!(route_point_to_point_with_strategy( + &world, + source, + sink, + GlobalRoutingStrategy::AStar + ) + .is_err()); + let context = RoutingSearchContext { + strategy: GlobalRoutingStrategy::AStar, + blocked_positions: &[], + access_reservations: &[], + active_sink: None, + max_route_steps: 256, + }; + let (route, routed) = route_point_to_point_with_strategy_and_allowed_contacts( + &world, + source, + sink, + context, + Vec::new(), + ) + .unwrap(); + assert!(route.path.len() > 128); + assert!(route.blocks.iter().any(|(_, b)| b.kind.is_repeater())); + let mut sim = + Simulator::from_settled_with_limits_and_trace(&World::from(&routed), 256, 50_000, 0)?; + for on in [false, true, false, true, false] { + sim.drive_inputs_with_limits(vec![(source, on)], 256, 50_000)?; + assert_eq!(sim.world()[sink].kind.is_powered(), on); + } + Ok(()) + } + #[test] fn route_point_to_point_supports_astar_strategy() { let source = Position(0, 1, 1); diff --git a/src/transform/place_and_route/global_pnr/search.rs b/src/transform/place_and_route/global_pnr/search.rs index 7e8afe5e..470c1032 100644 --- a/src/transform/place_and_route/global_pnr/search.rs +++ b/src/transform/place_and_route/global_pnr/search.rs @@ -219,6 +219,7 @@ mod tests { fn candidate(name: &str, volume: usize, block_count: usize, port: Position) -> LayoutCandidate { LayoutCandidate { + exact_core: None, module_name: name.to_owned(), world: World3D::new(DimSize(1, 1, 1)), bbox: BoundingBox { diff --git a/src/transform/place_and_route/local_placer/exact/compact.rs b/src/transform/place_and_route/local_placer/exact/compact.rs index 9773531a..daa68e08 100644 --- a/src/transform/place_and_route/local_placer/exact/compact.rs +++ b/src/transform/place_and_route/local_placer/exact/compact.rs @@ -72,6 +72,9 @@ pub struct CompactionConfig { pub max_removals_per_round: Option, /// Model params for every window (see `ExactPlacerConfig::model_params`). pub model_params: BTreeMap, + /// Auxiliary wire functions used by the starting layout, retained during + /// exact repair (for example partial ORs admitted during construction). + pub extra_signal_functions: std::collections::BTreeSet, /// Simulator rejections each attempt's workers may hit before giving up. pub max_refinements: usize, /// Search and verification constants for every attempt. @@ -129,6 +132,7 @@ impl Default for CompactionConfig { core_max_freed: 20, core_time_limit: Duration::from_secs(5), model_params: BTreeMap::new(), + extra_signal_functions: Default::default(), max_refinements: 8, tuning: ExactTuning::default(), attempt_time_limit: Duration::from_secs(20), @@ -953,6 +957,7 @@ impl ExactLocalPlacer { exact.max_refinements = config.max_refinements; exact.tuning = config.tuning.clone(); exact.model_params = config.model_params.clone(); + exact.extra_signal_functions = config.extra_signal_functions.clone(); exact.max_blocks = max_blocks; if config.timing { // `Stage` only has to hold arrival times: the longest path in diff --git a/src/transform/place_and_route/local_placer/exact/construct.rs b/src/transform/place_and_route/local_placer/exact/construct.rs index 0a058e79..7b7ee8a8 100644 --- a/src/transform/place_and_route/local_placer/exact/construct.rs +++ b/src/transform/place_and_route/local_placer/exact/construct.rs @@ -391,6 +391,9 @@ pub struct ConstructionConfig { /// Search and verification constants for every step. pub tuning: ExactTuning, pub gate_order: GateOrder, + /// Solve this many consecutive construction steps jointly before + /// committing a seam. One retains the original gate-at-a-time path. + pub gate_batch: usize, /// Outputs without a face policy stop crossing seams once no later gate /// reads them; they only need to stay observable somewhere in the box. /// Off, every output is carried to the last slice. @@ -400,11 +403,40 @@ pub struct ConstructionConfig { /// whole layout built so far; those variables were over 90% of a late /// step's encoding. The simulator still checks the whole layout. pub given_frozen_signals: bool, + /// Restrict each window to functions of already available/current nets, + /// their complements, and signals present in the previous layout. This + /// deliberately narrows synthesis; it is not solution-preserving pruning. + pub step_vocabulary: bool, + /// Permit intermediate ORs of each current gate's fan-in on wires, not + /// only the complete NOR function. Bounded to fan-in <= 8. + pub partial_or_vocabulary: bool, + /// Continuing nets must be observed on a torch or outward repeater, + /// rather than an arbitrary powered cell at the seam. Chained ORs keep + /// their required solid support and are exempt. + pub driving_seams: bool, + /// Carry one representative per Boolean function/complement pair. The + /// next window may regenerate the other polarity with a torch. Finished + /// output observations and chained-OR support contracts remain intact. + pub compress_complements: bool, + /// Extend existing outward repeater ports at the same X/Z coordinate: + /// a strongly powered block provides a tap, then repeaters carry forward. + /// Requires zero overlap; other cells and new logic remain SAT choices. + pub scaffold_seams: bool, + /// Spaced, reusable port grid (0 keeps free seam sites). Primary inputs + /// retain fixed slots; new nets use a nearby unused slot. With scaffolds, + /// this leaves room to tap each rail instead of packing inaccessible ports. + pub seam_pitch: usize, + pub seam_primary_stride: usize, + /// Restricted NOR-template initial solution, followed by exact compaction. + pub gate_scaffold: bool, /// After each step, spend up to this long minimizing the layout's block /// count (the model's cost) with the same frozen cells, so dead wires do /// not pile up. `None` keeps the first layout found. Five seconds halved /// the full adder's constructed blocks (about 290 to 127-164). pub step_optimize: Option, + /// Delete redundant blocks in the accepted window before relocation + /// optimization. Uses a fixed-layout SAT optimization with pinned ports. + pub step_delete: Option, /// Model params for every step (see `ExactPlacerConfig::model_params`). pub model_params: BTreeMap, /// Diagnostic only: see `ExactPlacerConfig::no_fold`. @@ -418,6 +450,11 @@ pub struct ConstructionConfig { /// Write the layout after every step of the successful attempt to this /// directory, for watching construction in the viewer (`progress.rs`). pub progress: Option, + /// Save the latest accepted prefix, including when a later step fails. + pub checkpoint: Option, + /// Resume a matching netlist/order from an accepted prefix. Backtracking + /// does not cross the loaded prefix; every subsequent step is verified. + pub resume: Option, /// After each step, ask for its nets sooner (`ExactPlacerConfig::timing`): /// each observed net at most its lower bound (the latest of its inputs' /// arrivals, plus a tick unless it is an OR a block can carry) plus a @@ -451,14 +488,26 @@ impl Default for ConstructionConfig { max_refinements: 8, tuning: ExactTuning::default(), gate_order: GateOrder::SmallestConeFirst, + gate_batch: 1, early_outputs: true, given_frozen_signals: true, + step_vocabulary: false, + partial_or_vocabulary: false, + driving_seams: false, + compress_complements: false, + scaffold_seams: false, + seam_pitch: 0, + seam_primary_stride: 1, + gate_scaffold: false, step_optimize: Some(Duration::from_secs(5)), + step_delete: None, model_params: BTreeMap::new(), no_fold: false, carry: None, carry_step_time_limit: None, progress: None, + checkpoint: None, + resume: None, step_timing: None, } } @@ -518,6 +567,156 @@ struct StepState { result: Option<(DimSize, ExactPlacement)>, } +#[derive(serde::Serialize, serde::Deserialize)] +struct ConstructionCheckpoint { + netlist: String, + outputs: Vec<(String, NetId)>, + order: Vec>, + step: usize, + rcell: String, + signals: Vec<([usize; 3], u64)>, + available: BTreeSet, + supports: Vec<(NetId, [usize; 3])>, +} + +impl ConstructionCheckpoint { + fn save( + placer: &ExactLocalPlacer, + order: &[Vec], + step: usize, + state: &StepState, + path: &std::path::Path, + ) -> eyre::Result<()> { + let (_, placement) = state + .result + .as_ref() + .expect("accepted step has a placement"); + let checkpoint = Self { + netlist: placer.netlist.to_text(), + outputs: placer.netlist.outputs.clone(), + order: order.to_vec(), + step, + rcell: placement.rcell.to_string(), + signals: state + .signals + .iter() + .map(|(p, &f)| ([p.0, p.1, p.2], f)) + .collect(), + available: state.available.clone(), + supports: state + .supports + .iter() + .map(|(&n, p)| (n, [p.0, p.1, p.2])) + .collect(), + }; + let temporary = path.with_extension("json.tmp"); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(&temporary, serde_json::to_vec_pretty(&checkpoint)?)?; + std::fs::rename(temporary, path)?; + Ok(()) + } + + fn load( + placer: &ExactLocalPlacer, + order: &[Vec], + config: &ConstructionConfig, + path: &std::path::Path, + ) -> eyre::Result<(usize, StepState)> { + let checkpoint: Self = serde_json::from_slice(&std::fs::read(path)?)?; + eyre::ensure!( + checkpoint.netlist == placer.netlist.to_text() + && checkpoint.outputs == placer.netlist.outputs + && checkpoint.order == order, + "checkpoint netlist/order mismatch" + ); + eyre::ensure!( + checkpoint.step > 0 && checkpoint.step < order.len(), + "checkpoint must be a partial construction" + ); + let document: crate::physical_cell::PhysicalCellDocument = checkpoint.rcell.parse()?; + let layout = ExactLayout::from_rcell(&document)?; + eyre::ensure!( + config.carry.is_none(), + "resuming carry tiles is not supported" + ); + for (name, policy) in &config.input_policies { + if *policy == InputPolicy::MinYFace { + eyre::ensure!( + layout.inputs.iter().any(|(n, p, _)| n == name && p.1 == 0), + "checkpoint does not satisfy input face policy for {name}" + ); + } + } + eyre::ensure!( + layout.dim.0 <= config.width && layout.dim.2 <= config.height, + "checkpoint cross-section cannot shrink" + ); + let mut expected = order[..checkpoint.step] + .iter() + .flatten() + .copied() + .chain(order[..checkpoint.step].iter().flatten().flat_map(|&net| { + placer.netlist.nets[net] + .gate_inputs + .iter() + .copied() + .filter(|&n| matches!(placer.netlist.nets[n].driver, NetDriver::Input(_))) + })) + .collect::>(); + for (name, _, _) in &layout.inputs { + expected.insert( + placer + .netlist + .input_net(name) + .ok_or_else(|| eyre::eyre!("checkpoint contains unknown input {name}"))?, + ); + } + eyre::ensure!( + expected == checkpoint.available, + "checkpoint available nets mismatch" + ); + let signals = checkpoint + .signals + .into_iter() + .map(|([x, y, z], f)| (Position(x, y, z), f)) + .collect::>(); + eyre::ensure!( + signals.len() == layout.cells.len() + && signals.keys().all(|p| layout.cells.contains_key(p)), + "checkpoint signal cells mismatch" + ); + let supports = checkpoint + .supports + .into_iter() + .map(|(n, [x, y, z])| (n, Position(x, y, z))) + .collect::>(); + eyre::ensure!( + supports.iter().all(|(&n, p)| n < placer.netlist.nets.len() + && layout.cells.get(p) == Some(&CellKind::Solid)), + "checkpoint support mismatch" + ); + Ok(( + checkpoint.step, + StepState { + label: "resumed prefix".into(), + length: layout.dim.1, + cells: layout.cells, + signals, + placed_inputs: layout + .inputs + .into_iter() + .map(|(n, p, d)| (n, (p, d))) + .collect(), + available: checkpoint.available, + supports, + result: None, + }, + )) + } +} + /// Bounds every output of `exact` by its delay in `placement`, with `Stage` /// sized to the placement's longest path (see `CompactionConfig::timing`). fn keep_delays(exact: &mut ExactPlacerConfig, placement: &ExactPlacement) { @@ -579,7 +778,16 @@ impl ExactLocalPlacer { ) -> eyre::Result<(ExactLayout, ExactPlacement, ConstructionReport)> { let started = Instant::now(); let netlist = &self.netlist; + eyre::ensure!(config.gate_batch > 0, "gate batch must be positive"); + eyre::ensure!( + config.carry.is_none() || config.gate_batch == 1, + "batched carry-tile construction is not supported" + ); let order = construction_steps(netlist, config.gate_order, config.carry.as_ref()); + let order = order + .chunks(config.gate_batch) + .map(|steps| steps.iter().flatten().copied().collect::>()) + .collect::>(); if order.is_empty() { bail!("netlist has no gates to construct"); } @@ -598,6 +806,14 @@ impl ExactLocalPlacer { let mut blocked = vec![Vec::new(); order.len()]; let mut windows = vec![Vec::new(); order.len()]; let mut step = 0; + if let Some(path) = &config.resume { + let (loaded_step, state) = ConstructionCheckpoint::load(self, &order, config, path)?; + step = loaded_step; + states.resize(step + 1, state.clone()); + states[step] = state; + tracing::info!(step, path = %path.display(), "construction resumes accepted prefix"); + } + let first_step = step; while step < order.len() { if deadline.is_some_and(|deadline| Instant::now() >= deadline) { bail!( @@ -615,6 +831,9 @@ impl ExactLocalPlacer { deadline, )? { Some((state, window_cells, entry)) => { + if let Some(path) = &config.checkpoint { + ConstructionCheckpoint::save(self, &order, step + 1, &state, path)?; + } report.steps.push(entry); states.truncate(step + 1); states.push(state); @@ -622,7 +841,7 @@ impl ExactLocalPlacer { step += 1; } None => { - if step == 0 || report.backtracks >= config.max_backtracks { + if step == first_step || report.backtracks >= config.max_backtracks { bail!( "construction could not place gate {} within {} slices", self.step_name(&order[step]), @@ -966,6 +1185,26 @@ impl ExactLocalPlacer { } } } + // With polarity-compressed frontiers a non-output inversion adds no + // information: its input's representative can regenerate either + // polarity in a later exact window. Avoid a whole transport window. + // Leave backtracking's window history unchanged unless it is off. + if config.compress_complements + && config.max_backtracks == 0 + && !is_last + && gates.len() == 1 + && needed_inputs.is_empty() + && state.result.is_some() + && netlist.nets[gates[0]].driver == NetDriver::Gate + && netlist.nets[gates[0]].gate_inputs.len() == 1 + && !netlist.outputs.iter().any(|(_, net)| *net == gates[0]) + { + let mut next = state.clone(); + next.label = format!("{gate_name} virtual polarity"); + next.available.extend(gates.iter().copied()); + tracing::info!(gate = gate_name, "construction keeps a virtual polarity"); + return Ok(Some((next, Vec::new(), (gate_name, 0, 0.0)))); + } let length = state.length; // A NOR built in stages (`chained_ors`): the step that adds a signal // to a chain keeps its support block fixed and the block's slice @@ -1039,6 +1278,56 @@ impl ExactLocalPlacer { exact.tuning = config.tuning.clone(); exact.model_params = config.model_params.clone(); exact.blocked = blocked.to_vec(); + if config.partial_or_vocabulary { + let values = netlist.net_values(); + exact + .extra_signal_functions + .extend(state.signals.values().copied()); + for &gate in gates { + let inputs = &netlist.nets[gate].gate_inputs; + eyre::ensure!( + inputs.len() <= 8, + "partial OR vocabulary supports fan-in <= 8" + ); + let functions = inputs + .iter() + .map(|&net| { + values[net] + .iter() + .enumerate() + .fold(0u64, |mask, (i, &on)| mask | (u64::from(on) << i)) + }) + .collect::>(); + for subset in 1..(1usize << inputs.len()) { + let function = functions + .iter() + .enumerate() + .filter(|(i, _)| subset & (1 << i) != 0) + .fold(0, |a, (_, f)| a | f); + exact.extra_signal_functions.insert(function); + } + } + } + if config.step_vocabulary { + let values = netlist.net_values(); + let full = u64::MAX >> (64 - (1usize << netlist.case_bits())); + let mut functions = state.signals.values().copied().collect::>(); + functions.extend(exact.extra_signal_functions.iter().copied()); + for &net in state + .available + .iter() + .chain(needed_inputs.iter()) + .chain(gates.iter()) + { + let function = values[net] + .iter() + .enumerate() + .fold(0u64, |mask, (i, &on)| mask | (u64::from(on) << i)); + functions.insert(function); + } + functions.extend(functions.clone().into_iter().map(|f| !f & full)); + exact.signal_functions = Some(functions); + } for (position, kind) in &state.cells { if position.1 < frozen { exact.fixed_cells.insert(*position, *kind); @@ -1153,6 +1442,333 @@ impl ExactLocalPlacer { .filter(|&net| consumers(net, step + 1)) .map(|net| (net, last_slice.clone())) .collect::>(); + if config.compress_complements { + let before = observations.len(); + let values = netlist.net_values(); + let full = u64::MAX >> (64 - (1usize << netlist.case_bits())); + let function = |net: NetId| { + values[net] + .iter() + .enumerate() + .fold(0u64, |mask, (i, &on)| mask | (u64::from(on) << i)) + }; + let mut representatives = BTreeMap::)>::new(); + let mut supports = Vec::new(); + for (net, sites) in observations { + if chained.contains(&net) { + supports.push((net, sites)); + continue; + } + let f = function(net); + let key = f.min(!f & full); + let priority = |net: NetId| { + ( + !matches!(netlist.nets[net].driver, NetDriver::Input(_)), + net, + ) + }; + if representatives + .get(&key) + .is_none_or(|(old, _)| priority(net) < priority(*old)) + { + representatives.insert(key, (net, sites)); + } + } + for (net, _) in &supports { + let f = function(*net); + representatives.remove(&f.min(!f & full)); + } + for (key, (net, _)) in &mut representatives { + let priority = |candidate: NetId| { + let f = function(candidate); + let carried = state.cells.iter().any(|(p, kind)| { + p.1 + 1 == length + && *kind == CellKind::Repeater(Direction::South) + && state.signals.get(p) == Some(&f) + }); + ( + !matches!(netlist.nets[candidate].driver, NetDriver::Input(_)), + !carried, + candidate, + ) + }; + if let Some(representative) = placed + .iter() + .copied() + .filter(|&candidate| { + let f = function(candidate); + f.min(!f & full) == *key + }) + .min_by_key(|&candidate| priority(candidate)) + { + *net = representative; + } + } + observations = representatives.into_values().chain(supports).collect(); + tracing::info!( + gate = gate_name, + before, + after = observations.len(), + "construction frontier compressed" + ); + } + if config.driving_seams { + for (net, _) in &observations { + if !chained.contains(net) { + exact + .driving_outputs + .insert(netlist.nets[*net].name.clone()); + } + } + } + if config.scaffold_seams { + eyre::ensure!( + overlap == 0 && window >= 2, + "scaffolded seams need zero overlap and window >= 2" + ); + let values = netlist.net_values(); + if config.seam_pitch > 0 { + let pitch = config.seam_pitch; + eyre::ensure!( + pitch >= 3 || (config.gate_scaffold && pitch == 2), + "seam pitch must leave tap clearance" + ); + let slots = (1..dim.2.saturating_sub(1)) + .step_by(pitch) + .flat_map(|z| { + (1..dim.0.saturating_sub(1)) + .step_by(pitch) + .map(move |x| Position(x, dim.1 - 1, z)) + }) + .filter(|p| !config.gate_scaffold || p.2 == 1) + .collect::>(); + let input_names = netlist.input_names(); + let input_slots = (0..input_names.len()) + .map(|i| { + slots + .get(i * config.seam_primary_stride) + .copied() + .ok_or_else(|| eyre::eyre!("not enough primary slots")) + }) + .collect::>>()?; + eyre::ensure!( + slots.len() >= observations.len().max(input_names.len()), + "not enough spaced seam slots" + ); + let function = |net: NetId| { + values[net] + .iter() + .enumerate() + .fold(0u64, |mask, (i, &on)| mask | (u64::from(on) << i)) + }; + let mut used = BTreeSet::new(); + used.extend(input_slots.iter().copied()); + let mut assigned = BTreeMap::::new(); + for (net, sites) in &mut observations { + if let NetDriver::Input(name) = &netlist.nets[*net].driver { + let slot = input_slots + [input_names.iter().position(|n| n == name).unwrap()]; + *sites = vec![slot]; + used.insert(slot); + assigned.insert(function(*net), slot); + } + } + // Reserve every surviving old rail before allocating + // new results; net order must not evict a live port. + for (net, sites) in &mut observations { + if matches!(netlist.nets[*net].driver, NetDriver::Input(_)) { + continue; + } + let f = function(*net); + if let Some(slot) = state.cells.iter().find_map(|(p, kind)| { + let slot = Position(p.0, dim.1 - 1, p.2); + (p.1 + 1 == length + && *kind == CellKind::Repeater(Direction::South) + && state.signals.get(p) == Some(&f) + && slots.contains(&slot) + && !used.contains(&slot)) + .then_some(slot) + }) { + *sites = vec![slot]; + used.insert(slot); + assigned.insert(f, slot); + } + } + for (net, sites) in &mut observations { + if matches!(netlist.nets[*net].driver, NetDriver::Input(_)) { + continue; + } + let f = function(*net); + if let Some(slot) = assigned.get(&f) { + *sites = vec![*slot]; + continue; + } + let old = state.cells.iter().find_map(|(p, kind)| { + (p.1 + 1 == length + && *kind == CellKind::Repeater(Direction::South) + && state.signals.get(p) == Some(&f)) + .then_some(Position(p.0, dim.1 - 1, p.2)) + }); + let parents = netlist.nets[*net] + .gate_inputs + .iter() + .filter_map(|parent| { + let f = function(*parent); + let full = u64::MAX >> (64 - (1usize << netlist.case_bits())); + assigned + .get(&f) + .or_else(|| assigned.get(&(!f & full))) + .copied() + }) + .collect::>(); + let slot = old + .filter(|p| slots.contains(p) && !used.contains(p)) + .or_else(|| { + slots + .iter() + .filter(|p| !used.contains(p)) + .min_by_key(|p| { + parents + .iter() + .map(|q| p.0.abs_diff(q.0) + p.2.abs_diff(q.2)) + .sum::() + }) + .copied() + }) + .ok_or_else(|| eyre::eyre!("spaced port allocation failed"))?; + *sites = vec![slot]; + used.insert(slot); + assigned.insert(f, slot); + } + // The complete seam is an interface, not spare logic space. + for x in 0..dim.0 { + for z in 0..dim.2 { + exact + .fixed_cells + .insert(Position(x, dim.1 - 1, z), CellKind::Air); + } + } + for (_, sites) in &observations { + let p = sites[0]; + exact + .fixed_cells + .insert(p, CellKind::Repeater(Direction::South)); + exact + .fixed_cells + .insert(Position(p.0, p.1, p.2 - 1), CellKind::Solid); + } + for &input in &needed_inputs { + let name = &netlist.nets[input].name; + let slot = + input_slots[input_names.iter().position(|n| n == name).unwrap()]; + let p = Position( + slot.0, + if config.gate_scaffold && length > 0 { + length - 1 + } else { + length + }, + slot.2, + ); + eyre::ensure!( + config.input_policies.get(name) != Some(&InputPolicy::MinYFace) + || length == 0, + "face input must enter at the first seam" + ); + exact + .input_sites + .insert(name.clone(), vec![(p, Direction::North)]); + if config.gate_scaffold { + exact + .fixed_cells + .insert(p, CellKind::Switch(Direction::North)); + exact.given_signals.remove(&p); + let tap_y = p.1 + 1; + exact + .fixed_cells + .insert(Position(p.0, tap_y, p.2), CellKind::Solid); + for y in tap_y + 1..dim.1 { + exact.fixed_cells.insert( + Position(p.0, y, p.2), + CellKind::Repeater(Direction::South), + ); + exact + .fixed_cells + .insert(Position(p.0, y, p.2 - 1), CellKind::Solid); + } + } + } + } + let mut scaffolded = 0; + // A signal consumed by this gate may retire at this step, + // so it has no outgoing observation. Still provide its + // incoming diode with a solid tap in the new window. + let full = u64::MAX >> (64 - (1usize << netlist.case_bits())); + for &input in gates + .iter() + .flat_map(|&gate| &netlist.nets[gate].gate_inputs) + { + let f = values[input] + .iter() + .enumerate() + .fold(0u64, |mask, (i, &on)| mask | (u64::from(on) << i)); + for (p, kind) in &state.cells { + if p.1 + 1 == length + && *kind == CellKind::Repeater(Direction::South) + && state + .signals + .get(p) + .is_some_and(|old| *old == f || *old == (!f & full)) + { + exact + .fixed_cells + .insert(Position(p.0, length, p.2), CellKind::Solid); + } + } + } + for (net, sites) in &mut observations { + if chained.contains(net) { + continue; + } + let function = values[*net] + .iter() + .enumerate() + .fold(0u64, |mask, (i, &on)| mask | (u64::from(on) << i)); + let port = state.cells.iter().find_map(|(p, kind)| { + (p.1 + 1 == length + && *kind == CellKind::Repeater(Direction::South) + && state.signals.get(p) == Some(&function)) + .then_some(*p) + }); + if let Some(port) = port { + scaffolded += 1; + if config.seam_pitch > 0 + && (sites[0].0 != port.0 || sites[0].2 != port.2) + { + continue; + } + *sites = vec![Position(port.0, dim.1 - 1, port.2)]; + for y in length..dim.1 { + let kind = if y == length { + CellKind::Solid + } else { + CellKind::Repeater(Direction::South) + }; + exact.fixed_cells.insert(Position(port.0, y, port.2), kind); + if y != length { + exact + .fixed_cells + .insert(Position(port.0, y, port.2 - 1), CellKind::Solid); + } + } + } + } + tracing::info!( + gate = gate_name, + scaffolded, + "construction extends fixed repeater rails" + ); + } // Finished outputs stay observable but may sit anywhere. let everywhere = (0..dim.0) .flat_map(|x| { @@ -1184,8 +1800,188 @@ impl ExactLocalPlacer { } exact.observations = Some(observations); } + if config.gate_scaffold { + eyre::ensure!( + config.scaffold_seams + && config.seam_pitch >= 3 + && overlap == 0 + && gates.len() == 1, + "NOR templates need a single gate, pitch >= 3 and zero-overlap scaffolded seams" + ); + let gate = gates[0]; + let values = netlist.net_values(); + let full = u64::MAX >> (64 - (1usize << netlist.case_bits())); + let function = |net: NetId| { + values[net] + .iter() + .enumerate() + .fold(0u64, |mask, (i, &on)| mask | (u64::from(on) << i)) + }; + let target = function(gate); + let observed = exact + .observations + .as_ref() + .is_none_or(|obs| obs.iter().any(|(net, _)| function(*net) == target)); + let mut template = BTreeMap::new(); + if observed { + eyre::ensure!( + netlist.nets[gate].driver == NetDriver::Gate, + "NOR template does not support explicit OR stages" + ); + let start_y = length + usize::from(length == 0); + let mut taps = Vec::new(); + for &input in &netlist.nets[gate].gate_inputs { + let f = function(input); + let old = state.cells.iter().find_map(|(p, kind)| { + if p.1 + 1 == length && *kind == CellKind::Repeater(Direction::South) { + let old = *state.signals.get(p)?; + if old == f || old == (!f & full) { + return Some((Position(p.0, start_y, p.2), old != f)); + } + } + None + }); + let new = needed_inputs.iter().find_map(|&net| { + let source = function(net); + if source != f && source != (!f & full) { + return None; + } + let site = exact.input_sites.get(&netlist.nets[net].name)?.first()?.0; + Some((Position(site.0, start_y, site.2), source != f)) + }); + taps.push(old.or(new).ok_or_else(|| { + eyre::eyre!("no template input tap for {}", netlist.nets[input].name) + })?); + } + let out = exact + .observations + .as_ref() + .and_then(|obs| { + obs.iter().find_map(|(net, sites)| { + (function(*net) == target + && sites.len() == 1 + && sites[0].1 + 1 == dim.1) + .then(|| sites[0].0) + }) + }) + .or_else(|| { + (1..dim.0.saturating_sub(2)) + .step_by(config.seam_pitch) + .find(|&x| { + exact + .fixed_cells + .get(&Position(x, dim.1 - 1, 1)) + .is_none_or(|k| *k == CellKind::Air) + }) + }) + .ok_or_else(|| eyre::eyre!("no output slot for NOR template"))?; + template = super::nor_scaffold::nor_template(&taps, out, start_y, dim)?; + } + // Search deletion subsets of a generic initial geometry. This + // is not an unrestricted placement solve; compaction frees + // spatial windows after the complete baseline is verified. + for x in 0..dim.0 { + for y in length..dim.1 { + for z in 0..dim.2 { + exact + .fixed_cells + .entry(Position(x, y, z)) + .or_insert(CellKind::Air); + } + } + } + for (p, kind) in template { + if let Some(old) = exact.fixed_cells.get(&p) { + if *old != CellKind::Air { + eyre::ensure!( + *old == kind, + "template/scaffold conflict at {p:?}: {old:?} / {kind:?}" + ); + continue; + } + } + exact.fixed_cells.remove(&p); + exact.removable_cells.insert(p, kind); + } + } let step_started = Instant::now(); + if config.gate_scaffold && std::env::var_os("EXACT_TEMPLATE_DIAGNOSE").is_some() { + let mut kinds = vec![CellKind::Air; dim.0 * dim.1 * dim.2]; + for (p, k) in exact.fixed_cells.iter().chain(exact.removable_cells.iter()) { + kinds[(p.2 * dim.1 + p.1) * dim.0 + p.0] = *k; + } + let world = super::verify::build_world(dim, &kinds); + let mut sim = + crate::world::simulator::Simulator::from_settled_with_limits_and_trace( + &crate::world::World::from(&world), + 4096, + 2_000_000, + 0, + )?; + let positions = world + .iter_block() + .into_iter() + .map(|(p, _)| p) + .collect::>(); + let names = netlist.input_names(); + let mut actual = BTreeMap::::new(); + for case in 0..(1usize << netlist.case_bits()) { + sim.advance_idle_cycles(61)?; + let drive = names + .iter() + .enumerate() + .filter(|(_, n)| !exact.absent_inputs.contains(*n)) + .map(|(i, n)| (exact.input_sites[n][0].0, case & (1 << i) != 0)) + .collect::>(); + sim.drive_inputs_with_limits(drive, 4096, 2_000_000)?; + for p in &positions { + if sim.world()[*p].kind.is_powered() { + *actual.entry(*p).or_default() |= 1 << case; + } + } + } + let values = netlist.net_values(); + let observations = exact.observations.clone().unwrap_or_else(|| { + netlist + .outputs + .iter() + .map(|(_, n)| (*n, positions.clone())) + .collect() + }); + let mismatches = observations + .iter() + .filter_map(|(net, sites)| { + let target = values[*net] + .iter() + .enumerate() + .fold(0u64, |m, (i, &on)| m | (u64::from(on) << i)); + (!sites + .iter() + .any(|p| actual.get(p).copied().unwrap_or(0) == target)) + .then(|| netlist.nets[*net].name.clone()) + }) + .collect::>(); + let frozen_mismatches = exact + .given_signals + .iter() + .filter(|(p, f)| actual.get(p).copied().unwrap_or(0) != **f) + .count(); + for (net, sites) in &observations { + if sites.len() == 1 { + tracing::info!(net=netlist.nets[*net].name,site=?sites[0],actual=actual.get(&sites[0]),"template port value"); + } + } + tracing::info!(gate=gate_name,?mismatches,frozen_mismatches,functions=?actual.values().copied().collect::>(),"template direct simulation"); + } let (outcome, stats) = self.place(&exact)?; + tracing::info!( + gate = gate_name, + encode_seconds = stats.encode_time.as_secs_f64(), + solve_seconds = stats.solve_time.as_secs_f64(), + variables = stats.variables, + clauses = stats.clauses, + "construction SAT model" + ); let ExactOutcome::Placed(placement) = outcome else { tracing::info!( gate = gate_name, @@ -1214,6 +2010,20 @@ impl ExactLocalPlacer { continue; }; let mut placement = placement; + if let Some(budget) = config.step_delete { + let mut deleting = exact.clone(); + deleting.given_signals.clear(); + let remaining = deadline.map(|d| d.saturating_duration_since(Instant::now())); + deleting.time_limit = Some(remaining.map_or(budget, |r| r.min(budget))); + let layout = ExactLayout::from_placement(dim, &placement); + if let Ok((ExactOutcome::Placed(better), _)) = + self.minimize_existing(&layout, &deleting) + { + if better.cells.len() < placement.cells.len() { + placement = better; + } + } + } if let Some(budget) = config.step_timing { let lower = self.step_lower_bounds(gates, state); placement = diff --git a/src/transform/place_and_route/local_placer/exact/dsl.rs b/src/transform/place_and_route/local_placer/exact/dsl.rs index 293ef957..70da2bbf 100644 --- a/src/transform/place_and_route/local_placer/exact/dsl.rs +++ b/src/transform/place_and_route/local_placer/exact/dsl.rs @@ -36,6 +36,60 @@ const MAX_INPUTS: usize = 6; /// The torch lower-bound search tracks sets of classes as `u64` masks. const MAX_BOUND_CLASSES: usize = 64; +#[cfg(test)] +mod vocabulary_tests { + use super::*; + use crate::graph::logic::LogicGraph; + #[test] + fn restricted_vocabulary_remaps_complements_and_preserves_sources() { + for expression in ["~(a|b|c)", "~(a|b|c|d|e|f)"] { + let graph = LogicGraph::from_assignments([("out".into(), expression.into())]) + .unwrap() + .prepare_place() + .unwrap(); + let netlist = NorNetlist::from_logic_graph(&graph).unwrap(); + let target = net_function(&netlist.net_values(), netlist.outputs[0].1); + let full = u64::MAX >> (64 - (1usize << netlist.case_bits())); + let mut config = ExactPlacerConfig::new(DimSize(3, 4, 3)); + config.signal_functions = Some([target, !target & full].into_iter().collect()); + let prepared = Prepared::new(&netlist, &config).unwrap(); + assert_eq!(prepared.classes[0].function, 0); + assert!(prepared.classes.len() < vocabulary(&netlist).len()); + assert_eq!(prepared.present_classes.len(), netlist.input_names().len()); + let index = prepared + .classes + .iter() + .position(|c| c.function == target) + .unwrap(); + let complement = prepared.classes[index].complement.unwrap(); + assert_eq!(prepared.classes[complement].function, !target & full); + assert_eq!(prepared.classes[complement].complement, Some(index)); + let values = netlist.net_values(); + let auxiliary = net_function(&values, netlist.input_net("a").unwrap()) + | net_function(&values, netlist.input_net("b").unwrap()); + assert!(!vocabulary(&netlist).iter().any(|c| c.function == auxiliary)); + config.extra_signal_functions.insert(auxiliary); + config + .signal_functions + .as_mut() + .unwrap() + .extend([auxiliary, !auxiliary & full]); + let extended = Prepared::new(&netlist, &config).unwrap(); + let index = extended + .classes + .iter() + .position(|c| c.function == auxiliary) + .unwrap(); + assert_eq!( + extended.classes[extended.classes[index].complement.unwrap()].function, + !auxiliary & full + ); + config.signal_functions = Some(Default::default()); + assert!(Prepared::new(&netlist, &config).is_err()); + } + } +} + pub(super) fn model() -> &'static Model { static MODEL: OnceLock = OnceLock::new(); MODEL.get_or_init(|| { @@ -164,7 +218,37 @@ impl Prepared { netlist.state.is_empty() || config.carry.is_none(), "a carry tile cannot hold state" ); - let classes = vocabulary(netlist); + let mut classes = vocabulary(netlist); + let full = u64::MAX >> (64 - (1usize << netlist.case_bits())); + for &function in &config.extra_signal_functions { + ensure!( + function & !full == 0, + "extra signal exceeds the input truth table" + ); + for function in [function, !function & full] { + if function != 0 + && function != full + && !classes.iter().any(|c| c.function == function) + { + classes.push(SignalClass { + name: format!("aux_{function:x}"), + function, + complement: None, + input: None, + stored: None, + }); + } + } + } + if let Some(functions) = &config.signal_functions { + classes.retain(|class| { + class.function == 0 || class.input.is_some() || functions.contains(&class.function) + }); + } + for index in 0..classes.len() { + let complement = !classes[index].function & full; + classes[index].complement = classes.iter().position(|c| c.function == complement); + } let class_names = unique_names(classes.iter().map(|class| class.name.as_str())); let values = netlist.net_values(); let cuts = netlist @@ -327,7 +411,7 @@ impl Prepared { .iter() .position(|class| class.function == function) else { - bail!("output `{name}` is constant; the exact placer needs a driven signal"); + bail!("output `{name}` has no signal class (constant or excluded by signal_functions)"); }; let cells = match positions { Some(positions) => positions @@ -409,6 +493,30 @@ impl Prepared { self.observations.iter().map(|o| IValue::sym(&o.symbol)), ) .domain("Tick", (0..=config.stage_levels).map(IValue::from)); + // CaseRank is indexed through a dense integer domain rather than all + // grid cells. Hard Air has no power; certified cells reset rank to zero. + // Soft Air must remain eligible when proof-core assumptions are dropped. + let case_ranks_enabled = matches!( + config.model_params.get("case_rank_levels"), + Some(IValue::Int(levels)) if *levels > 0 + ); + let ranked_cells = (0..geometry.len()) + .filter(|&cell| { + let position = geometry.position(cell); + case_ranks_enabled + && !config.given_signals.contains_key(&position) + && !(config.fixed_cells.get(&position) == Some(&CellKind::Air) + && !config.soft_fixed_cells.contains(&position)) + }) + .collect::>(); + instance.domain("RankedCell", (0..ranked_cells.len()).map(IValue::from)); + instance.fact("rank_cell"); + for (index, &cell) in ranked_cells.iter().enumerate() { + instance.row( + "rank_cell", + vec![cell_value(geometry, cell), IValue::from(index)], + ); + } let mirrors = mirror_tuples(config); let orders = mirrors.iter().map(|&(_, i, _, _)| i + 1).max().unwrap_or(0); instance @@ -737,7 +845,7 @@ impl Prepared { .map(|name| { program .option_lit("Sig", here, "Carry", &[IValue::sym(name)]) - .expect("every class is a signal option") + .unwrap_or(-1) }) .collect(), ); @@ -934,8 +1042,22 @@ fn ground( } None => model(), }; + // Older standalone models do not declare the opt-in case-rank mapping. + // Keep their instance and CNF unchanged unless they accept that feature. + let compatible = if !model.accepts_fact("rank_cell") || !model.accepts_domain("RankedCell") { + let mut copy = instance.clone(); + if !model.accepts_fact("rank_cell") { + copy.facts.remove("rank_cell"); + } + if !model.accepts_domain("RankedCell") { + copy.domains.remove("RankedCell"); + } + Some(copy) + } else { + None + }; model - .ground(instance, options) + .ground(compatible.as_ref().unwrap_or(instance), options) .map_err(|error| eyre!("grounding {} failed:\n{error}", model.name())) } diff --git a/src/transform/place_and_route/local_placer/exact/exact_placer.rsdsl b/src/transform/place_and_route/local_placer/exact/exact_placer.rsdsl index 9de712a0..8d622a7a 100644 --- a/src/transform/place_and_route/local_placer/exact/exact_placer.rsdsl +++ b/src/transform/place_and_route/local_placer/exact/exact_placer.rsdsl @@ -14,6 +14,9 @@ enum Attach { Floor, East, West, North, South } enum Source { FromDust, FromTorch, FromRepeater, FromSolid, FromSwitch } enum Sink { DustSink, RepeaterSink, SolidSink } +domain RankedCell: int; // dense indices of cells needing per-case ranks +fact rank_cell(Cell, RankedCell); + domain Case: int; // input cases; bit i of a case is input i domain Class; // signal vocabulary, one class per Boolean function domain Output; @@ -39,6 +42,10 @@ fact given(c: Cell) := any(given_sig(c, k) for k: Class); // Fixed or given cells whose units are not folded, so a refutation that needs // them names them (`ExactLocalPlacer::proof_core`). fact soft(Cell); +// Only hard-fixed Air is statically absent. Soft proof-core cells remain live. +fact known_air(c: Cell) := fixed(c, Air) and not soft(c); +fact known_signal(c: Cell) := given(c) and not soft(c); +fact hard_fixed(c: Cell) := not soft(c) and any(fixed(c, m) for m: Kind); // Tiling along X with a ripple carry (`CarryTiling` in mod.rs; see the // tiling rules below). Ghost cells stand for the previous tile. @@ -78,6 +85,11 @@ fact outward_ok(c: Cell) := or any(fixed(c, m) and m is Repeater(_) for m: Kind); param rank_levels: int = 24; +// Opt-in: justify merged dust power independently in each input case. +// Zero preserves the original single-function wire/rank model. +param case_rank_levels: int = 0; +// Optional output interface: driving observations must use outward repeaters. +param repeater_driving_outputs: bool = false; param stage_levels: int = 24; param max_blocks: int? = none; param max_repeaters: int? = none; // e.g. 0 forbids repeaters @@ -123,26 +135,31 @@ fn perp(d: Dir4) -> set = match d { @prefer(Air) @outside(Air) @display("{c} {member}") choice Kind[c: Cell] { - Air, - Solid, - Dust if has(step(c, Down)), - Torch(a: Attach) if has(step(c, dir(a))), - Repeater(d: Dir4) if has(step(c, Down)) and has(step(c, d)) + Air if not hard_fixed(c) or fixed(c, Air), + Solid if not hard_fixed(c) or fixed(c, Solid), + Dust if (not hard_fixed(c) or fixed(c, Dust)) and has(step(c, Down)), + Torch(a: Attach) if (not hard_fixed(c) or fixed(c, Torch(a))) and has(step(c, dir(a))), + Repeater(d: Dir4) if (not hard_fixed(c) or fixed(c, Repeater(d))) + and has(step(c, Down)) and has(step(c, d)) and (has(step(c, opposite(d))) or outward_ok(c)), - Switch(a: Attach, k: Class) if switch_site(c, a, k), + Switch(a: Attach, k: Class) if (not hard_fixed(c) or fixed(c, Switch(a, k))) + and switch_site(c, a, k), } @prefer(Empty) @display("{c} 신호={member}") choice Sig[c: Cell] { - Empty, - Carry(k: Class), + Empty if not known_signal(c) or known_air(c), + Carry(k: Class) if not known_air(c) and (not known_signal(c) or given_sig(c, k)), } @display("{c} 경우{k}에 켜짐") -def Powered[k: Case, c: Cell] := any(Sig[c] is Carry(j) for j: Class where on(j, k)); +def Powered[k: Case, c: Cell] := not known_air(c) and any(Sig[c] is Carry(j) for j: Class where on(j, k)); @display("{c} 꺼짐 신호") -def Unpowered[c: Cell] := any(Sig[c] is Carry(u) for u: Class where unpowered(u)); +def Unpowered[c: Cell] := not known_air(c) and any(Sig[c] is Carry(u) for u: Class where unpowered(u)); +// Only neighboring feed pairs query this predicate. Exporting it would +// eagerly materialize every pair of cells (quadratic space and time). +@internal def Cut[s: Cell, t: Cell] := Kind[t] is Dust | Repeater(_) and any(Sig[s] is Carry(n) and Sig[t] is Carry(m) for (n, m) in cut); @@ -188,6 +205,9 @@ var Contrib over Feeds; @encoding(order) @display("{c} 순위") int Rank[c: Cell] in 0..=rank_levels; +@encoding(order) @display("경우 {k}, 순위 셀 {i} 전원 순위") +int CaseRank[k: Case, i: RankedCell] in 0..=case_rank_levels; + @encoding(order) @display("{c} 단계") int Stage[c: Cell] in 0..=stage_levels; @@ -283,7 +303,7 @@ rule "토치는 반드시 꺼짐이 아닌 신호를 실음" { // ===== power relations ======================================================= rule "전원 관계: 가루 ↔ 옆 가루 / 계단 가루" { - forall c: Cell, d: Dir4 { + forall c: Cell, d: Dir4 where not known_air(c) { Feeds(c, step(c, d), FromDust, DustSink) |= Kind[c] is Dust and Kind[step(c, d)] is Dust; let up = step(step(c, d), Up); let stair = Kind[c] is Dust and Kind[up] is Dust and not Kind[step(c, Up)] is Solid; @@ -293,7 +313,7 @@ rule "전원 관계: 가루 ↔ 옆 가루 / 계단 가루" { } rule "전원 관계: 가루 → 받침·가리키는 블록(약전원), 뒤에서 읽는 리피터" { - forall c: Cell { + forall c: Cell where not known_air(c) { Feeds(c, step(c, Down), FromDust, SolidSink) |= Kind[c] is Dust; forall d: Dir4 { Feeds(c, step(c, d), FromDust, SolidSink) |= Points[c, d] and Kind[step(c, d)] is Solid; @@ -304,7 +324,7 @@ rule "전원 관계: 가루 → 받침·가리키는 블록(약전원), 뒤에 } rule "전원 관계: 토치 → 주변 가루·리피터, 위 블록(강전원)" { - forall c: Cell, d: Dir6 { + forall c: Cell, d: Dir6 where not known_air(c) { if d == Up { Feeds(c, step(c, d), FromTorch, SolidSink) |= Kind[c] is Torch(_) and Kind[step(c, d)] is Solid; @@ -320,7 +340,7 @@ rule "전원 관계: 토치 → 주변 가루·리피터, 위 블록(강전원)" } rule "전원 관계: 리피터 → 앞쪽 블록/가루/같은 방향 리피터" { - forall c: Cell, d: Dir4 { + forall c: Cell, d: Dir4 where not known_air(c) { let out = step(c, opposite(d)); Feeds(c, out, FromRepeater, SolidSink) |= Kind[c] is Repeater(d) and Kind[out] is Solid; Feeds(c, out, FromRepeater, DustSink) |= Kind[c] is Repeater(d) and Kind[out] is Dust; @@ -329,13 +349,13 @@ rule "전원 관계: 리피터 → 앞쪽 블록/가루/같은 방향 리피터" } rule "리피터가 다른 리피터 옆구리를 치면 잠김 → 금지" { - forall c: Cell, d: Dir4, p in perp(d) { + forall c: Cell, d: Dir4, p in perp(d) where not known_air(c) { require not (Kind[c] is Repeater(d) and Kind[step(c, opposite(d))] is Repeater(p)); } } rule "전원 관계: 블록 → 읽는 리피터; 강전원 블록 → 주변 가루" { - forall c: Cell { + forall c: Cell where not known_air(c) { forall d: Dir4 { Feeds(c, step(c, d), FromSolid, RepeaterSink) |= Kind[c] is Solid and Kind[step(c, d)] is Repeater(opposite(d)); @@ -347,7 +367,7 @@ rule "전원 관계: 블록 → 읽는 리피터; 강전원 블록 → 주변 } rule "전원 관계: 스위치 → 붙은 블록(강전원), 주변 가루·리피터" { - forall (c, a, k) in switch_site { + forall (c, a, k) in switch_site where not known_air(c) { let sw = Kind[c] is Switch(a, k); Feeds(c, step(c, dir(a)), FromSwitch, SolidSink) |= sw; forall d: Dir6 where d != dir(a) { @@ -361,7 +381,7 @@ rule "전원 관계: 스위치 → 붙은 블록(강전원), 주변 가루·리 } rule "스위치 옆 블록의 약전원은 토치는 무시하고 리피터는 받음 → 그런 배치 금지" { - forall (c, a) in switch_attach, d: Dir6 where d != dir(a) { + forall (c, a) in switch_attach, d: Dir6 where d != dir(a) and not known_air(c) { let t = step(c, d); forall b: Attach { require not (Kind[c] is Switch(a, _) and Kind[step(t, opposite(dir(b)))] is Torch(b)); @@ -385,7 +405,7 @@ rule "건전성: 원천이 켜지면 받는 쪽도 켜짐 (가루·리피터는 forall k: Case { let source = if strong { Strong[k, s] } else { Powered[k, s] }; require feeds and source and not open -> Powered[k, t]; - if to != SolidSink { + if to == RepeaterSink or (to == DustSink and case_rank_levels == 0) { require feeds and Powered[k, t] and not open -> source; } } @@ -403,7 +423,7 @@ rule "정당화: 기여 관계, 순위·단계 순서" { require used and not Cut[s, t] -> Stage[s] < Stage[t]; } } else { - if rank_levels > 0 { + if rank_levels > 0 and case_rank_levels == 0 { require used -> Rank[s] < Rank[t]; } if stage_levels > 0 { @@ -418,13 +438,39 @@ rule "정당화: 기여 관계, 순위·단계 순서" { @guarded(per = c) rule "켜진 것은 켜진 원천이 있어야 함" { - forall c: Cell where not given(c) { + forall c: Cell where not given(c) and not known_air(c) { require Kind[c] is Dust | Repeater(_) and not Unpowered[c] -> any(Contrib(s, t, f, to) for (s, t, f, to) in Feeds where t == c and to != SolidSink); forall k: Case { - require Powered[k, c] and Kind[c] is Solid - -> any(Contrib(s, t, f, to) and Powered[k, s] - for (s, t, f, to) in Feeds where t == c and to == SolidSink); + if case_rank_levels > 0 { + // Each powered case needs its own acyclic path to a real source. + // A strongly powered solid may feed dust only in its strong cases. + require Powered[k, c] and Kind[c] is Solid | Dust + -> any(any(Contrib(s, t, f, to) + and (if f == FromSolid and to == DustSink { Strong[k, s] } else { Powered[k, s] }) + and (if given(s) { + CaseRank[k, ci] > 0 + } else { + any(CaseRank[k, si] < CaseRank[k, ci] + for (source_cell, si) in rank_cell where source_cell == s) + }) + for (s, t, f, to) in Feeds + where t == c and (to == SolidSink or to == DustSink)) + for (target_cell, ci) in rank_cell where target_cell == c); + } else { + require Powered[k, c] and Kind[c] is Solid + -> any(Contrib(s, t, f, to) and Powered[k, s] + for (s, t, f, to) in Feeds where t == c and to == SolidSink); + } + } + } +} + +rule "경우별 전원 순위: 원천과 인증된 고정 셀에서 다시 시작" { + if case_rank_levels > 0 { + forall k: Case, (c, ci) in rank_cell { + require Kind[c] is Air | Torch(_) | Repeater(_) | Switch(_, _) + -> CaseRank[k, ci] == 0; } } } @@ -447,9 +493,15 @@ rule "대칭 제거: 가루·리피터는 기여 원천을 하나만 고름" { // Coverage needs one contributor; more only add order constraints, so // a layout justified by several is also justified by one of them. if symmetry_breaking { - forall c: Cell { - require at_most_one(Contrib(s, t, f, to) - for (s, t, f, to) in Feeds where t == c and to != SolidSink); + forall c: Cell where not known_air(c) { + if case_rank_levels == 0 { + require at_most_one(Contrib(s, t, f, to) + for (s, t, f, to) in Feeds where t == c and to != SolidSink); + } else { + require Kind[c] is Repeater(_) + -> at_most_one(Contrib(s, t, f, to) + for (s, t, f, to) in Feeds where t == c and to != SolidSink); + } } } } @@ -479,7 +531,7 @@ rule "대칭 제거: 받는 기여가 없으면 순위·단계는 0 (토치 단 // Lowering such a cell to 0 keeps every order constraint satisfied; this // also fixes the otherwise free integers of air cells. if symmetry_breaking { - forall c: Cell { + forall c: Cell where not known_air(c) { let fed = any(Contrib(s, t, f, to) for (s, t, f, to) in Feeds where t == c); if rank_levels > 0 { require Rank[c] >= 1 -> fed; @@ -491,6 +543,17 @@ rule "대칭 제거: 받는 기여가 없으면 순위·단계는 0 (토치 단 } } +// Hard Air cannot receive power. Preserve the original fed=false normalization +// without scanning every Feeds tuple for every fixed-empty cell. +rule "대칭 제거: 고정 공기의 순위·단계는 0" { + if symmetry_breaking { + forall c: Cell where known_air(c) { + require Rank[c] == 0; + require Stage[c] == 0; + } + } +} + // Source kinds have no incoming Rank inequality: lowering their rank to 0 // preserves outgoing comparisons. Torches/repeaters still keep their Stage, // which orders them after their inputs. Air/switches have no incoming Stage. @@ -501,6 +564,14 @@ rule "대칭 제거: 전원 시작점의 순위는 0 (공기·스위치는 단 require Kind[c] is Air | Switch(_,_) -> Stage[c] == 0; } } + // Certified frozen cells have no incoming justification inequalities. + // Lowering their stage keeps every outgoing inequality satisfied; actual + // arrival times are retained when timing is enabled. + if canonical_source_ranks and not timing { + forall c: Cell where given(c) { + require Stage[c] == 0; + } + } } // ===== implied lower bounds ================================================== @@ -518,7 +589,7 @@ rule "구조적 하한: 토치 수와 그 받침 블록 수" { rule "쓸모: 토치·리피터·가루는 무언가에 기여하거나 출력으로 관측됨" { if require_useful { - forall c: Cell where not given(c) { + forall c: Cell where not given(c) and not known_air(c) { require Kind[c] is Dust | Repeater(_) | Torch(_) -> any(Contrib(s, t, f, to) for (s, t, f, to) in Feeds where s == c) or any(Observe[o, c] for o: Output where output_site(o, c)); @@ -530,7 +601,7 @@ rule "쓸모: 토치·리피터·가루는 무언가에 기여하거나 출력 @display("출력 {o}을 {c}에서 관측") def Observe[o: Output, c: Cell] := - output_site(o, c) + not known_air(c) and output_site(o, c) and (if driving(o) { Kind[c] is Torch(_) or any(Kind[c] is Repeater(d) for d: Dir4 where not has(step(c, opposite(d)))) @@ -547,6 +618,14 @@ rule "출력: 허용된 위치에서 관측" { } } +rule "출력 계약: 구동 출력을 바깥 방향 리피터로 한정" { + if repeater_driving_outputs { + forall o: Output, c: Cell where driving(o) and output_site(o, c) { + require Observe[o, c] -> Kind[c] is Repeater(_); + } + } +} + // ===== tiling ================================================================ // // The tile repeats along X, and each copy passes a carry to the next. The @@ -643,7 +722,7 @@ rule "타이밍: 모든 전원 관계가 도착 시각 순서를 지킴" { require Feeds(s, t, f, to) -> Stage[s] <= Stage[t]; } } - forall c: Cell, a: Attach where given(c) { + forall c: Cell, a: Attach where given(c) and not known_air(c) { require Kind[c] is Torch(a) -> Stage[step(c, dir(a))] < Stage[c]; } } @@ -651,7 +730,7 @@ rule "타이밍: 모든 전원 관계가 도착 시각 순서를 지킴" { rule "타이밍: 출력은 정해진 틱 안에 도착" { if timing { - forall (o, d) in output_delay, c: Cell where output_site(o, c) { + forall (o, d) in output_delay, c: Cell where output_site(o, c) and not known_air(c) { require Observe[o, c] -> Stage[c] <= d; } } diff --git a/src/transform/place_and_route/local_placer/exact/mod.rs b/src/transform/place_and_route/local_placer/exact/mod.rs index 97508100..8fe894f7 100644 --- a/src/transform/place_and_route/local_placer/exact/mod.rs +++ b/src/transform/place_and_route/local_placer/exact/mod.rs @@ -25,6 +25,7 @@ mod encode; mod frat; mod layout; mod netlist; +mod nor_scaffold; mod progress; #[cfg(test)] mod search_benchmark; @@ -89,6 +90,13 @@ pub struct ExactPlacerConfig { /// justification (rank, stage, contributing source) in this solve; the /// simulator still checks the whole layout (rsdsl model only). pub given_signals: BTreeMap, + /// Optional restricted signal vocabulary. Zero and primary input classes + /// remain available. This is a synthesis restriction, not a complete + /// pruning rule; UNSAT is relative to these functions when supplied. + pub signal_functions: Option>, + /// Additional Boolean functions allowed on physical wires/supports. + /// They enlarge the synthesis vocabulary; all simulator checks remain. + pub extra_signal_functions: std::collections::BTreeSet, /// Partial layouts to exclude: at least one listed cell must differ. pub blocked: Vec>, /// Upper bound on a power chain between two torch stages (local rank). @@ -240,6 +248,8 @@ impl ExactPlacerConfig { fixed_cells: BTreeMap::new(), removable_cells: BTreeMap::new(), given_signals: BTreeMap::new(), + signal_functions: None, + extra_signal_functions: Default::default(), blocked: Vec::new(), rank_levels: 24, stage_levels: 24, diff --git a/src/transform/place_and_route/local_placer/exact/netlist.rs b/src/transform/place_and_route/local_placer/exact/netlist.rs index b251ac9a..1918b845 100644 --- a/src/transform/place_and_route/local_placer/exact/netlist.rs +++ b/src/transform/place_and_route/local_placer/exact/netlist.rs @@ -49,6 +49,134 @@ pub struct NorNetlist { } impl NorNetlist { + /// Inline positive ORs, including NOR-of-NOR double negations, into + /// consumers' fan-in sets. Remove unreachable intermediates. This trades + /// serial inversion stages for wider NOR gates without changing logic. + pub fn flatten_positive_logic(&self) -> eyre::Result { + ensure!( + self.state.is_empty(), + "positive-logic flattening requires combinational logic" + ); + fn positive(source: &NorNetlist, id: NetId) -> Option<&[NetId]> { + let net = &source.nets[id]; + if net.driver == NetDriver::Or { + return Some(&net.gate_inputs); + } + if net.driver == NetDriver::Gate && net.gate_inputs.len() == 1 { + let inner = &source.nets[net.gate_inputs[0]]; + if inner.driver == NetDriver::Gate { + return Some(&inner.gate_inputs); + } + } + None + } + fn expand(source: &NorNetlist, id: NetId, out: &mut BTreeSet) { + if let Some(inputs) = positive(source, id) { + for &input in inputs { + expand(source, input, out); + } + } else { + out.insert(id); + } + } + fn copy( + source: &NorNetlist, + result: &mut NorNetlist, + memo: &mut BTreeMap, + id: NetId, + ) -> NetId { + if let Some(&new) = memo.get(&id) { + return new; + } + let mut net = source.nets[id].clone(); + let inputs = if let Some(inputs) = positive(source, id) { + net.driver = NetDriver::Or; + inputs + } else { + &source.nets[id].gate_inputs + }; + let mut expanded = BTreeSet::new(); + for &input in inputs { + expand(source, input, &mut expanded); + } + net.gate_inputs = expanded + .into_iter() + .map(|n| copy(source, result, memo, n)) + .collect(); + let new = result.nets.len(); + result.nets.push(net); + memo.insert(id, new); + new + } + let mut result = Self { + nets: Vec::new(), + outputs: Vec::new(), + state: Vec::new(), + }; + let mut memo = BTreeMap::new(); + // Keep the input case ordering, including unused inputs. + for (id, net) in self.nets.iter().enumerate() { + if matches!(net.driver, NetDriver::Input(_)) { + memo.insert(id, result.nets.len()); + result.nets.push(net.clone()); + } + } + for (name, net) in &self.outputs { + let new = copy(self, &mut result, &mut memo, *net); + result.outputs.push((name.clone(), new)); + } + Ok(result) + } + + /// Duplicate internal logic between output cones while retaining shared + /// primary inputs. Trades more gates for fewer live signals in sequential + /// physical construction. Sharing inside each output cone is preserved. + pub fn isolate_output_cones(&self) -> eyre::Result { + ensure!( + self.state.is_empty(), + "output-cone isolation requires combinational logic" + ); + let mut result = Self { + nets: Vec::new(), + outputs: Vec::new(), + state: Vec::new(), + }; + let mut inputs = BTreeMap::new(); + for (id, net) in self.nets.iter().enumerate() { + if matches!(net.driver, NetDriver::Input(_)) { + inputs.insert(id, result.nets.len()); + result.nets.push(net.clone()); + } + } + fn copy( + source: &NorNetlist, + result: &mut NorNetlist, + memo: &mut BTreeMap, + id: NetId, + cone: usize, + ) -> NetId { + if let Some(&copied) = memo.get(&id) { + return copied; + } + let mut net = source.nets[id].clone(); + net.gate_inputs = net + .gate_inputs + .iter() + .map(|&input| copy(source, result, memo, input, cone)) + .collect(); + net.name = format!("{}_cone{cone}", net.name); + let copied = result.nets.len(); + result.nets.push(net); + memo.insert(id, copied); + copied + } + for (cone, (name, net)) in self.outputs.iter().enumerate() { + let output = copy(self, &mut result, &mut inputs.clone(), *net, cone); + result.outputs.push((name.clone(), output)); + } + Ok(result) + } + pub fn from_logic_graph(graph: &LogicGraph) -> eyre::Result { let mut net_of_node = HashMap::::new(); let mut nets = Vec::new(); diff --git a/src/transform/place_and_route/local_placer/exact/nor_scaffold.rs b/src/transform/place_and_route/local_placer/exact/nor_scaffold.rs new file mode 100644 index 00000000..cbd73c63 --- /dev/null +++ b/src/transform/place_and_route/local_placer/exact/nor_scaffold.rs @@ -0,0 +1,340 @@ +//! Deterministic starting geometry for a NOR cell, not an exact optimum. +//! +//! The upper repeater/dust line restores power between inputs and merges them. +//! It needs the exact model's opt-in case-ranked dust power semantics. Callers must provide actual strong +//! power at the tap solids and independently verify the complete resulting cell. +use std::collections::{BTreeMap, BTreeSet}; + +use eyre::ensure; + +use super::CellKind; +use crate::world::block::Direction; +use crate::world::position::{DimSize, Position}; + +/// Build a bounded NOR scaffold from strong-solid taps on z=1, y=start_y. +/// A true polarity flag means that the NOR consumes the complement of that tap. +/// The returned map includes the tap solids but does not generate their drivers. +/// The final repeater at `(output_x, dim.1 - 1, 1)` points out of the box. +/// +/// This is only a candidate: it does not certify simulation, isolation, timing, +/// or completeness of a later deletion-only exact search over these positions. +pub(super) fn nor_template( + taps: &[(Position, bool)], + output_x: usize, + start_y: usize, + dim: DimSize, +) -> eyre::Result> { + ensure!(!taps.is_empty(), "NOR scaffold needs at least one tap"); + ensure!(dim.2 >= 7, "NOR scaffold needs height >= 7"); + ensure!(output_x < dim.0, "output x is outside the box"); + let last_start = start_y + .checked_add(8) + .ok_or_else(|| eyre::eyre!("start y overflow"))?; + ensure!( + last_start < dim.1, + "NOR scaffold needs y=start_y+8 inside the box" + ); + let mut tap_x = BTreeSet::new(); + for &(position, _) in taps { + ensure!( + position.1 == start_y && position.2 == 1, + "tap {position:?} must lie on y=start_y, z=1" + ); + let end = position + .0 + .checked_add(1) + .ok_or_else(|| eyre::eyre!("tap x overflow"))?; + ensure!( + end < dim.0, + "tap {position:?} needs one column to its right" + ); + ensure!(tap_x.insert(position.0), "duplicate tap x={}", position.0); + } + let ordered = tap_x.iter().copied().collect::>(); + ensure!( + ordered.windows(2).all(|pair| pair[1] - pair[0] >= 3), + "tap spacing must be at least three columns" + ); + + let mut cells = BTreeMap::new(); + fn put( + cells: &mut BTreeMap, + dim: DimSize, + position: Position, + kind: CellKind, + ) -> eyre::Result<()> { + ensure!( + dim.bound_on(position), + "scaffold block {kind:?} at {position:?} is outside {dim:?}" + ); + if let Some(previous) = cells.get(&position) { + ensure!( + *previous == kind, + "scaffold collision at {position:?}: {previous:?} versus {kind:?}" + ); + } else { + cells.insert(position, kind); + } + Ok(()) + } + fn dust( + cells: &mut BTreeMap, + dim: DimSize, + x: usize, + y: usize, + z: usize, + ) -> eyre::Result<()> { + ensure!(z > 0, "dust needs a support below it"); + put(cells, dim, Position(x, y, z - 1), CellKind::Solid)?; + put(cells, dim, Position(x, y, z), CellKind::Dust) + } + + let mut merge_x = vec![output_x]; + let mut entry_x = BTreeSet::new(); + for &(tap, inverted) in taps { + let x1 = tap.0 + 1; + put(&mut cells, dim, tap, CellKind::Solid)?; + put( + &mut cells, + dim, + Position(x1, start_y, 1), + CellKind::Torch(Direction::West), + )?; + put(&mut cells, dim, Position(x1, start_y, 2), CellKind::Solid)?; + let x = x1; + let mut z = if inverted { + 3 + } else { + put( + &mut cells, + dim, + Position(x1, start_y, 3), + CellKind::Torch(Direction::Bottom), + )?; + put(&mut cells, dim, Position(x1, start_y, 4), CellKind::Solid)?; + 5 + }; + let mut y = start_y; + dust(&mut cells, dim, x, y, z)?; + while z < 5 { + y += 1; + z += 1; + dust(&mut cells, dim, x, y, z)?; + } + while y < start_y + 2 { + y += 1; + dust(&mut cells, dim, x, y, z)?; + } + put( + &mut cells, + dim, + Position(x, start_y + 3, 4), + CellKind::Solid, + )?; + put( + &mut cells, + dim, + Position(x, start_y + 3, 5), + CellKind::Repeater(Direction::South), + )?; + merge_x.push(x); + entry_x.insert(x); + } + // Include the output column even when it lies beyond all input columns. + let left = *merge_x.iter().min().unwrap(); + let right = *merge_x.iter().max().unwrap(); + for x in left..=right { + put( + &mut cells, + dim, + Position(x, start_y + 4, 4), + CellKind::Solid, + )?; + // Sources feed actual dust entries, never a repeater side. All other + // bus positions restore power toward the output, including adjacent + // repeaters in a sparse pitch-three layout. + let kind = if x == output_x || entry_x.contains(&x) { + CellKind::Dust + } else if x < output_x { + CellKind::Repeater(Direction::West) + } else { + CellKind::Repeater(Direction::East) + }; + put(&mut cells, dim, Position(x, start_y + 4, 5), kind)?; + } + put( + &mut cells, + dim, + Position(output_x, start_y + 5, 4), + CellKind::Torch(Direction::South), + )?; + dust(&mut cells, dim, output_x, start_y + 5, 3)?; + dust(&mut cells, dim, output_x, start_y + 6, 2)?; + dust(&mut cells, dim, output_x, start_y + 7, 1)?; + for y in start_y + 8..dim.1 { + put(&mut cells, dim, Position(output_x, y, 0), CellKind::Solid)?; + put( + &mut cells, + dim, + Position(output_x, y, 1), + CellKind::Repeater(Direction::South), + )?; + } + Ok(cells) +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use super::*; + use crate::transform::place_and_route::local_placer::exact::{ + ExactLocalPlacer, ExactOutcome, ExactPlacerConfig, NorNetlist, + }; + use crate::world::simulator::Simulator; + use crate::world::World; + + #[test] + fn two_negative_taps_are_and_in_simulator_and_exact_model() -> eyre::Result<()> { + check_two_negative_scaffold(false) + } + + #[test] + fn pitch_three_negative_taps_preserve_intermediate_pass_rail() -> eyre::Result<()> { + check_two_negative_scaffold(true) + } + + fn check_two_negative_scaffold(pass_rail: bool) -> eyre::Result<()> { + let dim = DimSize(24, 11, 7); + let output = Position(7, 10, 1); + let mut inputs = vec![ + Position(9, 0, 1), + Position(if pass_rail { 15 } else { 13 }, 0, 1), + ]; + if pass_rail { + inputs.push(Position(12, 0, 1)); + } + let netlist = NorNetlist::from_text(if pass_rail { + "na=NOR(a); nb=NOR(b); out=NOR(na,nb); pass=OR(c)" + } else { + "na=NOR(a); nb=NOR(b); out=NOR(na,nb)" + })?; + let placer = ExactLocalPlacer::from_netlist(netlist); + let mut config = ExactPlacerConfig::new(dim) + .with_input_site("a", inputs[0], Direction::North) + .with_input_site("b", inputs[1], Direction::North) + .with_output_sites("out", [output]); + if pass_rail { + config = config + .with_input_site("c", inputs[2], Direction::North) + .with_output_sites("pass", [Position(12, 10, 1)]); + } + for x in 0..dim.0 { + for y in 0..dim.1 { + for z in 0..dim.2 { + config.fixed_cells.insert(Position(x, y, z), CellKind::Air); + } + } + } + config.fixed_cells.extend(nor_template( + &[ + (Position(inputs[0].0, 1, 1), true), + (Position(inputs[1].0, 1, 1), true), + ], + 7, + 1, + dim, + )?); + if pass_rail { + config + .fixed_cells + .insert(Position(12, 1, 1), CellKind::Solid); + for y in 2..dim.1 { + config + .fixed_cells + .insert(Position(12, y, 0), CellKind::Solid); + config + .fixed_cells + .insert(Position(12, y, 1), CellKind::Repeater(Direction::South)); + } + config.driving_outputs.insert("pass".into()); + } + for &input in &inputs { + config + .fixed_cells + .insert(input, CellKind::Switch(Direction::North)); + } + let mut kinds = vec![CellKind::Air; dim.0 * dim.1 * dim.2]; + for (&p, &kind) in &config.fixed_cells { + kinds[p.0 + dim.0 * (p.1 + dim.1 * p.2)] = kind; + } + let physical = super::super::verify::build_world(dim, &kinds); + let world = World::from(&physical); + let cases = 1usize << inputs.len(); + for from in 0..cases { + for to in 0..cases { + let mut sim = + Simulator::from_settled_with_limits_and_trace(&world, 4096, 2_000_000, 0)?; + for value in [from, to] { + sim.advance_idle_cycles(61)?; + sim.drive_inputs_with_limits( + inputs + .iter() + .enumerate() + .map(|(i, &p)| (p, value & (1 << i) != 0)) + .collect(), + 4096, + 2_000_000, + )?; + assert_eq!( + sim.world()[output].kind.is_powered(), + value & 3 == 3, + "physical AND mismatch: from={from},to={to},driving={value}" + ); + if pass_rail { + assert_eq!( + sim.world()[Position(12, 10, 1)].kind.is_powered(), + value & 4 != 0, + "pass rail contaminated at {from}->{to}, driving {value}" + ); + } + assert!( + !sim.world() + .iter_block() + .iter() + .any(|(p, _)| sim.is_torch_burned_out(*p)), + "physical torch burnout" + ); + } + } + } + eprintln!( + "two-negative scaffold pass_rail={pass_rail} physical static={cases}/ordered={} PASS", + cases * cases + ); + config.driving_outputs.insert("out".into()); + for (name, value) in [ + ("case_rank_levels", rsdsl::IValue::Int(8)), + ("symmetry_breaking", rsdsl::IValue::Bool(true)), + ("canonical_source_ranks", rsdsl::IValue::Bool(true)), + ] { + config.model_params.insert(name.into(), value); + } + config.stage_levels = 48; + config.time_limit = Some(Duration::from_secs(20)); + let (outcome, stats) = placer.place(&config)?; + eprintln!( + "two-negative scaffold exact placed={} encode={:?} solve={:?} refinements={}", + matches!(outcome, ExactOutcome::Placed(_)), + stats.encode_time, + stats.solve_time, + stats.refinements + ); + assert!( + matches!(outcome, ExactOutcome::Placed(_)), + "exact fixed scaffold failed: {outcome:?}" + ); + assert_eq!(stats.refinements, 0); + Ok(()) + } +} diff --git a/src/transform/place_and_route/local_placer/exact/tests.rs b/src/transform/place_and_route/local_placer/exact/tests.rs index 45269059..36b6a673 100644 --- a/src/transform/place_and_route/local_placer/exact/tests.rs +++ b/src/transform/place_and_route/local_placer/exact/tests.rs @@ -54,6 +54,101 @@ fn expect_placed(placer: &ExactLocalPlacer, config: &ExactPlacerConfig) -> Exact } } +#[test] +fn case_ranks_admit_dust_or_with_opposite_power_paths() -> eyre::Result<()> { + // Both switch supports are weakly powered by the OR dust, while each + // supplies strong power only for its own input. The justification path + // therefore runs in opposite directions for input cases 01 and 10. + let placer = ExactLocalPlacer::from_netlist(NorNetlist::from_text("out=OR(a,b)")?); + let dim = DimSize(5, 3, 2); + let mut config = ExactPlacerConfig::new(dim) + .with_input_site("a", Position(0, 1, 1), Direction::East) + .with_input_site("b", Position(4, 1, 1), Direction::West) + .with_output_sites("out", [Position(2, 1, 1)]); + for x in 0..5 { + for y in 0..3 { + for z in 0..2 { + config.fixed_cells.insert(Position(x, y, z), CellKind::Air); + } + } + } + for (position, kind) in [ + (Position(0, 1, 1), CellKind::Switch(Direction::East)), + (Position(1, 1, 1), CellKind::Solid), + (Position(2, 1, 1), CellKind::Dust), + (Position(2, 1, 0), CellKind::Solid), + (Position(3, 1, 1), CellKind::Solid), + (Position(4, 1, 1), CellKind::Switch(Direction::West)), + ] { + config.fixed_cells.insert(position, kind); + } + config.time_limit = Some(Duration::from_secs(10)); + assert!(matches!(placer.place(&config)?.0, ExactOutcome::Infeasible)); + config + .model_params + .insert("case_rank_levels".into(), rsdsl::IValue::Int(8)); + config + .model_params + .insert("symmetry_breaking".into(), rsdsl::IValue::Bool(true)); + let (outcome, stats) = placer.place(&config)?; + assert!( + matches!(outcome, ExactOutcome::Placed(_)), + "{outcome:?} {stats:?}" + ); + assert_eq!(stats.refinements, 0); + Ok(()) +} + +#[test] +fn nor_scaffold_is_verified_for_both_input_polarities() -> eyre::Result<()> { + for inverted in [false, true] { + let netlist = NorNetlist::from_text(if inverted { + "na=NOR(a); out=NOR(na,b)" + } else { + "out=NOR(a,b)" + })?; + let placer = ExactLocalPlacer::from_netlist(netlist); + let dim = DimSize(12, 11, 7); + let mut config = ExactPlacerConfig::new(dim) + .with_input_site("a", Position(1, 0, 1), Direction::North) + .with_input_site("b", Position(7, 0, 1), Direction::North) + .with_output_sites("out", [Position(5, 10, 1)]); + for x in 0..dim.0 { + for y in 0..dim.1 { + for z in 0..dim.2 { + config.fixed_cells.insert(Position(x, y, z), CellKind::Air); + } + } + } + let template = super::nor_scaffold::nor_template( + &[(Position(1, 1, 1), inverted), (Position(7, 1, 1), false)], + 5, + 1, + dim, + )?; + config.fixed_cells.extend(template); + config + .fixed_cells + .insert(Position(1, 0, 1), CellKind::Switch(Direction::North)); + config + .fixed_cells + .insert(Position(7, 0, 1), CellKind::Switch(Direction::North)); + config.driving_outputs.insert("out".into()); + config + .model_params + .insert("case_rank_levels".into(), rsdsl::IValue::Int(8)); + config.stage_levels = 48; + config.time_limit = Some(Duration::from_secs(20)); + let (outcome, stats) = placer.place(&config)?; + assert!( + matches!(outcome, ExactOutcome::Placed(_)), + "inverted={inverted} {outcome:?} {stats:?}" + ); + assert_eq!(stats.refinements, 0); + } + Ok(()) +} + #[test] fn nor_netlist_folds_or_nodes_into_gate_inputs() { let netlist = NorNetlist::from_logic_graph(&full_adder_graph("nor10")).unwrap(); @@ -71,6 +166,47 @@ fn nor_netlist_folds_or_nodes_into_gate_inputs() { } } +#[test] +fn isolated_output_cones_preserve_truth_and_only_share_inputs() { + let source = + NorNetlist::from_logic_graph(&graph(&[("shared", "~(a|b)"), ("out", "~(shared|c)")])) + .unwrap(); + let isolated = source.isolate_output_cones().unwrap(); + assert_eq!(source.input_names(), isolated.input_names()); + let before = source.net_values(); + let after = isolated.net_values(); + for ((name, old), (new_name, new)) in source.outputs.iter().zip(&isolated.outputs) { + assert_eq!(name, new_name); + assert_eq!(before[*old], after[*new]); + } + let cone = |root: NetId| { + let mut seen = std::collections::BTreeSet::new(); + let mut stack = vec![root]; + while let Some(net) = stack.pop() { + if seen.insert(net) { + stack.extend(isolated.nets[net].gate_inputs.iter().copied()); + } + } + seen + }; + let cones = isolated + .outputs + .iter() + .map(|(_, root)| cone(*root)) + .collect::>(); + for (i, left) in cones.iter().enumerate() { + for right in &cones[i + 1..] { + assert!(left + .intersection(right) + .all(|&net| matches!(isolated.nets[net].driver, NetDriver::Input(_)))); + } + } + assert!(isolated.gates().count() > source.gates().count()); + let mut sequential = source; + sequential.state.push(sequential.outputs[0].1); + assert!(sequential.isolate_output_cones().is_err()); +} + /// The raw expression, the prepared graph, and the NOR netlist must compute /// the same function, including operator chains with three operands. #[test] @@ -176,6 +312,8 @@ fn exact_placer_builds_a_verified_nor_gate() { let build = reparsed.build().unwrap(); let verification = reparsed.verify(&build).unwrap(); assert!(verification.failures.is_empty(), "{document}"); + config.no_fold = true; + expect_placed(&placer, &config); } #[test] diff --git a/src/verilog/ast.rs b/src/verilog/ast.rs index 3320b9c8..66fe255b 100644 --- a/src/verilog/ast.rs +++ b/src/verilog/ast.rs @@ -39,7 +39,7 @@ pub struct Assignment { pub struct Instance { pub module_name: String, pub instance_name: String, - pub connections: Vec<(String, String)>, + pub connections: Vec<(String, Expr)>, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -70,6 +70,21 @@ pub enum AlwaysStmt { pub enum Expr { Ident(String), Number(usize), + SizedNumber { + value: u128, + width: usize, + }, + Select { + value: Box, + msb: usize, + lsb: usize, + }, + Concat(Vec), + Mux { + select: Box, + when_true: Box, + when_false: Box, + }, Not(Box), Binary { op: BinaryOp, @@ -81,6 +96,9 @@ pub enum Expr { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BinaryOp { Add, + Equal, + GreaterEqual, + Sub, And, Xor, Or, @@ -91,10 +109,36 @@ impl Expr { match self { Expr::Ident(name) => name.clone(), Expr::Number(value) => value.to_string(), + Expr::SizedNumber { value, width } => format!("{width}'h{value:x}"), + Expr::Select { value, msb, lsb } if msb == lsb => { + format!("{}_{msb}", value.to_logic_stmt()) + } + Expr::Select { value, msb, lsb } => format!("{}[{msb}:{lsb}]", value.to_logic_stmt()), + Expr::Concat(parts) => format!( + "{{{}}}", + parts + .iter() + .map(Expr::to_logic_stmt) + .collect::>() + .join(",") + ), + Expr::Mux { + select, + when_true, + when_false, + } => format!( + "({}?{}:{})", + select.to_logic_stmt(), + when_true.to_logic_stmt(), + when_false.to_logic_stmt() + ), Expr::Not(expr) => format!("~({})", expr.to_logic_stmt()), Expr::Binary { op, left, right } => { let op = match op { BinaryOp::Add => "+", + BinaryOp::Equal => "==", + BinaryOp::GreaterEqual => ">=", + BinaryOp::Sub => "-", BinaryOp::And => "&", BinaryOp::Xor => "^", BinaryOp::Or => "|", diff --git a/src/verilog/lexer.rs b/src/verilog/lexer.rs index 60204a64..b48f77a7 100644 --- a/src/verilog/lexer.rs +++ b/src/verilog/lexer.rs @@ -14,6 +14,13 @@ pub enum Token { If, Ident(String), Number(usize), + SizedNumber { value: u128, width: usize }, + LBrace, + RBrace, + Question, + Equal, + GreaterEqual, + Minus, LParen, RParen, LBracket, @@ -55,13 +62,27 @@ pub fn lex(source: &str) -> eyre::Result> { ',' => tokens.push(Token::Comma), ';' => tokens.push(Token::Semi), ':' => tokens.push(Token::Colon), + '=' if chars.get(index + 1) == Some(&'=') => { + tokens.push(Token::Equal); + index += 2; + continue; + } '=' => tokens.push(Token::Eq), + '{' => tokens.push(Token::LBrace), + '}' => tokens.push(Token::RBrace), + '?' => tokens.push(Token::Question), '@' => tokens.push(Token::At), '~' => tokens.push(Token::Not), '&' => tokens.push(Token::And), '^' => tokens.push(Token::Xor), '|' => tokens.push(Token::Or), '+' => tokens.push(Token::Plus), + '-' => tokens.push(Token::Minus), + '>' if chars.get(index + 1) == Some(&'=') => { + tokens.push(Token::GreaterEqual); + index += 2; + continue; + } '*' => tokens.push(Token::Star), '<' if chars.get(index + 1) == Some(&'=') => { tokens.push(Token::Le); @@ -75,7 +96,41 @@ pub fn lex(source: &str) -> eyre::Result> { index += 1; } let text = chars[start..index].iter().collect::(); - tokens.push(Token::Number(text.parse()?)); + if chars.get(index) == Some(&'\'') { + let width: usize = text.parse()?; + eyre::ensure!( + (1..=128).contains(&width), + "literal width must be in 1..=128" + ); + index += 1; + let radix = match chars.get(index).map(char::to_ascii_lowercase) { + Some('b') => 2, + Some('h') => 16, + Some('d') => 10, + _ => eyre::bail!("expected unsigned sized binary, hex, or decimal literal"), + }; + index += 1; + let start = index; + while index < chars.len() + && (chars[index].is_ascii_alphanumeric() || chars[index] == '_') + { + index += 1; + } + let digits = chars[start..index] + .iter() + .filter(|c| **c != '_') + .collect::(); + let value = u128::from_str_radix(&digits, radix) + .map_err(|_| eyre::eyre!("invalid two-state sized literal `{digits}`"))?; + let value = if width == 128 { + value + } else { + value & ((1u128 << width) - 1) + }; + tokens.push(Token::SizedNumber { value, width }); + } else { + tokens.push(Token::Number(text.parse()?)); + } continue; } ch if is_ident_start(ch) => { @@ -142,6 +197,31 @@ fn is_ident_continue(ch: char) -> bool { mod tests { use super::{lex, Token}; + #[test] + fn sized_literals_are_two_state_and_keep_declared_width() -> eyre::Result<()> { + assert_eq!( + lex("4'h1f 8'b1010_0101 7'd127")?, + vec![ + Token::SizedNumber { + value: 15, + width: 4 + }, + Token::SizedNumber { + value: 165, + width: 8 + }, + Token::SizedNumber { + value: 127, + width: 7 + }, + ] + ); + for bad in ["0'h0", "129'h1", "4'b10xz", "8'shff", "4'h"] { + assert!(lex(bad).is_err(), "{bad}"); + } + Ok(()) + } + #[test] fn lexes_combinational_module_subset() -> eyre::Result<()> { let tokens = lex(r#" diff --git a/src/verilog/lower.rs b/src/verilog/lower.rs index d8015e32..e7d1faff 100644 --- a/src/verilog/lower.rs +++ b/src/verilog/lower.rs @@ -54,12 +54,23 @@ fn collect_assignments( .connections .iter() .map(|(port, signal)| { - ( + let signal = match signal { + Expr::Ident(name) => name.clone(), + Expr::Select { value, msb, lsb } + if msb == lsb && matches!(value.as_ref(), Expr::Ident(_)) => + { + signal.to_logic_stmt() + } + _ => eyre::bail!( + "expression instance bindings require typed logical IR lowering" + ), + }; + Ok(( port.clone(), - rewrite_signal(signal, instance_prefix, substitutions), - ) + rewrite_signal(&signal, instance_prefix, substitutions), + )) }) - .collect::>(); + .collect::>>()?; assignments.extend(collect_assignments( context, child, @@ -85,6 +96,27 @@ fn rewrite_expr( match expr { Expr::Ident(name) => Expr::Ident(rewrite_signal(name, instance_prefix, substitutions)), Expr::Number(value) => Expr::Number(*value), + Expr::SizedNumber { .. } => expr.clone(), + Expr::Select { value, msb, lsb } => Expr::Select { + value: Box::new(rewrite_expr(value, instance_prefix, substitutions)), + msb: *msb, + lsb: *lsb, + }, + Expr::Concat(parts) => Expr::Concat( + parts + .iter() + .map(|p| rewrite_expr(p, instance_prefix, substitutions)) + .collect(), + ), + Expr::Mux { + select, + when_true, + when_false, + } => Expr::Mux { + select: Box::new(rewrite_expr(select, instance_prefix, substitutions)), + when_true: Box::new(rewrite_expr(when_true, instance_prefix, substitutions)), + when_false: Box::new(rewrite_expr(when_false, instance_prefix, substitutions)), + }, Expr::Not(expr) => Expr::Not(Box::new(rewrite_expr(expr, instance_prefix, substitutions))), Expr::Binary { op, left, right } => Expr::Binary { op: *op, diff --git a/src/verilog/parser.rs b/src/verilog/parser.rs index e9caad9e..5cb75623 100644 --- a/src/verilog/parser.rs +++ b/src/verilog/parser.rs @@ -186,7 +186,7 @@ impl Parser { }) } - fn parse_named_connections(&mut self) -> eyre::Result> { + fn parse_named_connections(&mut self) -> eyre::Result> { let mut connections = vec![self.parse_named_connection()?]; while self.consume(&Token::Comma) { connections.push(self.parse_named_connection()?); @@ -194,20 +194,32 @@ impl Parser { Ok(connections) } - fn parse_named_connection(&mut self) -> eyre::Result<(String, String)> { + fn parse_named_connection(&mut self) -> eyre::Result<(String, Expr)> { if !self.consume(&Token::Dot) { eyre::bail!("unsupported Verilog construct: positional instance ports"); } let port = self.expect_ident()?; self.expect(Token::LParen)?; - let signal = self.parse_signal_name()?; + let signal = self.parse_expr()?; self.expect(Token::RParen)?; Ok((port, signal)) } fn parse_expr(&mut self) -> eyre::Result { - self.parse_or() + let select = self.parse_or()?; + if self.consume(&Token::Question) { + let when_true = self.parse_expr()?; + self.expect(Token::Colon)?; + let when_false = self.parse_expr()?; + Ok(Expr::Mux { + select: Box::new(select), + when_true: Box::new(when_true), + when_false: Box::new(when_false), + }) + } else { + Ok(select) + } } fn parse_or(&mut self) -> eyre::Result { @@ -237,9 +249,9 @@ impl Parser { } fn parse_and(&mut self) -> eyre::Result { - let mut expr = self.parse_add()?; + let mut expr = self.parse_equal()?; while self.consume(&Token::And) { - let rhs = self.parse_add()?; + let rhs = self.parse_equal()?; expr = Expr::Binary { op: BinaryOp::And, left: Box::new(expr), @@ -249,12 +261,45 @@ impl Parser { Ok(expr) } + fn parse_equal(&mut self) -> eyre::Result { + let mut expr = self.parse_compare()?; + while self.consume(&Token::Equal) { + let right = self.parse_compare()?; + expr = Expr::Binary { + op: BinaryOp::Equal, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + fn parse_compare(&mut self) -> eyre::Result { + let mut expr = self.parse_add()?; + while self.consume(&Token::GreaterEqual) { + let right = self.parse_add()?; + expr = Expr::Binary { + op: BinaryOp::GreaterEqual, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + fn parse_add(&mut self) -> eyre::Result { let mut expr = self.parse_unary()?; - while self.consume(&Token::Plus) { + loop { + let op = if self.consume(&Token::Plus) { + BinaryOp::Add + } else if self.consume(&Token::Minus) { + BinaryOp::Sub + } else { + break; + }; let rhs = self.parse_unary()?; expr = Expr::Binary { - op: BinaryOp::Add, + op, left: Box::new(expr), right: Box::new(rhs), }; @@ -273,10 +318,33 @@ impl Parser { fn parse_primary(&mut self) -> eyre::Result { match self.next() { Some(Token::Ident(name)) => { - let name = self.finish_signal_name(name)?; - Ok(Expr::Ident(name)) + let mut value = Expr::Ident(name); + if self.consume(&Token::LBracket) { + let msb = self.expect_number()?; + let lsb = if self.consume(&Token::Colon) { + self.expect_number()? + } else { + msb + }; + self.expect(Token::RBracket)?; + value = Expr::Select { + value: Box::new(value), + msb, + lsb, + }; + } + Ok(value) } Some(Token::Number(value)) => Ok(Expr::Number(value)), + Some(Token::SizedNumber { value, width }) => Ok(Expr::SizedNumber { value, width }), + Some(Token::LBrace) => { + let mut parts = vec![self.parse_expr()?]; + while self.consume(&Token::Comma) { + parts.push(self.parse_expr()?); + } + self.expect(Token::RBrace)?; + Ok(Expr::Concat(parts)) + } Some(Token::LParen) => { let expr = self.parse_expr()?; self.expect(Token::RParen)?; @@ -297,16 +365,8 @@ impl Parser { fn parse_signal_name(&mut self) -> eyre::Result { let name = self.expect_ident()?; - self.finish_signal_name(name) - } - - fn finish_signal_name(&mut self, name: String) -> eyre::Result { - if self.consume(&Token::LBracket) { - let index = self.expect_number()?; - self.expect(Token::RBracket)?; - return Ok(format!("{name}_{index}")); - } - + eyre::ensure!(self.peek() != Some(&Token::LBracket), + "static selects are currently supported only in expressions, not procedural destinations, conditions, or clocks"); Ok(name) } @@ -373,6 +433,47 @@ mod tests { use super::parse_module; use crate::verilog::ast::{AlwaysSensitivity, AlwaysStmt, BinaryOp, Expr, PortDirection}; + #[test] + fn rejects_procedural_select_instead_of_aliasing_another_signal() { + assert!(parse_module("module m(clk,a,a_0,q); input clk; input [1:0] a; input a_0; output reg q; always @(posedge clk) begin if (a[0]) begin q <= a_0; end end endmodule").is_err()); + } + + #[test] + fn named_input_port_bindings_preserve_expressions() -> eyre::Result<()> { + let module = parse_module("module m(a,y); input [7:0] a; output [3:0] y; child u(.x({1'b0,a[6:4]}), .y(y)); endmodule")?; + assert!( + matches!(&module.instances[0].connections[0].1, Expr::Concat(parts) if parts.len() == 2) + ); + assert!(matches!(&module.instances[0].connections[1].1, Expr::Ident(name) if name == "y")); + Ok(()) + } + + #[test] + fn parses_right_associative_mux_and_real_bus_select() -> eyre::Result<()> { + let module = parse_module("module m(a,y); input [3:0] a; output [6:0] y; assign y = a == 4'd0 ? 7'h3f : a[0] ? 7'h06 : 7'h00; endmodule")?; + let Expr::Mux { + select, when_false, .. + } = &module.assignments[0].expr + else { + panic!("expected mux") + }; + assert!(matches!( + select.as_ref(), + Expr::Binary { + op: BinaryOp::Equal, + .. + } + )); + let Expr::Mux { select, .. } = when_false.as_ref() else { + panic!("expected right associative mux") + }; + assert!(matches!( + select.as_ref(), + Expr::Select { msb: 0, lsb: 0, .. } + )); + Ok(()) + } + #[test] fn parses_half_adder_module() -> eyre::Result<()> { let module = parse_module( diff --git a/src/verilog/rtl.rs b/src/verilog/rtl.rs index e4d3563a..16c5d291 100644 --- a/src/verilog/rtl.rs +++ b/src/verilog/rtl.rs @@ -79,9 +79,22 @@ pub enum RtlAssignKind { pub enum RtlExpr { Signal(RtlSignalRef), Const { - value: usize, + value: u128, width: usize, }, + Select { + value: Box, + msb: usize, + lsb: usize, + }, + Concat(Vec), + Resize { + value: Box, + width: usize, + }, + Equal(Box, Box), + GreaterEqual(Box, Box), + Sub(Box, Box), Not(Box), Add(Box, Box), Mux { @@ -105,6 +118,26 @@ pub struct RtlModule { } impl RtlModule { + /// Elaborate an unsigned input-port expression in the declared child-port width. + /// Output bindings must be checked separately and restricted to named signals. + pub fn elaborate_expr(&self, expr: &AstExpr, expected_width: usize) -> eyre::Result { + eyre::ensure!( + (1..=128).contains(&expected_width), + "port width must be in 1..=128" + ); + let value = elaborate( + expr, + Some(expected_width), + &self.signal_index, + &self.signals, + )?; + resize(value, expected_width, &self.signals) + } + + pub fn expr_width(&self, expr: &RtlExpr) -> eyre::Result { + rtl_expr_width(expr, &self.signals) + } + pub fn signal_ref(&self, name: &str) -> eyre::Result { self.signal_index .get(name) @@ -138,11 +171,15 @@ pub fn lower_rtl_module(module: &VerilogModule) -> eyre::Result { for declaration in &module.declarations { let kind = rtl_signal_kind(declaration.direction)?; - let width = declaration - .range - .as_ref() - .map(|range| range.msb.abs_diff(range.lsb) + 1) - .unwrap_or(1); + let width = if let Some(range) = &declaration.range { + eyre::ensure!( + range.lsb == 0 && range.msb < 128, + "only unsigned packed [N-1:0] vectors of width 1..=128 are supported" + ); + range.msb + 1 + } else { + 1 + }; for name in &declaration.names { if signal_index.contains_key(name) { eyre::bail!("duplicate RTL signal `{name}` in module `{}`", module.name); @@ -175,7 +212,7 @@ pub fn lower_rtl_module(module: &VerilogModule) -> eyre::Result { let continuous_assigns = module .assignments .iter() - .map(|assignment| lower_continuous_assign(assignment, &signal_index)) + .map(|assignment| lower_continuous_assign(assignment, &signal_index, &signals)) .collect::>>()?; let processes = module @@ -190,7 +227,7 @@ pub fn lower_rtl_module(module: &VerilogModule) -> eyre::Result { }; Ok(RtlProcess { sensitivity, - statements: vec![lower_always_stmt(&always.body, &signal_index)?], + statements: vec![lower_always_stmt(&always.body, &signal_index, &signals)?], }) }) .collect::>>()?; @@ -218,16 +255,23 @@ fn rtl_signal_kind(direction: Option) -> eyre::Result, + signals: &[RtlSignal], ) -> eyre::Result { Ok(RtlAssign { output: signal_ref(&assignment.output, signal_index)?, - expr: lower_expr(&assignment.expr, signal_index)?, + expr: lower_assignment_expr( + &assignment.expr, + signal_ref(&assignment.output, signal_index)?, + signal_index, + signals, + )?, }) } fn lower_always_stmt( stmt: &AstAlwaysStmt, signal_index: &HashMap, + signals: &[RtlSignal], ) -> eyre::Result { match stmt { AstAlwaysStmt::If { @@ -235,13 +279,18 @@ fn lower_always_stmt( then_branch, } => Ok(RtlStmt::If { condition: signal_expr(condition, signal_index)?, - then_branch: vec![lower_always_stmt(then_branch, signal_index)?], + then_branch: vec![lower_always_stmt(then_branch, signal_index, signals)?], else_branch: Vec::new(), }), AstAlwaysStmt::NonBlockingAssign { output, data } => Ok(RtlStmt::Assign { kind: RtlAssignKind::NonBlocking, lhs: signal_ref(output, signal_index)?, - rhs: lower_expr(data, signal_index)?, + rhs: lower_assignment_expr( + data, + signal_ref(output, signal_index)?, + signal_index, + signals, + )?, }), } } @@ -253,7 +302,7 @@ fn lower_expr( match expr { AstExpr::Ident(name) => signal_expr(name, signal_index), AstExpr::Number(value) => Ok(RtlExpr::Const { - value: *value, + value: *value as u128, width: constant_width(*value), }), AstExpr::Not(expr) => Ok(RtlExpr::Not(Box::new(lower_expr(expr, signal_index)?))), @@ -261,15 +310,242 @@ fn lower_expr( let left = Box::new(lower_expr(left, signal_index)?); let right = Box::new(lower_expr(right, signal_index)?); Ok(match op { + crate::verilog::ast::BinaryOp::Equal => RtlExpr::Equal(left, right), + crate::verilog::ast::BinaryOp::GreaterEqual => RtlExpr::GreaterEqual(left, right), + crate::verilog::ast::BinaryOp::Sub => RtlExpr::Sub(left, right), crate::verilog::ast::BinaryOp::Add => RtlExpr::Add(left, right), crate::verilog::ast::BinaryOp::And => RtlExpr::And(left, right), crate::verilog::ast::BinaryOp::Xor => RtlExpr::Xor(left, right), crate::verilog::ast::BinaryOp::Or => RtlExpr::Or(left, right), }) } + _ => eyre::bail!("extended expression requires typed elaboration"), } } +// Legacy scalar/counter syntax retains its historical unsized-number behavior. +// New packed-expression syntax requires explicitly sized constants. +fn uses_typed_syntax(expr: &AstExpr) -> bool { + match expr { + AstExpr::Ident(_) | AstExpr::Number(_) => false, + AstExpr::Not(value) => uses_typed_syntax(value), + AstExpr::Binary { op, left, right } => { + matches!( + op, + crate::verilog::ast::BinaryOp::Equal + | crate::verilog::ast::BinaryOp::GreaterEqual + | crate::verilog::ast::BinaryOp::Sub + ) || uses_typed_syntax(left) + || uses_typed_syntax(right) + } + _ => true, + } +} + +fn has_unsized_literal(expr: &AstExpr) -> bool { + match expr { + AstExpr::Number(_) => true, + AstExpr::Ident(_) | AstExpr::SizedNumber { .. } => false, + AstExpr::Not(value) | AstExpr::Select { value, .. } => has_unsized_literal(value), + AstExpr::Binary { left, right, .. } => { + has_unsized_literal(left) || has_unsized_literal(right) + } + AstExpr::Concat(parts) => parts.iter().any(has_unsized_literal), + AstExpr::Mux { + select, + when_true, + when_false, + } => { + has_unsized_literal(select) + || has_unsized_literal(when_true) + || has_unsized_literal(when_false) + } + } +} + +fn lower_assignment_expr( + expr: &AstExpr, + output: RtlSignalRef, + index: &HashMap, + signals: &[RtlSignal], +) -> eyre::Result { + if has_unsized_literal(expr) && !uses_typed_syntax(expr) { + return lower_expr(expr, index); + } + let target = signals[output.signal.0].width; + let value = elaborate(expr, Some(target), index, signals)?; + resize(value, target, signals) +} + +fn rtl_expr_width(expr: &RtlExpr, signals: &[RtlSignal]) -> eyre::Result { + Ok(match expr { + RtlExpr::Signal(signal) => { + signals + .get(signal.signal.0) + .ok_or_else(|| eyre::eyre!("unknown RTL signal"))? + .width + } + RtlExpr::Const { width, .. } | RtlExpr::Resize { width, .. } => *width, + RtlExpr::Select { msb, lsb, .. } => msb - lsb + 1, + RtlExpr::Concat(parts) => parts + .iter() + .map(|p| rtl_expr_width(p, signals)) + .collect::>>()? + .iter() + .sum(), + RtlExpr::Equal(..) | RtlExpr::GreaterEqual(..) => 1, + RtlExpr::Not(value) => rtl_expr_width(value, signals)?, + RtlExpr::Sub(a, b) + | RtlExpr::Add(a, b) + | RtlExpr::And(a, b) + | RtlExpr::Or(a, b) + | RtlExpr::Xor(a, b) => rtl_expr_width(a, signals)?.max(rtl_expr_width(b, signals)?), + RtlExpr::Mux { + when_true, + when_false, + .. + } => rtl_expr_width(when_true, signals)?.max(rtl_expr_width(when_false, signals)?), + }) +} + +fn resize(value: RtlExpr, width: usize, signals: &[RtlSignal]) -> eyre::Result { + if rtl_expr_width(&value, signals)? == width { + Ok(value) + } else { + Ok(RtlExpr::Resize { + value: Box::new(value), + width, + }) + } +} + +fn natural_width( + expr: &AstExpr, + index: &HashMap, + signals: &[RtlSignal], +) -> eyre::Result { + let width = match expr { + AstExpr::Ident(name) => signals[signal_ref(name, index)?.signal.0].width, + AstExpr::Number(_) => eyre::bail!("unsized constants are unsupported in packed expressions; use an explicitly sized literal"), + AstExpr::SizedNumber { width, .. } => *width, + AstExpr::Select { value, msb, lsb } => { + let width = natural_width(value, index, signals)?; + eyre::ensure!(lsb <= msb && *msb < width, "static select [{msb}:{lsb}] is outside width {width} or ascending"); + msb - lsb + 1 + } + AstExpr::Concat(parts) => { + eyre::ensure!(!parts.is_empty(), "empty concatenation"); + parts.iter().map(|p| natural_width(p, index, signals)).collect::>>()?.iter().sum() + } + AstExpr::Not(value) => natural_width(value, index, signals)?, + AstExpr::Binary { op, left, right } => { + let width = natural_width(left, index, signals)?.max(natural_width(right, index, signals)?); + if matches!(op, crate::verilog::ast::BinaryOp::Equal | crate::verilog::ast::BinaryOp::GreaterEqual) { 1 } else { width } + } + AstExpr::Mux { select, when_true, when_false } => { + natural_width(select, index, signals)?; + natural_width(when_true, index, signals)?.max(natural_width(when_false, index, signals)?) + } + }; + eyre::ensure!( + (1..=128).contains(&width), + "packed expression width must be in 1..=128, got {width}" + ); + Ok(width) +} + +fn elaborate( + expr: &AstExpr, + context: Option, + index: &HashMap, + signals: &[RtlSignal], +) -> eyre::Result { + use crate::verilog::ast::BinaryOp; + let natural = natural_width(expr, index, signals)?; + let width = natural.max(context.unwrap_or(0)); + let value = match expr { + AstExpr::Ident(name) => signal_expr(name, index)?, + AstExpr::SizedNumber { value, width } => RtlExpr::Const { + value: *value, + width: *width, + }, + AstExpr::Number(_) => unreachable!("natural_width rejects unsized literals"), + AstExpr::Select { value, msb, lsb } => RtlExpr::Select { + value: Box::new(elaborate(value, None, index, signals)?), + msb: *msb, + lsb: *lsb, + }, + AstExpr::Concat(parts) => RtlExpr::Concat( + parts + .iter() + .map(|p| elaborate(p, None, index, signals)) + .collect::>()?, + ), + AstExpr::Not(value) => RtlExpr::Not(Box::new(resize( + elaborate(value, Some(width), index, signals)?, + width, + signals, + )?)), + AstExpr::Binary { op, left, right } => { + let operand_width = if matches!(op, BinaryOp::Equal | BinaryOp::GreaterEqual) { + natural_width(left, index, signals)?.max(natural_width(right, index, signals)?) + } else { + width + }; + let left = Box::new(resize( + elaborate(left, Some(operand_width), index, signals)?, + operand_width, + signals, + )?); + let right = Box::new(resize( + elaborate(right, Some(operand_width), index, signals)?, + operand_width, + signals, + )?); + match op { + BinaryOp::Add => RtlExpr::Add(left, right), + BinaryOp::And => RtlExpr::And(left, right), + BinaryOp::Or => RtlExpr::Or(left, right), + BinaryOp::Xor => RtlExpr::Xor(left, right), + BinaryOp::Equal => RtlExpr::Equal(left, right), + BinaryOp::GreaterEqual => RtlExpr::GreaterEqual(left, right), + BinaryOp::Sub => RtlExpr::Sub(left, right), + } + } + AstExpr::Mux { + select, + when_true, + when_false, + } => { + let mut select = elaborate(select, None, index, signals)?; + let select_width = rtl_expr_width(&select, signals)?; + if select_width != 1 { + select = RtlExpr::Not(Box::new(RtlExpr::Equal( + Box::new(select), + Box::new(RtlExpr::Const { + value: 0, + width: select_width, + }), + ))); + } + RtlExpr::Mux { + select: Box::new(select), + when_true: Box::new(resize( + elaborate(when_true, Some(width), index, signals)?, + width, + signals, + )?), + when_false: Box::new(resize( + elaborate(when_false, Some(width), index, signals)?, + width, + signals, + )?), + } + } + }; + Ok(value) +} + fn constant_width(value: usize) -> usize { (usize::BITS - value.leading_zeros()).max(1) as usize } @@ -294,6 +570,67 @@ mod tests { use super::*; use crate::verilog::parser::parse_module; + #[test] + fn input_port_expression_uses_child_width_context() -> eyre::Result<()> { + let ast = parse_module("module m(a); input [7:0] a; child u(.x(a + 8'd1)); endmodule")?; + let rtl = lower_rtl_module(&ast)?; + let expr = rtl.elaborate_expr(&ast.instances[0].connections[0].1, 9)?; + let RtlExpr::Add(left, right) = &expr else { + panic!("expected add") + }; + assert_eq!(rtl.expr_width(left)?, 9); + assert_eq!(rtl.expr_width(right)?, 9); + assert_eq!(rtl.expr_width(&expr)?, 9); + Ok(()) + } + + #[test] + fn packed_add_extends_before_arithmetic_and_truncates_after() -> eyre::Result<()> { + let rtl = lower("module m(a,b,y,z); input [7:0] a,b; output [8:0] y; output [3:0] z; assign y = a + {1'b0,b}; assign z = a + 8'd1; endmodule")?; + assert_eq!(rtl.expr_width(&rtl.continuous_assigns[0].expr)?, 9); + let RtlExpr::Add(left, right) = &rtl.continuous_assigns[0].expr else { + panic!("expected add") + }; + assert_eq!(rtl.expr_width(left)?, 9); + assert_eq!(rtl.expr_width(right)?, 9); + let RtlExpr::Resize { value, width: 4 } = &rtl.continuous_assigns[1].expr else { + panic!("expected output truncation") + }; + assert_eq!(rtl.expr_width(value)?, 8); + Ok(()) + } + + #[test] + fn packed_select_concat_and_multibit_condition_have_explicit_widths() -> eyre::Result<()> { + let rtl = lower("module m(a,y); input [7:0] a; output [4:0] y; assign y = a ? {1'b1,a[7:4]} : 5'd0; endmodule")?; + let RtlExpr::Mux { + select, + when_true, + when_false, + } = &rtl.continuous_assigns[0].expr + else { + panic!("expected mux") + }; + assert_eq!(rtl.expr_width(select)?, 1); + assert_eq!(rtl.expr_width(when_true)?, 5); + assert_eq!(rtl.expr_width(when_false)?, 5); + assert!(matches!(select.as_ref(), RtlExpr::Not(_))); + Ok(()) + } + + #[test] + fn rejects_unsupported_packed_semantics() { + for source in [ + "module m(a,y); input [0:7] a; output y; assign y = a[0]; endmodule", + "module m(a,y); input [7:4] a; output y; assign y = a[4]; endmodule", + "module m(a,y); input [7:0] a; output y; assign y = a[8]; endmodule", + "module m(a,y); input [7:0] a; output y; assign y = a[0:2]; endmodule", + "module m(a,y); input [7:0] a; output y; assign y = a == 0; endmodule", + ] { + assert!(lower(source).is_err(), "unexpectedly accepted {source}"); + } + } + #[test] fn lowers_latch_and_clocked_process_shapes() -> eyre::Result<()> { let rtl = lower(d_latch_source())?; diff --git a/src/world/mod.rs b/src/world/mod.rs index 596c506c..2620ac59 100644 --- a/src/world/mod.rs +++ b/src/world/mod.rs @@ -1,11 +1,11 @@ use std::cmp; -use std::collections::BTreeMap; use std::fmt::Debug; use std::ops::{Index, IndexMut}; +use std::sync::Arc; use block::{Block, BlockKind, Direction, RedstoneState, RedstoneStateType}; use itertools::Itertools; -use position::{DimSize, Position, PositionIndex}; +use position::{DimSize, Position}; pub mod block; pub mod gate; @@ -30,15 +30,16 @@ impl World { #[derive(Clone, Default)] pub struct World3D { pub size: DimSize, - // z, y, z - pub map: Vec>>, + // [z][y][x]. Clones share planes and rows. A write detaches only its + // plane's row handles and its row's blocks; untouched planes remain shared. + pub map: Vec>>>>, } impl World3D { pub fn new(size: DimSize) -> Self { Self { size, - map: vec![vec![vec![Block::default(); size.0]; size.1]; size.2], + map: vec![Arc::new(vec![Arc::new(vec![Block::default(); size.0]); size.1]); size.2], } } @@ -216,34 +217,17 @@ impl World3D { impl<'a> From<&'a World> for World3D { fn from(value: &'a World) -> Self { - let mut block_map: BTreeMap = BTreeMap::default(); - - for block in &value.blocks { - block_map.insert(block.0.index(&value.size), &block.1); - } - - let map: Vec>> = (0..value.size.2) - .map(|z| { - (0..value.size.1) - .map(|y| { - (0..value.size.0) - .map(|x| { - let pos = PositionIndex( - x + y * value.size.0 + z * value.size.0 * value.size.1, - ); - - block_map.get(&pos).map(|&&block| block).unwrap_or_default() - }) - .collect() - }) - .collect() - }) - .collect(); - - Self { - size: value.size, - map, + let mut world = Self::new(value.size); + // Sparse worlds may contain repeated coordinates: applying in order + // preserves the previous last-entry-wins semantics, including Air. + for (position, block) in &value.blocks { + // Never flatten invalid coordinates: an oversized x could alias a + // different valid row in the former linear-index lookup. + if value.size.bound_on(*position) { + world[*position] = *block; + } } + world } } @@ -266,7 +250,8 @@ impl Index for World3D { impl IndexMut for World3D { fn index_mut(&mut self, index: Position) -> &mut Self::Output { - &mut self.map[index.2][index.1][index.0] + let plane = Arc::make_mut(&mut self.map[index.2]); + &mut Arc::make_mut(&mut plane[index.1])[index.0] } } @@ -299,3 +284,90 @@ impl Debug for World3D { Ok(()) } } + +#[cfg(test)] +mod storage_tests { + use super::*; + + #[test] + fn cloning_shares_planes_and_rows_and_detaches_only_written_regions() { + let p = Position(1, 1, 1); + let mut original = World3D::new(DimSize(4, 3, 2)); + original[p].kind = BlockKind::Switch { is_on: false }; + let mut branch = original.clone(); + let sibling = original.clone(); + for z in 0..2 { + assert!(Arc::ptr_eq(&original.map[z], &branch.map[z])); + for y in 0..3 { + assert!(Arc::ptr_eq(&original.map[z][y], &branch.map[z][y])); + } + } + branch[p].kind = BlockKind::Switch { is_on: true }; + assert!(Arc::ptr_eq(&original.map[0], &branch.map[0])); + assert!(!Arc::ptr_eq(&original.map[1], &branch.map[1])); + assert!(!Arc::ptr_eq(&original.map[1][1], &branch.map[1][1])); + for z in 0..2 { + for y in 0..3 { + if (z, y) != (1, 1) { + assert!(Arc::ptr_eq(&original.map[z][y], &branch.map[z][y])); + } + } + } + assert!(!original[p].kind.is_powered()); + assert!(!sibling[p].kind.is_powered()); + assert!(branch[p].kind.is_powered()); + let detached_plane = Arc::as_ptr(&branch.map[1]); + let detached_row = Arc::as_ptr(&branch.map[1][1]); + branch[Position(2, 1, 1)].kind = BlockKind::RedstoneBlock; + assert_eq!(Arc::as_ptr(&branch.map[1]), detached_plane); + assert_eq!(Arc::as_ptr(&branch.map[1][1]), detached_row); + assert!(original[Position(2, 1, 1)].kind.is_air()); + // Mutating the source after the fork must also preserve both branches. + original[p].kind = BlockKind::RedstoneBlock; + assert_eq!(sibling[p].kind, BlockKind::Switch { is_on: false }); + assert_eq!(branch[p].kind, BlockKind::Switch { is_on: true }); + } + + #[test] + fn sparse_conversion_keeps_last_duplicates_and_ignores_invalid_coordinates() { + let p = Position(1, 1, 1); + let q = Position(2, 1, 1); + let powered = Block { + kind: BlockKind::RedstoneBlock, + ..Default::default() + }; + let world = World { + size: DimSize(3, 2, 2), + blocks: vec![ + (p, powered), + (p, Block::default()), + (q, Block::default()), + (q, powered), + (Position(3, 0, 0), powered), // must not alias (0,1,0) + (Position(0, 2, 0), powered), // must not alias (0,0,1) + (Position(0, 0, 2), powered), + (Position(usize::MAX, usize::MAX, usize::MAX), powered), + ], + }; + let dense = World3D::from(&world); + assert_eq!(dense.iter_block(), vec![(q, powered)]); + assert!(dense[p].kind.is_air()); + assert!(dense[Position(0, 1, 0)].kind.is_air()); + assert!(dense[Position(0, 0, 1)].kind.is_air()); + } + + #[test] + fn sparse_world_conversion_and_shared_empty_rows_preserve_coordinates() { + let mut empty = World3D::new(DimSize(3, 2, 2)); + empty[Position(2, 1, 1)].kind = BlockKind::RedstoneBlock; + assert_eq!(empty.iter_block().len(), 1); + assert!(empty[Position(2, 0, 0)].kind.is_air()); + let sparse = World::from(&empty); + let mut roundtrip = World3D::from(&sparse); + assert_eq!(roundtrip.iter_block(), empty.iter_block()); + let branch = roundtrip.clone(); + roundtrip[Position(2, 1, 1)] = Block::default(); + assert!(roundtrip.iter_block().is_empty()); + assert_eq!(branch.iter_block(), empty.iter_block()); + } +} diff --git a/src/world/simulator.rs b/src/world/simulator.rs index 2bcc19ab..cbe4a713 100644 --- a/src/world/simulator.rs +++ b/src/world/simulator.rs @@ -1217,6 +1217,13 @@ impl Simulator { let started = self.profile.as_ref().map(|_| Instant::now()); let mut relaxation_passes = 0; let mut targets_evaluated = 0; + // Relaxation passes are numerical iterations, not physical ticks. + // Notify consumers only after the dust network reaches its fixed point. + let before = self + .redstone_positions + .iter() + .map(|&pos| (pos, self.world[pos])) + .collect::>(); let mut any_changed = false; let mut changed = true; while changed { @@ -1240,42 +1247,68 @@ impl Simulator { } else { self.redstone_input_strength(pos) }; - (next_strength != strength).then_some((pos, strength, next_strength, block)) + (next_strength != strength).then_some((pos, next_strength)) }) .collect::>(); - for (pos, previous_strength, next_strength, block) in next_strengths { + for (pos, next_strength) in next_strengths { let BlockKind::Redstone { strength, .. } = &mut self.world[pos].kind else { continue; }; *strength = next_strength; - if previous_strength > 0 && next_strength == 0 { - let BlockKind::Redstone { state, .. } = block.kind else { - unreachable!() - }; - for target in self.redstone_propagate_targets(pos, state) { - if !self.world.size.bound_on(target) { + changed = true; + any_changed = true; + } + } + + for (pos, before_block) in before { + let BlockKind::Redstone { + strength: before_strength, + state, + .. + } = before_block.kind + else { + continue; + }; + let BlockKind::Redstone { + strength: after_strength, + .. + } = self.world[pos].kind + else { + continue; + }; + let is_on = after_strength > 0; + if (before_strength > 0) == is_on { + continue; + } + for target in self.redstone_propagate_targets(pos, state) { + if !self.world.size.bound_on(target) { + continue; + } + let event_type = if self.world[target].kind.is_redstone() { + if is_on { + if after_strength <= 1 { continue; } - let event_type = if self.world[target].kind.is_redstone() { - EventType::RedstoneOff - } else { - self.redstone_output_event_type(target, false) - }; - self.schedule_event( - 0, - Event { - id: None, - from_id: None, - event_type, - target_position: target, - direction: target.diff(pos), - }, - ); + EventType::RedstoneOn { + strength: after_strength - 1, + } + } else { + EventType::RedstoneOff } - } - changed = true; - any_changed = true; + } else { + self.redstone_output_event_type(target, is_on) + }; + self.schedule_event( + 0, + Event { + id: None, + from_id: None, + event_type, + target_position: target, + direction: target.diff(pos), + }, + ); } } @@ -1331,6 +1364,8 @@ impl Simulator { let (next_on_count, next_on_base_count) = self.cobble_power_counts(pos); (next_on_count != on_count || next_on_base_count != on_base_count).then_some(( pos, + on_count, + on_base_count, next_on_count, next_on_base_count, )) @@ -1338,11 +1373,49 @@ impl Simulator { .collect::>(); let changed = !updates.is_empty(); - for (pos, next_on_count, next_on_base_count) in updates { + for (pos, before_count, before_base_count, next_on_count, next_on_base_count) in updates { self.world[pos].kind = BlockKind::Cobble { on_count: next_on_count, on_base_count: next_on_base_count, }; + // Counts are reconstructed from settled physical sources. Notify + // consumers when this correction changes their logical power, + // just as the ordinary cobble event path does. + for target in pos.forwards() { + if !self.world.size.bound_on(target) { + continue; + } + let kind = self.world[target].kind; + let event_type = if kind.is_redstone() { + if (before_base_count > 0) == (next_on_base_count > 0) { + continue; + } + if next_on_base_count > 0 { + EventType::HardOn + } else { + EventType::HardOff + } + } else { + if kind.is_cobble() || (before_count > 0) == (next_on_count > 0) { + continue; + } + if next_on_count > 0 { + EventType::SoftOn + } else { + EventType::SoftOff + } + }; + self.schedule_event( + 0, + Event { + id: None, + from_id: None, + event_type, + target_position: target, + direction: target.diff(pos), + }, + ); + } } if let (Some(profile), Some(started)) = (&mut self.profile, started) { profile.cobble_normalization_time += started.elapsed(); @@ -1353,8 +1426,11 @@ impl Simulator { } fn cobble_power_counts(&self, target: Position) -> (usize, usize) { - let mut sources = 0; - let mut hard_sources = 0; + // External boundary drives have no neighbouring physical block. Keep + // their self-key when recomputing power from cached physical inputs. + let external = usize::from(self.hard_power_sources.contains(&(target, target))); + let mut sources = external; + let mut hard_sources = external; for input in &self.cobble_power_inputs[target.index(&self.world.size).0] { let active = match self.world[input.source].kind { BlockKind::Torch { is_on } @@ -1373,6 +1449,9 @@ impl Simulator { } fn cobble_disables_attached_torch(&self, target: Position) -> bool { + if self.hard_power_sources.contains(&(target, target)) { + return true; + } self.cobble_power_inputs[target.index(&self.world.size).0] .iter() .any(|input| { @@ -2425,6 +2504,164 @@ mod test { Ok(()) } + #[test] + fn normalized_cobble_release_notifies_both_downstream_repeaters() -> eyre::Result<()> { + use crate::transform::place_and_route::placed_node::PlacedNode; + let support = Position(2, 2, 1); + let consumers = [ + (Position(3, 2, 1), Direction::West), + (Position(2, 3, 1), Direction::South), + ]; + let mut world = World3D::new(DimSize(6, 6, 3)); + world[support] = PlacedNode::new_cobble(support).block; + for (p, direction) in consumers { + world[p.down().unwrap()] = PlacedNode::new_cobble(p.down().unwrap()).block; + world[p] = PlacedNode::new_repeater(p, direction).block; + } + let mut sim = Simulator::from(&World::from(&world))?; + sim.drive_inputs_with_limits(vec![(support, true)], 256, 50_000)?; + for (p, _) in consumers { + assert!(sim.world()[p].kind.is_powered()); + } + // Reproduce the stale extra soft count after the final real source + // goes away. Count normalization must send the missing falling edge. + sim.world[support].kind = BlockKind::Cobble { + on_count: 2, + on_base_count: 1, + }; + sim.drive_inputs_with_limits(vec![(support, false)], 256, 50_000)?; + assert!(!sim.world()[support].kind.is_powered()); + for (p, _) in consumers { + assert!( + !sim.world()[p].kind.is_powered(), + "repeater retained power after normalized block release" + ); + } + sim.drive_inputs_with_limits(vec![(support, true)], 256, 50_000)?; + for (p, _) in consumers { + assert!(sim.world()[p].kind.is_powered()); + } + Ok(()) + } + + #[test] + fn normalized_dust_recovery_notifies_downstream_repeater() -> eyre::Result<()> { + use crate::transform::place_and_route::placed_node::PlacedNode; + let mut world = World3D::new(DimSize(7, 3, 3)); + for x in 1..=5 { + let p = Position(x, 1, 1); + world[p.down().unwrap()] = PlacedNode::new_cobble(p.down().unwrap()).block; + world[p] = if x == 4 { + PlacedNode::new_repeater(p, Direction::West).block + } else { + PlacedNode::new_redstone(p).block + }; + } + world.initialize_redstone_states(); + let left = Position(1, 1, 1); + let right = Position(3, 1, 1); + let repeater = Position(4, 1, 1); + let output = Position(5, 1, 1); + let mut sim = Simulator::from(&World::from(&world))?; + // Represent the transient after an OFF wave cleared downstream dust, + // while an independent source still powers the first dust. No queued + // ON event remains: normalization itself must notify the repeater. + if let BlockKind::Redstone { + on_count, strength, .. + } = &mut sim.world[left].kind + { + *on_count = 1; + *strength = 15; + } + sim.redstone_power_sources.insert((left, left)); + // A separate transient starts and finishes powered at the repeater + // input, but crosses zero during numerical relaxation. That is not + // a physical falling edge and must not enqueue a repeater OFF. + let mut transient = sim.clone(); + if let BlockKind::Redstone { strength, .. } = &mut transient.world[right].kind { + *strength = 1; + } + transient.queue.clear(); + transient.normalize_redstone_strengths(); + assert!(transient.world[right].kind.is_powered()); + assert!( + !transient + .queue + .iter() + .flatten() + .any(|event| event.target_position == repeater + && matches!(event.event_type, EventType::SoftOff)), + "an internal relaxation dip emitted a physical OFF event" + ); + sim.normalize_redstone_strengths(); + sim.run_with_max_cycles(256)?; + assert!(sim.world()[right].kind.is_powered()); + assert!( + sim.world()[repeater].kind.is_powered(), + "normalization recovered dust without notifying repeater" + ); + assert!(sim.world()[output].kind.is_powered()); + sim.drive_inputs_with_limits(vec![(left, true), (right, true)], 256, 50_000)?; + assert!(sim.world()[repeater].kind.is_powered()); + // Removing the right direct source temporarily clears its dust chain. + // The left source remains on, so normalization restores that chain. + sim.drive_inputs_with_limits(vec![(right, false)], 256, 50_000)?; + assert!(sim.world()[right].kind.is_powered()); + assert!( + sim.world()[repeater].kind.is_powered(), + "powered input recovered but repeater remained off" + ); + assert!(sim.world()[output].kind.is_powered()); + sim.drive_inputs_with_limits(vec![(left, false)], 256, 50_000)?; + assert!(!sim.world()[right].kind.is_powered()); + assert!(!sim.world()[repeater].kind.is_powered()); + assert!(!sim.world()[output].kind.is_powered()); + Ok(()) + } + + #[test] + fn externally_driven_cobble_survives_normalization_and_controls_torch() -> eyre::Result<()> { + let support = Position(1, 1, 1); + let torch = Position(1, 1, 2); + let world = World { + size: DimSize(3, 3, 4), + blocks: vec![ + ( + support, + Block { + kind: BlockKind::Cobble { + on_count: 0, + on_base_count: 0, + }, + direction: Direction::None, + }, + ), + ( + torch, + Block { + kind: BlockKind::Torch { is_on: true }, + direction: Direction::Bottom, + }, + ), + ], + }; + let mut sim = Simulator::from(&world)?; + for powered in [true, true, false, false, true, false] { + sim.advance_idle_cycles(MANUAL_INPUT_IDLE_CYCLES)?; + sim.drive_inputs_with_limits(vec![(support, powered)], 256, 50_000)?; + assert_eq!( + sim.world()[support].kind, + BlockKind::Cobble { + on_count: usize::from(powered), + on_base_count: usize::from(powered) + } + ); + assert_eq!(sim.world()[torch].kind.is_powered(), !powered); + assert!(!sim.is_torch_burned_out(torch)); + } + Ok(()) + } + #[test] pub fn unittest_simulator_init_states() { let _ = tracing_subscriber::fmt::try_init(); diff --git a/test/alu_display/README.md b/test/alu_display/README.md new file mode 100644 index 00000000..9be49d00 --- /dev/null +++ b/test/alu_display/README.md @@ -0,0 +1,115 @@ +# ALU / 표시 회로 실험 + +`bcd_to_7seg.v`는 조건식 기반, `bcd_to_7seg_sop.v`는 공유 숫자 판정 기반의 동일한 디코더다. `seg[0]`부터 a,b,c,d,e,f,g이고 active-high다. blank와 잘못된 BCD(10~15)는 모두 OFF다. + +`bin8_to_bcd.v`는 8단계 조합 Double Dabble이며 입력 0~255를 세 자리 BCD로 변환한다. `alu8.v`의 연산/flag 계약은 [계획 문서](../../docs/alu_display_compiler_plan.md)에 있다. + +독립 정답과 실제 parser/RTL 결과 비교: + +```sh +cargo test --release --test alu_display +``` + +기본 숫자 표시용 디코더(`blank=0`)의 물리 컴파일, NBT 재로딩, 16개 상태와256개 순서쌍 전이 검증: + +```sh +BIT_LITERAL=1 NARY_STAGGER=1 PNR_PIN_ACCESS=1 \ +PNR_ASTAR_EXPANSIONS=20000 PNR_MAX_STEPS=256 \ + cargo run --release --example decoder_nbt -- \ + target/alu-display/decoder-digit-verified \ + test/alu_display/bcd_to_7seg_minimized.v \ + test/alu_display/bcd_digit_to_7seg.v +``` + +실제 성공 기록은23.308초,20개 cell,62개 배선,5,703블록이다. `verification.json`은 NBT 재읽기 및 모든256개 전이를 연속512회 입력 적용으로 검증한 뒤 생성한다. invalid BCD10~15도 모두 OFF다. `bcd_to_7seg_minimized.v`는 전체 진리표를 보존하는 일반 POS식이며 `python3 tools/alu_display/minimize_decoder.py`로 재생성할 수 있다. + +마지막 wrapper 인자를 생략하면 blank 제어를 포함한32상태/1,024전이 실험이다. 이 모드의 전체 전이 검증은 미완료다. 정적32개를 통과한 배치에서도 burnout이 검출되어 최종 성공으로 인정하지 않았다. 실패 시 RTL 검증 성공과 물리 검증 성공을 구분한다. 생성된 NBT, 포트 위치, IR, candidate cache는 지정한 산출물 디렉터리에 남는다. NBT 출력은7개의 논리 segment 신호이며 실제 lamp 표시판은 후속 단계다. + +## Unsigned 가산기 물리 실험 + +전체 decoder도 Exact 구성으로 생성했다. `sh tools/alu_display/whole_decoder_exact.sh FRESH_DIRECTORY`는 일반 Verilog를24-NOR로 내리고 범용 NOR 템플릿의 블록 부분집합을 SAT로 선택해 초기해를 만든다. Global PnR 없이4,132블록/220×7×33 NBT를 생성했고, cold 구성185.937초와16개 입력·256개 전이/NBT 재읽기를 확인했다. fixture는 `nbt/alu-display-decoder-whole-exact.nbt`, 요약은 `whole_decoder_exact_verification.json`이다. 자유로운 단일 SAT 최적해는 아니며 출력은 anywhere-observable이다. [모델 변경·압축·이전 실패 기록](../../docs/whole_decoder_construct.md)을 확인한다. + +20개 물리 셀로 나누지 않고 decoder 전체를 하나의 SAT 모델에 넣는 실험은 `whole_decoder_exact` 예제와 [전체 Exact 실험 기록](../../docs/whole_decoder_exact.md)을 참고한다. 기존4,187블록 fixture와 다른 경로이며, 초기 자유 배치 실험은 모두 시간 제한 내 UNKNOWN이었다. + +Exact SAT 셀 후보와 자유3D를 함께 사용하는 decoder는4,187블록,36×64×56이며 모든256개 전이를 통과했다. 같은 자유3D에서 recipe만 쓰는 대조군은4,312블록이다. 재현 명령, cold 시간과 메모리 상태를 정규화하는 cache 계약은 [Exact 전역 후보 실험](../../docs/exact_global_candidates.md)에 있다. + +`adder_nbt`는 Verilog `{1'b0,a}+{1'b0,b}`에서 시작해 Logical IR, associative cell mapping, 전역 배치/배선, NBT 내보내기까지 실행한다. `1`, `2`, `4`, `8` bit를 받는다. 검증 정답은 graph 평가가 아닌 독립 정수 덧셈 `a+b`다. + +작동을 확인한 설정은 local box `10x10x5`, ranked beam `256`, A* expansion budget `20,000`, 예약된 입출력 diode와 verified n-ary cell이다. seed를 바꾸어 얻은 결과가 아니다. 기본 `8x8x4` box에서는 XOR의 출력 diode escape 확보가 실패할 수 있으므로 아래 설정을 명시한다. + +```sh +# 첫 실행에 cache가 비어 있을 때만 cold 측정이다. +ADDER_DIM=10x10x5 ADDER_BEAM=256 \ + ADDER_CACHE_DIR=target/alu-display/arithmetic-candidate-cache \ + cargo run --release --example adder_nbt -- 2 target/alu-display/adder-2 + +# 같은 설정의 검증된 local candidate를 재사용한다. +ADDER_DIM=10x10x5 ADDER_BEAM=256 \ + ADDER_CACHE_DIR=target/alu-display/arithmetic-candidate-cache \ + cargo run --release --example adder_nbt -- 4 target/alu-display/adder-4 +ADDER_DIM=10x10x5 ADDER_BEAM=256 \ + ADDER_CACHE_DIR=target/alu-display/arithmetic-candidate-cache \ + cargo run --release --example adder_nbt -- 8 target/alu-display/adder-8 +``` + +실제 기록(2026-11-08): + +| 폭 | 원래 PnR 시간 | candidate cache | 블록 수 | 수정된 simulator로 저장 NBT 재검증 | +| --- | ---: | --- | ---: | --- | +| 2bit | 6.507초 | cold | 645 | 정적 16개 + 모든 ordered transition 256쌍 | +| 4bit | 7.648초 | warm, 17개 instance 모두 재사용 | 1,925 | 정적 256개 전수 + 연속 walk 256개 | +| 8bit | 38.709초 | warm | 3,433 | 정적 289개 + 같은 순서의 연속 walk 289개 | + +8bit는 directed carry/boundary 33개와 seed `0xadd8202611080001`의 무작위 256개다. directed 항목에는 중복이 있으며 **전체 65,536개 입력 또는 모든 전이를 증명한 결과가 아니다**. 4bit walk도 전체 입력 순서쌍 검사가 아니다. + +저장된 파일만 재검증하면 PnR를 다시 실행하거나 NBT를 수정하지 않는다. 현재 실제 성공 산출물 경로는 다음과 같다. + +```sh +cargo run --release --example adder_nbt -- --verify-only 2 target/alu-display/adder-2-large-io-diagnostic +cargo run --release --example adder_nbt -- --verify-only 4 target/alu-display/adder-4 +cargo run --release --example adder_nbt -- --verify-only 8 target/alu-display/adder-8 +``` + +8bit는 처음 연속 walk 47번째 `(252,52) → (149,247)`에서 `sum_6`이 틀렸다. 원인은 carry 수식이나 저장 NBT가 아니라 simulator의 dust 강도 정규화 후 상승 이벤트 누락이었다. fixpoint 전후 ON/OFF 통지를 수정한 뒤 **같은 NBT 파일**로 위 검사를 통과했다. 원래 실패 기록은 산출물의 `diagnostics/prior-*/failure.json`에 보존한다. + +`verification.json`에는 `verification_after_simfix`, `nbt_unchanged`, NBT/interface fingerprint, 검증 범위와 시간을 기록한다. verify-only의 `compile_ms`는 원래 컴파일 기록이고 `verification_ms`/`total_ms`는 이번 재검증 시간이다. `experiment.json`의 `cache_entries_before`와 실제 로그의 reused 수로 cold/warm을 구분한다. 현재 runner는 PnR의 assembled-layout verifier에서도 정적 입력과 연속 walk를 검사하며, 최종 NBT 재읽기 검증을 별도로 수행한다. + +## 최소 cover 디코더 실험 + +`bcd_to_7seg_minimized.v`는 동일한 32개 입력 계약을 ordinary Verilog로 표현한 대안이다. `tools/alu_display/minimize_decoder.py`가 4-input prime implicant를 전부 열거하고, 각 segment의 최소 항 수 SOP/POS cover 조합 648개를 비교해 공유 recipe 입력 연결 수가 가장 작은 구성을 선택한다. 10~15는 OFF이며 don't-care를 사용하지 않는다. 임의 Boolean network 전체에 대한 전역 최적성을 주장하지 않는다. + +```sh +python3 tools/alu_display/minimize_decoder.py +cargo test --release --lib minimized_decoder_covers -- --nocapture +``` + +현재 literal-cell mapping에서 blank=0 버전은 18→20개 cell로 늘지만 입력 연결은 90→62개로 줄어든다. blank 입력을 포함하면 25→20개 cell, 연결 104→69개다. 최대 fanout은 10→11이므로 이 역시 tradeoff이며, 연결 수 감소만으로 배선 성공이나 속도 향상을 단정하지 않는다. 전체 16/32개 값 및 각 leaf 진리표는 검증했고 NBT와 전이 검증은 별도 실험이다. `bcd_digit_to_7seg.v` wrapper와 함께 사용하면 기존 blank=0 물리 runner 입력 계약을 유지한다. + +## 하나의 carry chain을 공유하는 ADD/SUB + +`add_sub1.v`, `add_sub2.v`, `add_sub4.v`, `add_sub8.v`는 일반 Verilog로 `adjusted_b = b XOR subtract`, `carry_0 = subtract`와 bit별 sum/carry를 명시한다. ADD/SUB를 따로 합성한 뒤 자동으로 합쳐 주는 최적화에 의존하지 않는다. `y`는 하위 Nbit, `c`는 ADD에서 carry, SUB에서 **borrow가 없으면 1**이다. + +```sh +python3 tools/alu_display/generate_add_sub.py +cargo test --release --lib explicit_shared_add_sub -- --nocapture +ADD_SUB_CACHE_DIR=target/alu-display/add-sub-candidate-cache \ + cargo run --release --example add_sub_nbt -- 2 target/alu-display/add-sub-2-transition-filter +# 저장된 실제 성공 NBT만 다시 검증: +cargo run --release --example add_sub_nbt -- --verify-only 2 target/alu-display/add-sub-2-transition-filter +``` + +독립 정수 ADD/SUB와 carry/no-borrow oracle로 1/2/4bit 논리 전수 검사를 통과했다. **2bit 물리 결과**는 12개 cell/24개 route/1,333블록, warm cache PnR 6.040초이며 NBT 재읽기 후 32개 정적 입력과 1,024개 ordered transition을 모두 통과했다. 첫 배치의 특정 전이에서 torch burnout이 발생했지만, PnR 내부의 전수 전이 verifier가 이를 거부하고 두 번째 배치를 선택했다. 결과를 통과시키기 위해 검증을 완화하거나 블록을 손으로 수정하지 않았다. + +4bit 첫 물리 실행은 12개 배치/배선 시도 중 10개 routing 실패와 2개 verifier 실패로 69.460초 후 종료했다. 마지막 후보는 `a=0, b=1, subtract=0`에서 `c=0`이어야 하는 조건을 만족하지 못했다. `target/alu-display/add-sub-4/failure.json`에 보존했으며 이는 논리 전수 검사 성공과 별개인 물리 실패다. 최신 routing contract를 적용한 단 한 번의 재실행은 성공했다. 24개 cell/48개 route/2,906블록이며 PnR 77.012초, NBT 재검증을 포함해 82.135초였다. 512개 정적 입력 전수와 연속 walk 512개를 통과했다. 10번째 시도에서 선택되었으며 앞선 8개 routing 실패와 1개 verifier 실패를 통과로 세지 않았다. candidate cache version 변경으로 3개 정의를 새로 생성하고 21개 같은 instance에서 재사용했으므로 이 측정은 완전한 warm-cache 실행이 아니다. 성공 산출물은 `target/alu-display/add-sub-4-restored-direct/verification.json`이다. + +8bit ADD/SUB는 같은 seed와 예산으로 한 번 실행했으나 12개 시도가 모두 배선 단계에서 실패했다. 48개 cell의 candidate는 모두 재사용했고 준비 시간은 1.09ms였으며, 총 136.385초 후 종료했다. 마지막 실패 연결은 `add_sub_bit_50.y → add_sub_bit_52.a0`이고 `target/alu-display/add-sub-8/failure.json`에 기록했다. 성공 NBT는 생성되지 않았으므로 계획한 정적 578개와 연속 walk 578개 물리 검증도 수행되지 않았다. 다음 병목은 전역 배선이며, 앞 절의 8bit ADD 성공을 ADD/SUB 성공으로 해석하면 안 된다. + +```sh +# 성공한 4bit 저장 NBT 재검증: +cargo run --release --example add_sub_nbt -- --verify-only 4 target/alu-display/add-sub-4-restored-direct +# 8bit의 현재 실패를 재현하는 동일 설정(성공 명령이 아님): +ADD_SUB_CACHE_DIR=target/alu-display/add-sub-candidate-cache \ + cargo run --release --example add_sub_nbt -- 8 target/alu-display/add-sub-8 +``` + +`add_sub_nbt`는 `ADD_SUB_DIM`(기본 `10x10x5`), `ADD_SUB_BEAM`(기본 256), `ADD_SUB_CACHE_DIR`를 사용한다. 기존 `adder_nbt`의 CLI와 파일 계약은 바꾸지 않았다. 1/2bit는 정적 전수+모든 ordered transition, 4bit는 정적 512개 전수+512개 walk, 8bit를 실행할 경우 directed/random operand pair를 두 연산으로 검사하는 bounded coverage다. diff --git a/test/alu_display/add_sub1.v b/test/alu_display/add_sub1.v new file mode 100644 index 00000000..27417e89 --- /dev/null +++ b/test/alu_display/add_sub1.v @@ -0,0 +1,19 @@ +// Generated by tools/alu_display/generate_add_sub.py. +// subtract=0: a+b; subtract=1: a-b; c=carry / no-borrow. +// One explicitly shared carry chain; no arithmetic-sharing pass assumed. +module add_sub(a, b, subtract, y, c); + input [0:0] a, b; + input subtract; + output [0:0] y; + output c; + wire [0:0] adjusted_b; + wire carry_0, carry_1; + wire propagate_0, sum_0; + assign adjusted_b = b ^ {subtract}; + assign carry_0 = subtract; + assign propagate_0 = a[0] ^ adjusted_b[0]; + assign sum_0 = propagate_0 ^ carry_0; + assign carry_1 = (a[0] & adjusted_b[0]) | (propagate_0 & carry_0); + assign y = {sum_0}; + assign c = carry_1; +endmodule diff --git a/test/alu_display/add_sub2.v b/test/alu_display/add_sub2.v new file mode 100644 index 00000000..5234c2c1 --- /dev/null +++ b/test/alu_display/add_sub2.v @@ -0,0 +1,22 @@ +// Generated by tools/alu_display/generate_add_sub.py. +// subtract=0: a+b; subtract=1: a-b; c=carry / no-borrow. +// One explicitly shared carry chain; no arithmetic-sharing pass assumed. +module add_sub(a, b, subtract, y, c); + input [1:0] a, b; + input subtract; + output [1:0] y; + output c; + wire [1:0] adjusted_b; + wire carry_0, carry_1, carry_2; + wire propagate_0, sum_0, propagate_1, sum_1; + assign adjusted_b = b ^ {subtract, subtract}; + assign carry_0 = subtract; + assign propagate_0 = a[0] ^ adjusted_b[0]; + assign sum_0 = propagate_0 ^ carry_0; + assign carry_1 = (a[0] & adjusted_b[0]) | (propagate_0 & carry_0); + assign propagate_1 = a[1] ^ adjusted_b[1]; + assign sum_1 = propagate_1 ^ carry_1; + assign carry_2 = (a[1] & adjusted_b[1]) | (propagate_1 & carry_1); + assign y = {sum_1, sum_0}; + assign c = carry_2; +endmodule diff --git a/test/alu_display/add_sub4.v b/test/alu_display/add_sub4.v new file mode 100644 index 00000000..70f988a5 --- /dev/null +++ b/test/alu_display/add_sub4.v @@ -0,0 +1,28 @@ +// Generated by tools/alu_display/generate_add_sub.py. +// subtract=0: a+b; subtract=1: a-b; c=carry / no-borrow. +// One explicitly shared carry chain; no arithmetic-sharing pass assumed. +module add_sub(a, b, subtract, y, c); + input [3:0] a, b; + input subtract; + output [3:0] y; + output c; + wire [3:0] adjusted_b; + wire carry_0, carry_1, carry_2, carry_3, carry_4; + wire propagate_0, sum_0, propagate_1, sum_1, propagate_2, sum_2, propagate_3, sum_3; + assign adjusted_b = b ^ {subtract, subtract, subtract, subtract}; + assign carry_0 = subtract; + assign propagate_0 = a[0] ^ adjusted_b[0]; + assign sum_0 = propagate_0 ^ carry_0; + assign carry_1 = (a[0] & adjusted_b[0]) | (propagate_0 & carry_0); + assign propagate_1 = a[1] ^ adjusted_b[1]; + assign sum_1 = propagate_1 ^ carry_1; + assign carry_2 = (a[1] & adjusted_b[1]) | (propagate_1 & carry_1); + assign propagate_2 = a[2] ^ adjusted_b[2]; + assign sum_2 = propagate_2 ^ carry_2; + assign carry_3 = (a[2] & adjusted_b[2]) | (propagate_2 & carry_2); + assign propagate_3 = a[3] ^ adjusted_b[3]; + assign sum_3 = propagate_3 ^ carry_3; + assign carry_4 = (a[3] & adjusted_b[3]) | (propagate_3 & carry_3); + assign y = {sum_3, sum_2, sum_1, sum_0}; + assign c = carry_4; +endmodule diff --git a/test/alu_display/add_sub8.v b/test/alu_display/add_sub8.v new file mode 100644 index 00000000..f699585e --- /dev/null +++ b/test/alu_display/add_sub8.v @@ -0,0 +1,40 @@ +// Generated by tools/alu_display/generate_add_sub.py. +// subtract=0: a+b; subtract=1: a-b; c=carry / no-borrow. +// One explicitly shared carry chain; no arithmetic-sharing pass assumed. +module add_sub(a, b, subtract, y, c); + input [7:0] a, b; + input subtract; + output [7:0] y; + output c; + wire [7:0] adjusted_b; + wire carry_0, carry_1, carry_2, carry_3, carry_4, carry_5, carry_6, carry_7, carry_8; + wire propagate_0, sum_0, propagate_1, sum_1, propagate_2, sum_2, propagate_3, sum_3, propagate_4, sum_4, propagate_5, sum_5, propagate_6, sum_6, propagate_7, sum_7; + assign adjusted_b = b ^ {subtract, subtract, subtract, subtract, subtract, subtract, subtract, subtract}; + assign carry_0 = subtract; + assign propagate_0 = a[0] ^ adjusted_b[0]; + assign sum_0 = propagate_0 ^ carry_0; + assign carry_1 = (a[0] & adjusted_b[0]) | (propagate_0 & carry_0); + assign propagate_1 = a[1] ^ adjusted_b[1]; + assign sum_1 = propagate_1 ^ carry_1; + assign carry_2 = (a[1] & adjusted_b[1]) | (propagate_1 & carry_1); + assign propagate_2 = a[2] ^ adjusted_b[2]; + assign sum_2 = propagate_2 ^ carry_2; + assign carry_3 = (a[2] & adjusted_b[2]) | (propagate_2 & carry_2); + assign propagate_3 = a[3] ^ adjusted_b[3]; + assign sum_3 = propagate_3 ^ carry_3; + assign carry_4 = (a[3] & adjusted_b[3]) | (propagate_3 & carry_3); + assign propagate_4 = a[4] ^ adjusted_b[4]; + assign sum_4 = propagate_4 ^ carry_4; + assign carry_5 = (a[4] & adjusted_b[4]) | (propagate_4 & carry_4); + assign propagate_5 = a[5] ^ adjusted_b[5]; + assign sum_5 = propagate_5 ^ carry_5; + assign carry_6 = (a[5] & adjusted_b[5]) | (propagate_5 & carry_5); + assign propagate_6 = a[6] ^ adjusted_b[6]; + assign sum_6 = propagate_6 ^ carry_6; + assign carry_7 = (a[6] & adjusted_b[6]) | (propagate_6 & carry_6); + assign propagate_7 = a[7] ^ adjusted_b[7]; + assign sum_7 = propagate_7 ^ carry_7; + assign carry_8 = (a[7] & adjusted_b[7]) | (propagate_7 & carry_7); + assign y = {sum_7, sum_6, sum_5, sum_4, sum_3, sum_2, sum_1, sum_0}; + assign c = carry_8; +endmodule diff --git a/test/alu_display/alu8.v b/test/alu_display/alu8.v new file mode 100644 index 00000000..92a23f7e --- /dev/null +++ b/test/alu_display/alu8.v @@ -0,0 +1,29 @@ +// op: 0 ADD, 1 SUB, 2 AND, 3 OR, 4 XOR, 5 PASS A, 6 SHL1, 7 SHR1. +// C: ADD carry / SUB no-borrow / shift에서 빠진 bit. V: ADD/SUB signed overflow. +module alu8(a, b, op, y, z, n, c, v); + input [7:0] a, b; + input [2:0] op; + output [7:0] y; + output z, n, c, v; + wire sub; + wire [7:0] rhs; + wire [8:0] wide; + assign sub = op == 3'd1; + assign rhs = sub ? ~b : b; + assign wide = {1'b0, a} + {1'b0, rhs} + (sub ? 9'd1 : 9'd0); + assign y = op == 3'd0 ? wide[7:0] : + op == 3'd1 ? wide[7:0] : + op == 3'd2 ? a & b : + op == 3'd3 ? a | b : + op == 3'd4 ? a ^ b : + op == 3'd5 ? a : + op == 3'd6 ? {a[6:0], 1'b0} : {1'b0, a[7:1]}; + assign z = y == 8'd0; + assign n = y[7]; + assign c = op == 3'd0 ? wide[8] : + op == 3'd1 ? wide[8] : + op == 3'd6 ? a[7] : + op == 3'd7 ? a[0] : 1'b0; + assign v = op == 3'd0 ? (~(a[7] ^ b[7]) & (y[7] ^ a[7])) : + op == 3'd1 ? ((a[7] ^ b[7]) & (y[7] ^ a[7])) : 1'b0; +endmodule diff --git a/test/alu_display/bcd_digit_to_7seg.v b/test/alu_display/bcd_digit_to_7seg.v new file mode 100644 index 00000000..11da99f2 --- /dev/null +++ b/test/alu_display/bcd_digit_to_7seg.v @@ -0,0 +1,7 @@ +// 첫 물리 표시 실험: 세 자리 000~255 표시의 기본 모드인 blank=0. +// bcd_to_7seg.v 또는 bcd_to_7seg_sop.v와 함께 컴파일한다. +module bcd_digit_to_7seg(digit, seg); + input [3:0] digit; + output [6:0] seg; + bcd_to_7seg decoder(.digit(digit), .blank(1'b0), .seg(seg)); +endmodule diff --git a/test/alu_display/bcd_to_7seg.v b/test/alu_display/bcd_to_7seg.v new file mode 100644 index 00000000..7db410e6 --- /dev/null +++ b/test/alu_display/bcd_to_7seg.v @@ -0,0 +1,19 @@ +// seg[0..6] = a,b,c,d,e,f,g. 1이면 해당 segment를 켠다. +// blank 우선, BCD 범위 밖(10..15)은 모두 끈다. +module bcd_to_7seg(digit, blank, seg); + input [3:0] digit; + input blank; + output [6:0] seg; + + assign seg = blank ? 7'h00 : + digit == 4'd0 ? 7'h3f : + digit == 4'd1 ? 7'h06 : + digit == 4'd2 ? 7'h5b : + digit == 4'd3 ? 7'h4f : + digit == 4'd4 ? 7'h66 : + digit == 4'd5 ? 7'h6d : + digit == 4'd6 ? 7'h7d : + digit == 4'd7 ? 7'h07 : + digit == 4'd8 ? 7'h7f : + digit == 4'd9 ? 7'h6f : 7'h00; +endmodule diff --git a/test/alu_display/bcd_to_7seg_minimized.v b/test/alu_display/bcd_to_7seg_minimized.v new file mode 100644 index 00000000..b69b0b6a --- /dev/null +++ b/test/alu_display/bcd_to_7seg_minimized.v @@ -0,0 +1,31 @@ +// Generated by tools/alu_display/minimize_decoder.py. +// Exact 4-input covers: invalid BCD 10..15 is OFF; no don't-care. +// Shared recipe estimate before blank: 20 cells, 62 input edges. +module bcd_to_7seg(digit, blank, seg); + input [3:0] digit; + input blank; + output [6:0] seg; + wire term_0, term_1, term_2, term_3, term_4, term_5, term_6, term_7, term_8, term_9, term_10, term_11, term_12; + wire a, b, c, d, e, f, g; + assign term_0 = ~digit[2] | ~digit[3]; + assign term_1 = digit[1] | digit[2] | digit[3]; + assign term_2 = digit[1] | ~digit[2]; + assign term_3 = ~digit[1] | ~digit[3]; + assign term_4 = ~digit[1] | digit[2]; + assign term_5 = digit[0] | digit[1] | ~digit[2]; + assign term_6 = digit[0] | ~digit[1] | digit[2]; + assign term_7 = digit[0] | ~digit[1] | ~digit[2]; + assign term_8 = ~digit[0] | digit[2] | digit[3]; + assign term_9 = ~digit[0] | digit[1] | digit[2] | digit[3]; + assign term_10 = ~digit[0] | digit[1] | ~digit[2]; + assign term_11 = ~digit[0] | ~digit[1]; + assign term_12 = ~digit[0] | ~digit[1] | ~digit[2]; + assign a = ~blank & (term_0 & term_3 & term_5 & term_9); + assign b = ~blank & (term_0 & term_3 & term_7 & term_10); + assign c = ~blank & (term_0 & term_3 & term_6); + assign d = ~blank & (term_0 & term_3 & term_5 & term_9 & term_12); + assign e = ~blank & (term_2 & term_3 & (~digit[0])); + assign f = ~blank & (term_0 & term_4 & term_8 & term_11); + assign g = ~blank & (term_0 & term_1 & term_3 & term_12); + assign seg = {g, f, e, d, c, b, a}; +endmodule diff --git a/test/alu_display/bcd_to_7seg_sop.v b/test/alu_display/bcd_to_7seg_sop.v new file mode 100644 index 00000000..4ba20e1c --- /dev/null +++ b/test/alu_display/bcd_to_7seg_sop.v @@ -0,0 +1,26 @@ +// 공유 숫자 판정과 segment OR. bit0=a, active-high, invalid/blank=OFF. +module bcd_to_7seg(digit, blank, seg); + input [3:0] digit; + input blank; + output [6:0] seg; + wire d0, d1, d2, d3, d4, d5, d6, d7, d8, d9; + wire a, b, c, d, e, f, g; + assign d0 = digit == 4'd0; + assign d1 = digit == 4'd1; + assign d2 = digit == 4'd2; + assign d3 = digit == 4'd3; + assign d4 = digit == 4'd4; + assign d5 = digit == 4'd5; + assign d6 = digit == 4'd6; + assign d7 = digit == 4'd7; + assign d8 = digit == 4'd8; + assign d9 = digit == 4'd9; + assign a = ~blank & (d0 | d2 | d3 | d5 | d6 | d7 | d8 | d9); + assign b = ~blank & (d0 | d1 | d2 | d3 | d4 | d7 | d8 | d9); + assign c = ~blank & (d0 | d1 | d3 | d4 | d5 | d6 | d7 | d8 | d9); + assign d = ~blank & (d0 | d2 | d3 | d5 | d6 | d8 | d9); + assign e = ~blank & (d0 | d2 | d6 | d8); + assign f = ~blank & (d0 | d4 | d5 | d6 | d8 | d9); + assign g = ~blank & (d2 | d3 | d4 | d5 | d6 | d8 | d9); + assign seg = {g, f, e, d, c, b, a}; +endmodule diff --git a/test/alu_display/bin8_to_bcd.v b/test/alu_display/bin8_to_bcd.v new file mode 100644 index 00000000..78ac5345 --- /dev/null +++ b/test/alu_display/bin8_to_bcd.v @@ -0,0 +1,52 @@ +// unsigned 0..255. 8번의 add-3/shift를 조합회로로 펼친다. +module bin8_to_bcd(value, hundreds, tens, ones); + input [7:0] value; + output [3:0] hundreds, tens, ones; + wire [11:0] stage0, stage1, stage2, stage3, stage4, stage5, stage6, stage7, stage8; + wire [3:0] h0, t0, o0, h1, t1, o1, h2, t2, o2, h3, t3, o3, h4, t4, o4, h5, t5, o5, h6, t6, o6, h7, t7, o7; + wire [11:0] adjusted0, adjusted1, adjusted2, adjusted3, adjusted4, adjusted5, adjusted6, adjusted7; + assign stage0 = 12'd0; + assign h0 = stage0[11:8] >= 4'd5 ? stage0[11:8] + 4'd3 : stage0[11:8]; + assign t0 = stage0[7:4] >= 4'd5 ? stage0[7:4] + 4'd3 : stage0[7:4]; + assign o0 = stage0[3:0] >= 4'd5 ? stage0[3:0] + 4'd3 : stage0[3:0]; + assign adjusted0 = {h0, t0, o0}; + assign stage1 = {adjusted0[10:0], value[7]}; + assign h1 = stage1[11:8] >= 4'd5 ? stage1[11:8] + 4'd3 : stage1[11:8]; + assign t1 = stage1[7:4] >= 4'd5 ? stage1[7:4] + 4'd3 : stage1[7:4]; + assign o1 = stage1[3:0] >= 4'd5 ? stage1[3:0] + 4'd3 : stage1[3:0]; + assign adjusted1 = {h1, t1, o1}; + assign stage2 = {adjusted1[10:0], value[6]}; + assign h2 = stage2[11:8] >= 4'd5 ? stage2[11:8] + 4'd3 : stage2[11:8]; + assign t2 = stage2[7:4] >= 4'd5 ? stage2[7:4] + 4'd3 : stage2[7:4]; + assign o2 = stage2[3:0] >= 4'd5 ? stage2[3:0] + 4'd3 : stage2[3:0]; + assign adjusted2 = {h2, t2, o2}; + assign stage3 = {adjusted2[10:0], value[5]}; + assign h3 = stage3[11:8] >= 4'd5 ? stage3[11:8] + 4'd3 : stage3[11:8]; + assign t3 = stage3[7:4] >= 4'd5 ? stage3[7:4] + 4'd3 : stage3[7:4]; + assign o3 = stage3[3:0] >= 4'd5 ? stage3[3:0] + 4'd3 : stage3[3:0]; + assign adjusted3 = {h3, t3, o3}; + assign stage4 = {adjusted3[10:0], value[4]}; + assign h4 = stage4[11:8] >= 4'd5 ? stage4[11:8] + 4'd3 : stage4[11:8]; + assign t4 = stage4[7:4] >= 4'd5 ? stage4[7:4] + 4'd3 : stage4[7:4]; + assign o4 = stage4[3:0] >= 4'd5 ? stage4[3:0] + 4'd3 : stage4[3:0]; + assign adjusted4 = {h4, t4, o4}; + assign stage5 = {adjusted4[10:0], value[3]}; + assign h5 = stage5[11:8] >= 4'd5 ? stage5[11:8] + 4'd3 : stage5[11:8]; + assign t5 = stage5[7:4] >= 4'd5 ? stage5[7:4] + 4'd3 : stage5[7:4]; + assign o5 = stage5[3:0] >= 4'd5 ? stage5[3:0] + 4'd3 : stage5[3:0]; + assign adjusted5 = {h5, t5, o5}; + assign stage6 = {adjusted5[10:0], value[2]}; + assign h6 = stage6[11:8] >= 4'd5 ? stage6[11:8] + 4'd3 : stage6[11:8]; + assign t6 = stage6[7:4] >= 4'd5 ? stage6[7:4] + 4'd3 : stage6[7:4]; + assign o6 = stage6[3:0] >= 4'd5 ? stage6[3:0] + 4'd3 : stage6[3:0]; + assign adjusted6 = {h6, t6, o6}; + assign stage7 = {adjusted6[10:0], value[1]}; + assign h7 = stage7[11:8] >= 4'd5 ? stage7[11:8] + 4'd3 : stage7[11:8]; + assign t7 = stage7[7:4] >= 4'd5 ? stage7[7:4] + 4'd3 : stage7[7:4]; + assign o7 = stage7[3:0] >= 4'd5 ? stage7[3:0] + 4'd3 : stage7[3:0]; + assign adjusted7 = {h7, t7, o7}; + assign stage8 = {adjusted7[10:0], value[0]}; + assign hundreds = stage8[11:8]; + assign tens = stage8[7:4]; + assign ones = stage8[3:0]; +endmodule diff --git a/test/alu_display/display_u8.v b/test/alu_display/display_u8.v new file mode 100644 index 00000000..656b9aac --- /dev/null +++ b/test/alu_display/display_u8.v @@ -0,0 +1,10 @@ +// 000..255를 세 자리로 직접 표시한다. seg[0..6]은 a..g, active-high. +module display_u8(value, hundreds_seg, tens_seg, ones_seg); + input [7:0] value; + output [6:0] hundreds_seg, tens_seg, ones_seg; + wire [3:0] hundreds, tens, ones; + bin8_to_bcd convert(.value(value), .hundreds(hundreds), .tens(tens), .ones(ones)); + bcd_to_7seg h(.digit(hundreds), .blank(1'b0), .seg(hundreds_seg)); + bcd_to_7seg t(.digit(tens), .blank(1'b0), .seg(tens_seg)); + bcd_to_7seg o(.digit(ones), .blank(1'b0), .seg(ones_seg)); +endmodule diff --git a/test/alu_display/nbt/alu-display-adder2-verified-73x10x9.nbt b/test/alu_display/nbt/alu-display-adder2-verified-73x10x9.nbt new file mode 100644 index 00000000..223f0afb Binary files /dev/null and b/test/alu_display/nbt/alu-display-adder2-verified-73x10x9.nbt differ diff --git a/test/alu_display/nbt/alu-display-adder2-verified-73x10x9.outputs.json b/test/alu_display/nbt/alu-display-adder2-verified-73x10x9.outputs.json new file mode 100644 index 00000000..d38067f6 --- /dev/null +++ b/test/alu_display/nbt/alu-display-adder2-verified-73x10x9.outputs.json @@ -0,0 +1,29 @@ +{ + "format": "redstone-compiler.outputs.v1", + "outputs": [ + { + "name": "sum_0", + "position": [ + 5, + 6, + 2 + ] + }, + { + "name": "sum_1", + "position": [ + 36, + 6, + 2 + ] + }, + { + "name": "sum_2", + "position": [ + 75, + 9, + 1 + ] + } + ] +} \ No newline at end of file diff --git a/test/alu_display/nbt/alu-display-adder8-verified.nbt b/test/alu_display/nbt/alu-display-adder8-verified.nbt new file mode 100644 index 00000000..bde090bb Binary files /dev/null and b/test/alu_display/nbt/alu-display-adder8-verified.nbt differ diff --git a/test/alu_display/nbt/alu-display-adder8-verified.outputs.json b/test/alu_display/nbt/alu-display-adder8-verified.outputs.json new file mode 100644 index 00000000..bcc605e7 --- /dev/null +++ b/test/alu_display/nbt/alu-display-adder8-verified.outputs.json @@ -0,0 +1,77 @@ +{ + "format": "redstone-compiler.outputs.v1", + "outputs": [ + { + "name": "sum_0", + "position": [ + 8, + 4, + 2 + ] + }, + { + "name": "sum_1", + "position": [ + 21, + 14, + 2 + ] + }, + { + "name": "sum_2", + "position": [ + 8, + 42, + 2 + ] + }, + { + "name": "sum_3", + "position": [ + 21, + 62, + 2 + ] + }, + { + "name": "sum_4", + "position": [ + 8, + 90, + 2 + ] + }, + { + "name": "sum_5", + "position": [ + 21, + 110, + 2 + ] + }, + { + "name": "sum_6", + "position": [ + 8, + 138, + 2 + ] + }, + { + "name": "sum_7", + "position": [ + 21, + 158, + 2 + ] + }, + { + "name": "sum_8", + "position": [ + 9, + 181, + 1 + ] + } + ] +} \ No newline at end of file diff --git a/test/alu_display/nbt/alu-display-decoder-digit-verified.nbt b/test/alu_display/nbt/alu-display-decoder-digit-verified.nbt new file mode 100644 index 00000000..01d5da99 Binary files /dev/null and b/test/alu_display/nbt/alu-display-decoder-digit-verified.nbt differ diff --git a/test/alu_display/nbt/alu-display-decoder-digit-verified.outputs.json b/test/alu_display/nbt/alu-display-decoder-digit-verified.outputs.json new file mode 100644 index 00000000..7875d1c2 --- /dev/null +++ b/test/alu_display/nbt/alu-display-decoder-digit-verified.outputs.json @@ -0,0 +1,61 @@ +{ + "format": "redstone-compiler.outputs.v1", + "outputs": [ + { + "name": "seg_0", + "position": [ + 27, + 75, + 1 + ] + }, + { + "name": "seg_1", + "position": [ + 55, + 75, + 1 + ] + }, + { + "name": "seg_2", + "position": [ + 21, + 91, + 1 + ] + }, + { + "name": "seg_3", + "position": [ + 94, + 56, + 1 + ] + }, + { + "name": "seg_4", + "position": [ + 60, + 50, + 1 + ] + }, + { + "name": "seg_5", + "position": [ + 49, + 94, + 1 + ] + }, + { + "name": "seg_6", + "position": [ + 83, + 75, + 1 + ] + } + ] +} \ No newline at end of file diff --git a/test/alu_display/nbt/alu-display-decoder-exact-verified.nbt b/test/alu_display/nbt/alu-display-decoder-exact-verified.nbt new file mode 100644 index 00000000..34f74546 Binary files /dev/null and b/test/alu_display/nbt/alu-display-decoder-exact-verified.nbt differ diff --git a/test/alu_display/nbt/alu-display-decoder-exact-verified.outputs.json b/test/alu_display/nbt/alu-display-decoder-exact-verified.outputs.json new file mode 100644 index 00000000..c1d827fb --- /dev/null +++ b/test/alu_display/nbt/alu-display-decoder-exact-verified.outputs.json @@ -0,0 +1,61 @@ +{ + "format": "redstone-compiler.outputs.v1", + "outputs": [ + { + "name": "seg_0", + "position": [ + 32, + 20, + 38 + ] + }, + { + "name": "seg_1", + "position": [ + 47, + 21, + 43 + ] + }, + { + "name": "seg_2", + "position": [ + 24, + 23, + 13 + ] + }, + { + "name": "seg_3", + "position": [ + 33, + 24, + 48 + ] + }, + { + "name": "seg_4", + "position": [ + 46, + 22, + 53 + ] + }, + { + "name": "seg_5", + "position": [ + 34, + 19, + 58 + ] + }, + { + "name": "seg_6", + "position": [ + 34, + 20, + 63 + ] + } + ] +} \ No newline at end of file diff --git a/test/alu_display/nbt/alu-display-decoder-layered-verified.nbt b/test/alu_display/nbt/alu-display-decoder-layered-verified.nbt new file mode 100644 index 00000000..ddf36c3e Binary files /dev/null and b/test/alu_display/nbt/alu-display-decoder-layered-verified.nbt differ diff --git a/test/alu_display/nbt/alu-display-decoder-layered-verified.outputs.json b/test/alu_display/nbt/alu-display-decoder-layered-verified.outputs.json new file mode 100644 index 00000000..f87801e6 --- /dev/null +++ b/test/alu_display/nbt/alu-display-decoder-layered-verified.outputs.json @@ -0,0 +1,61 @@ +{ + "format": "redstone-compiler.outputs.v1", + "outputs": [ + { + "name": "seg_0", + "position": [ + 27, + 75, + 10 + ] + }, + { + "name": "seg_1", + "position": [ + 55, + 75, + 10 + ] + }, + { + "name": "seg_2", + "position": [ + 21, + 91, + 10 + ] + }, + { + "name": "seg_3", + "position": [ + 94, + 56, + 10 + ] + }, + { + "name": "seg_4", + "position": [ + 60, + 50, + 10 + ] + }, + { + "name": "seg_5", + "position": [ + 49, + 94, + 10 + ] + }, + { + "name": "seg_6", + "position": [ + 83, + 75, + 10 + ] + } + ] +} \ No newline at end of file diff --git a/test/alu_display/nbt/alu-display-decoder-whole-exact.nbt b/test/alu_display/nbt/alu-display-decoder-whole-exact.nbt new file mode 100644 index 00000000..178d2117 Binary files /dev/null and b/test/alu_display/nbt/alu-display-decoder-whole-exact.nbt differ diff --git a/test/alu_display/nbt/alu-display-decoder-whole-exact.outputs.json b/test/alu_display/nbt/alu-display-decoder-whole-exact.outputs.json new file mode 100644 index 00000000..dab55401 --- /dev/null +++ b/test/alu_display/nbt/alu-display-decoder-whole-exact.outputs.json @@ -0,0 +1,95 @@ +{ + "inputs": [ + { + "name": "digit_0", + "position": [ + 1, + 39, + 1 + ] + }, + { + "name": "digit_1", + "position": [ + 7, + 9, + 1 + ] + }, + { + "name": "digit_2", + "position": [ + 13, + 0, + 1 + ] + }, + { + "name": "digit_3", + "position": [ + 19, + 19, + 1 + ] + } + ], + "outputs": [ + { + "name": "seg_0", + "position": [ + 25, + 106, + 2 + ] + }, + { + "name": "seg_1", + "position": [ + 25, + 135, + 4 + ] + }, + { + "name": "seg_2", + "position": [ + 4, + 77, + 1 + ] + }, + { + "name": "seg_3", + "position": [ + 1, + 215, + 4 + ] + }, + { + "name": "seg_4", + "position": [ + 4, + 47, + 1 + ] + }, + { + "name": "seg_5", + "position": [ + 25, + 176, + 2 + ] + }, + { + "name": "seg_6", + "position": [ + 1, + 205, + 4 + ] + } + ], + "format": "redstone-compiler.interface.v1" +} diff --git a/test/alu_display/oracle.rs b/test/alu_display/oracle.rs new file mode 100644 index 00000000..2a133660 --- /dev/null +++ b/test/alu_display/oracle.rs @@ -0,0 +1,11 @@ +// RTL의 hex 표와 별도로, 켜져야 할 segment의 이름으로 기대값을 정의한다. +pub fn segment_mask(digit: usize, blank: bool) -> usize { + if blank || digit > 9 { + return 0; + } + let lit = [ + "abcdef", "bc", "abdeg", "abcdg", "bcfg", "acdfg", "acdefg", "abc", "abcdefg", "abcdfg", + ][digit]; + lit.bytes() + .fold(0, |mask, segment| mask | (1 << (segment - b'a'))) +} diff --git a/test/alu_display/whole_decoder_exact_verification.json b/test/alu_display/whole_decoder_exact_verification.json new file mode 100644 index 00000000..74feb1fb --- /dev/null +++ b/test/alu_display/whole_decoder_exact_verification.json @@ -0,0 +1,49 @@ +{ + "blocks": 4132, + "dim": [ + 33, + 220, + 7 + ], + "nbt_roundtrip": true, + "ordered_transitions": 256, + "static_cases": 16, + "status": "verified", + "transition_drives": 512, + "mode": "bounded_nor_template_exact_construction", + "source": "bcd_digit_to_7seg.v (blank=0)", + "nor_gates": 24, + "construct_ms": 185937, + "seed": 1, + "workers": 4, + "minecraft_size": [ + 220, + 7, + 33 + ], + "nbt_sha256": "27a14e26953d131ffada6011fcb2513f71b4408b077edbea3e5a5eab09d74305", + "nbt": "alu-display-decoder-whole-exact.nbt", + "global_pnr": false, + "unrestricted_monolithic_solved": false, + "note": "Generic NOR template deletion subsets provide the initial solution; this is not a global optimum or an unrestricted simultaneous placement solve.", + "validation": { + "core_release_passed": 456, + "core_failed": 0, + "core_ignored": 56, + "rsdsl_release_passed": 9, + "wasm_native_release_passed": 3, + "workspace_all_targets_release_check": true + }, + "compaction": { + "global_delete_blocks": 4132, + "global_delete_wall_ms": 231310, + "window_candidate_blocks": 4594, + "window_candidate_dim": [ + 33, + 218, + 7 + ], + "window_wall_ms": 79247, + "selected": "constructed baseline; fewer blocks" + } +} diff --git a/test/fixtures/source_map/counter.v b/test/fixtures/source_map/counter.v new file mode 100644 index 00000000..0160623b --- /dev/null +++ b/test/fixtures/source_map/counter.v @@ -0,0 +1,8 @@ + +module counter(clk, q); + input clk; + output reg [1:0] q; + always @(posedge clk) begin + q <= q + 1; + end +endmodule diff --git a/test/fixtures/source_map/d-flip-flop.v b/test/fixtures/source_map/d-flip-flop.v new file mode 100644 index 00000000..620868bc --- /dev/null +++ b/test/fixtures/source_map/d-flip-flop.v @@ -0,0 +1,25 @@ + +module not_clk(clk, clk_n); + input clk; + output clk_n; + assign clk_n = ~clk; +endmodule + +module d_latch(d, en, q); + input d, en; + output reg q; + always @(*) begin + if (en) begin + q <= d; + end + end +endmodule + +module d_flip_flop(d, clk, q); + input d, clk; + output q; + wire clk_n, master_q; + not_clk inv(.clk(clk), .clk_n(clk_n)); + d_latch master(.d(d), .en(clk_n), .q(master_q)); + d_latch slave(.d(master_q), .en(clk), .q(q)); +endmodule diff --git a/tests/alu_display.rs b/tests/alu_display.rs new file mode 100644 index 00000000..7da587c5 --- /dev/null +++ b/tests/alu_display.rs @@ -0,0 +1,189 @@ +use std::collections::HashMap; + +use redstone_compiler::verilog::parser; +use redstone_compiler::verilog::rtl::{lower_rtl_module, RtlExpr, RtlModule, RtlSignalId}; + +#[path = "../test/alu_display/oracle.rs"] +mod oracle; + +// RTL lowering의 결과를 평가하고 독립적인 산술/segment oracle과 비교한다. +// parser/상수 폭/조건식/출력 전체 할당을 실제 fixture로 검증한다. +fn evaluate(rtl: &RtlModule, expr: &RtlExpr, values: &HashMap) -> u128 { + let result = match expr { + RtlExpr::Signal(signal) => values[&signal.signal], + RtlExpr::Const { value, .. } => *value, + RtlExpr::GreaterEqual(left, right) => { + u128::from(evaluate(rtl, left, values) >= evaluate(rtl, right, values)) + } + RtlExpr::Sub(left, right) => { + evaluate(rtl, left, values).wrapping_sub(evaluate(rtl, right, values)) + } + RtlExpr::Equal(left, right) => { + u128::from(evaluate(rtl, left, values) == evaluate(rtl, right, values)) + } + RtlExpr::Mux { + select, + when_true, + when_false, + } => evaluate( + rtl, + if evaluate(rtl, select, values) != 0 { + when_true + } else { + when_false + }, + values, + ), + RtlExpr::Resize { value, width } => { + let mask = if *width == 128 { + u128::MAX + } else { + (1u128 << width) - 1 + }; + evaluate(rtl, value, values) & mask + } + RtlExpr::Not(value) => !evaluate(rtl, value, values), + RtlExpr::And(left, right) => evaluate(rtl, left, values) & evaluate(rtl, right, values), + RtlExpr::Or(left, right) => evaluate(rtl, left, values) | evaluate(rtl, right, values), + RtlExpr::Xor(left, right) => evaluate(rtl, left, values) ^ evaluate(rtl, right, values), + RtlExpr::Add(left, right) => { + evaluate(rtl, left, values).wrapping_add(evaluate(rtl, right, values)) + } + RtlExpr::Select { value, msb, lsb } => { + (evaluate(rtl, value, values) >> lsb) & ((1u128 << (msb - lsb + 1)) - 1) + } + RtlExpr::Concat(parts) => parts.iter().fold(0, |value, part| { + (value << rtl.expr_width(part).unwrap()) | evaluate(rtl, part, values) + }), + }; + let width = rtl.expr_width(expr).unwrap(); + result + & if width == 128 { + u128::MAX + } else { + (1u128 << width) - 1 + } +} + +#[test] +fn bcd_decoder_all_digits_and_blank_match_independent_segments() -> eyre::Result<()> { + for source in [ + include_str!("../test/alu_display/bcd_to_7seg.v"), + include_str!("../test/alu_display/bcd_to_7seg_sop.v"), + ] { + let modules = parser::parse_modules(source)?; + let rtl = lower_rtl_module(&modules[0])?; + let digit_signal = rtl.signal_ref("digit")?; + let blank_signal = rtl.signal_ref("blank")?; + let seg_signal = rtl.signal_ref("seg")?; + assert_eq!(rtl.signal_width(digit_signal)?, 4); + assert_eq!(rtl.signal_width(blank_signal)?, 1); + assert_eq!(rtl.signal_width(seg_signal)?, 7); + for blank in [false, true] { + for digit in 0..16 { + let mut values = HashMap::from([ + (digit_signal.signal, digit as u128), + (blank_signal.signal, u128::from(blank)), + ]); + for assignment in &rtl.continuous_assigns { + let mask = (1u128 << rtl.signal_width(assignment.output)?) - 1; + values.insert( + assignment.output.signal, + evaluate(&rtl, &assignment.expr, &values) & mask, + ); + } + assert_eq!( + values[&seg_signal.signal], + oracle::segment_mask(digit, blank) as u128, + "digit={digit}, blank={blank}" + ); + } + } + } + Ok(()) +} + +fn run(rtl: &RtlModule, inputs: &[(&str, u128)]) -> eyre::Result> { + let mut values = HashMap::new(); + for (name, value) in inputs { + values.insert(rtl.signal_ref(name)?.signal, *value); + } + for assignment in &rtl.continuous_assigns { + let width = rtl.signal_width(assignment.output)?; + let mask = if width == 128 { + u128::MAX + } else { + (1u128 << width) - 1 + }; + values.insert( + assignment.output.signal, + evaluate(rtl, &assignment.expr, &values) & mask, + ); + } + Ok(values) +} + +#[test] +fn double_dabble_all_256_values_match_decimal_arithmetic() -> eyre::Result<()> { + let modules = parser::parse_modules(include_str!("../test/alu_display/bin8_to_bcd.v"))?; + let rtl = lower_rtl_module(&modules[0])?; + let h = rtl.signal_ref("hundreds")?.signal; + let t = rtl.signal_ref("tens")?.signal; + let o = rtl.signal_ref("ones")?.signal; + for value in 0..256 { + let values = run(&rtl, &[("value", value)])?; + assert_eq!( + [values[&h], values[&t], values[&o]], + [value / 100, (value / 10) % 10, value % 10], + "value={value}" + ); + } + Ok(()) +} + +#[test] +fn alu_all_524288_operands_and_operations_match_arithmetic() -> eyre::Result<()> { + let modules = parser::parse_modules(include_str!("../test/alu_display/alu8.v"))?; + let rtl = lower_rtl_module(&modules[0])?; + let outputs = ["y", "z", "n", "c", "v"].map(|name| rtl.signal_ref(name).unwrap().signal); + for a in 0..256u128 { + for b in 0..256u128 { + for op in 0..8 { + let signed_a = a as u8 as i8 as i16; + let signed_b = b as u8 as i8 as i16; + let (y, c, v) = match op { + 0 => ( + (a + b) & 255, + a + b > 255, + !(-128..=127).contains(&(signed_a + signed_b)), + ), + 1 => ( + a.wrapping_sub(b) & 255, + a >= b, + !(-128..=127).contains(&(signed_a - signed_b)), + ), + 2 => (a & b, false, false), + 3 => (a | b, false, false), + 4 => (a ^ b, false, false), + 5 => (a, false, false), + 6 => ((a * 2) & 255, a >= 128, false), + 7 => (a / 2, a % 2 != 0, false), + _ => unreachable!(), + }; + let values = run(&rtl, &[("a", a), ("b", b), ("op", op)])?; + assert_eq!( + outputs.map(|id| values[&id]), + [ + y, + u128::from(y == 0), + u128::from(y >= 128), + u128::from(c), + u128::from(v) + ], + "a={a}, b={b}, op={op}" + ); + } + } + } + Ok(()) +} diff --git a/tests/alu_display_hierarchy.rs b/tests/alu_display_hierarchy.rs new file mode 100644 index 00000000..bb5cbf99 --- /dev/null +++ b/tests/alu_display_hierarchy.rs @@ -0,0 +1,47 @@ +use redstone_compiler::ir::{LogicalDesign, LogicalPortDirection, RoutablePortDirection}; + +#[test] +fn three_digit_display_compiles_through_typed_hierarchy() -> eyre::Result<()> { + let source = [ + include_str!("../test/alu_display/bin8_to_bcd.v"), + include_str!("../test/alu_display/bcd_to_7seg.v"), + include_str!("../test/alu_display/display_u8.v"), + ] + .join("\n"); + let logical = LogicalDesign::from_verilog_source(&source)?; + let top = logical.module("display_u8").unwrap(); + assert_eq!(top.instances.len(), 4); + assert_eq!( + top.ports + .iter() + .filter(|p| p.direction == LogicalPortDirection::Output) + .count(), + 3 + ); + // 입력 상수도 폭을 가진 일반 셀로 남아 계층 인스턴스와 함께 처리된다. + assert!(!top.cells.is_empty()); + let serialized = logical.to_string(); + let parsed: LogicalDesign = serialized.parse()?; + let routable = parsed.lower_to_routable()?; + routable.validate()?; + let top = routable + .modules + .iter() + .find(|m| m.name == routable.top) + .unwrap(); + assert_eq!( + top.ports + .iter() + .filter(|p| p.direction == RoutablePortDirection::Input) + .count(), + 8 + ); + assert_eq!( + top.ports + .iter() + .filter(|p| p.direction == RoutablePortDirection::Output) + .count(), + 21 + ); + Ok(()) +} diff --git a/tests/alu_display_review.rs b/tests/alu_display_review.rs new file mode 100644 index 00000000..d43dfec7 --- /dev/null +++ b/tests/alu_display_review.rs @@ -0,0 +1,141 @@ +use redstone_compiler::ir::LogicalDesign; + +#[test] +fn independent_child_outputs_allow_acyclic_cross_instance_wiring() -> eyre::Result<()> { + // 첫 child의 low 출력은 b 입력에 의존하지 않는다. 전개하면 y=a, z=b다. + let source = "module split(a,b,lo,hi); input [1:0] a,b; output [1:0] lo,hi; assign lo=a; assign hi=b; endmodule + module top(a,b,y,z); input [1:0] a,b; output [1:0] y,z; wire [1:0] u,v; + split first(.a(a),.b(v),.lo(u),.hi(z)); + split second(.a(b),.b(u),.lo(v),.hi(y)); endmodule"; + let design = LogicalDesign::from_verilog_source(source)?; + let routable = design.lower_to_routable()?; + routable.validate()?; + for a in 0..4 { + for b in 0..4 { + let input = [a & 1 != 0, a & 2 != 0, b & 1 != 0, b & 2 != 0]; + assert_eq!(evaluate(&routable, &input), input); + } + } + Ok(()) +} + +#[test] +fn scalar_port_names_cannot_alias_generated_bus_bit_names() -> eyre::Result<()> { + let source = + "module top(a,a_0,y); input [1:0] a; input a_0; output y; assign y=a[0]^a_0; endmodule"; + let design = LogicalDesign::from_verilog_source(source)?; + let routable = design.lower_to_routable()?; + routable.validate()?; + for bits in 0..8 { + let input = [bits & 1 != 0, bits & 2 != 0, bits & 4 != 0]; + assert_eq!(evaluate(&routable, &input), [input[0] ^ input[2]]); + } + Ok(()) +} + +// 실제 물리 배치 대신 생성된 Routable leaf 논리를 평가한다. +// 입력 목록은 선언 순서이며 버스 내부는 LSB부터다. +fn evaluate(design: &redstone_compiler::ir::RoutableDesign, input: &[bool]) -> Vec { + use std::collections::HashMap; + + use redstone_compiler::ir::{ + Endpoint, RoutableModuleBody, RoutableNodeKind, RoutablePortDirection, + }; + let top = design.module(&design.top).unwrap(); + let input_ports = top + .ports + .iter() + .filter(|p| p.direction == RoutablePortDirection::Input) + .collect::>(); + assert_eq!(input_ports.len(), input.len()); + let mut values = input_ports + .iter() + .zip(input) + .map(|(p, &v)| { + ( + Endpoint::SelfPort { + port: p.name.clone(), + }, + v, + ) + }) + .collect::>(); + let RoutableModuleBody::Composite { instances, nets } = &top.body else { + panic!("expected flat composite") + }; + let mut remaining = instances.iter().collect::>(); + while !remaining.is_empty() { + for net in nets { + if let Some(v) = values.get(&net.driver).copied() { + for sink in &net.sinks { + values.insert(sink.clone(), v); + } + } + } + let before = remaining.len(); + remaining.retain(|instance| { + let leaf = design.module(&instance.module).unwrap(); + let endpoint = |name: &str| Endpoint::InstancePort { + instance: instance.name.clone(), + port: name.to_owned(), + }; + if leaf + .ports + .iter() + .filter(|p| p.direction == RoutablePortDirection::Input) + .any(|p| !values.contains_key(&endpoint(&p.name))) + { + return true; + } + let RoutableModuleBody::Leaf { nodes } = &leaf.body else { + panic!("expected leaf") + }; + let mut computed = HashMap::new(); + let mut unresolved = nodes.iter().collect::>(); + while !unresolved.is_empty() { + let previous = unresolved.len(); + unresolved.retain(|node| { + let args = node + .inputs + .iter() + .map(|id| computed.get(id).copied()) + .collect::>>(); + let Some(args) = args else { + return true; + }; + let result = match &node.kind { + RoutableNodeKind::Input { name } => values[&endpoint(name)], + RoutableNodeKind::Not => !args[0], + RoutableNodeKind::Or => args.iter().any(|v| *v), + RoutableNodeKind::Output { name } => { + values.insert(endpoint(name), args[0]); + args[0] + } + _ => panic!("unexpected sequential logic"), + }; + computed.insert(node.id, result); + false + }); + assert!(unresolved.len() < previous); + } + false + }); + assert!(remaining.len() < before); + } + for net in nets { + if let Some(v) = values.get(&net.driver).copied() { + for sink in &net.sinks { + values.insert(sink.clone(), v); + } + } + } + top.ports + .iter() + .filter(|p| p.direction == RoutablePortDirection::Output) + .map(|p| { + values[&Endpoint::SelfPort { + port: p.name.clone(), + }] + }) + .collect() +} diff --git a/tools/alu_display/generate_add_sub.py b/tools/alu_display/generate_add_sub.py new file mode 100644 index 00000000..9a67d83f --- /dev/null +++ b/tools/alu_display/generate_add_sub.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +"""Emit ordinary unsigned RTL with one shared ripple-carry add/sub chain.""" +from pathlib import Path + + +def source(width): + assert width in (1, 2, 4, 8) + lines = ["// Generated by tools/alu_display/generate_add_sub.py.", + "// subtract=0: a+b; subtract=1: a-b; c=carry / no-borrow.", + "// One explicitly shared carry chain; no arithmetic-sharing pass assumed.", + "module add_sub(a, b, subtract, y, c);", f" input [{width-1}:0] a, b;", + " input subtract;", f" output [{width-1}:0] y;", " output c;", + f" wire [{width-1}:0] adjusted_b;", + " wire " + ", ".join(f"carry_{i}" for i in range(width+1)) + ";", + " wire " + ", ".join(f"propagate_{i}, sum_{i}" for i in range(width)) + ";", + " assign adjusted_b = b ^ {" + ", ".join(["subtract"]*width) + "};", + " assign carry_0 = subtract;"] + for bit in range(width): + lines.extend([f" assign propagate_{bit} = a[{bit}] ^ adjusted_b[{bit}];", + f" assign sum_{bit} = propagate_{bit} ^ carry_{bit};", + f" assign carry_{bit+1} = (a[{bit}] & adjusted_b[{bit}]) | (propagate_{bit} & carry_{bit});"]) + lines.extend([" assign y = {" + ", ".join(f"sum_{i}" for i in reversed(range(width))) + "};", + f" assign c = carry_{width};", "endmodule", ""]) + return "\n".join(lines) + + +if __name__ == "__main__": + for width in (1, 2, 4, 8): + path = Path(f"test/alu_display/add_sub{width}.v") + path.write_text(source(width)) + print(path) diff --git a/tools/alu_display/minimize_decoder.py b/tools/alu_display/minimize_decoder.py new file mode 100644 index 00000000..7794f304 --- /dev/null +++ b/tools/alu_display/minimize_decoder.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""Enumerate exact 4-input prime covers, then minimize shared recipe pin edges. + +Digits 10..15 are OFF, never don't-care. Each segment chooses a minimum-term +SOP or POS prime cover. All combinations of these covers are compared; this +is not a claim of globally optimal arbitrary Boolean-network synthesis. +""" +from functools import reduce +from itertools import combinations, product +from pathlib import Path +import argparse + +MASKS = (0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0, 0, 0, 0, 0, 0) + + +def covers_for(target): + cubes = [] + for literals in product((-1, 0, 1), repeat=4): + covered = sum(1 << value for value in range(16) + if all(bit == -1 or (value >> index) & 1 == bit + for index, bit in enumerate(literals))) + if not covered & ~target: + cubes.append((literals, covered)) + primes = [(literals, covered) for literals, covered in cubes + if not any(covered != other and covered & other == covered + for _, other in cubes)] + for size in range(1, len(primes) + 1): + covers = [tuple(literals for literals, _ in selected) + for selected in combinations(primes, size) + if reduce(int.__or__, (covered for _, covered in selected), 0) == target] + if covers: + return covers + raise ValueError("constant outputs need a separate source recipe") + + +def options(): + result = [] + for segment in range(7): + on = sum(1 << value for value in range(16) if MASKS[value] >> segment & 1) + result.append([(mode, cover) for mode, target in (("sop", on), ("pos", on ^ 0xFFFF)) + for cover in covers_for(target)]) + return result + + +def cost(selected): + unique = {(mode, term) for mode, terms in selected for term in terms + if sum(bit != -1 for bit in term) > 1} + # A singleton literal is directly wired/absorbed by the final recipe. + edges = sum(sum(bit != -1 for bit in term) for _, term in unique) + edges += sum(len(terms) for _, terms in selected) + return edges, len(unique) + 7, selected + + +def evaluate(mode, terms, value): + matches = [all(bit == -1 or (value >> index) & 1 == bit + for index, bit in enumerate(term)) for term in terms] + return any(matches) if mode == "sop" else not any(matches) + + +def expression(mode, term): + literals = [] + for index, value in enumerate(term): + if value == -1: + continue + positive = value == 1 if mode == "sop" else value == 0 + literals.append(("" if positive else "~") + f"digit[{index}]") + return (" & " if mode == "sop" else " | ").join(literals) + + +def generate(): + choices = options() + selected = min(product(*choices), key=cost) + for digit in range(16): + actual = sum(int(evaluate(mode, terms, digit)) << index + for index, (mode, terms) in enumerate(selected)) + assert actual == MASKS[digit], (digit, actual, MASKS[digit]) + for blank in (0, 1): + assert (actual if not blank else 0) == (MASKS[digit] if not blank else 0) + unique = sorted({(mode, term) for mode, terms in selected for term in terms + if sum(bit != -1 for bit in term) > 1}) + names = {key: f"term_{index}" for index, key in enumerate(unique)} + edges, cells, _ = cost(selected) + lines = ["// Generated by tools/alu_display/minimize_decoder.py.", + "// Exact 4-input covers: invalid BCD 10..15 is OFF; no don't-care.", + f"// Shared recipe estimate before blank: {cells} cells, {edges} input edges.", + "module bcd_to_7seg(digit, blank, seg);", + " input [3:0] digit;", " input blank;", " output [6:0] seg;", + " wire " + ", ".join(names.values()) + ";", " wire a, b, c, d, e, f, g;"] + for key, name in names.items(): + lines.append(f" assign {name} = {expression(*key)};") + for index, (mode, terms) in enumerate(selected): + operands = [names.get((mode, term), f"({expression(mode, term)})") for term in terms] + joined = (" | " if mode == "sop" else " & ").join(operands) + lines.append(f" assign {chr(97 + index)} = ~blank & ({joined});") + lines.extend([" assign seg = {g, f, e, d, c, b, a};", "endmodule", ""]) + return "\n".join(lines), len(list(product(*choices))), edges, cells + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, default=Path("test/alu_display/bcd_to_7seg_minimized.v")) + args = parser.parse_args() + text, combinations_count, edges, cells = generate() + args.output.write_text(text) + print(f"checked {combinations_count} combinations of minimum-term SOP/POS covers") + print(f"16 digit values and blank=0/1 verified; estimated cells={cells}, input_edges={edges}") + print(args.output) diff --git a/tools/alu_display/whole_decoder_exact.sh b/tools/alu_display/whole_decoder_exact.sh new file mode 100755 index 00000000..d333efcb --- /dev/null +++ b/tools/alu_display/whole_decoder_exact.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# Generic NOR-template initial geometry; SAT selects deletion subsets. +# Ordinary exact windows compact the verified complete NBT afterwards. +set -eu +if [ "$#" -ne 1 ]; then + printf 'Usage: %s FRESH_OUTPUT_DIRECTORY\n' "$0" >&2 + exit 2 +fi +cd "$(dirname "$0")/../.." +unset CONSTRUCT_RESUME CONSTRUCT_NETLIST CONSTRUCT_COMPACT_ONLY +CONSTRUCT_WIDTH=33 CONSTRUCT_HEIGHT=7 \ +CONSTRUCT_ORDER=smallest-cone CONSTRUCT_FLATTEN_POSITIVE=1 \ +CONSTRUCT_ISOLATE_OUTPUTS=0 CONSTRUCT_MAX_FANIN=0 CONSTRUCT_GATE_BATCH=1 \ +CONSTRUCT_STEP_VOCABULARY=1 CONSTRUCT_COMPRESS_COMPLEMENTS=1 \ +CONSTRUCT_PARTIAL_ORS=1 CONSTRUCT_DRIVING_SEAMS=1 \ +CONSTRUCT_REPEATER_SEAMS=1 CONSTRUCT_SCAFFOLD_SEAMS=1 \ +CONSTRUCT_SEAM_PITCH=3 CONSTRUCT_PRIMARY_STRIDE=2 \ +CONSTRUCT_GATE_SCAFFOLD=1 CONSTRUCT_CASE_RANK_LEVELS=8 \ +CONSTRUCT_STAGE_LEVELS=48 CONSTRUCT_CANONICAL_RANKS=1 \ +CONSTRUCT_SYMMETRY=1 CONSTRUCT_WINDOW=10 CONSTRUCT_MAX_WINDOW=10 \ +CONSTRUCT_OVERLAP=0 CONSTRUCT_MAX_OVERLAP=0 \ +CONSTRUCT_STEP_OPT_MS=0 CONSTRUCT_DELETE_MS=0 CONSTRUCT_RESTARTS=0 \ +CONSTRUCT_STEP_SECONDS=60 CONSTRUCT_WORKERS=4 CONSTRUCT_BUDGET_SECONDS=3600 \ + cargo run --release --example whole_decoder_construct -- "$1" diff --git a/tools/nbt-viewer/README.md b/tools/nbt-viewer/README.md index dc64c243..4494735a 100644 --- a/tools/nbt-viewer/README.md +++ b/tools/nbt-viewer/README.md @@ -24,6 +24,22 @@ Build the optional Rust simulator WASM package: npm.cmd run build:wasm ``` +Rebuild WASM after changing the Rust simulator, then build the viewer: + +```sh +npm run build:wasm # cargo build --release for wasm32-unknown-unknown + wasm-bindgen +npm run build +``` + +`prepare:examples` (also run by `dev` and `build`) regenerates the example +manifest from the top level of `test/` and curated fixtures in +`test/alu_display/nbt/`. Place an optional matching `.outputs.json` beside each +NBT to expose output probes. Duplicate filenames across these directories are +rejected. Do not add entries only to the generated public manifest: rebuilding +will replace them. The curated ALU/display fixtures include the verified digit +7-segment decoder and 2-bit/8-bit adders; their Rust simulator fixes require a +fresh WASM build, not only a Vite refresh. + Start the development server: ```powershell diff --git a/tools/nbt-viewer/public/examples/alu-display-adder2-verified-73x10x9.nbt b/tools/nbt-viewer/public/examples/alu-display-adder2-verified-73x10x9.nbt new file mode 100644 index 00000000..223f0afb Binary files /dev/null and b/tools/nbt-viewer/public/examples/alu-display-adder2-verified-73x10x9.nbt differ diff --git a/tools/nbt-viewer/public/examples/alu-display-adder2-verified-73x10x9.outputs.json b/tools/nbt-viewer/public/examples/alu-display-adder2-verified-73x10x9.outputs.json new file mode 100644 index 00000000..d38067f6 --- /dev/null +++ b/tools/nbt-viewer/public/examples/alu-display-adder2-verified-73x10x9.outputs.json @@ -0,0 +1,29 @@ +{ + "format": "redstone-compiler.outputs.v1", + "outputs": [ + { + "name": "sum_0", + "position": [ + 5, + 6, + 2 + ] + }, + { + "name": "sum_1", + "position": [ + 36, + 6, + 2 + ] + }, + { + "name": "sum_2", + "position": [ + 75, + 9, + 1 + ] + } + ] +} \ No newline at end of file diff --git a/tools/nbt-viewer/public/examples/alu-display-adder8-verified.nbt b/tools/nbt-viewer/public/examples/alu-display-adder8-verified.nbt new file mode 100644 index 00000000..bde090bb Binary files /dev/null and b/tools/nbt-viewer/public/examples/alu-display-adder8-verified.nbt differ diff --git a/tools/nbt-viewer/public/examples/alu-display-adder8-verified.outputs.json b/tools/nbt-viewer/public/examples/alu-display-adder8-verified.outputs.json new file mode 100644 index 00000000..bcc605e7 --- /dev/null +++ b/tools/nbt-viewer/public/examples/alu-display-adder8-verified.outputs.json @@ -0,0 +1,77 @@ +{ + "format": "redstone-compiler.outputs.v1", + "outputs": [ + { + "name": "sum_0", + "position": [ + 8, + 4, + 2 + ] + }, + { + "name": "sum_1", + "position": [ + 21, + 14, + 2 + ] + }, + { + "name": "sum_2", + "position": [ + 8, + 42, + 2 + ] + }, + { + "name": "sum_3", + "position": [ + 21, + 62, + 2 + ] + }, + { + "name": "sum_4", + "position": [ + 8, + 90, + 2 + ] + }, + { + "name": "sum_5", + "position": [ + 21, + 110, + 2 + ] + }, + { + "name": "sum_6", + "position": [ + 8, + 138, + 2 + ] + }, + { + "name": "sum_7", + "position": [ + 21, + 158, + 2 + ] + }, + { + "name": "sum_8", + "position": [ + 9, + 181, + 1 + ] + } + ] +} \ No newline at end of file diff --git a/tools/nbt-viewer/public/examples/alu-display-decoder-digit-verified.nbt b/tools/nbt-viewer/public/examples/alu-display-decoder-digit-verified.nbt new file mode 100644 index 00000000..01d5da99 Binary files /dev/null and b/tools/nbt-viewer/public/examples/alu-display-decoder-digit-verified.nbt differ diff --git a/tools/nbt-viewer/public/examples/alu-display-decoder-digit-verified.outputs.json b/tools/nbt-viewer/public/examples/alu-display-decoder-digit-verified.outputs.json new file mode 100644 index 00000000..7875d1c2 --- /dev/null +++ b/tools/nbt-viewer/public/examples/alu-display-decoder-digit-verified.outputs.json @@ -0,0 +1,61 @@ +{ + "format": "redstone-compiler.outputs.v1", + "outputs": [ + { + "name": "seg_0", + "position": [ + 27, + 75, + 1 + ] + }, + { + "name": "seg_1", + "position": [ + 55, + 75, + 1 + ] + }, + { + "name": "seg_2", + "position": [ + 21, + 91, + 1 + ] + }, + { + "name": "seg_3", + "position": [ + 94, + 56, + 1 + ] + }, + { + "name": "seg_4", + "position": [ + 60, + 50, + 1 + ] + }, + { + "name": "seg_5", + "position": [ + 49, + 94, + 1 + ] + }, + { + "name": "seg_6", + "position": [ + 83, + 75, + 1 + ] + } + ] +} \ No newline at end of file diff --git a/tools/nbt-viewer/public/examples/alu-display-decoder-exact-verified.nbt b/tools/nbt-viewer/public/examples/alu-display-decoder-exact-verified.nbt new file mode 100644 index 00000000..34f74546 Binary files /dev/null and b/tools/nbt-viewer/public/examples/alu-display-decoder-exact-verified.nbt differ diff --git a/tools/nbt-viewer/public/examples/alu-display-decoder-exact-verified.outputs.json b/tools/nbt-viewer/public/examples/alu-display-decoder-exact-verified.outputs.json new file mode 100644 index 00000000..c1d827fb --- /dev/null +++ b/tools/nbt-viewer/public/examples/alu-display-decoder-exact-verified.outputs.json @@ -0,0 +1,61 @@ +{ + "format": "redstone-compiler.outputs.v1", + "outputs": [ + { + "name": "seg_0", + "position": [ + 32, + 20, + 38 + ] + }, + { + "name": "seg_1", + "position": [ + 47, + 21, + 43 + ] + }, + { + "name": "seg_2", + "position": [ + 24, + 23, + 13 + ] + }, + { + "name": "seg_3", + "position": [ + 33, + 24, + 48 + ] + }, + { + "name": "seg_4", + "position": [ + 46, + 22, + 53 + ] + }, + { + "name": "seg_5", + "position": [ + 34, + 19, + 58 + ] + }, + { + "name": "seg_6", + "position": [ + 34, + 20, + 63 + ] + } + ] +} \ No newline at end of file diff --git a/tools/nbt-viewer/public/examples/alu-display-decoder-layered-verified.nbt b/tools/nbt-viewer/public/examples/alu-display-decoder-layered-verified.nbt new file mode 100644 index 00000000..ddf36c3e Binary files /dev/null and b/tools/nbt-viewer/public/examples/alu-display-decoder-layered-verified.nbt differ diff --git a/tools/nbt-viewer/public/examples/alu-display-decoder-layered-verified.outputs.json b/tools/nbt-viewer/public/examples/alu-display-decoder-layered-verified.outputs.json new file mode 100644 index 00000000..f87801e6 --- /dev/null +++ b/tools/nbt-viewer/public/examples/alu-display-decoder-layered-verified.outputs.json @@ -0,0 +1,61 @@ +{ + "format": "redstone-compiler.outputs.v1", + "outputs": [ + { + "name": "seg_0", + "position": [ + 27, + 75, + 10 + ] + }, + { + "name": "seg_1", + "position": [ + 55, + 75, + 10 + ] + }, + { + "name": "seg_2", + "position": [ + 21, + 91, + 10 + ] + }, + { + "name": "seg_3", + "position": [ + 94, + 56, + 10 + ] + }, + { + "name": "seg_4", + "position": [ + 60, + 50, + 10 + ] + }, + { + "name": "seg_5", + "position": [ + 49, + 94, + 10 + ] + }, + { + "name": "seg_6", + "position": [ + 83, + 75, + 10 + ] + } + ] +} \ No newline at end of file diff --git a/tools/nbt-viewer/public/examples/alu-display-decoder-whole-exact.nbt b/tools/nbt-viewer/public/examples/alu-display-decoder-whole-exact.nbt new file mode 100644 index 00000000..178d2117 Binary files /dev/null and b/tools/nbt-viewer/public/examples/alu-display-decoder-whole-exact.nbt differ diff --git a/tools/nbt-viewer/public/examples/alu-display-decoder-whole-exact.outputs.json b/tools/nbt-viewer/public/examples/alu-display-decoder-whole-exact.outputs.json new file mode 100644 index 00000000..dab55401 --- /dev/null +++ b/tools/nbt-viewer/public/examples/alu-display-decoder-whole-exact.outputs.json @@ -0,0 +1,95 @@ +{ + "inputs": [ + { + "name": "digit_0", + "position": [ + 1, + 39, + 1 + ] + }, + { + "name": "digit_1", + "position": [ + 7, + 9, + 1 + ] + }, + { + "name": "digit_2", + "position": [ + 13, + 0, + 1 + ] + }, + { + "name": "digit_3", + "position": [ + 19, + 19, + 1 + ] + } + ], + "outputs": [ + { + "name": "seg_0", + "position": [ + 25, + 106, + 2 + ] + }, + { + "name": "seg_1", + "position": [ + 25, + 135, + 4 + ] + }, + { + "name": "seg_2", + "position": [ + 4, + 77, + 1 + ] + }, + { + "name": "seg_3", + "position": [ + 1, + 215, + 4 + ] + }, + { + "name": "seg_4", + "position": [ + 4, + 47, + 1 + ] + }, + { + "name": "seg_5", + "position": [ + 25, + 176, + 2 + ] + }, + { + "name": "seg_6", + "position": [ + 1, + 205, + 4 + ] + } + ], + "format": "redstone-compiler.interface.v1" +} diff --git a/tools/nbt-viewer/public/examples/manifest.json b/tools/nbt-viewer/public/examples/manifest.json index 82729402..fc99276b 100644 --- a/tools/nbt-viewer/public/examples/manifest.json +++ b/tools/nbt-viewer/public/examples/manifest.json @@ -31,6 +31,54 @@ "size": 888, "outputsPath": "examples/adder2-synth-exact-2x16x8.outputs.json" }, + { + "kind": "nbt", + "name": "alu-display-adder2-verified-73x10x9.nbt", + "file": "alu-display-adder2-verified-73x10x9.nbt", + "path": "examples/alu-display-adder2-verified-73x10x9.nbt", + "size": 2601, + "outputsPath": "examples/alu-display-adder2-verified-73x10x9.outputs.json" + }, + { + "kind": "nbt", + "name": "alu-display-adder8-verified.nbt", + "file": "alu-display-adder8-verified.nbt", + "path": "examples/alu-display-adder8-verified.nbt", + "size": 11857, + "outputsPath": "examples/alu-display-adder8-verified.outputs.json" + }, + { + "kind": "nbt", + "name": "alu-display-decoder-digit-verified.nbt", + "file": "alu-display-decoder-digit-verified.nbt", + "path": "examples/alu-display-decoder-digit-verified.nbt", + "size": 17926, + "outputsPath": "examples/alu-display-decoder-digit-verified.outputs.json" + }, + { + "kind": "nbt", + "name": "alu-display-decoder-exact-verified.nbt", + "file": "alu-display-decoder-exact-verified.nbt", + "path": "examples/alu-display-decoder-exact-verified.nbt", + "size": 15031, + "outputsPath": "examples/alu-display-decoder-exact-verified.outputs.json" + }, + { + "kind": "nbt", + "name": "alu-display-decoder-layered-verified.nbt", + "file": "alu-display-decoder-layered-verified.nbt", + "path": "examples/alu-display-decoder-layered-verified.nbt", + "size": 17673, + "outputsPath": "examples/alu-display-decoder-layered-verified.outputs.json" + }, + { + "kind": "nbt", + "name": "alu-display-decoder-whole-exact.nbt", + "file": "alu-display-decoder-whole-exact.nbt", + "path": "examples/alu-display-decoder-whole-exact.nbt", + "size": 13303, + "outputsPath": "examples/alu-display-decoder-whole-exact.outputs.json" + }, { "kind": "nbt", "name": "alu-export.nbt", @@ -159,6 +207,38 @@ "size": 445, "outputsPath": "examples/dff-exact-2x6x4.outputs.json" }, + { + "kind": "nbt", + "name": "dff-global-2x7x4.nbt", + "file": "dff-global-2x7x4.nbt", + "path": "examples/dff-global-2x7x4.nbt", + "size": 536, + "outputsPath": "examples/dff-global-2x7x4.outputs.json" + }, + { + "kind": "nbt", + "name": "dff-global-43blocks.nbt", + "file": "dff-global-43blocks.nbt", + "path": "examples/dff-global-43blocks.nbt", + "size": 507, + "outputsPath": "examples/dff-global-43blocks.outputs.json" + }, + { + "kind": "nbt", + "name": "dff-global-44blocks.nbt", + "file": "dff-global-44blocks.nbt", + "path": "examples/dff-global-44blocks.nbt", + "size": 510, + "outputsPath": "examples/dff-global-44blocks.outputs.json" + }, + { + "kind": "nbt", + "name": "dff-global-optimized.nbt", + "file": "dff-global-optimized.nbt", + "path": "examples/dff-global-optimized.nbt", + "size": 500, + "outputsPath": "examples/dff-global-optimized.outputs.json" + }, { "kind": "nbt", "name": "dust-step-up-primitive.nbt", @@ -405,37 +485,5 @@ "file": "xor.nbt", "path": "examples/xor.nbt", "size": 581 - }, - { - "kind": "nbt", - "name": "dff-global-2x7x4.nbt", - "file": "dff-global-2x7x4.nbt", - "path": "examples/dff-global-2x7x4.nbt", - "size": 536, - "outputsPath": "examples/dff-global-2x7x4.outputs.json" - }, - { - "kind": "nbt", - "name": "dff-global-44blocks.nbt", - "file": "dff-global-44blocks.nbt", - "path": "examples/dff-global-44blocks.nbt", - "size": 510, - "outputsPath": "examples/dff-global-44blocks.outputs.json" - }, - { - "kind": "nbt", - "name": "dff-global-43blocks.nbt", - "file": "dff-global-43blocks.nbt", - "path": "examples/dff-global-43blocks.nbt", - "size": 507, - "outputsPath": "examples/dff-global-43blocks.outputs.json" - }, - { - "kind": "nbt", - "name": "dff-global-optimized.nbt", - "file": "dff-global-optimized.nbt", - "path": "examples/dff-global-optimized.nbt", - "size": 500, - "outputsPath": "examples/dff-global-optimized.outputs.json" } ] diff --git a/tools/nbt-viewer/scripts/prepare-examples.mjs b/tools/nbt-viewer/scripts/prepare-examples.mjs index f895100e..ce0364cb 100644 --- a/tools/nbt-viewer/scripts/prepare-examples.mjs +++ b/tools/nbt-viewer/scripts/prepare-examples.mjs @@ -2,42 +2,49 @@ import { copyFile, mkdir, readdir, stat, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; const repoRoot = join(process.cwd(), '..', '..'); -const sourceRoot = join(repoRoot, 'test'); +const sourceRoots = [join(repoRoot, 'test'), join(repoRoot, 'test', 'alu_display', 'nbt')]; const targetRoot = join(process.cwd(), 'public', 'examples'); await mkdir(targetRoot, { recursive: true }); -const entries = await readdir(sourceRoot, { withFileTypes: true }); const examples = []; +const names = new Set(); -for (const entry of entries) { - if (!entry.isFile() || (!entry.name.endsWith('.nbt') && !entry.name.endsWith('.rsnap'))) continue; - - const source = join(sourceRoot, entry.name); - const target = join(targetRoot, entry.name); - const kind = entry.name.endsWith('.rsnap') ? 'snapshot' : 'nbt'; - const outputsName = entry.name.replace(/\.nbt$/, '.outputs.json'); - const outputsSource = join(sourceRoot, outputsName); - const outputsTarget = join(targetRoot, outputsName); - const info = await stat(source); - await copyFile(source, target); - const example = { - kind, - name: entry.name, - file: entry.name, - path: `examples/${entry.name}`, - size: info.size, - }; - if (kind === 'nbt') { - try { - await stat(outputsSource); - await copyFile(outputsSource, outputsTarget); - example.outputsPath = `examples/${outputsName}`; - } catch (error) { - if (error?.code !== 'ENOENT') throw error; +for (const sourceRoot of sourceRoots) { + const entries = await readdir(sourceRoot, { withFileTypes: true }); + + for (const entry of entries) { + if (!entry.isFile() || (!entry.name.endsWith('.nbt') && !entry.name.endsWith('.rsnap'))) continue; + + if (names.has(entry.name)) throw new Error(`Duplicate example filename: ${entry.name}`); + names.add(entry.name); + const source = join(sourceRoot, entry.name); + const target = join(targetRoot, entry.name); + const kind = entry.name.endsWith('.rsnap') ? 'snapshot' : 'nbt'; + const outputsName = entry.name.replace(/\.nbt$/, '.outputs.json'); + const outputsSource = join(sourceRoot, outputsName); + const outputsTarget = join(targetRoot, outputsName); + const info = await stat(source); + await copyFile(source, target); + const example = { + kind, + name: entry.name, + file: entry.name, + path: `examples/${entry.name}`, + size: info.size, + }; + if (kind === 'nbt') { + try { + await stat(outputsSource); + await copyFile(outputsSource, outputsTarget); + example.outputsPath = `examples/${outputsName}`; + } catch (error) { + if (error?.code !== 'ENOENT') throw error; + } } + examples.push(example); } - examples.push(example); + } examples.sort((a, b) => a.name.localeCompare(b.name)); diff --git a/tools/nbt-viewer/src/main.ts b/tools/nbt-viewer/src/main.ts index d994a3f4..0ac3f352 100644 --- a/tools/nbt-viewer/src/main.ts +++ b/tools/nbt-viewer/src/main.ts @@ -3259,7 +3259,7 @@ window.addEventListener('keydown', event => { async function exampleList(): Promise { if (!exampleCache) { - const response = await fetch(resolveAssetPath('examples/manifest.json')); + const response = await fetch(resolveAssetPath('examples/manifest.json'), { cache: 'no-store' }); if (!response.ok) throw new Error(`Failed to load examples: ${response.status}`); exampleCache = (await response.json()) as ExampleFile[]; }