Skip to content

Methods, initial blocks, FSM cycle fusion, DFacsimile - #414

Merged
soronpo merged 207 commits into
mainfrom
static-func
Jul 24, 2026
Merged

Methods, initial blocks, FSM cycle fusion, DFacsimile#414
soronpo merged 207 commits into
mainfrom
static-func

Conversation

@soronpo

@soronpo soronpo commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

Oron Port and others added 30 commits June 19, 2026 04:03
Introduce `dfhdl.options.ToolOptions.Location` (dftools|local), threaded
through the linter/simulator/builder/programmer options following the
existing OnError pattern, and exposed as the `--tools-location` CLI flag.
When set to `dftools`, `Tool.exec` runs the bare tool inside the pinned
DFTools Apptainer image (resolved/cached by the new `DFToolsImage` via
Scalapptainer) instead of resolving it on the host PATH; `local` keeps the
existing PATH-based behavior. Default is `local` until a DFTools image
release is published.

Adds the scalapptainer dependency to the lib subproject.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework DFToolsImage from one combined image to a per-cluster image set
(synth-verilog, synth-vhdl, pnr, sim-llvm, sim-verilator, sim-iverilog,
wavegen, program). `imageFor(exec, vhdl)` maps each in-image executable to
its image (yosys resolves by backend dialect; ghdl-as-simulator -> sim-llvm).
Images are resolved/cached independently, with a per-image dev/test override
`-Ddfhdl.dftools.sif.<image>=<path>`. Tool.exec now selects the image per
executable before spawning the apptainer argv.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When running a tool inside a DFTools image, the command runs on Linux, so
backslash path separators in the relative source/include arguments must be
converted to forward slashes (otherwise e.g. iverilog sees a literal
"hdl\rotWord.v" and fails to find the file). Validated end-to-end: the AES
CipherSim simulation now runs to $finish through the sim-iverilog image
(verilator/iverilog on Windows via WSL2 apptainer exec).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…inary name

- Tool.exec: a produced artifact (path with a separator, e.g. verilator's
  obj_dir/V<top>) runs by its relative ./path inside the mounted cwd, with the
  Linux name (forward slashes, .exe stripped), in its producing tool's image.
- Verilator: use the `verilator` wrapper inside the image (not the Windows
  verilator_bin), and report the verilated binary as obj_dir/V<top> (no .exe)
  under DFTools so producedFiles/run resolve the actual Linux artifact.

Validated: AES CipherSim simulates to $finish through both the sim-iverilog
and sim-verilator images (verilate -> compile C++ with g++/make in-image ->
run produced binary).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DFHDL now runs the external EDA tools via the pinned DFTools images by
default; pass --tools-location local to use tools on PATH instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tool version/availability detection scanned only the host PATH (installedVersion),
so in dftools mode tools that look up their version before running — Verilator's
lint config (getInstalledVersion) and NVC's produced-file naming — failed with
"<tool> could not be found" even though the tool lives in the image. This made the
DFTools CI gate's `test` run fail on issues.IssuesSpec i142/i147 (Verilog linting).

