Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2903adf
compiler_stages: remove SanityCheckSpec
Aug 7, 2026
03afffb
docs: elaboration-vs-hardware scope rules, enum/localparam mapping, o…
soronpo Aug 7, 2026
fc13a87
Merge branch 'training' of https://github.com/DFiantHDL/dfhdl_by_agen…
Aug 7, 2026
506c3f4
plugin+core+platforms: `@hw.annotation.setName` replaces `@targetName…
Aug 7, 2026
a3a157c
plugin+docs: forbid multi-block DFHDL parameters; defaults only in th…
Aug 7, 2026
bdf077c
docs: enum match wildcard `case _` rules for translations and fresh FSMs
Aug 7, 2026
cc101ae
plugin: auto-@top injected fully qualified to support a design class …
Aug 7, 2026
e498a38
docs: Bitwise Operations subsection for elementwise `&`/`|`/`^`/`~` o…
Aug 7, 2026
1366e73
core+stages+docs: target-context widening replaces carry promotion; n…
Aug 7, 2026
3f7430b
core+ir+stages: carry ops are purely a printed spelling; `.eby` is th…
Aug 7, 2026
ca1d98d
ir+core: symbolic width-fit proofs accept provable parametric relatio…
Aug 7, 2026
3028784
remove redundant case
Aug 7, 2026
7e0549d
plugin+core: non-literal widths collapse at the IntParam boundary; DF…
Aug 7, 2026
e102b66
plugin+core: `reduce`-over-slices guide rail, testable through the re…
Aug 7, 2026
703bb34
plugin+core: not-a-member errors keep only their core sentence
Aug 8, 2026
dbf374f
plugin+core: restore the did-you-mean hint on stripped not-a-member e…
Aug 8, 2026
3fe8269
core+docs: `.width`/`.length` queries on DFTypes and bit-accurate values
Aug 8, 2026
0d386c0
core+docs: a `val` binding a width/length query keeps its name in the…
Aug 8, 2026
a8066cc
docs: inter-dependent design parameters
Aug 8, 2026
520fd8d
ir+core+stages: value `.width`/`.length` become width/length query FU…
Aug 8, 2026
9afce5b
plugin+core: dedicated error for a single-line `process`/`initial` bl…
Aug 8, 2026
53ac2b5
core+docs: target-context widening crosses `.sel` like Verilog's `?:`…
Aug 9, 2026
4c33ad5
core+docs: target-context widening crosses `if`/`match` expressions (…
Aug 9, 2026
88de1ce
core+docs: widening crosses shift left operands and unary minus; warn…
Aug 9, 2026
477e737
lib+docs+ips: add NVC as a Verilog lint/simulation tool (v95/v2001)
Aug 9, 2026
e3a89a8
lib+docs: DFTools v1.2.0; add the Xezim simulator via its new sim-xez…
Aug 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .claude/commands/bugfix.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,27 @@ Issue #427 (`out_data <> bars(0).out_data`, where the port width comes from a de
one of these. It read like a macro bug and was a Scala 3 compiler bug; the DFHDL fix was six
`asInstanceOf`s. Work it in this order instead.

### An untyped tree the plugin synthesizes resolves in the USER's scope

A pre-typer phase that injects an untyped tree gets that tree name-resolved as if the user had
written it, so any unqualified `Ident` in it is captured by a same-named user definition. The
auto-`@top` injection spelled the annotation `Ident("top")`, and a design class itself named
`top` (the standard Verilog top-module name) captured it: the annotation then referred to the
class being annotated, and scalac reported `Cyclic reference involving class top` at the class
definition, with nothing pointing at the plugin (issue #458). Anchor every synthesized library
reference at the root (`Select(Select(Ident(nme.ROOTPKG), "dfhdl"), ...)`, i.e.
`_root_.dfhdl.top`) — a bare `Ident("dfhdl")` can itself be captured by a user package or object
named `dfhdl`. Rightmost-name-based detection helpers (`rightmostName`) keep matching the
qualified spelling, so only the construction site changes.

The tell for this species: a resolution-flavored error (cyclic reference, ambiguity, "not
found") positioned on ordinary user code that appears or vanishes with the *name* of a
definition, not its body — the reporter of #458 bisected the whole class body before renaming
the class revealed it. Minimize by renaming the identifier, not by editing the body. The
regression test is a lib spec design using the colliding name with NO explicit annotation (the
auto-injection path must fire); it pins the fix at compile level, since the unfixed plugin
fails the whole test-scope compilation.

### Minimize outside DFHDL, early

Get off the DFHDL types as fast as possible. Two plugin-free sandboxes:
Expand Down Expand Up @@ -320,6 +341,45 @@ Two mechanism notes from that fix:
above does not always cost you the diagnostic; verify per case with `assertCompileError` plus
one manual compile for the position.

### A rule about a value's type belongs where the value ENTERS the algebra

Issue #455's collapse rule ("a non-literal width is `Int` at the type level") was first
implemented per operation, rewriting each return type to a guarded fold, and that shape fails
structurally: every *internal* call site that constructs with an exact type parameter
(`DFBits(updatedWidth)` inside `resizeBits`, `repeat`, `.bits`) is left stuck on the fold and
needs an exact-typed `raw` twin. Implementing the same rule at the single point where a value
*converts into* the algebra's carrier (`IntParam.fromValue`) needed three lines and no signature
changes: only user-facing entries collapse, and the typed internal plumbing never sees the rule.
Mechanism notes from getting the conversion family right:

- **A conversion whose RESULT type contains a match type is dead on arrival, in both
directions.** Expected-type-driven eligibility cannot invert the match type (`IntParam[8]`
wanted, `IntParam[Collapse[?T]]` offered: the candidate is rejected before the argument is
tried), and an arg-driven term singleton (`cellDim.type` of an `Int` method parameter) leaves
it STUCK in the result (`IsConst` on a TermRef does not reduce, not even to `false`).
- **Bound-narrowing beats evidence-gating.** Restricting the precise conversion's bound
(`IntP & Singleton` to `Int & Singleton`) excludes exactly the species that must collapse (a
`DFConstInt32` parameter's singleton). An `=:=`-to-match-type evidence gate also excludes
ABSTRACT type parameters (`CN <: Int & Singleton` in generic library code, `Matrix`), which
must *defer* precision, not drop it: the evidence is unprovable for both, and only the
reflection level could tell them apart. Such generic code lives in `lib`, so core+stages
green is NOT enough blast radius for a conversion-family change.

### A custom reporter that rewrites positions must re-own rendering-position sanity AND dedup

`CustomReporter` drops a diagnostic's outer position chain to suppress inline-stack printing.
That is only sound when the innermost frame is trustworthy, and a diagnostic raised on a
macro-synthesized tree is not: its innermost frame pairs the CURRENT unit's source file with the
span of the `${...}` splice in the macro's own source (`Exact.scala`), so the rendered position
clamps to the unit's last line with an offset-sized column (`Playground.scala:13:12843`). The raw
compiler never shows this frame because it renders *and dedups* through the outer chain
(`UniqueMessagePositions`), which the re-reporting also bypassed (it forwarded straight to
`orig.doReport`), so one inline-expansion error rendered up to three times. The fix is paired,
and each half needs the other: normalize the position (walk innermost to outermost, keep the
first frame whose source IS the compiled unit and whose span fits inside it), and dedup the
normalized (position, message) in `isHidden`, where a swallowed duplicate is also never counted,
keeping the "N errors found" summary consistent with what is rendered.

### Probing type-level behaviour

Two traps, each of which cost several cycles here:
Expand Down
28 changes: 28 additions & 0 deletions .claude/commands/new-stage.md
Original file line number Diff line number Diff line change
Expand Up @@ -1602,6 +1602,34 @@ Mirror `plantClonedMembers`'s per-member mechanics when a custom per-ref remap i
`dfc.mutableDB.newRefFor(cloned.ownerRef, dfc.owner.asIR)` → zip `m.getRefs` with
`cloned.getRefs` and `newRefFor` each cloned ref to the (remapped) original target.

### Materializing a type's width parameter as a standalone member

To replace a member with the VALUE behind an `IntParamRef` (e.g. folding a `width`/`length`
query into the parameter it queries — `DropStructsVecs.lengthFoldPatch` is the model), inside a
`MetaDesign(member, ReplaceWithLast(ChangeRefAndRemove))`:

```scala
val lengthParam =
vecType.cellDimParamRefs.head.get.asInstanceOf[IntParam[Int]].cloneAnonValueAndDepsHere
lengthParam.toScalaIntOpt match
case Some(int) => dfhdl.core.DFConstInt32(int, named = true)(using dfc.setMeta(func.meta))
case None =>
dfhdl.core.DFVal.Alias.AsIs.ident(lengthParam.toDFConst(using dfc.anonymize))(using
dfc.setMeta(func.meta))
```

Three load-bearing details: `cloneAnonValueAndDepsHere` first, because an anonymous width cone
is already read by the TYPE that carries it and an anonymous value may be read exactly once
(mistake 22); a literal must MINT a member (`DFConstInt32(int, named = true)`) and a named
target must be WRAPPED in an ident, because `ReplaceWithLast` needs a member to be created in
the MetaDesign (bare `toDFConst` on a pre-existing constant creates none); and the replacement
carries the original's meta via `dfc.setMeta(member.meta)` so a named binding keeps its name.

Relatedly, a new `Func.Op` whose result is constant over a NON-constant argument (the
`width`/`length` type queries) must also be taught to `IntExprCalc` (linearization through the
argument TYPE's width params, product-base equivalence so `vec.width` matches `W * N`), or
every symbolic width-equivalence check against such an expression fails at elaboration.

### Compile-time constant evaluation of values

`dfVal.getConstDataThroughParams[Any]` returns `Some(data)` when the (possibly substituted)
Expand Down
3 changes: 2 additions & 1 deletion .claude/commands/verilog-to-dfhdl.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ for all the mechanics this skill deliberately omits:
- **[Transitioning from Verilog][from-verilog]** (`docs/transitioning/from-verilog/index.md`) -
module/param/port mapping, `logic`/`reg`/`wire` → `VAR init`, `UInt`/`Bits`/`SInt` choice, numeric
literals, `$clog2` → `.until`/`.to`, `always` → `process`, blocking/non-blocking → `:=`/`:==`, FSM →
`enum extends Encoded`, integer `case` → `match`, functions/tasks → methods, all operators (shift,
`enum extends Encoded`, integer `case` → `match`, `default:`/`others` → `case _` (kept for formal
equivalence unless binary encoding with exactly 2^n cases), functions/tasks → methods, all operators (shift,
`|&~`, reductions, `.repeat`, `++`/`.toBits`, part-select `-:`/`+:`, signed arithmetic), `generate
for` → Scala `for`, reserved-keyword escaping (backtick / `@targetName`), `Bits` init `all(0)`,
ternary → `.sel`.
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ internals → plugin → compiler_ir → core → compiler_stages → lib → pl

Located in `plugin/src/main/scala/plugin/`, in the order `Plugin.initialize` lists them:

1. `PreTyperPhase` — untyped parse-tree rewrites (`<>` precedence, auto-`@top`)
1. `PreTyperPhase` — untyped parse-tree rewrites (`<>` precedence, auto-`@top`, the single-line `process`/`initial` block error)
2. `TopAnnotPhase` — top-level annotation processing
3. `PureCheckPhase` — purity analysis for elaboration caching
4. `CodeDigestPhase` — code digests, the elaboration cache keys
Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ val compilerVersion = "3.8.4"
// The DFTools binary toolchain release this DFHDL build targets (versioned independently of
// DFHDL). Surfaced to the library via lib's generated `dftools.properties` and read by
// DFToolsImage. Bump when adopting a new DFTools release.
val dftoolsVersion = "v1.1.1"
val dftoolsVersion = "v1.2.0"
// The vga-monitor-sim release wrapped by the `dfhdl.ips.video.vga.vga_monitor` foreign IP. This is
// the single source of truth: it is surfaced to the IP code via the generated `vga-monitor.properties`
// resource (read by `vga_monitor.version`), like core's version.properties. Since v0.3.0 the release names
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,76 @@ object Ident:
if (alias.hasTagOf[IdentTag]) Some(alias.relValRef.get)
else None

// A relative widening alias: a same-kind (UInt/SInt/Bits) width extension whose delta folds
// to a positive literal `k` for every parameter assignment. This is the value-level twin of
// `IntParamRef.widenDeltaOpt`, which additionally constrains the printed SPELLING to the
// anonymous `base + k` width form; this extractor recognizes the widening SHAPE itself,
// whether the target width is spelled relatively or folded to a literal.
object Eby:
def unapply(alias: DFVal.Alias.AsIs)(using MemberGetSet): Option[(DFVal, Int)] =
val relVal = alias.relValRef.get
val deltaOpt = (alias.dfType, relVal.dfType) match
case (DFUInt(toW), DFUInt(fromW)) => toW.constDiffFrom(fromW)
case (DFSInt(toW), DFSInt(fromW)) => toW.constDiffFrom(fromW)
case (DFBits(toW), DFBits(fromW)) => toW.constDiffFrom(fromW)
case _ => None
deltaOpt.filter(_ > 0).map((relVal, _))

// A carry-spelled arithmetic func: a binary `+`/`-`/`*` over two anonymous same-kind widening
// aliases whose bases sit at a common width `cw`, with the func landing exactly at the carry
// width (`cw + 1` for `+`/`-`, `2 * cw` for `*`). This is precisely the shape the carry
// operators (`+^`, `-^`, `*^`) elaborate to, so the printers reconstruct the carry spelling
// from it. The width equations are decided symbolically (linear forms), so a parametric
// `a *^ b` over `SInt(W)` operands reconstructs even though its operand delta `W` is not a
// literal. Extraction is conservative: an undecidable equation simply does not match, and the
// func prints in its structural (operand-widened) form, which is always equivalent.
object CarryFunc:
// A same-kind conversion alias operand candidate. No widening check here: the carry-width
// equations below imply the widening (widths are positive, so `fw == max + 1` or
// `fw == xw + yw` cannot hold for a narrowing operand), and a decidability gate would
// wrongly reject symbolic deltas such as the `W` of a parametric carry mul.
private def widenBase(v: DFVal)(using MemberGetSet): Option[DFVal] = v match
case alias: DFVal.Alias.AsIs if alias.isAnonymous =>
val relVal = alias.relValRef.get
val sameKind = (alias.dfType, relVal.dfType) match
case (DFUInt(_), DFUInt(_)) => true
case (DFSInt(_), DFSInt(_)) => true
case _ => false
if (sameKind) Some(relVal) else None
case _ => None
private def widthLinear(v: DFVal)(using MemberGetSet): Option[IntExprCalc.Linear] =
IntExprCalc.DataCalc.linearOfTypeWidth(v.dfType)
def unapply(func: DFVal.Func)(using MemberGetSet): Option[(DFVal, DFVal)] =
import IntExprCalc.DataCalc.*
func.op match
case FuncOp.+ | FuncOp.- | FuncOp.`*` =>
func.args match
case aRef :: bRef :: Nil =>
for
x <- widenBase(aRef.get)
y <- widenBase(bRef.get)
fwL <- widthLinear(func)
xwL <- widthLinear(x)
ywL <- widthLinear(y)
carryL <- func.op match
case FuncOp.`*` =>
// a carry mul keeps its operands' own widths: fw == xw + yw
Some(add(xwL, ywL))
case _ =>
// the aligned (common) width is the wider operand's; require decidability
val dxy = sub(xwL, ywL)
if (isConst(dxy) && dxy.offset >= 0) Some(addConst(xwL, 1))
else if (isConst(dxy)) Some(addConst(ywL, 1))
else None
d = sub(fwL, carryL)
if isConst(d) && d.offset == 0
yield (x, y)
case _ => None
case _ => None
end match
end unapply
end CarryFunc

extension (member: DFMember)
// The kind-level half of the unreferenced-anonymous sweeps: whether this member MAY be
// dropped when nothing reads it. SHARED by the `DropUnreferencedAnons` compiler stage and
Expand Down
25 changes: 24 additions & 1 deletion compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,24 @@ object DFVal:
protected def protIsFullyAnonymous(using MemberGetSet): Boolean =
args.forall(_.get.isFullyAnonymous)
protected def protGetConstData(using MemberGetSet, ConstData.CachePolicy): ConstData[Any] =
op match
// width/length read the argument's TYPE, never its data, so the query is constant
// even when the argument value itself is not (a port, a variable)
case Func.Op.width | Func.Op.length =>
val argType = this.args.head.get.dfType
val widthOpt = (op, argType) match
case (Func.Op.length, vec: DFVector) => vec.lengthIntOpt
case _ => argType.widthIntOpt
widthOpt match
case Some(i) => ConstData.KnownConst(Some(BigInt(i)))
// a parameter-dependent width: still a constant, resolved per instantiation
case None => ConstData.UnknownConst(this)
case _ => protGetConstDataFromArgs
end protGetConstData
private def protGetConstDataFromArgs(using
MemberGetSet,
ConstData.CachePolicy
): ConstData[Any] =
val args = this.args.map(_.get)
val argConstData = args.map(_.getConstData[Any])
if (argConstData.exists(_ == ConstData.NotConst)) ConstData.NotConst
Expand All @@ -817,7 +835,7 @@ object DFVal:
val argData = argConstData.collect { case ConstData.KnownConst(d) => d }
val argTypes = args.map(_.dfType)
ConstData.KnownConst(calcFuncData(dfType, op, argTypes, argData))
end protGetConstData
end protGetConstDataFromArgs
protected def `prot_=~`(that: DFMember)(using MemberGetSet): Boolean = that match
case that: Func =>
// `Op.Def` needs no specialization here: its `staticRef` is a STABLE identity key
Expand Down Expand Up @@ -846,6 +864,11 @@ object DFVal:
case unary_-, unary_~, unary_!
case rising, falling
case clog2, max, min, abs, sel
// type queries over the single argument: `width` is the total bit width of the
// argument's type, `length` its element count (a vector) or bit count (bits/uint/sint).
// Both read the argument's TYPE, never its data, so they are constant even over a
// non-constant argument (see `protGetConstData`).
case width, length
// special-case of initFile construct for vectors of bits
case InitFile(format: InitFileFormat, path: String)
// A method (def-design) application: a call of a static function or an ED
Expand Down
Loading
Loading