Add DFToolsImage.probe and a memoized dftoolsInstalledVersion that runs the tool's
versionCmd inside its image and parses it with the same extractVersion. getInstalledVersion
now uses it in dftools mode; NVC reads the version via getInstalledVersion instead of the
host-only installedVersion. Verified locally: IssuesSpec Verilog-linting tests pass in
dftools mode against the locally built sifs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DFToolsImage.version was hardcoded. Define the targeted DFTools release as
`dftoolsVersion` in build.sbt and surface it through the generated
version.properties resource (alongside the DFHDL version), read by
DFToolsImage the same way hdl.dfhdlVersion reads its version. Bumping the
DFTools release is now a one-line build.sbt change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Use Apptainer.pull (apptainer's native https:// SIF transport, with cache-reuse)
to fetch the release asset, replacing the manual curl download.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the dependency versions out of the `dependencies` object and the inlined
scalapptainer version out of `lib` into a single VERSIONS block at the top,
alongside compilerVersion and dftoolsVersion. The dependencies object and lib
now reference these names.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Source-compatible; lib compiles and the DFTools image pull/exec/version-probe
path works end-to-end (i142 verilator lint passes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tool objects are singletons accessed concurrently; the previous memoization used
an unsynchronized `var` (a data race). Replace it with a parameterless `lazy val`
(the `using ToolOptions` was unused), which initializes once and publishes safely
across threads — matching the existing `installedVersion` lazy val.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add `isToolInWindows = osIsWindows && !usesDFTools` (the tool runs as a Windows
binary only on a Windows host AND outside the Linux DFTools image) plus a matching
`toolSeparatorChar`. Key the tool-facing launcher name, the produced-binary name,
`vvp`/`osExe` siblings, and path conversion off these instead of the raw host
`osIsWindows`. In dftools mode the tool then naturally resolves to its plain Linux
`binExec` with `/`-separated paths, so the bespoke container handling is removed:

- `containerExec` (and Verilator's override) — runExec is already the in-image name
- the produced-artifact `.exe`-drop / backslash rewrite in `Tool.exec`
- the blanket `cmd.replace('\','/')` — source paths convert via `!isToolInWindows`
- `verilatedBinary`'s `usesDFTools` special case

The local PATH lookup uses a dedicated `hostExec`; local behaviour is unchanged
(there `isToolInWindows == osIsWindows`). Verified Windows->container: verilator
lint, iverilog v2001 sim, and verilator sim all pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Under `sbtn` an external tool that floods stdout (e.g. a runaway verilator
simulation) could not be stopped with Ctrl+C: the run executes in the detached
sbt server JVM and the cancel is forwarded by the client, but the output flood
saturates the client<->server channel so the client never gets to deliver it.

Cap forwarded console output continuously (~500 lines/s) so the channel stays
idle most of each window, leaving headroom for the cancel to arrive promptly. A
burst/adaptive cap does not work — a flood re-saturates the channel instantly —
so the rate must be capped continuously. Output below the cap is untouched and
nothing is dropped. Also drop the post-kill backlog silently (`aborted`) so a
cancelled run ends promptly instead of trickling the buffered backlog out at the
throttled rate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When an external tool floods stdout while running inside the DFTools image on
Windows + WSL2, Ctrl+C could not stop it. Two launcher-specific causes:

- scala-cli (and similar) hide the Ctrl+C from the app JVM entirely — no SIGINT,
  no thread-interrupt, no shutdown. The console Ctrl+C does reach the in-VM
  process as SIGINT, but killing the in-VM tool alone doesn't stop the flood: the
  host wsl.exe relay keeps draining its buffered output. Only killing wsl.exe does.

Run the tool inside the VM through a signal-trapping bash wrapper that, on
INT/TERM/HUP, kills our host wsl.exe via WSL->Windows interop
(`wmic ... parentprocessid=<jvmPid> call terminate`) — dropping the relay buffer
and unblocking the JVM's waitFor — then force-kills the in-VM tool subtree and
drops a `.dfhdl-cancel` marker. The wrapper is installed via `tee` (stdin) and
invoked by path so it survives wsl.exe arg re-quoting; the JVM passes its own pid.

JVM side: a daemon marker watcher (fallback kill + clean-interrupt detection) and
a single post-waitFor marker check, so the run unwinds as a clean
ToolInterruptedException rather than reading the killed launcher's exit code as an
error. Normal runs pay no latency.

This complements the output throttle (committed separately) that fixes the sbtn
channel-saturation case. Confirmed working for local & dftools tools under both
sbtn and scala-cli. Design notes in docs/dev/ctrl-c-cancellation.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A no-throttle experiment showed the output throttle does two jobs under an sbtn
flood, not one: besides keeping the client<->server channel idle enough to deliver
the Ctrl+C, it back-pressures the tool so the flood backlog stays upstream (in
wsl.exe, where killing the launcher drops it) instead of being slurped into JVM /
sbt-server memory at full speed (observed multi-GB), where killing the launcher no
longer stops the flood. Comment-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The generated-file cache keyed artifacts by the design-content hash only
(getDataHash), which is identical across toolchains since it depends solely
on the source. Local and DFTools builds therefore shared one cache slot and
clobbered each other, e.g. iverilog failing with "VVP input file 14.0 can
not be run with run time version 13.0" after a switch. Key gen-files by
keyHash too, which folds in otherDeps (tool, version, location, backend), so
each toolchain gets its own artifact slot.

Some tools also keep cache-unmanaged build intermediates in the sandbox that
the analyzer/builder updates in place; these survive a switch and break the
next run. Add a shared Tool.staleToolArtifacts + purgeStaleToolArtifactsOnSwitch
fingerprint-stamp mechanism (purges only on a toolchain switch, so
same-toolchain runs stay incremental):
- Verilator: purge obj_dir (stale *.d/*.o embed the other toolchain's
  verilated.cpp include path -> make fails with "No rule to make target"
  / "multiple target patterns").
- GHDL: purge work-obj*.cf (records per-unit source paths; the / vs \
  difference makes GHDL report duplicate definitions, -Wlibrary).
NVC needs nothing: its producedFiles already enumerates every WORK.* unit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Switch the documentation engine from MkDocs to ProperDocs (a maintained
fork of MkDocs 1.6.1), keeping the Material for MkDocs 9.7.6 theme so the
rendered site is byte-for-byte identical to before.

- rename mkdocs.yml -> properdocs.yml (ProperDocs' native config name;
  the mkdocs.yml fallback is deprecated and warns)
- requirements.txt: mkdocs -> properdocs
- CI dfdocs.yml: `mkdocs build` -> `properdocs build`
- workflows: update path triggers mkdocs.yml -> properdocs.yml
- CLAUDE.md: document the new config file and build command

requirements.txt also carries pre-existing docs dependency bumps
(pymdown-extensions, mkdocs-drawio, schemdraw) that were already in the
working tree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The docs/dev/ notes are internal and were never part of the site nav.
Relocating them to a top-level devdocs/ keeps them out of the built site
and the docs CI path triggers.

- docs/dev/ctrl-c-cancellation.md -> devdocs/ (fix relative link depth:
  ../../lib -> ../lib)
- add devdocs/interfaces-views-plan.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With the default tools-location=dftools, running a proprietary tool that has
no DFTools image (Questa, Vivado, Quartus, Diamond, Gowin) failed with e.g.
"no DFTools image for tool 'vlog'". Make such tools transparently run from the
local PATH instead of erroring.

Split DFToolsImage.imageFor into an Option-returning imageForOpt (the single
source of truth for the tool->image mapping) plus the throwing imageFor, and
gate Tool.usesDFTools on a new supportsDFTools (= imageForOpt(binExec).isDefined).
An unsupported tool now always resolves to local regardless of the option, which
also keeps the version-probe and exec paths from ever hitting imageFor for it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…p_` prefix)

The plugin previously synthesized a separate `top_<Design>` object extending
`DFApp` to host the generated `main`. Replace this with a `main` injected
directly into the design's companion object, so the companion (e.g. `Foo`) is
itself the runnable entry point — improving the run/runMain UX.

- `DFApp` becomes a class without `main` (renamed to `run`); `setInitials`/
  `setDsn`/`getDsnArg` are made public so the injected `main` can drive an
  instance. `appCompileTime` is derived from a `topClass` passed to
  `setInitials` (falling back to `this.getClass` for `ManualDFApp`).
- `TopAnnotPhase` injects `main` into the companion module class, instantiating
  `DFApp`, priming it, and rerouting argv to `run`. The design `__dfc` is still
  wired by `MetaContextPlacerPhase` from the `@top` annotation.
- `PreTyperPhase` synthesizes an empty (non-Synthetic) companion before the
  typer when a `@top` class lacks one, so the namer establishes real companion
  linkage (a post-typer module would not link, causing a clashing mirror
  class). Non-Synthetic lets `mergeCompanionDefs` fold in the desugared
  default-getter companion for designs with default args.
- Update downstream references: `testApps` (`util.EmptyDesign`,
  `dfhdl.AES.CipherSim`), the reflective CLI test helper, docs, and the
  new-stage skill.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Capture the per-invocation latency the DFTools (Apptainer-in-WSL) path adds
vs local execution on Windows+WSL2: ~160ms fixed overhead per tool process
launch (~100ms wsl.exe spawn + ~60ms apptainer container start), constant
across tools/images, and per-launch rather than per-sim-step. Includes the
apptainer-instance vs exec-<sif> comparison (~110ms/exec win) and a Git
Bash/MSYS2 timing gotcha to avoid when re-measuring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mePos mutation)

Class metadata (name/position/doc/annotations) was previously applied by a
plugin-injected `setClsNamePos` call that mutated the elaborated design block
(`getSet.replace`). Replace this with a declarative inheritance chain that the
container reads to construct the design block correctly at creation — no
mutation.

- `HasClsMetaArgs` now exposes `__clsMetaArgs: List[ClsMetaArgs]`. The compiler
  plugin injects, per DFHDL class, an override
  `newClsMetaArgs :: super.__clsMetaArgs`, so the leaf's `__clsMetaArgs` yields
  the full chain (most-derived first; only concrete classes appear). For the
  override to take effect via virtual dispatch, `MetaContextPlacerPhase` is now
  an `IdentityDenotTransformer` with `changesMembers = true` and enters the
  override symbol with `enteredAfter(this)`, copying the inherited symbol's
  flags/info for an exact signature match.
- `Design`/`Interface` build their block directly in `initOwner` from the chain:
  the leaf (head) names the design/interface; for a blackbox IP the base-most
  concrete class in the chain names the IP `typeName`. The former
  `setClsNamePos` implementations and the global-tag side effects move here.
- `ResourceContext` derives `resourceType` and constraint annotations from the
  chain instead of via `setClsNamePos`.
- `EDBlackBox.source` becomes a `def` (was a constructor-param field) so
  `mkInstMode` is safe to evaluate during construction, allowing the block's
  final `instMode` to be set at creation.
- `CoreSpec.PluginSpec` reads `__clsMetaArgs` instead of `setClsNamePos`.

Verified: compiler_stages 437/437 (incl. qsys blackbox), core 74/74, and the
IDTop elaborate printout is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… streams

Fixes the intermittent Windows AccessDenied on version.properties during
copyResources, from two compounding causes:

- The core resourceGenerator rewrote version.properties unconditionally every
  build, bumping its mtime and forcing copyResources to re-copy it each run;
  it now writes only when the content actually changes.
- Both readers (hdl.dfhdlVersion and DFToolsImage.version) left
  getResourceAsStream open, holding a Windows file lock that blocks the rename;
  both now close the stream.

DFToolsImage now reads its own lib-owned dftools.properties rather than sharing
core's version.properties, so the dfhdl version and the dftools version are
decoupled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Oron Port and others added 15 commits July 22, 2026 00:12
Values of width <= 32 now compute as int over int-typed state: 32-bit
adds wrap for free with no mask AND, and 32-bit rotate patterns compile
to a single ror instruction. Measurements showed the int form only pays
with int-typed storage (computing in int over the long signal array
loses more to the per-access truncate/widen boundary than the
arithmetic saves), so registers and spilled comb values move into
width-typed kernel fields and the shared signal array is made coherent
only at kernel entry (registers load, pokes land) and exit (registers
and spills store; peeks, scheduler wait bounds, and text-output
arguments read after the call returns) - legal under the bulk-run
contract, with the sync cost amortized over each call's cycles and the
state-checkpoint invariant preserved at call boundaries. runWatch is
specialized on the generation-time watch node (new watchNode parameter
on Codegen.compile, runtime argument cross-checked).

SHA farm n=32: 2.16 -> 2.69 Mcycles/s (above single-threaded Verilator's
2.48); n=8: 10.7 -> 13.6; small designs unchanged-to-better. States stay
bit-identical to the interpreter tier everywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move chains of eight or more int-typed moves (shift registers, delay
pipelines) now keep their registers in a shared chain array and commit
as overlapping 8-lane Vector API moves instead of scalar field-to-field
copies, which profiling showed to be ~38% of large-design cycle time.
All lanes load before any lane stores, preserving the same
read-before-overwrite guarantee the consumer-first commit order gives
the scalar form. Chain registers are read by constant index everywhere
else; all remaining state stays in width-typed fields, which measure
faster for compute than array-backed state.

The vector path engages only when the host JVM resolves
jdk.incubator.vector (JPMS forbids resolving a boot-loader incubator
module into a running JVM, so it must be a launch flag); otherwise the
commit silently stays scalar. The repo .jvmopts adds the flag so sbt
server sims vectorize out of the box. New sysprops:
-Ddfhdl.sim.codegen.dumpSource=true prints the generated kernel Java,
-Ddfhdl.sim.codegen.noVector=true forces the scalar commit.

SHAFarm throughput (forked JDK 25, bit-identical aggregates): n=32
2.82 -> 3.79 Mcycles/s, n=64 1.33 -> 1.68 - ahead of single-threaded
Verilator at both sizes (2.52 / 1.50).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A reg alias reading a dcl that the proper-alias mechanism replaces with
a REG dcl created its din chain against the removed dcl, leaving a
dangling reference. The reg-group din chain now resolves its rel val
through the proper-alias replacement map to reference the new REG dcl
directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The DFiantHDL/benchmarks repository joins the build as the `benchmarks`
submodule: a never-published sbt module depending on lib (Apache 2.0,
per-derived-work licensing documented in that repo), with every
top-level work folder auto-discovered as a source directory. The
benchmark sources previously kept uncommitted under compiler_stages
test scope (dfhdl.sim.bench) live there now, peeking state through the
public typed simulation API.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A single-arg FuncOp.|/&/^ is a bit reduction (Bits/UInt -> Bit), but
buildFunc routed it through the binary-op cases, where rdAt(_, resW = 1)
truncated the operand to bit 0 instead of reducing it. Found by the SERV
benchmark port: `cnt_en := cnt_lsb.|` read only bit 0 of the ring
counter, parking the CPU after its first instruction fetch.

Reductions now lower explicitly: or-reduce compares against zero,
and-reduce against all-ones, and xor-reduce folds the 64-bit-normalized
lane xor down to bit 0. Regression coverage in WideSimSpec exercises all
three on single-lane (4-bit) and multi-lane (100-bit) operands on both
kernel tiers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New .claude/commands/verilog-to-dfhdl.md skill capturing the Verilog-to-DFHDL
porting workflow, the clock/reset magnet wiring across a hierarchy, initFile
memory handling and the memory-vs-reset gap, CONST params, and emitter
gotchas; it defers all language basics to the user guide (from-verilog,
design-domains, type-system). Bumps the benchmarks submodule to the serv
baseline-port-names and clock/reset-magnet rework.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A related domain shares the target's clock by default and also its reset.
`@timing.related(target, includeReset = false)` keeps the shared clock but
drops the reset: the domain's registers/memories are excluded from reset
initialization and rely on their init values instead.

- IR/frontend: add `includeReset: Boolean = true` to Timing.Related / related
- reset resolution: new `resolvedRstAnnot` chain-walk helper severs the reset
  on an includeReset=false link; wired into hasResolvedRstCfg and ToED's two
  reset reads (clock keeps resolving through the full chain)
- tests: ToEDSpec (init-instead-of-reset lowering) + PrintCodeStringSpec
  (codeString round-trip)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Related Domains section: explain that includeReset = false keeps the shared
clock but drops the reset, so the domain's registers/memories rely on their
init values instead of a reset-initialization block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Support the nested RTDomain that keeps servant_ram's initFile memory out
of the reset. DFacsimile's minimum builder now walks a @timing.related
RT DomainBlock's members inline (new isRelatedRTDomain predicate: RT
domain with a Timing.Related annotation and no own Timing.Clock),
sharing the parent's clock; nested clock-owning or ED domains stay
unsupported. It also binds an explicitly-declared Clk/Rst magnet input
to its deasserted-low constant (active-high) instead of leaving an
unpatched MOV, so a design that reads its reset combinationally
(serv_state's `!i_rst.actual`) simulates.

Verified: the mini Servant top is bit-exact against the reference on
both the Interpreter and Codegen tiers; all dfhdl.sim.* tests pass.
Also updates the verilog-to-dfhdl skill (memory-vs-reset pattern and the
String CONST / DFacsimile note) and bumps the benchmarks submodule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A DFVector register accessed only cell-wise (every referrer an ApplyIdx into
it, cell width <= 64) now lowers to a long[]-backed memory instead of a wide
register: an async MEMRD node (O(1) read) plus ordered synchronous write
ports, if (we) mem[addr] = (mem[addr] & ~mask) | ((data << pos) & mask), the
guard being the enclosing path condition so byte enables and branch guards
fold into we. Anything not cleanly cell-wise (a whole-vector read/connection,
a wider cell) falls back to the wide model, so no design regresses.

This replaces the packed-bits lowering where each dynamic index expanded into
a barrel-shift network over the whole vector -- O(memory-size) nodes evaluated
and committed every cycle -- which made 32 KiB RAM un-runnable and even the
256 B mini about 10x slower than Verilator. Now the mini Codegen tier runs at
~6 Mcycles/s (about Verilator's speed, up from 0.59) and the 32 KiB SERV tops
run at ~5.7, all bit-exact against Verilator.

Memory-less designs are untouched: the Codegen write-commit emission is gated
on memWrites, and the Interpreter guards its commit with a hasMem flag, so the
generated code and per-cycle path are identical to before (A/B on
sha_farm/protocol_engine stayed within run-to-run noise on both tiers).

Adds ByteMemDut (a byte-enable RAM, the Servant firmware-RAM shape) to
CompositeSimSpec, verified on both tiers alongside the existing RegFileDut.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rays)

Replace the memory node's isReg gate with an access-pattern classifier
vecRepr(dcl): Ram | CombArray | Wide. A DFVector accessed only cell-wise
(every referrer an ApplyIdx into it, cell <= 64) lowers by storage class: a
register to the RAM node as before, a combinational VAR wire to a sweep-local
scratch array instead of a barrel-shift network. Anything not cleanly
cell-wise stays the wide model (const vectors already lower to a ROM node at
their use site).

The comb array is a kernel-owned long[] cleared each sweep, with three
version-threaded ops so the combinational scheduler orders writes before
same-sweep reads: ANEW (clear + version-0), ALOAD (arr[addr]), ASTORE
(arr[addr] = data). A guarded/partial cell write folds to
store(i, mux(we, insert(load(i), part, pos), load(i))); DFacsimile threads the
current version node through the body walk (no env/mux merge, like the RAM).

Fixes a latent unrollCombFor bug the comb array exposed: the index of v(i) is
a cached value read, stale across unroll iterations, so every write hit
address 0. unrollCombFor now drops per-iteration cache entries (procOverlay in
a process, else nodeOf) so iterator-dependent reads rebuild against the new
binding.

Adds CombVecDut (a combinational cell-wise wire vector) to CompositeSimSpec,
verified on both tiers. Memory/comb-less designs are unchanged: the Codegen
emission is gated so their generated code is identical (sha_farm and
protocol_engine A/B outputs identical, throughput within noise).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Profiling the SERV bit-serial core (control-dominated, ~1400 one-bit ops
per cycle) showed the Codegen path is memory/field-access bound, so these
three passes cut field traffic and node count with no observable change.

- CSE (value numbering) in the netlist builder: pure combinational ops
  are hash-consed on (op, width, inputs), collapsing the repeated bit
  extracts a bit-serial decoder emits at many sites (~18% fewer comb ops).
  Side-effecting / order-sensitive nodes (MOV, ROM, memory, comb arrays)
  never enter the cache.
- Copy propagation: pure-alias MOV nodes (hierarchy port connections,
  wire renames) are forwarded so consumers, register-nexts, memory-write
  operands, and peek observers read the source directly. Post-commit
  observers (text-output snaps, scheduler wait bounds, the watch aggregate)
  are pinned and preserved. Also rescues shift-register move-chain
  detection when a reg-to-reg move hops through an alias.
- Dead-code elimination in Codegen: run/settle/stepDirty emit only the
  backward cone from registers, memory writes, the observed set, and the
  watch node, so the forwarded alias MOVs drop out of every method.

SERV steady-state Codegen throughput (bit-exact vs Verilator, no
regression on sha_farm / protocol_engine):
  hello-mini  5.98 -> 7.38 Mcps  (+23%)
  hello-32k   5.89 -> 7.32 Mcps  (+24%)
  phil-32k    5.46 -> 7.06 Mcps  (+29%)

727 compiler_stages + 144 sim tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@soronpo soronpo changed the title Methods, initial blocks, FSM cycle fusion, DFacsimilie Methods, initial blocks, FSM cycle fusion, DFacsimile Jul 22, 2026
Oron Port and others added 13 commits July 22, 2026 22:53
- Add `derives CanEqual` to the PCont enum in DFacsimile so the
  strictEquality match on the parameterless PCont.Wrap case compiles
  (compiler_stages was failing to compile).
- Pin svgdom to 0.1.23 in the DFDocs workflow. svgdom 0.1.24+ tightened
  ensureXMLSerializableName, which throws on the hdelk-generated SVG for
  user-guide/connectivity; 0.1.23 is the last version the docs built
  cleanly on. Unrelated to this PR's code (also broke unrelated branches).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… sbt.version to 1.12.14, and sbt-ci-release to 1.12.0
The foreign-IP DPI flags were built as a single `-LDFLAGS "<multi word>"`
string, but the tool exec layer tokenizes the command with a plain
split-on-space that does not honor quotes, so verilator received the inner
tokens (e.g. `-l:libinteractive_dpi.so`) as stray top-level options and
aborted with "Invalid option". Emit each linker token under its own
`-LDFLAGS` (verilator concatenates repeated occurrences in order), so no
space-containing value has to survive the split. Same behavior on host
Linux, dftools, macOS, and Windows.

This first surfaced with the interactive foreign IP, the first to link a
DPI library into the verilated binary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DFToolsImage.isAvailable caught every Throwable and returned false, so a real
resolve failure (no container runtime, blocked unprivileged user namespaces, a
corrupt or absent asset) was indistinguishable from an image that is simply not
configured. The only downstream symptom was the misleading "could not be found
in its DFTools image". Print the caught exception before returning false so the
actual cause shows up in the log.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Build runs the test suite in the committed `dftools` location, executing
each EDA tool inside a rootless Apptainer image. Ubuntu 24.04 blocks the
unprivileged user namespace Apptainer's rootless engine needs
(kernel.apparmor_restrict_unprivileged_userns=1), which the previous commit
surfaced as scalapptainer.UserNamespaceException. Clear the AppArmor sysctl so
the images can run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DFToolsImage passed an explicit `dest` to Apptainer.pull, but scalapptainer
0.5.3 only creates the images cache dir on the derived-name path, not for an
explicit dest, so apptainer's pull failed ("could not open temporary file for
copy: ... no such file or directory") on a fresh runner. Pull by `name` (the
asset minus .sif), which routes through the dir-creating path and lands the SIF
at the same <imagesDir>/<asset>. Root cause also fixed upstream in scalapptainer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Drop the pull-by-name workaround in DFToolsImage: scalapptainer 0.5.4 creates
  the parent dir for an explicit `dest` in pull/build, so pass `dest` directly
  again. Bump scalapptainerVersion 0.5.3 -> 0.5.4.
- Bump interactiveSimVersion 0.4.1 -> 0.4.2 to pick up the claim_heartbeat
  WIDTHTRUNC fix, clearing the verilator -Wall warnings on the IP wrappers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pull in dfhdl-ips 98d519b: the vga/interactive viewer os.proc(...).spawn()
calls now pass destroyOnExit=false, so a missing viewer executable no
longer throws uncaught on os-lib's shutdown-hook/monitor threads (nor
leaves a JVM shutdown hook that re-throws the launch failure at exit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
scalapptainer stores pulled SIFs under ~/.scalapptainer/images by
content-addressed name, and both DFHDL and scalapptainer short-circuit
the pull when the SIF is already present. Cache that folder so the
per-run `[dftools] downloading image ...` pulls are skipped. Keyed on
dftoolsVersion (which pins the exact image set via the bundled
dftools.lock.json); the prefix restore-key lets a version bump reuse
every unchanged, content-addressed image.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@soronpo
soronpo merged commit 9b6edc9 into main Jul 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant