From c3a86da67e52217cf19101dcd257d64067cd78b5 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sun, 7 Jun 2026 13:58:46 +0300 Subject: [PATCH 01/72] add fork-join support, local blocks via `locally`, and `DropForkJoins` and `DropLocalBlocks` stages for backend dialects that do not support these constructs --- .claude/commands/ir-reference.md | 25 ++ .../scala/dfhdl/compiler/ir/DFMember.scala | 45 +++ .../compiler/printing/DFOwnerPrinter.scala | 17 ++ .../dfhdl/compiler/printing/Printer.scala | 10 +- .../compiler/stages/BackendPrepStage.scala | 2 + .../dfhdl/compiler/stages/DropForkJoins.scala | 164 +++++++++++ .../compiler/stages/DropLocalBlocks.scala | 73 +++++ .../dfhdl/compiler/stages/SimplifyRTOps.scala | 19 +- .../stages/verilog/VerilogOwnerPrinter.scala | 22 ++ .../stages/vhdl/VHDLOwnerPrinter.scala | 4 + .../compiler/stages/vhdl/VHDLPrinter.scala | 10 +- .../scala/StagesSpec/DropForkJoinsSpec.scala | 272 ++++++++++++++++++ .../StagesSpec/DropLocalBlocksSpec.scala | 86 ++++++ .../StagesSpec/PrintCodeStringSpec.scala | 53 ++++ .../scala/StagesSpec/PrintVHDLCodeSpec.scala | 106 +++++++ .../StagesSpec/PrintVerilogCodeSpec.scala | 178 +++++++++++- core/src/main/scala/dfhdl/core/Fork.scala | 49 ++++ .../main/scala/dfhdl/core/LocalBlock.scala | 33 +++ core/src/main/scala/dfhdl/core/Wait.scala | 13 +- core/src/main/scala/dfhdl/hdl.scala | 2 + 20 files changed, 1162 insertions(+), 21 deletions(-) create mode 100644 compiler/stages/src/main/scala/dfhdl/compiler/stages/DropForkJoins.scala create mode 100644 compiler/stages/src/main/scala/dfhdl/compiler/stages/DropLocalBlocks.scala create mode 100644 compiler/stages/src/test/scala/StagesSpec/DropForkJoinsSpec.scala create mode 100644 compiler/stages/src/test/scala/StagesSpec/DropLocalBlocksSpec.scala create mode 100644 core/src/main/scala/dfhdl/core/Fork.scala create mode 100644 core/src/main/scala/dfhdl/core/LocalBlock.scala diff --git a/.claude/commands/ir-reference.md b/.claude/commands/ir-reference.md index 1812ae51d..a19412abc 100644 --- a/.claude/commands/ir-reference.md +++ b/.claude/commands/ir-reference.md @@ -39,6 +39,8 @@ DFMember (sealed) │ ├── DFDesignBlock — module / design definition │ ├── DomainBlock — clock-domain grouping │ ├── ProcessBlock — always / process block +│ ├── ForkBlock — fork-join (forkJoin/forkJoinAny/forkJoinNone); join mode +│ ├── LocalBlock — local statement block (locally:); a fork branch when owned by ForkBlock │ ├── StepBlock — FSM step │ ├── DFConditional.Block — if / match clause │ │ ├── DFIfElseBlock @@ -511,6 +513,29 @@ final case class ProcessBlock.Sensitivity.List(refs: List[DFVal.Ref]) // proces --- +### ForkBlock & LocalBlock +```scala +final case class ForkBlock( + join: ForkBlock.Join, // All | Any | None + ownerRef: DFOwner.Ref, + meta: Meta, + tags: DFTags +) +enum ForkBlock.Join: All, Any, None // forkJoin / forkJoinAny / forkJoinNone + +final case class LocalBlock( // produced by `locally:` + ownerRef: DFOwner.Ref, + meta: Meta, + tags: DFTags +) +``` +ED-domain only (for now). A `LocalBlock` whose `getOwner` is a `ForkBlock` is a concurrent +fork branch; elsewhere it is a plain local statement scope. SystemVerilog emits both natively +(`fork…join[_any|_none]`, `begin[: name]…end`); `DropForkJoins` lowers forks to processes + +handshake signals for VHDL/old-Verilog, and `DropLocalBlocks` flattens local blocks for VHDL. + +--- + ### StepBlock ```scala final case class StepBlock( diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala index eb33e8dbd..d57a6adf4 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala @@ -1216,6 +1216,8 @@ sealed trait DFBlock extends DFOwner object DFBlock: given ReadWriter[DFBlock] = ReadWriter.merge( summon[ReadWriter[ProcessBlock]], + summon[ReadWriter[ForkBlock]], + summon[ReadWriter[LocalBlock]], summon[ReadWriter[DFConditional.Block]], summon[ReadWriter[DFLoop.Block]], summon[ReadWriter[StepBlock]], @@ -1264,6 +1266,49 @@ object ProcessBlock: List(refs.map(_.copyAsNewRef)).asInstanceOf[this.type] end ProcessBlock +final case class ForkBlock( + join: ForkBlock.Join, + ownerRef: DFOwner.Ref, + meta: Meta, + tags: DFTags +) extends DFBlock, + DFOwnerNamed derives ReadWriter: + protected def `prot_=~`(that: DFMember)(using MemberGetSet): Boolean = that match + case that: ForkBlock => + this.join == that.join && + this.meta =~ that.meta && this.tags =~ that.tags + case _ => false + protected def setMeta(meta: Meta): this.type = copy(meta = meta).asInstanceOf[this.type] + protected def setTags(tags: DFTags): this.type = copy(tags = tags).asInstanceOf[this.type] + lazy val getRefs: List[DFRef.TwoWayAny] = meta.getRefs + def copyWithNewRefs(using RefGen): this.type = copy( + meta = meta.copyWithNewRefs, + ownerRef = ownerRef.copyAsNewRef + ).asInstanceOf[this.type] +end ForkBlock +object ForkBlock: + enum Join derives CanEqual, ReadWriter: + case All, Any, None + +final case class LocalBlock( + ownerRef: DFOwner.Ref, + meta: Meta, + tags: DFTags +) extends DFBlock, + DFOwnerNamed derives ReadWriter: + protected def `prot_=~`(that: DFMember)(using MemberGetSet): Boolean = that match + case that: LocalBlock => + this.meta =~ that.meta && this.tags =~ that.tags + case _ => false + protected def setMeta(meta: Meta): this.type = copy(meta = meta).asInstanceOf[this.type] + protected def setTags(tags: DFTags): this.type = copy(tags = tags).asInstanceOf[this.type] + lazy val getRefs: List[DFRef.TwoWayAny] = meta.getRefs + def copyWithNewRefs(using RefGen): this.type = copy( + meta = meta.copyWithNewRefs, + ownerRef = ownerRef.copyAsNewRef + ).asInstanceOf[this.type] +end LocalBlock + object DFConditional: sealed trait Block extends DFBlock derives ReadWriter: type THeader <: Header diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/printing/DFOwnerPrinter.scala b/compiler/ir/src/main/scala/dfhdl/compiler/printing/DFOwnerPrinter.scala index 44a601841..07df12d8a 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/printing/DFOwnerPrinter.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/printing/DFOwnerPrinter.scala @@ -50,6 +50,9 @@ trait AbstractOwnerPrinter extends AbstractPrinter: case ch: DFConditional.Header => ch.dfType =~ DFUnit // process blocks case pb: ProcessBlock => true + // fork and local blocks + case _: ForkBlock => true + case _: LocalBlock => true // loops case _: DFLoop.Block => true // the rest are not directly viewable @@ -176,6 +179,8 @@ trait AbstractOwnerPrinter extends AbstractPrinter: |${csDFMatchEnd}""" case ih: DFConditional.DFIfHeader => csChains def csProcessBlock(pb: ProcessBlock): String + def csForkBlock(fb: ForkBlock): String + def csLocalBlock(lb: LocalBlock): String def csDomainBlock(pb: DomainBlock): String end AbstractOwnerPrinter @@ -375,6 +380,18 @@ protected trait DFOwnerPrinter extends AbstractOwnerPrinter: case Sensitivity.List(refs) if refs.isEmpty => "" case Sensitivity.List(refs) => refs.map(_.refCodeString).mkStringBrackets s"${named}process${senList}:\n${body.hindent}" + def csForkBlock(fb: ForkBlock): String = + val body = csDFOwnerBody(fb) + val named = fb.meta.nameOpt.map(n => s"val $n = ").getOrElse("") + val kw = fb.join match + case ForkBlock.Join.All => "forkJoin" + case ForkBlock.Join.Any => "forkJoinAny" + case ForkBlock.Join.None => "forkJoinNone" + s"${named}${kw}:\n${body.hindent}" + def csLocalBlock(lb: LocalBlock): String = + val body = csDFOwnerBody(lb) + val named = lb.meta.nameOpt.map(n => s"val $n = ").getOrElse("") + s"${named}locally:\n${body.hindent}" def csStepBlock(stepBlock: StepBlock): String = val body = csDFOwnerBody(stepBlock) val name = stepBlock.getName diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/printing/Printer.scala b/compiler/ir/src/main/scala/dfhdl/compiler/printing/Printer.scala index 05b43d421..82b2750c0 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/printing/Printer.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/printing/Printer.scala @@ -101,6 +101,8 @@ trait Printer case InstMode.Def => csDFDesignDefInst(inst) case _ => csDFDesignBlockInst(inst) case pb: ProcessBlock => csProcessBlock(pb) + case fb: ForkBlock => csForkBlock(fb) + case lb: LocalBlock => csLocalBlock(lb) case stepBlock: StepBlock => csStepBlock(stepBlock) case forBlock: DFLoop.DFForBlock => csDFForBlock(forBlock) case whileBlock: DFLoop.DFWhileBlock => csDFWhileBlock(whileBlock) @@ -303,13 +305,15 @@ class DFPrinter(using val getSet: MemberGetSet, val printerOptions: PrinterOptio val trigger = wait.triggerRef.get trigger.dfType match case _: DFBoolOrBit => + // `ir.Wait(X)` resumes when X is true: `waitUntil(X)`. A negated trigger `not inner` + // renders back as `waitWhile(inner)`. trigger match case DFVal.Func(op = FuncOp.rising | FuncOp.falling) => s"waitUntil(${wait.triggerRef.refCodeString})" - case DFVal.Func(op = FuncOp.unary_!, args = List(triggerRef)) => - s"waitUntil(${triggerRef.refCodeString})" + case DFVal.Func(op = FuncOp.unary_!, args = List(innerRef)) => + s"waitWhile(${innerRef.refCodeString})" case _ => - s"waitWhile(${wait.triggerRef.refCodeString})" + s"waitUntil(${wait.triggerRef.refCodeString})" case DFTime => s"${wait.triggerRef.refCodeString}.wait" case _ => wait.triggerRef.get.getConstData[Option[BigInt]] match diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/BackendPrepStage.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/BackendPrepStage.scala index 47cf626a9..1ec36158e 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/BackendPrepStage.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/BackendPrepStage.scala @@ -11,6 +11,8 @@ case object BackendPrepStage NamedVerilogSelection, NamedVHDLSelection, ToED, + DropForkJoins, + DropLocalBlocks, ApplyInvertConstraint, DropStructsVecs, MatchToIf, diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropForkJoins.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropForkJoins.scala new file mode 100644 index 000000000..28f743264 --- /dev/null +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropForkJoins.scala @@ -0,0 +1,164 @@ +package dfhdl.compiler.stages + +import dfhdl.compiler.analysis.* +import dfhdl.compiler.ir.* +import dfhdl.compiler.patching.* +import dfhdl.options.CompilerOptions +import dfhdl.core.DomainType.ED +import dfhdl.compiler.stages.verilog.VerilogDialect +import scala.annotation.tailrec + +//format: off +/** Lowers fork-join (`forkJoin` / `forkJoinAny` / `forkJoinNone`) into multiple concurrent + * processes synchronized by start/done handshake signals. + * + * SystemVerilog (sv2005+) emits `fork ... join[_any|_none]` natively. VHDL has no fork-join at + * all, so every mode is lowered. Old Verilog (v95/v2001) supports `fork ... join` (wait-all) + * natively and only lacks `join_any` / `join_none` (SystemVerilog 2005+), so under those dialects + * only `forkJoinAny` / `forkJoinNone` are lowered while `forkJoin` is left native. + * + * For a fork with branch local-blocks `b0..bN` the stage: + * 1. allocates a `_start_i` / `_done_i` Bit signal pair per branch in the + * enclosing domain; + * 2. replaces the fork in the parent process with: drive every `start_i` high, a join wait + * (All: wait for all dones; Any: wait for any done; None: no wait), then re-arm the + * `start_i` signals (All/Any only); + * 3. emits one `process` per branch (a forever loop) that waits for its `start_i`, runs the + * branch body, raises `done_i`, then waits for `start_i` to drop and clears `done_i`. + * + * The branch body is kept inside its `LocalBlock`; for VHDL [[DropLocalBlocks]] later flattens + * it, while Verilog keeps it as a native `begin ... end` block. + * + * Notes / limitations: + * - Multiple branches assigning the same signal is the user's responsibility, exactly as in + * SystemVerilog (this stage does not arbitrate writes). + * - `join_none` branches are single-shot: the parent does not re-arm the handshake, so a + * parent that re-enters the fork before a `join_none` branch finishes is a race (mirrors the + * inherent difficulty of mapping dynamic process spawning onto static RTL processes). + * - The handshake uses `waitUntil`/`waitWhile`, which lower to correct `wait until ...` (VHDL) + * and `wait(...)` (Verilog) — verified equivalent to native SystemVerilog fork-join by + * simulation. + */ +//format: on +case object DropForkJoins extends HierarchyStage: + def dependencies: List[Stage] = List(ToED) + def nullifies: Set[Stage] = Set() + + override def runCondition(using co: CompilerOptions): Boolean = + co.backend match + case _: dfhdl.backends.vhdl => true + case be: dfhdl.backends.verilog => + be.dialect match + case VerilogDialect.v95 | VerilogDialect.v2001 => true + case _ => false + + // A fork that contains no nested fork — safe to lower this pass. Nested forks are lowered + // innermost-first across repeated passes (Pattern 9) so an outer and inner fork are never + // patched together. + private def isInnermost(fork: ForkBlock)(using MemberGetSet): Boolean = + !fork.members(MemberView.Flattened).exists { + case _: ForkBlock => true + case _ => false + } + + // Whether this fork needs lowering for the target backend. VHDL has no fork-join at all, so + // every mode is lowered. Old Verilog (v95/v2001) supports `fork ... join` (wait-all) natively + // and only lacks `join_any` / `join_none` (SystemVerilog 2005+), so only those are lowered. + private def needsLowering(fork: ForkBlock)(using co: CompilerOptions): Boolean = + co.backend match + case _: dfhdl.backends.vhdl => true + case be: dfhdl.backends.verilog => + be.dialect match + case VerilogDialect.v95 | VerilogDialect.v2001 => + fork.join != ForkBlock.Join.All + case _ => false // sv2005+: emitted natively, never lowered + case _ => false + + @tailrec private def lowerRepeatedly(db: DB)(using RefGen, CompilerOptions): DB = + given MemberGetSet = db.getSet + val patches = db.members.view.collect { + case fork: ForkBlock if isInnermost(fork) && needsLowering(fork) => fork + }.flatMap(lowerFork).toList + if (patches.isEmpty) db + else lowerRepeatedly(db.patch(patches)) + + private def lowerFork(fork: ForkBlock)(using MemberGetSet, RefGen): List[(DFMember, Patch)] = + val branches = fork.members(MemberView.Folded).collect { case lb: LocalBlock => lb } + // degenerate empty fork: just drop it + if (branches.isEmpty) List(fork -> Patch.Remove(isMoved = false)) + else + val parentProc = fork.getOwnerProcessBlock + val forkName = fork.getName + val joinMode = fork.join + + // (1): handshake signals, declared right *before* the parent process. Both the parent + // (trigger + join-wait) and every branch process reference these, so declaring them + // first keeps all references pointing backward (valid member order without relying on a + // later ordering stage). + val signalsDsn = + new MetaDesign(parentProc, Patch.Add.Config.Before, domainType = ED): + val startSignals = branches.indices.toList.map { i => + (Bit <> VAR)(using dfc.setName(s"${forkName}_start_$i")) + } + val doneSignals = branches.indices.toList.map { i => + (Bit <> VAR)(using dfc.setName(s"${forkName}_done_$i")) + } + + // (3): one forever-process per branch, placed right *after* the parent process. + val branchesDsn = + new MetaDesign(parentProc, Patch.Add.Config.After, domainType = ED): + branches.lazyZip(signalsDsn.startSignals).lazyZip(signalsDsn.doneSignals).foreach { + (branchLB, start, done) => + process.forever { + waitUntil(start) + // relocate the branch local block (and its whole subtree) into this process + plantMembers(fork, branchLB :: branchLB.members(MemberView.Flattened)) + done :== 1 + waitWhile(start) + done :== 0 + } + } + + // removal patches for every relocated member (branch local blocks + their descendants) + val relocatedMembers = + branches.flatMap(branchLB => branchLB :: branchLB.members(MemberView.Flattened)) + val removalPatches = relocatedMembers.map(m => m -> Patch.Remove(isMoved = true)) + + // (2): replace the fork in the parent process with the trigger + join-wait logic. + val parentDsn = + new MetaDesign( + fork, + Patch.Add.Config.ReplaceWithLast( + Patch.Replace.Config.FullReplacement, + // the fork's only references are its branch local-blocks' ownerRefs (from inside + // the fork); those are handled by the relocation above, so we must not redirect + // them onto a generated statement here. + Patch.Replace.RefFilter.Outside(fork) + ), + domainType = ED + ): + // the replacement lands inside the parent process, so assert process scope to + // permit non-blocking assignments and waits directly (no nested process). + given dfhdl.core.DFC.Scope.Process = dfhdl.core.DFC.Scope.Process + signalsDsn.startSignals.foreach(_ :== 1) + joinMode match + case ForkBlock.Join.All => + waitUntil(signalsDsn.doneSignals.reduce(_ && _)) + signalsDsn.startSignals.foreach(_ :== 0) + case ForkBlock.Join.Any => + waitUntil(signalsDsn.doneSignals.reduce(_ || _)) + signalsDsn.startSignals.foreach(_ :== 0) + case ForkBlock.Join.None => + // no wait, no re-arm (single-shot; see limitations) + + List(signalsDsn.patch, branchesDsn.patch) ++ removalPatches ++ List(parentDsn.patch) + end if + end lowerFork + + def transformSubDB(rootDB: DB)(using MemberGetSet, CompilerOptions, RefGen): DB = + lowerRepeatedly(subDB) +end DropForkJoins + +extension [T: HasDB](t: T) + def dropForkJoins(using CompilerOptions): DB = + StageRunner.run(DropForkJoins)(t.db) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropLocalBlocks.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropLocalBlocks.scala new file mode 100644 index 000000000..61595aa02 --- /dev/null +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropLocalBlocks.scala @@ -0,0 +1,73 @@ +package dfhdl.compiler.stages + +import dfhdl.compiler.analysis.* +import dfhdl.compiler.ir.* +import dfhdl.compiler.patching.* +import dfhdl.options.CompilerOptions +import scala.annotation.tailrec + +//format: off +/** Flattens `LocalBlock`s into their enclosing owner. VHDL has no in-process named statement + * block, so every local block (produced by `locally:`) is dissolved: its members are promoted + * to the parent owner and the block itself is removed. + * + * This stage runs only for VHDL backends. For Verilog (all dialects) local blocks are emitted + * natively as `begin : name ... end` named blocks and are left untouched. + * + * Fork-join branch local blocks are consumed earlier by [[DropForkJoins]]; by the time this + * stage runs only standalone local blocks remain. + * + * ==Rule: dissolve local block== + * {{{ + * // Before + * process: + * x :== 1 + * locally: + * y :== 2 + * z :== 3 + * + * // After + * process: + * x :== 1 + * y :== 2 + * z :== 3 + * }}} + * + * Nested local blocks are dissolved innermost-first across repeated passes. + */ +//format: on +case object DropLocalBlocks extends HierarchyStage: + def dependencies: List[Stage] = List(DropForkJoins, DropLocalDcls) + def nullifies: Set[Stage] = Set() + + override def runCondition(using co: CompilerOptions): Boolean = + co.backend match + case _: dfhdl.backends.vhdl => true + case _ => false + + // A local block that does not itself contain another local block (safe to dissolve this pass). + private def isInnermost(lb: LocalBlock)(using MemberGetSet): Boolean = + !lb.members(MemberView.Flattened).exists { + case _: LocalBlock => true + case _ => false + } + + @tailrec private def dissolveRepeatedly(db: DB): DB = + given MemberGetSet = db.getSet + val patches: List[(DFMember, Patch)] = + db.members.view.collect { + case lb: LocalBlock if isInnermost(lb) => + // redirect all refs to `lb` (its children's ownerRefs) to `lb`'s owner, then remove `lb`. + // the children keep their positions in the flat member list, now owned by the parent. + lb -> Patch.Replace(lb.getOwner, Patch.Replace.Config.ChangeRefAndRemove) + }.toList + if (patches.isEmpty) db + else dissolveRepeatedly(db.patch(patches)) + + def transformSubDB(rootDB: DB)(using MemberGetSet, CompilerOptions, RefGen): DB = + dissolveRepeatedly(subDB) +end DropLocalBlocks + +extension [T: HasDB](t: T) + def dropLocalBlocks(using CompilerOptions): DB = + StageRunner.run(DropLocalBlocks)(t.db) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/SimplifyRTOps.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/SimplifyRTOps.scala index 2053fb688..2f0927c63 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/SimplifyRTOps.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/SimplifyRTOps.scala @@ -163,7 +163,7 @@ case object SimplifyRTOps extends HierarchyStage: Patch.Add.Config.ReplaceWithLast(Patch.Replace.Config.FullReplacement), dfhdl.core.DomainType.RT ): - // If the trigger is a rising or falling edge, we need to negate it + // `ir.Wait(X)` resumes when X is true, so the loop must run *while* `not X`. val fixedTrigger = trigger match case DFVal.Func(op = op @ (FuncOp.rising | FuncOp.falling), args = List(DFRef(arg))) => if (trigger.isAnonReferencedByWait) @@ -174,13 +174,26 @@ case object SimplifyRTOps extends HierarchyStage: case FuncOp.falling => (!argFE.reg(1, init = 0)).||(argFE)(using dfc.setMeta(trigger.meta)) else !(trigger.asValOf[dfhdl.core.DFBool]) + // trigger is `not inner` (e.g. from `waitWhile(inner)`) -> loop while `inner` + case DFVal.Func(op = FuncOp.unary_!, args = List(DFRef(inner))) => + inner.asValOf[dfhdl.core.DFBoolOrBit] + // plain trigger (e.g. from `waitUntil(trigger)`) -> loop while `not trigger` case _ => - trigger.asValOf[dfhdl.core.DFBoolOrBit] + trigger.dfType match + case DFBit => !(trigger.asValOf[dfhdl.core.DFBit]) + case _ => !(trigger.asValOf[dfhdl.core.DFBool]) val whileBlock = dfhdl.core.DFWhile.Block(fixedTrigger)(using dfc.setMeta(waitMember.meta)) dfc.enterOwner(whileBlock) dfc.exitOwner() - Some(dsn.patch) + // a `not inner` trigger (from `waitWhile`) is discarded above (we loop on `inner`). + // If it was created solely for this wait, drop it so it does not linger as a dead value. + val triggerRemoval = trigger match + case DFVal.Func(op = FuncOp.unary_!, args = List(_)) + if trigger.originMembers.forall(_ == waitMember) => + List(trigger -> Patch.Remove()) + case _ => Nil + dsn.patch :: triggerRemoval // replace wait statements with time durations with cycles case waitMember @ Wait(triggerRef = DFRef(cyclesVal @ DFDecimal.Val(DFUInt(_)))) => val replaceWithWhile = cyclesVal match diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogOwnerPrinter.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogOwnerPrinter.scala index 0c5ba2f08..22e31e761 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogOwnerPrinter.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogOwnerPrinter.scala @@ -257,6 +257,28 @@ protected trait VerilogOwnerPrinter extends AbstractOwnerPrinter: else s" @${refs.map(_.refCodeString).mkString("(", sensitivityListSep, ")")}" s"$dcl${named}$alwaysKW$senList\nbegin\n${body.hindent}\nend" end csProcessBlock + def csForkBlock(fb: ForkBlock): String = + val body = csDFMembers(fb.members(MemberView.Folded)) + val label = fb.meta.nameOpt.map(n => s" : $n").getOrElse("") + val joinKW = fb.join match + case ForkBlock.Join.All => "join" + case ForkBlock.Join.Any => "join_any" + case ForkBlock.Join.None => "join_none" + s"fork$label\n${body.hindent}\n$joinKW" + def csLocalBlock(lb: LocalBlock): String = + val (statements, dcls) = lb + .members(MemberView.Folded) + .partition { + case dcl: DFVal.Dcl => false + case const: DFVal.Const if !const.isAnonymous => false + case _ => true + } + val label = lb.meta.nameOpt.map(n => s" : $n").getOrElse("") + val dcl = + if (dcls.isEmpty) "" + else s"${csDFMembers(dcls)}\n" + val body = dcl + csDFMembers(statements) + s"begin$label\n${body.hindent}\nend" val forInteratorDclSupport: Boolean = printer.dialect match case VerilogDialect.v95 | VerilogDialect.v2001 => false diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLOwnerPrinter.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLOwnerPrinter.scala index a59cfd1f8..cf75f39ab 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLOwnerPrinter.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLOwnerPrinter.scala @@ -244,6 +244,10 @@ protected trait VHDLOwnerPrinter extends AbstractOwnerPrinter: |${body.hindent} |end process;""" end csProcessBlock + // fork-join and local blocks are lowered away (DropForkJoins / DropLocalBlocks) before + // VHDL printing; these are only safety nets. + def csForkBlock(fb: ForkBlock): String = printer.unsupported + def csLocalBlock(lb: LocalBlock): String = printer.unsupported def csStepBlock(stepBlock: StepBlock): String = printer.unsupported def csDFForBlock(forBlock: DFLoop.DFForBlock): String = val body = csDFOwnerBody(forBlock) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLPrinter.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLPrinter.scala index 10b5f9e6e..afb6da599 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLPrinter.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLPrinter.scala @@ -45,15 +45,15 @@ class VHDLPrinter(val dialect: VHDLDialect)(using val trigger = wait.triggerRef.get trigger.dfType match case _: DFBoolOrBit => + // `ir.Wait(X)` resumes when X is true -> `wait until X`. trigger match - // rising or falling edge does not need to be negated case DFVal.Func(op = FuncOp.rising | FuncOp.falling) => s"wait until ${wait.triggerRef.refCodeString};" - // no need for `not not`, so just skipping the not operation - case DFVal.Func(op = FuncOp.unary_!, args = List(triggerRef)) => - s"wait until ${printer.csFixedCond(triggerRef)};" + // X = `not inner` -> `wait until not inner` (avoids a `not (not ...)`) + case DFVal.Func(op = FuncOp.unary_!, args = List(innerRef)) => + s"wait until not ${printer.csFixedCond(innerRef)};" case _ => - s"wait until not ${printer.csFixedCond(wait.triggerRef)};" + s"wait until ${printer.csFixedCond(wait.triggerRef)};" case DFTime => s"wait for ${wait.triggerRef.refCodeString};" case _ => printer.unsupported end csWait diff --git a/compiler/stages/src/test/scala/StagesSpec/DropForkJoinsSpec.scala b/compiler/stages/src/test/scala/StagesSpec/DropForkJoinsSpec.scala new file mode 100644 index 000000000..106c5ca38 --- /dev/null +++ b/compiler/stages/src/test/scala/StagesSpec/DropForkJoinsSpec.scala @@ -0,0 +1,272 @@ +package StagesSpec + +import dfhdl.* +import dfhdl.compiler.stages.dropForkJoins +// scalafmt: { align.tokens = [{code = "<>"}, {code = "="}, {code = "=>"}, {code = ":="}]} + +class DropForkJoinsSpec extends StageSpec: + test("forkJoin (all) lowering under VHDL"): + given options.CompilerOptions.Backend = _.vhdl + class FJ extends EDDesign: + val a = Bit <> OUT + val b = Bit <> OUT + process: + val fk = forkJoin: + locally: + a :== 1 + locally: + b :== 1 + end FJ + val fj = (new FJ).dropForkJoins + assertCodeString( + fj, + """|class FJ extends EDDesign: + | val a = Bit <> OUT + | val b = Bit <> OUT + | val fk_start_0 = Bit <> VAR + | val fk_start_1 = Bit <> VAR + | val fk_done_0 = Bit <> VAR + | val fk_done_1 = Bit <> VAR + | process: + | fk_start_0 :== 1 + | fk_start_1 :== 1 + | waitUntil(fk_done_0 && fk_done_1) + | fk_start_0 :== 0 + | fk_start_1 :== 0 + | process: + | waitUntil(fk_start_0) + | locally: + | a :== 1 + | fk_done_0 :== 1 + | waitWhile(fk_start_0) + | fk_done_0 :== 0 + | process: + | waitUntil(fk_start_1) + | locally: + | b :== 1 + | fk_done_1 :== 1 + | waitWhile(fk_start_1) + | fk_done_1 :== 0 + |end FJ + |""".stripMargin + ) + + test("forkJoinAny lowering under VHDL"): + given options.CompilerOptions.Backend = _.vhdl + class FJ extends EDDesign: + val a = Bit <> OUT + val b = Bit <> OUT + process: + val fk = forkJoinAny: + locally: + a :== 1 + locally: + b :== 1 + end FJ + val fj = (new FJ).dropForkJoins + assertCodeString( + fj, + """|class FJ extends EDDesign: + | val a = Bit <> OUT + | val b = Bit <> OUT + | val fk_start_0 = Bit <> VAR + | val fk_start_1 = Bit <> VAR + | val fk_done_0 = Bit <> VAR + | val fk_done_1 = Bit <> VAR + | process: + | fk_start_0 :== 1 + | fk_start_1 :== 1 + | waitUntil(fk_done_0 || fk_done_1) + | fk_start_0 :== 0 + | fk_start_1 :== 0 + | process: + | waitUntil(fk_start_0) + | locally: + | a :== 1 + | fk_done_0 :== 1 + | waitWhile(fk_start_0) + | fk_done_0 :== 0 + | process: + | waitUntil(fk_start_1) + | locally: + | b :== 1 + | fk_done_1 :== 1 + | waitWhile(fk_start_1) + | fk_done_1 :== 0 + |end FJ + |""".stripMargin + ) + + test("forkJoinNone lowering under VHDL"): + given options.CompilerOptions.Backend = _.vhdl + class FJ extends EDDesign: + val a = Bit <> OUT + val b = Bit <> OUT + process: + val fk = forkJoinNone: + locally: + a :== 1 + locally: + b :== 1 + end FJ + val fj = (new FJ).dropForkJoins + assertCodeString( + fj, + """|class FJ extends EDDesign: + | val a = Bit <> OUT + | val b = Bit <> OUT + | val fk_start_0 = Bit <> VAR + | val fk_start_1 = Bit <> VAR + | val fk_done_0 = Bit <> VAR + | val fk_done_1 = Bit <> VAR + | process: + | fk_start_0 :== 1 + | fk_start_1 :== 1 + | process: + | waitUntil(fk_start_0) + | locally: + | a :== 1 + | fk_done_0 :== 1 + | waitWhile(fk_start_0) + | fk_done_0 :== 0 + | process: + | waitUntil(fk_start_1) + | locally: + | b :== 1 + | fk_done_1 :== 1 + | waitWhile(fk_start_1) + | fk_done_1 :== 0 + |end FJ + |""".stripMargin + ) + + test("nested fork lowering under VHDL"): + given options.CompilerOptions.Backend = _.vhdl + class FJ extends EDDesign: + val a = Bit <> OUT + val b = Bit <> OUT + val c = Bit <> OUT + process: + val fk = forkJoin: + locally: + a :== 1 + locally: + val fkInner = forkJoin: + locally: + b :== 1 + locally: + c :== 1 + end FJ + val fj = (new FJ).dropForkJoins + assertCodeString( + fj, + """|class FJ extends EDDesign: + | val a = Bit <> OUT + | val b = Bit <> OUT + | val c = Bit <> OUT + | val fkInner_start_0 = Bit <> VAR + | val fkInner_start_1 = Bit <> VAR + | val fkInner_done_0 = Bit <> VAR + | val fkInner_done_1 = Bit <> VAR + | val fk_start_0 = Bit <> VAR + | val fk_start_1 = Bit <> VAR + | val fk_done_0 = Bit <> VAR + | val fk_done_1 = Bit <> VAR + | process: + | fk_start_0 :== 1 + | fk_start_1 :== 1 + | waitUntil(fk_done_0 && fk_done_1) + | fk_start_0 :== 0 + | fk_start_1 :== 0 + | process: + | waitUntil(fk_start_0) + | locally: + | a :== 1 + | fk_done_0 :== 1 + | waitWhile(fk_start_0) + | fk_done_0 :== 0 + | process: + | waitUntil(fk_start_1) + | locally: + | fkInner_start_0 :== 1 + | fkInner_start_1 :== 1 + | waitUntil(fkInner_done_0 && fkInner_done_1) + | fkInner_start_0 :== 0 + | fkInner_start_1 :== 0 + | fk_done_1 :== 1 + | waitWhile(fk_start_1) + | fk_done_1 :== 0 + | process: + | waitUntil(fkInner_start_0) + | locally: + | b :== 1 + | fkInner_done_0 :== 1 + | waitWhile(fkInner_start_0) + | fkInner_done_0 :== 0 + | process: + | waitUntil(fkInner_start_1) + | locally: + | c :== 1 + | fkInner_done_1 :== 1 + | waitWhile(fkInner_start_1) + | fkInner_done_1 :== 0 + |end FJ + |""".stripMargin + ) + + test("Verilog v2001 keeps forkJoin (all) native but lowers forkJoinAny"): + given options.CompilerOptions.Backend = _.verilog.v2001 + class FJ extends EDDesign: + val a = Bit <> OUT + val b = Bit <> OUT + process: + val fkAll = forkJoin: + locally: + a :== 1 + locally: + b :== 1 + val fkAny = forkJoinAny: + locally: + a :== 0 + locally: + b :== 0 + end FJ + val fj = (new FJ).dropForkJoins + assertCodeString( + fj, + """|class FJ extends EDDesign: + | val a = Bit <> OUT + | val b = Bit <> OUT + | val fkAny_start_0 = Bit <> VAR + | val fkAny_start_1 = Bit <> VAR + | val fkAny_done_0 = Bit <> VAR + | val fkAny_done_1 = Bit <> VAR + | process: + | val fkAll = forkJoin: + | locally: + | a :== 1 + | locally: + | b :== 1 + | fkAny_start_0 :== 1 + | fkAny_start_1 :== 1 + | waitUntil(fkAny_done_0 || fkAny_done_1) + | fkAny_start_0 :== 0 + | fkAny_start_1 :== 0 + | process: + | waitUntil(fkAny_start_0) + | locally: + | a :== 0 + | fkAny_done_0 :== 1 + | waitWhile(fkAny_start_0) + | fkAny_done_0 :== 0 + | process: + | waitUntil(fkAny_start_1) + | locally: + | b :== 0 + | fkAny_done_1 :== 1 + | waitWhile(fkAny_start_1) + | fkAny_done_1 :== 0 + |end FJ + |""".stripMargin + ) +end DropForkJoinsSpec diff --git a/compiler/stages/src/test/scala/StagesSpec/DropLocalBlocksSpec.scala b/compiler/stages/src/test/scala/StagesSpec/DropLocalBlocksSpec.scala new file mode 100644 index 000000000..4b4e42f1b --- /dev/null +++ b/compiler/stages/src/test/scala/StagesSpec/DropLocalBlocksSpec.scala @@ -0,0 +1,86 @@ +package StagesSpec + +import dfhdl.* +import dfhdl.compiler.stages.dropLocalBlocks +// scalafmt: { align.tokens = [{code = "<>"}, {code = "="}, {code = "=>"}, {code = ":="}]} + +class DropLocalBlocksSpec extends StageSpec: + test("standalone local block flattening under VHDL"): + given options.CompilerOptions.Backend = _.vhdl + class LB extends EDDesign: + val a = Bit <> OUT + val b = Bit <> OUT + process: + a :== 1 + locally: + b :== 1 + a :== 0 + end LB + val lb = (new LB).dropLocalBlocks + assertCodeString( + lb, + """|class LB extends EDDesign: + | val a = Bit <> OUT + | val b = Bit <> OUT + | process: + | a :== 1 + | b :== 1 + | a :== 0 + |end LB + |""".stripMargin + ) + + test("nested local blocks flattening under VHDL"): + given options.CompilerOptions.Backend = _.vhdl + class LB extends EDDesign: + val a = Bit <> OUT + val b = Bit <> OUT + val c = Bit <> OUT + process: + a :== 1 + locally: + b :== 1 + locally: + c :== 1 + end LB + val lb = (new LB).dropLocalBlocks + assertCodeString( + lb, + """|class LB extends EDDesign: + | val a = Bit <> OUT + | val b = Bit <> OUT + | val c = Bit <> OUT + | process: + | a :== 1 + | b :== 1 + | c :== 1 + |end LB + |""".stripMargin + ) + + test("local block keeps under Verilog"): + given options.CompilerOptions.Backend = _.verilog + class LB extends EDDesign: + val a = Bit <> OUT + val b = Bit <> OUT + process: + a :== 1 + locally: + b :== 1 + a :== 0 + end LB + val lb = (new LB).dropLocalBlocks + assertCodeString( + lb, + """|class LB extends EDDesign: + | val a = Bit <> OUT + | val b = Bit <> OUT + | process: + | a :== 1 + | locally: + | b :== 1 + | a :== 0 + |end LB + |""".stripMargin + ) +end DropLocalBlocksSpec diff --git a/compiler/stages/src/test/scala/StagesSpec/PrintCodeStringSpec.scala b/compiler/stages/src/test/scala/StagesSpec/PrintCodeStringSpec.scala index d36455f08..949e02af7 100644 --- a/compiler/stages/src/test/scala/StagesSpec/PrintCodeStringSpec.scala +++ b/compiler/stages/src/test/scala/StagesSpec/PrintCodeStringSpec.scala @@ -329,6 +329,59 @@ class PrintCodeStringSpec extends StageSpec(stageCreatesUnrefAnons = true): |""".stripMargin ) } + test("Fork-join and local blocks") { + class FJ extends EDDesign: + val a = Bit <> OUT + val b = Bit <> OUT + val c = Bit <> OUT + process: + val namedBlk = locally: + a :== 1 + locally: + b :== 1 + val namedFork = forkJoin: + locally: + a :== 0 + locally: + b :== 0 + forkJoinAny: + locally: + a :== 1 + locally: + c :== 1 + forkJoinNone: + locally: + c :== 0 + end FJ + val fj = (new FJ).getCodeString + assertNoDiff( + fj, + """|class FJ extends EDDesign: + | val a = Bit <> OUT + | val b = Bit <> OUT + | val c = Bit <> OUT + | process: + | val namedBlk = locally: + | a :== 1 + | locally: + | b :== 1 + | val namedFork = forkJoin: + | locally: + | a :== 0 + | locally: + | b :== 0 + | forkJoinAny: + | locally: + | a :== 1 + | locally: + | c :== 1 + | forkJoinNone: + | locally: + | c :== 0 + |end FJ + |""".stripMargin + ) + } test("Named anonymous multireference") { class IDMultiRef extends DFDesign: val data = UInt(32) <> IN diff --git a/compiler/stages/src/test/scala/StagesSpec/PrintVHDLCodeSpec.scala b/compiler/stages/src/test/scala/StagesSpec/PrintVHDLCodeSpec.scala index 8c46bdf2a..34fc36d61 100644 --- a/compiler/stages/src/test/scala/StagesSpec/PrintVHDLCodeSpec.scala +++ b/compiler/stages/src/test/scala/StagesSpec/PrintVHDLCodeSpec.scala @@ -1674,4 +1674,110 @@ class PrintVHDLCodeSpec extends StageSpec: |end Foo_arch;""".stripMargin ) } + test("fork-join lowering to processes + handshake signals") { + given options.PrinterOptions.Align = false + class FJ3 extends EDDesign: + val a = Bit <> OUT + val b = Bit <> OUT + val c = Bit <> OUT + process: + val jAll = forkJoin: + locally: + a :== 0 + locally: + b :== 0 + val jAny = forkJoinAny: + locally: + a :== 1 + locally: + c :== 1 + val jNone = forkJoinNone: + locally: + c :== 0 + end FJ3 + val fj = (new FJ3).getCompiledCodeString + assertNoDiff( + fj, + """|library ieee; + |use ieee.std_logic_1164.all; + |use ieee.numeric_std.all; + |use work.dfhdl_pkg.all; + | + |entity FJ3 is + |port ( + | a : out std_logic; + | b : out std_logic; + | c : out std_logic + |); + |end FJ3; + | + |architecture FJ3_arch of FJ3 is + | signal jAll_start_0 : std_logic; + | signal jAll_start_1 : std_logic; + | signal jAll_done_0 : std_logic; + | signal jAll_done_1 : std_logic; + | signal jAny_start_0 : std_logic; + | signal jAny_start_1 : std_logic; + | signal jAny_done_0 : std_logic; + | signal jAny_done_1 : std_logic; + | signal jNone_start_0 : std_logic; + | signal jNone_done_0 : std_logic; + |begin + | process + | begin + | jAll_start_0 <= '1'; + | jAll_start_1 <= '1'; + | wait until jAll_done_0 and jAll_done_1; + | jAll_start_0 <= '0'; + | jAll_start_1 <= '0'; + | jAny_start_0 <= '1'; + | jAny_start_1 <= '1'; + | wait until jAny_done_0 or jAny_done_1; + | jAny_start_0 <= '0'; + | jAny_start_1 <= '0'; + | jNone_start_0 <= '1'; + | end process; + | process + | begin + | wait until jAll_start_0; + | a <= '0'; + | jAll_done_0 <= '1'; + | wait until not jAll_start_0; + | jAll_done_0 <= '0'; + | end process; + | process + | begin + | wait until jAll_start_1; + | b <= '0'; + | jAll_done_1 <= '1'; + | wait until not jAll_start_1; + | jAll_done_1 <= '0'; + | end process; + | process + | begin + | wait until jAny_start_0; + | a <= '1'; + | jAny_done_0 <= '1'; + | wait until not jAny_start_0; + | jAny_done_0 <= '0'; + | end process; + | process + | begin + | wait until jAny_start_1; + | c <= '1'; + | jAny_done_1 <= '1'; + | wait until not jAny_start_1; + | jAny_done_1 <= '0'; + | end process; + | process + | begin + | wait until jNone_start_0; + | c <= '0'; + | jNone_done_0 <= '1'; + | wait until not jNone_start_0; + | jNone_done_0 <= '0'; + | end process; + |end FJ3_arch;""".stripMargin + ) + } end PrintVHDLCodeSpec diff --git a/compiler/stages/src/test/scala/StagesSpec/PrintVerilogCodeSpec.scala b/compiler/stages/src/test/scala/StagesSpec/PrintVerilogCodeSpec.scala index 544eccfe7..651ad20c3 100644 --- a/compiler/stages/src/test/scala/StagesSpec/PrintVerilogCodeSpec.scala +++ b/compiler/stages/src/test/scala/StagesSpec/PrintVerilogCodeSpec.scala @@ -878,13 +878,13 @@ class PrintVerilogCodeSpec extends StageSpec: | always | begin | x <= 1'b1; - | wait(i); + | wait(~i); | #50ms; | x <= 1'b0; | @(posedge i); | #50us; | x <= 1'b1; - | wait(~i); + | wait(i); | #50ns; | x <= 1'b0; | #1ns; @@ -1695,4 +1695,178 @@ class PrintVerilogCodeSpec extends StageSpec: |endmodule""".stripMargin ) } + test("fork-join and local blocks") { + class FJ extends EDDesign: + val a = Bit <> OUT + val b = Bit <> OUT + val c = Bit <> OUT + process: + val namedBlk = locally: + a :== 1 + locally: + b :== 1 + val namedFork = forkJoin: + locally: + a :== 0 + locally: + b :== 0 + forkJoinAny: + locally: + a :== 1 + locally: + c :== 1 + forkJoinNone: + locally: + c :== 0 + end FJ + val fj = (new FJ).getCompiledCodeString + assertNoDiff( + fj, + """|`default_nettype none + |`timescale 1ns/1ps + | + |module FJ( + | output logic a, + | output logic b, + | output logic c + |); + | `include "dfhdl_defs.svh" + | always + | begin + | begin : namedBlk + | a <= 1'b1; + | end + | begin + | b <= 1'b1; + | end + | fork : namedFork + | begin + | a <= 1'b0; + | end + | begin + | b <= 1'b0; + | end + | join + | fork + | begin + | a <= 1'b1; + | end + | begin + | c <= 1'b1; + | end + | join_any + | fork + | begin + | c <= 1'b0; + | end + | join_none + | end + |endmodule + |""".stripMargin + ) + } + test("forkJoin (wait-all) stays native under old Verilog (v2001)") { + given options.CompilerOptions.Backend = _.verilog.v2001 + // v2001 supports `fork ... join` (wait-all) natively, so forkJoin is NOT lowered there. + // Local blocks also remain native `begin ... end`. + class FJv extends EDDesign: + val a = Bit <> OUT + val b = Bit <> OUT + process: + locally: + a :== 1 + val j = forkJoin: + locally: + a :== 0 + locally: + b :== 0 + end FJv + val fj = (new FJv).getCompiledCodeString + assertNoDiff( + fj, + """|`default_nettype none + |`timescale 1ns/1ps + | + |module FJv( + | output reg a, + | output reg b + |); + | `include "dfhdl_defs.vh" + | always + | begin + | begin + | a <= 1'b1; + | end + | fork : j + | begin + | a <= 1'b0; + | end + | begin + | b <= 1'b0; + | end + | join + | end + |endmodule""".stripMargin + ) + } + test("forkJoinAny is lowered to handshake processes under old Verilog (v2001)") { + given options.CompilerOptions.Backend = _.verilog.v2001 + // v2001 lacks join_any/join_none, so forkJoinAny is lowered to multiple always blocks with + // start/done handshake signals (parent waits for ANY done). Local blocks stay native. + class FJvAny extends EDDesign: + val a = Bit <> OUT + val b = Bit <> OUT + process.forever: + val j = forkJoinAny: + locally: + a :== 0 + locally: + b :== 0 + end FJvAny + val fj = (new FJvAny).getCompiledCodeString + assertNoDiff( + fj, + """|`default_nettype none + |`timescale 1ns/1ps + | + |module FJvAny( + | output reg a, + | output reg b + |); + | `include "dfhdl_defs.vh" + | reg j_start_0; + | reg j_start_1; + | reg j_done_0; + | reg j_done_1; + | always + | begin + | j_start_0 <= 1'b1; + | j_start_1 <= 1'b1; + | wait(j_done_0 | j_done_1); + | j_start_0 <= 1'b0; + | j_start_1 <= 1'b0; + | end + | always + | begin + | wait(j_start_0); + | begin + | a <= 1'b0; + | end + | j_done_0 <= 1'b1; + | wait(~j_start_0); + | j_done_0 <= 1'b0; + | end + | always + | begin + | wait(j_start_1); + | begin + | b <= 1'b0; + | end + | j_done_1 <= 1'b1; + | wait(~j_start_1); + | j_done_1 <= 1'b0; + | end + |endmodule""".stripMargin + ) + } end PrintVerilogCodeSpec diff --git a/core/src/main/scala/dfhdl/core/Fork.scala b/core/src/main/scala/dfhdl/core/Fork.scala new file mode 100644 index 000000000..8aff654ff --- /dev/null +++ b/core/src/main/scala/dfhdl/core/Fork.scala @@ -0,0 +1,49 @@ +package dfhdl.core +import dfhdl.internals.* +import dfhdl.compiler.ir + +object Fork: + type Block = DFOwner[ir.ForkBlock] + object Block: + def apply(join: ir.ForkBlock.Join)(using DFC): Block = + ir.ForkBlock( + join, + dfc.owner.ref, + dfc.getMeta, + dfc.tags + ).addMember.asFE + end Block + + object Ops: + protected type EDDomainOnly[A] = AssertGiven[ + A <:< DomainType.ED, + "Fork-join is only allowed under event-driven (ED) domains (RT support is planned)." + ] + protected type InProcess = AssertGiven[ + DFC.Scope.Process, + "Fork-join must be placed inside a process." + ] + private def forkBlock(join: ir.ForkBlock.Join)(block: DFC.Scope.Process ?=> Unit)(using + DFC + ): Unit = + val owner = Block(join) + dfc.enterOwner(owner) + block(using DFC.Scope.Process) + dfc.exitOwner() + object forkJoin: + def apply(block: DFC.Scope.Process ?=> Unit)(using + dt: DomainType + )(using EDDomainOnly[dt.type], InProcess, DFC): Unit = + forkBlock(ir.ForkBlock.Join.All)(block) + object forkJoinAny: + def apply(block: DFC.Scope.Process ?=> Unit)(using + dt: DomainType + )(using EDDomainOnly[dt.type], InProcess, DFC): Unit = + forkBlock(ir.ForkBlock.Join.Any)(block) + object forkJoinNone: + def apply(block: DFC.Scope.Process ?=> Unit)(using + dt: DomainType + )(using EDDomainOnly[dt.type], InProcess, DFC): Unit = + forkBlock(ir.ForkBlock.Join.None)(block) + end Ops +end Fork diff --git a/core/src/main/scala/dfhdl/core/LocalBlock.scala b/core/src/main/scala/dfhdl/core/LocalBlock.scala new file mode 100644 index 000000000..b4d20fa23 --- /dev/null +++ b/core/src/main/scala/dfhdl/core/LocalBlock.scala @@ -0,0 +1,33 @@ +package dfhdl.core +import dfhdl.internals.* +import dfhdl.compiler.ir + +object LocalBlock: + type Block = DFOwner[ir.LocalBlock] + object Block: + def apply(using DFC): Block = + ir.LocalBlock( + dfc.owner.ref, + dfc.getMeta, + dfc.tags + ).addMember.asFE + end Block + + object Ops: + // the actual local-block construction + body execution, kept out of the inline method + private def locallyDFHDL[A](body: => A)(using dfc: DFC): A = + val owner = Block.apply(using dfc) + dfc.enterOwner(owner) + val ret = body + dfc.exitOwner() + ret + // `locally` is overloaded just like `println`/`print` in TextOut.scala: inside a process + // scope it constructs a DFHDL LocalBlock; otherwise it falls back to scala.Predef.locally. + transparent inline def locally[A](inline body: => A): A = + compiletime.summonFrom { + case given DFC.Scope.Process => + locallyDFHDL(body)(using compiletime.summonInline[DFC]) + case _ => scala.Predef.locally(body) + } + end Ops +end LocalBlock diff --git a/core/src/main/scala/dfhdl/core/Wait.scala b/core/src/main/scala/dfhdl/core/Wait.scala index 90fbbf975..5652cb0b9 100644 --- a/core/src/main/scala/dfhdl/core/Wait.scala +++ b/core/src/main/scala/dfhdl/core/Wait.scala @@ -50,6 +50,8 @@ object Wait: } extension (lhs: DFValOf[DFUInt[Int]]) def cy(using DFCG): Cycles = Cycles(lhs) + // `ir.Wait(X)` means "block until X becomes true" (resume when X is true). So `waitUntil` + // stores the trigger as-is, and `waitWhile` stores its negation. def waitWhile(cond: DFValOf[DFBoolOrBit])(using DFC): Wait = trydf { cond.asIR match @@ -58,16 +60,11 @@ object Wait: "`waitWhile` does not support rising/falling edges. Use `waitUntil` instead." ) case _ => - Wait(cond) + import DFBoolOrBit.Val.Ops.not + Wait(cond.not) } def waitUntil(trigger: DFValOf[DFBoolOrBit])(using DFC): Wait = trydf { - trigger.asIR match - // special case for rising/falling edges, the trigger remains as is inside the wait - case ir.DFVal.Func(op = FuncOp.rising | FuncOp.falling) => - Wait(trigger) - case _ => - import DFBoolOrBit.Val.Ops.not - Wait(trigger.not) + Wait(trigger) } end Ops end Wait diff --git a/core/src/main/scala/dfhdl/hdl.scala b/core/src/main/scala/dfhdl/hdl.scala index f3783ff12..c56e6e432 100644 --- a/core/src/main/scala/dfhdl/hdl.scala +++ b/core/src/main/scala/dfhdl/hdl.scala @@ -78,6 +78,8 @@ protected object hdl: export core.<> export core.X export core.Process.Ops.* + export core.Fork.Ops.* + export core.LocalBlock.Ops.* // shorthand for annotating a DFBits value (useful for string interpolation) type B[W <: Int] = core.DFValOf[Bits[W]] From b184a9c4eeccc7a428c7c760dbd44da414c1895d Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sun, 7 Jun 2026 14:52:43 +0300 Subject: [PATCH 02/72] fix flaky iverilog execution --- .../scala/dfhdl/tools/toolsCore/Tool.scala | 30 +++++++++++++++++-- .../dfhdl/tools/toolsCore/Verilator.scala | 3 +- lib/src/test/scala/util/FullCompileSpec.scala | 2 +- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala b/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala index e5a94084a..cc2c648bd 100644 --- a/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala +++ b/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala @@ -83,8 +83,33 @@ trait Tool: if (this.convertWindowsToLinuxPaths) path.forceWindowsToLinuxPath else path // Extra environment variables to set for the spawned tool process, merged over the inherited - // environment. Empty by default; tools override this to inject/normalize env vars they need. - protected def execEnv: Map[String, String] = Map.empty + // environment. Defaults to the Windows DLL-search guard below; tools that override this should + // fold in `winDllPathEnv` so they keep that protection. + protected def execEnv: Map[String, String] = winDllPathEnv + + // Windows DLL-hell guard. + // Many bundled tools (oss-cad-suite's iverilog/ivl, ghdl, nvc, verilator, ...) dynamically link + // the MinGW runtime `libstdc++-6.dll` / `libgcc_s_seh-1.dll`. For Icarus Verilog these live in + // `\lib` (a sibling of the `bin` that holds the launcher, and two levels above `ivl.exe` + // itself) rather than next to the executable, so the loader resolves them through PATH. If an + // unrelated toolchain on PATH (Git's mingw64, Quartus, Intel FPGA, ...) ships an incompatible + // copy earlier than oss-cad-suite, the tool loads the wrong DLL and is killed at load time with + // STATUS_ENTRYPOINT_NOT_FOUND (0xC0000139) or an access violation, producing no output. We + // prepend the executable's own directory plus its sibling `lib` and `lib\ivl` so the tool + // always finds its own bundled runtime DLLs first. No-op off Windows. + protected final def winDllPathEnv: Map[String, String] = + if (!osIsWindows || runExecFullPath.isEmpty) Map.empty + else + Option(Paths.get(runExecFullPath).getParent) match + case None => Map.empty + case Some(exeDir) => + val root = Option(exeDir.getParent) + val dllDirs = + (exeDir :: + root.toList.flatMap(r => List(r.resolve("lib"), r.resolve("lib").resolve("ivl")))) + .map(_.toString) + val pathSep = java.io.File.pathSeparator + Map("PATH" -> (dllDirs :+ sys.env.getOrElse("PATH", "")).mkString(pathSep)) protected def designFiles(using getSet: MemberGetSet): List[String] = getSet.designDB.srcFiles.collect { @@ -214,6 +239,7 @@ trait Tool: |Path: ${Paths.get(execPath).toAbsolutePath()} |Command: $fullExec""".stripMargin ) + end if end exec override def toString(): String = binExec end Tool diff --git a/lib/src/main/scala/dfhdl/tools/toolsCore/Verilator.scala b/lib/src/main/scala/dfhdl/tools/toolsCore/Verilator.scala index fbd6038cf..7d52e2601 100644 --- a/lib/src/main/scala/dfhdl/tools/toolsCore/Verilator.scala +++ b/lib/src/main/scala/dfhdl/tools/toolsCore/Verilator.scala @@ -32,7 +32,8 @@ object Verilator extends VerilogLinter, VerilogSimulator: // `undefined reference to WinMain`. Normalizing VERILATOR_ROOT to forward slashes for the // spawned process avoids this (no-op on non-Windows, where there are no backslashes). override protected def execEnv: Map[String, String] = - sys.env.get("VERILATOR_ROOT").map(root => "VERILATOR_ROOT" -> root.replace('\\', '/')).toMap + super.execEnv ++ + sys.env.get("VERILATOR_ROOT").map(root => "VERILATOR_ROOT" -> root.replace('\\', '/')).toMap protected def lintCmdLanguageFlag(dialect: VerilogDialect): String = val language = dialect match diff --git a/lib/src/test/scala/util/FullCompileSpec.scala b/lib/src/test/scala/util/FullCompileSpec.scala index 74c54f47e..2aaa6d5e6 100644 --- a/lib/src/test/scala/util/FullCompileSpec.scala +++ b/lib/src/test/scala/util/FullCompileSpec.scala @@ -23,7 +23,7 @@ abstract class FullCompileSpec extends FunSuite: given options.OnError = _.Exception given options.LinterOptions.WError = true def verilogLinters(using CompilerOptions): List[LinterOptions._VerilogLinter] = - List(verilator, vlog, xvlog) // missing iverilog + List(verilator, iverilog, vlog, xvlog) def vhdlLinters(using CompilerOptions): List[LinterOptions._VHDLLinter] = List(ghdl, nvc, vcom, xvhdl) extension [D <: core.Design](cd: CompiledDesign) From 4befd13bbfb6d7fcd5c7d4f647bdb7e35f0c40f1 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sun, 7 Jun 2026 15:08:18 +0300 Subject: [PATCH 03/72] update to Scala 3.8.4 and munit update --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 18c0f6d7d..4f71ee466 100755 --- a/build.sbt +++ b/build.sbt @@ -6,7 +6,7 @@ commands += DFHDLCommands.docExamplesRefUpdate // format: off val projectName = "dfhdl" -val compilerVersion = "3.8.3" +val compilerVersion = "3.8.4" inThisBuild( List( @@ -145,7 +145,7 @@ lazy val platforms = project lazy val dependencies = new { private val scodecV = "1.2.5" - private val munitV = "1.3.0" + private val munitV = "1.3.2" private val airframelogV = "2026.1.6" private val oslibV = "0.11.8" private val scallopV = "6.0.0" From 9f4b4016cfad72fb6b55310a6f8f757b114c6a28 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Mon, 8 Jun 2026 11:08:56 +0300 Subject: [PATCH 04/72] add support for RT process fork-join-all, and disable lowering of fork-join-any, since dynamic span logic is currently not supported --- .../compiler/stages/BackendPrepStage.scala | 4 +- .../dfhdl/compiler/stages/DropForkJoins.scala | 276 ++++++++++++------ .../compiler/stages/DropLocalBlocks.scala | 65 +++-- .../compiler/stages/DropTimedRTWaits.scala | 5 +- .../stages/verilog/VerilogOwnerPrinter.scala | 23 +- .../stages/vhdl/VHDLOwnerPrinter.scala | 2 +- .../scala/StagesSpec/DropForkJoinsSpec.scala | 204 ++++++------- .../StagesSpec/DropLocalBlocksSpec.scala | 59 +++- .../scala/StagesSpec/PrintVHDLCodeSpec.scala | 164 +++++++---- .../StagesSpec/PrintVerilogCodeSpec.scala | 129 +++++--- core/src/main/scala/dfhdl/core/Fork.scala | 38 ++- core/src/test/scala/CoreSpec/ForkSpec.scala | 37 +++ 12 files changed, 662 insertions(+), 344 deletions(-) create mode 100644 core/src/test/scala/CoreSpec/ForkSpec.scala diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/BackendPrepStage.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/BackendPrepStage.scala index 1ec36158e..9a9f7f0e1 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/BackendPrepStage.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/BackendPrepStage.scala @@ -11,8 +11,8 @@ case object BackendPrepStage NamedVerilogSelection, NamedVHDLSelection, ToED, - DropForkJoins, - DropLocalBlocks, + DropForkJoinsED, + DropLocalBlocksED, ApplyInvertConstraint, DropStructsVecs, MatchToIf, diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropForkJoins.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropForkJoins.scala index 28f743264..ce3ffb2b5 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropForkJoins.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropForkJoins.scala @@ -4,85 +4,112 @@ import dfhdl.compiler.analysis.* import dfhdl.compiler.ir.* import dfhdl.compiler.patching.* import dfhdl.options.CompilerOptions -import dfhdl.core.DomainType.ED -import dfhdl.compiler.stages.verilog.VerilogDialect +import dfhdl.core.DomainType +import dfhdl.core.{assign, nbassign} import scala.annotation.tailrec +// frontend Bit value alias (kept fully-qualified to avoid clashing with `ir.DFBit`) +private type BitFE = dfhdl.core.DFValOf[dfhdl.core.DFBit] + //format: off -/** Lowers fork-join (`forkJoin` / `forkJoinAny` / `forkJoinNone`) into multiple concurrent - * processes synchronized by start/done handshake signals. +/** Lowers fork-join into multiple concurrent processes synchronized by start/done handshake + * signals. + * + * Only `forkJoin` (join-all) is ever lowered: the parent blocks until *all* branches complete, so + * a fixed set of synthesized processes faithfully represents it (a looping parent never overlaps + * with still-running branches). `forkJoinAny` / `forkJoinNone` let the parent continue while + * branches keep running, which can dynamically spawn overlapping threads (e.g. a fork inside a + * loop) that a static lowering cannot represent — so they are *never* lowered. They are emitted + * natively only where supported (SystemVerilog `join_any` / `join_none`) and rejected by the + * backend elsewhere; under register-transfer (RT) they are rejected at the frontend. * - * SystemVerilog (sv2005+) emits `fork ... join[_any|_none]` natively. VHDL has no fork-join at - * all, so every mode is lowered. Old Verilog (v95/v2001) supports `fork ... join` (wait-all) - * natively and only lacks `join_any` / `join_none` (SystemVerilog 2005+), so under those dialects - * only `forkJoinAny` / `forkJoinNone` are lowered while `forkJoin` is left native. + * The lowering structure is domain-agnostic and shared by two concrete stages: + * - [[DropForkJoinsED]] lowers join-all fork-joins in the event-driven (ED) domain for VHDL, + * which has no native fork-join. Old Verilog (v95/v2001) and SystemVerilog emit `fork ... join` + * natively, so nothing is lowered there. Handshake signals are plain `Bit <> VAR` driven with + * non-blocking assignments. + * - [[DropForkJoinsRT]] lowers join-all fork-joins in the register-transfer (RT) domain into the + * per-branch `process.forever` + handshake form, which the RT->FSM pipeline (`SimplifyRTOps` + * -> `DropRTWaits` -> `DropRTProcess`) then turns into a clocked state-machine. Handshake + * signals must hold value across clock edges, so they are registered (`Bit <> VAR.REG`) and + * driven with blocking assignments (equivalent to `.din :=`). * - * For a fork with branch local-blocks `b0..bN` the stage: - * 1. allocates a `_start_i` / `_done_i` Bit signal pair per branch in the - * enclosing domain; + * For a fork with branch local-blocks `b0..bN` the lowering: + * 1. allocates a `_start_i` / `_done_i` Bit signal pair per branch in the enclosing + * domain, declared right *before* the parent process; * 2. replaces the fork in the parent process with: drive every `start_i` high, a join wait - * (All: wait for all dones; Any: wait for any done; None: no wait), then re-arm the - * `start_i` signals (All/Any only); - * 3. emits one `process` per branch (a forever loop) that waits for its `start_i`, runs the - * branch body, raises `done_i`, then waits for `start_i` to drop and clears `done_i`. + * (All: wait for all dones; Any: wait for any done; None: no wait), then re-arm the `start_i` + * signals (All/Any only); + * 3. emits one `process.forever` per branch (placed right *after* the parent process) that waits + * for its `start_i`, runs the branch body, raises `done_i`, then waits for `start_i` to drop + * and clears `done_i`. * - * The branch body is kept inside its `LocalBlock`; for VHDL [[DropLocalBlocks]] later flattens - * it, while Verilog keeps it as a native `begin ... end` block. + * The branch body is kept inside its `LocalBlock`; for VHDL [[DropLocalBlocksED]] later flattens + * it (Verilog keeps it as a native `begin ... end` block), and for RT [[DropLocalBlocksRT]] + * flattens it before the FSM pipeline (RT process bodies cannot carry local blocks). * * Notes / limitations: * - Multiple branches assigning the same signal is the user's responsibility, exactly as in * SystemVerilog (this stage does not arbitrate writes). - * - `join_none` branches are single-shot: the parent does not re-arm the handshake, so a - * parent that re-enters the fork before a `join_none` branch finishes is a race (mirrors the - * inherent difficulty of mapping dynamic process spawning onto static RTL processes). - * - The handshake uses `waitUntil`/`waitWhile`, which lower to correct `wait until ...` (VHDL) - * and `wait(...)` (Verilog) — verified equivalent to native SystemVerilog fork-join by - * simulation. + * - `join_none` branches are single-shot: the parent does not re-arm the handshake. + * - Nested forks are lowered innermost-first across repeated passes so an outer and inner fork + * are never patched together. */ //format: on -case object DropForkJoins extends HierarchyStage: - def dependencies: List[Stage] = List(ToED) +private abstract class DropForkJoins extends HierarchyStage: def nullifies: Set[Stage] = Set() - override def runCondition(using co: CompilerOptions): Boolean = - co.backend match - case _: dfhdl.backends.vhdl => true - case be: dfhdl.backends.verilog => - be.dialect match - case VerilogDialect.v95 | VerilogDialect.v2001 => true - case _ => false + // which forks this stage is responsible for (selected by domain) + protected def handlesFork(fork: ForkBlock)(using MemberGetSet): Boolean + // whether a handled fork needs lowering for the current target + protected def needsLowering(fork: ForkBlock)(using CompilerOptions): Boolean + // the domain of the generated members (drives the MetaDesign domainType) + protected def metaDesignDomain: DomainType + // declare the per-branch start/done handshake signal pair (ED: `Bit <> VAR`, RT: `Bit <> VAR.REG`). + // returns the signals patch and the start/done signal lists (referenced by the parent/branch + // designs below). The signal modifier is the only emit detail that needs a statically-known + // domain, so each concrete stage builds this design itself. + protected def makeSignals(parentProc: ProcessBlock, forkName: String, count: Int)(using + MemberGetSet, + RefGen + ): (DFMember, Patch, List[BitFE], List[BitFE]) + // drive a handshake bit. ED uses a non-blocking assignment; RT uses a (registered) blocking + // assignment — equivalent to `.din :=` on the reg signal. Both are constraint-free at the IR + // level, so the parent/branch designs below are shared across domains. + protected def driveBit(sig: BitFE, value: BitFE)(using dfhdl.core.DFC): Unit + + // a constraint-free Bit literal for the handshake drives + protected final def bitConst(b: Boolean)(using dfhdl.core.DFC): BitFE = + dfhdl.core.DFVal.Const(dfhdl.core.DFBit, Some(b)) // A fork that contains no nested fork — safe to lower this pass. Nested forks are lowered - // innermost-first across repeated passes (Pattern 9) so an outer and inner fork are never - // patched together. + // innermost-first across repeated passes so an outer and inner fork are never patched together. private def isInnermost(fork: ForkBlock)(using MemberGetSet): Boolean = !fork.members(MemberView.Flattened).exists { case _: ForkBlock => true case _ => false } - // Whether this fork needs lowering for the target backend. VHDL has no fork-join at all, so - // every mode is lowered. Old Verilog (v95/v2001) supports `fork ... join` (wait-all) natively - // and only lacks `join_any` / `join_none` (SystemVerilog 2005+), so only those are lowered. - private def needsLowering(fork: ForkBlock)(using co: CompilerOptions): Boolean = - co.backend match - case _: dfhdl.backends.vhdl => true - case be: dfhdl.backends.verilog => - be.dialect match - case VerilogDialect.v95 | VerilogDialect.v2001 => - fork.join != ForkBlock.Join.All - case _ => false // sv2005+: emitted natively, never lowered - case _ => false + // removal patches for every relocated member (branch local blocks + their descendants) + protected final def branchRemovalPatches( + branches: List[LocalBlock] + )(using MemberGetSet): List[(DFMember, Patch)] = + branches + .flatMap(branchLB => branchLB :: branchLB.members(MemberView.Flattened)) + .map(m => m -> Patch.Remove(isMoved = true)) @tailrec private def lowerRepeatedly(db: DB)(using RefGen, CompilerOptions): DB = given MemberGetSet = db.getSet val patches = db.members.view.collect { - case fork: ForkBlock if isInnermost(fork) && needsLowering(fork) => fork + case fork: ForkBlock if handlesFork(fork) && isInnermost(fork) && needsLowering(fork) => fork }.flatMap(lowerFork).toList if (patches.isEmpty) db else lowerRepeatedly(db.patch(patches)) - private def lowerFork(fork: ForkBlock)(using MemberGetSet, RefGen): List[(DFMember, Patch)] = + protected final def lowerFork(fork: ForkBlock)(using + MemberGetSet, + RefGen + ): List[(DFMember, Patch)] = val branches = fork.members(MemberView.Folded).collect { case lb: LocalBlock => lb } // degenerate empty fork: just drop it if (branches.isEmpty) List(fork -> Patch.Remove(isMoved = false)) @@ -91,67 +118,53 @@ case object DropForkJoins extends HierarchyStage: val forkName = fork.getName val joinMode = fork.join - // (1): handshake signals, declared right *before* the parent process. Both the parent - // (trigger + join-wait) and every branch process reference these, so declaring them - // first keeps all references pointing backward (valid member order without relying on a - // later ordering stage). - val signalsDsn = - new MetaDesign(parentProc, Patch.Add.Config.Before, domainType = ED): - val startSignals = branches.indices.toList.map { i => - (Bit <> VAR)(using dfc.setName(s"${forkName}_start_$i")) - } - val doneSignals = branches.indices.toList.map { i => - (Bit <> VAR)(using dfc.setName(s"${forkName}_done_$i")) - } + // (1): handshake signals, declared right *before* the parent process. + val (sigMember, sigPatch, startSignals, doneSignals) = + makeSignals(parentProc, forkName, branches.length) // (3): one forever-process per branch, placed right *after* the parent process. val branchesDsn = - new MetaDesign(parentProc, Patch.Add.Config.After, domainType = ED): - branches.lazyZip(signalsDsn.startSignals).lazyZip(signalsDsn.doneSignals).foreach { - (branchLB, start, done) => - process.forever { - waitUntil(start) - // relocate the branch local block (and its whole subtree) into this process - plantMembers(fork, branchLB :: branchLB.members(MemberView.Flattened)) - done :== 1 - waitWhile(start) - done :== 0 - } + new MetaDesign(parentProc, Patch.Add.Config.After, domainType = metaDesignDomain): + branches.lazyZip(startSignals).lazyZip(doneSignals).foreach { (branchLB, start, done) => + process.forever { + waitUntil(start) + // relocate the branch local block (and its whole subtree) into this process + plantMembers(fork, branchLB :: branchLB.members(MemberView.Flattened)) + driveBit(done, bitConst(true)) + waitWhile(start) + driveBit(done, bitConst(false)) + } } - // removal patches for every relocated member (branch local blocks + their descendants) - val relocatedMembers = - branches.flatMap(branchLB => branchLB :: branchLB.members(MemberView.Flattened)) - val removalPatches = relocatedMembers.map(m => m -> Patch.Remove(isMoved = true)) - // (2): replace the fork in the parent process with the trigger + join-wait logic. val parentDsn = new MetaDesign( fork, Patch.Add.Config.ReplaceWithLast( Patch.Replace.Config.FullReplacement, - // the fork's only references are its branch local-blocks' ownerRefs (from inside - // the fork); those are handled by the relocation above, so we must not redirect - // them onto a generated statement here. + // the fork's only references are its branch local-blocks' ownerRefs (from inside the + // fork); those are handled by the relocation above, so we must not redirect them onto + // a generated statement here. Patch.Replace.RefFilter.Outside(fork) ), - domainType = ED + domainType = metaDesignDomain ): - // the replacement lands inside the parent process, so assert process scope to - // permit non-blocking assignments and waits directly (no nested process). + // the replacement lands inside the parent process, so assert process scope to permit + // waits directly (no nested process). given dfhdl.core.DFC.Scope.Process = dfhdl.core.DFC.Scope.Process - signalsDsn.startSignals.foreach(_ :== 1) + startSignals.foreach(s => driveBit(s, bitConst(true))) joinMode match case ForkBlock.Join.All => - waitUntil(signalsDsn.doneSignals.reduce(_ && _)) - signalsDsn.startSignals.foreach(_ :== 0) + waitUntil(doneSignals.reduce(_ && _)) + startSignals.foreach(s => driveBit(s, bitConst(false))) case ForkBlock.Join.Any => - waitUntil(signalsDsn.doneSignals.reduce(_ || _)) - signalsDsn.startSignals.foreach(_ :== 0) + waitUntil(doneSignals.reduce(_ || _)) + startSignals.foreach(s => driveBit(s, bitConst(false))) case ForkBlock.Join.None => // no wait, no re-arm (single-shot; see limitations) - List(signalsDsn.patch, branchesDsn.patch) ++ removalPatches ++ List(parentDsn.patch) + List((sigMember, sigPatch), branchesDsn.patch) ++ + branchRemovalPatches(branches) ++ List(parentDsn.patch) end if end lowerFork @@ -159,6 +172,89 @@ case object DropForkJoins extends HierarchyStage: lowerRepeatedly(subDB) end DropForkJoins +// Lowers ED-domain join-all fork-joins. Runs after RT->ED conversion; gated to VHDL, the only +// backend with no native fork-join. Old Verilog and SystemVerilog emit `fork ... join` natively, +// and `join_any` / `join_none` are never lowered (see the class doc). +case object DropForkJoinsED extends DropForkJoins: + def dependencies: List[Stage] = List(ToED) + + override def runCondition(using co: CompilerOptions): Boolean = + co.backend match + case _: dfhdl.backends.vhdl => true + case _ => false + + protected def metaDesignDomain: DomainType = DomainType.ED + + protected def handlesFork(fork: ForkBlock)(using MemberGetSet): Boolean = fork.isInEDDomain + + // Only join-all is lowered (VHDL has no native fork-join). join-any / join-none are never + // lowered — they stay as fork blocks and the VHDL printer rejects them (unsupported). + protected def needsLowering(fork: ForkBlock)(using co: CompilerOptions): Boolean = + fork.join == ForkBlock.Join.All + + protected def makeSignals(parentProc: ProcessBlock, forkName: String, count: Int)(using + MemberGetSet, + RefGen + ): (DFMember, Patch, List[BitFE], List[BitFE]) = + val dsn = + new MetaDesign(parentProc, Patch.Add.Config.Before, domainType = DomainType.ED): + val startSignals = (0 until count).toList.map { i => + (Bit <> VAR)(using dfc.setName(s"${forkName}_start_$i")) + } + val doneSignals = (0 until count).toList.map { i => + (Bit <> VAR)(using dfc.setName(s"${forkName}_done_$i")) + } + val (m, p) = dsn.patch + (m, p, dsn.startSignals, dsn.doneSignals) + + protected def driveBit(sig: BitFE, value: BitFE)(using dfhdl.core.DFC): Unit = + sig.nbassign(value) +end DropForkJoinsED + +// Lowers RT-domain `forkJoin` (join-all) into the per-branch handshake form, *before* the RT->FSM +// pipeline. Always runs (RT fork-join has no native HDL target — it must always become a clocked +// FSM, regardless of backend). `forkJoinAny` / `forkJoinNone` are rejected at the frontend under +// RT, so only join-all reaches here. Handshake signals are registered so they hold across edges. +case object DropForkJoinsRT extends DropForkJoins: + // No dependencies: this stage runs at the very front of the RT lowering, on the raw RT-domain + // forks, before the RT-wait pipeline. It deliberately does NOT depend on ToRT — RT forks are + // written directly in RT-domain processes (already RT after elaboration), and depending on ToRT + // would drag the whole DF->RT conversion chain into the RT-wait pipeline's dependency closure, + // changing the behaviour of stages (e.g. SimplifyRTOps) tested in isolation. + def dependencies: List[Stage] = List() + + override def runCondition(using co: CompilerOptions): Boolean = true + + protected def metaDesignDomain: DomainType = DomainType.RT + + protected def handlesFork(fork: ForkBlock)(using MemberGetSet): Boolean = fork.isInRTDomain + + // only join-all is supported under RT (join-any / join-none are rejected at the frontend) + protected def needsLowering(fork: ForkBlock)(using co: CompilerOptions): Boolean = + fork.join == ForkBlock.Join.All + + protected def makeSignals(parentProc: ProcessBlock, forkName: String, count: Int)(using + MemberGetSet, + RefGen + ): (DFMember, Patch, List[BitFE], List[BitFE]) = + val dsn = + new MetaDesign(parentProc, Patch.Add.Config.Before, domainType = DomainType.RT): + val startSignals = (0 until count).toList.map { i => + (Bit <> VAR.REG)(using dfc.setName(s"${forkName}_start_$i")) + } + val doneSignals = (0 until count).toList.map { i => + (Bit <> VAR.REG)(using dfc.setName(s"${forkName}_done_$i")) + } + val (m, p) = dsn.patch + (m, p, dsn.startSignals, dsn.doneSignals) + + // registered blocking assignment (equivalent to `.din :=` on the reg signal) + protected def driveBit(sig: BitFE, value: BitFE)(using dfhdl.core.DFC): Unit = + sig.assign(value) +end DropForkJoinsRT + extension [T: HasDB](t: T) - def dropForkJoins(using CompilerOptions): DB = - StageRunner.run(DropForkJoins)(t.db) + def dropForkJoinsED(using CompilerOptions): DB = + StageRunner.run(DropForkJoinsED)(t.db) + def dropForkJoinsRT(using CompilerOptions): DB = + StageRunner.run(DropForkJoinsRT)(t.db) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropLocalBlocks.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropLocalBlocks.scala index 61595aa02..a0f48f2e6 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropLocalBlocks.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropLocalBlocks.scala @@ -7,15 +7,20 @@ import dfhdl.options.CompilerOptions import scala.annotation.tailrec //format: off -/** Flattens `LocalBlock`s into their enclosing owner. VHDL has no in-process named statement - * block, so every local block (produced by `locally:`) is dissolved: its members are promoted - * to the parent owner and the block itself is removed. +/** Flattens `LocalBlock`s into their enclosing owner: each local block (produced by `locally:`) is + * dissolved, promoting its members to the parent owner and removing the block itself. * - * This stage runs only for VHDL backends. For Verilog (all dialects) local blocks are emitted - * natively as `begin : name ... end` named blocks and are left untouched. - * - * Fork-join branch local blocks are consumed earlier by [[DropForkJoins]]; by the time this - * stage runs only standalone local blocks remain. + * The dissolve logic is domain-agnostic and shared by two concrete stages: + * - [[DropLocalBlocksED]] runs only for VHDL backends, which have no in-process named statement + * block. For Verilog (all dialects) local blocks are emitted natively as `begin : name ... end` + * and are left untouched. Fork-join branch local blocks are consumed earlier by + * [[DropForkJoinsED]]; by the time this stage runs only standalone local blocks remain. + * - [[DropLocalBlocksRT]] runs always (any backend) and flattens *all* RT-domain local blocks — + * both standalone `locally:` blocks and fork-join branch wrappers (produced by + * [[DropForkJoinsRT]]) — *before* the RT->FSM pipeline. An RT process body cannot carry a + * `LocalBlock`: the RT->FSM step extraction (`DropRTWaits` / `FlattenStepBlocks` / + * `DropRTProcess`) only recurses through `StepBlock`s, so waits/statements nested in a local + * block would be invisible to it. * * ==Rule: dissolve local block== * {{{ @@ -36,14 +41,11 @@ import scala.annotation.tailrec * Nested local blocks are dissolved innermost-first across repeated passes. */ //format: on -case object DropLocalBlocks extends HierarchyStage: - def dependencies: List[Stage] = List(DropForkJoins, DropLocalDcls) +private abstract class DropLocalBlocks extends HierarchyStage: def nullifies: Set[Stage] = Set() - override def runCondition(using co: CompilerOptions): Boolean = - co.backend match - case _: dfhdl.backends.vhdl => true - case _ => false + // which local blocks this stage is responsible for (selected by domain) + protected def handlesBlock(lb: LocalBlock)(using MemberGetSet): Boolean // A local block that does not itself contain another local block (safe to dissolve this pass). private def isInnermost(lb: LocalBlock)(using MemberGetSet): Boolean = @@ -56,7 +58,7 @@ case object DropLocalBlocks extends HierarchyStage: given MemberGetSet = db.getSet val patches: List[(DFMember, Patch)] = db.members.view.collect { - case lb: LocalBlock if isInnermost(lb) => + case lb: LocalBlock if handlesBlock(lb) && isInnermost(lb) => // redirect all refs to `lb` (its children's ownerRefs) to `lb`'s owner, then remove `lb`. // the children keep their positions in the flat member list, now owned by the parent. lb -> Patch.Replace(lb.getOwner, Patch.Replace.Config.ChangeRefAndRemove) @@ -68,6 +70,35 @@ case object DropLocalBlocks extends HierarchyStage: dissolveRepeatedly(subDB) end DropLocalBlocks +// Flattens ED-domain local blocks for VHDL (which has no named statement block). Verilog keeps +// them native, so this stage is a no-op there. +case object DropLocalBlocksED extends DropLocalBlocks: + def dependencies: List[Stage] = List(DropForkJoinsED, DropLocalDcls) + + override def runCondition(using co: CompilerOptions): Boolean = + co.backend match + case _: dfhdl.backends.vhdl => true + case _ => false + + protected def handlesBlock(lb: LocalBlock)(using MemberGetSet): Boolean = lb.isInEDDomain +end DropLocalBlocksED + +// Flattens all RT-domain local blocks before the RT->FSM pipeline (RT process bodies cannot carry +// local blocks). Always runs, regardless of backend. +case object DropLocalBlocksRT extends DropLocalBlocks: + // Only needs the forks lowered first (so branch local blocks exist as standalone blocks). Unlike + // the ED variant it must NOT depend on DropLocalDcls: this stage runs early, before the RT-wait + // pipeline, whereas DropLocalDcls belongs late in BackendPrepStage — pulling it in here would + // force it to run far too early and corrupt the pipeline order. + def dependencies: List[Stage] = List(DropForkJoinsRT) + + override def runCondition(using co: CompilerOptions): Boolean = true + + protected def handlesBlock(lb: LocalBlock)(using MemberGetSet): Boolean = lb.isInRTDomain +end DropLocalBlocksRT + extension [T: HasDB](t: T) - def dropLocalBlocks(using CompilerOptions): DB = - StageRunner.run(DropLocalBlocks)(t.db) + def dropLocalBlocksED(using CompilerOptions): DB = + StageRunner.run(DropLocalBlocksED)(t.db) + def dropLocalBlocksRT(using CompilerOptions): DB = + StageRunner.run(DropLocalBlocksRT)(t.db) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropTimedRTWaits.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropTimedRTWaits.scala index d00154edc..0d8ac9345 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropTimedRTWaits.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropTimedRTWaits.scala @@ -15,7 +15,10 @@ import scala.collection.mutable */ //format: on case object DropTimedRTWaits extends HierarchyStage: - def dependencies: List[Stage] = List() + // DropForkJoinsRT / DropLocalBlocksRT must run before any RT-wait lowering so that RT fork-joins + // are already lowered to the per-branch handshake form (and their local blocks flattened) before + // waits are turned into FSM steps. Anchoring at the root of the RT-wait chain guarantees this. + def dependencies: List[Stage] = List(DropLocalBlocksRT) def nullifies: Set[Stage] = Set(DropUnreferencedAnons) def transformSubDB(rootDB: DB)(using MemberGetSet, CompilerOptions, RefGen): DB = val patches = subDB.members.collect { diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogOwnerPrinter.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogOwnerPrinter.scala index 22e31e761..e56f54f64 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogOwnerPrinter.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogOwnerPrinter.scala @@ -258,13 +258,22 @@ protected trait VerilogOwnerPrinter extends AbstractOwnerPrinter: s"$dcl${named}$alwaysKW$senList\nbegin\n${body.hindent}\nend" end csProcessBlock def csForkBlock(fb: ForkBlock): String = - val body = csDFMembers(fb.members(MemberView.Folded)) - val label = fb.meta.nameOpt.map(n => s" : $n").getOrElse("") - val joinKW = fb.join match - case ForkBlock.Join.All => "join" - case ForkBlock.Join.Any => "join_any" - case ForkBlock.Join.None => "join_none" - s"fork$label\n${body.hindent}\n$joinKW" + // `join_any` / `join_none` exist only in SystemVerilog (sv2005+). Old Verilog (v95/v2001) has + // only `fork ... join` (wait-all); the other modes are never lowered for it (see DropForkJoins) + // and must be rejected here rather than emitted as invalid code. + val joinAnyNoneSupported = printer.dialect match + case VerilogDialect.v95 | VerilogDialect.v2001 => false + case _ => true + if (fb.join != ForkBlock.Join.All && !joinAnyNoneSupported) printer.unsupported + else + val body = csDFMembers(fb.members(MemberView.Folded)) + val label = fb.meta.nameOpt.map(n => s" : $n").getOrElse("") + val joinKW = fb.join match + case ForkBlock.Join.All => "join" + case ForkBlock.Join.Any => "join_any" + case ForkBlock.Join.None => "join_none" + s"fork$label\n${body.hindent}\n$joinKW" + end csForkBlock def csLocalBlock(lb: LocalBlock): String = val (statements, dcls) = lb .members(MemberView.Folded) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLOwnerPrinter.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLOwnerPrinter.scala index cf75f39ab..4d0cc9a57 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLOwnerPrinter.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLOwnerPrinter.scala @@ -244,7 +244,7 @@ protected trait VHDLOwnerPrinter extends AbstractOwnerPrinter: |${body.hindent} |end process;""" end csProcessBlock - // fork-join and local blocks are lowered away (DropForkJoins / DropLocalBlocks) before + // fork-join and local blocks are lowered away (DropForkJoinsED / DropLocalBlocksED) before // VHDL printing; these are only safety nets. def csForkBlock(fb: ForkBlock): String = printer.unsupported def csLocalBlock(lb: LocalBlock): String = printer.unsupported diff --git a/compiler/stages/src/test/scala/StagesSpec/DropForkJoinsSpec.scala b/compiler/stages/src/test/scala/StagesSpec/DropForkJoinsSpec.scala index 106c5ca38..9af9651d9 100644 --- a/compiler/stages/src/test/scala/StagesSpec/DropForkJoinsSpec.scala +++ b/compiler/stages/src/test/scala/StagesSpec/DropForkJoinsSpec.scala @@ -1,7 +1,7 @@ package StagesSpec import dfhdl.* -import dfhdl.compiler.stages.dropForkJoins +import dfhdl.compiler.stages.{dropForkJoinsED, dropForkJoinsRT} // scalafmt: { align.tokens = [{code = "<>"}, {code = "="}, {code = "=>"}, {code = ":="}]} class DropForkJoinsSpec extends StageSpec: @@ -17,7 +17,7 @@ class DropForkJoinsSpec extends StageSpec: locally: b :== 1 end FJ - val fj = (new FJ).dropForkJoins + val fj = (new FJ).dropForkJoinsED assertCodeString( fj, """|class FJ extends EDDesign: @@ -51,95 +51,6 @@ class DropForkJoinsSpec extends StageSpec: |""".stripMargin ) - test("forkJoinAny lowering under VHDL"): - given options.CompilerOptions.Backend = _.vhdl - class FJ extends EDDesign: - val a = Bit <> OUT - val b = Bit <> OUT - process: - val fk = forkJoinAny: - locally: - a :== 1 - locally: - b :== 1 - end FJ - val fj = (new FJ).dropForkJoins - assertCodeString( - fj, - """|class FJ extends EDDesign: - | val a = Bit <> OUT - | val b = Bit <> OUT - | val fk_start_0 = Bit <> VAR - | val fk_start_1 = Bit <> VAR - | val fk_done_0 = Bit <> VAR - | val fk_done_1 = Bit <> VAR - | process: - | fk_start_0 :== 1 - | fk_start_1 :== 1 - | waitUntil(fk_done_0 || fk_done_1) - | fk_start_0 :== 0 - | fk_start_1 :== 0 - | process: - | waitUntil(fk_start_0) - | locally: - | a :== 1 - | fk_done_0 :== 1 - | waitWhile(fk_start_0) - | fk_done_0 :== 0 - | process: - | waitUntil(fk_start_1) - | locally: - | b :== 1 - | fk_done_1 :== 1 - | waitWhile(fk_start_1) - | fk_done_1 :== 0 - |end FJ - |""".stripMargin - ) - - test("forkJoinNone lowering under VHDL"): - given options.CompilerOptions.Backend = _.vhdl - class FJ extends EDDesign: - val a = Bit <> OUT - val b = Bit <> OUT - process: - val fk = forkJoinNone: - locally: - a :== 1 - locally: - b :== 1 - end FJ - val fj = (new FJ).dropForkJoins - assertCodeString( - fj, - """|class FJ extends EDDesign: - | val a = Bit <> OUT - | val b = Bit <> OUT - | val fk_start_0 = Bit <> VAR - | val fk_start_1 = Bit <> VAR - | val fk_done_0 = Bit <> VAR - | val fk_done_1 = Bit <> VAR - | process: - | fk_start_0 :== 1 - | fk_start_1 :== 1 - | process: - | waitUntil(fk_start_0) - | locally: - | a :== 1 - | fk_done_0 :== 1 - | waitWhile(fk_start_0) - | fk_done_0 :== 0 - | process: - | waitUntil(fk_start_1) - | locally: - | b :== 1 - | fk_done_1 :== 1 - | waitWhile(fk_start_1) - | fk_done_1 :== 0 - |end FJ - |""".stripMargin - ) - test("nested fork lowering under VHDL"): given options.CompilerOptions.Backend = _.vhdl class FJ extends EDDesign: @@ -157,7 +68,7 @@ class DropForkJoinsSpec extends StageSpec: locally: c :== 1 end FJ - val fj = (new FJ).dropForkJoins + val fj = (new FJ).dropForkJoinsED assertCodeString( fj, """|class FJ extends EDDesign: @@ -214,8 +125,11 @@ class DropForkJoinsSpec extends StageSpec: |""".stripMargin ) - test("Verilog v2001 keeps forkJoin (all) native but lowers forkJoinAny"): - given options.CompilerOptions.Backend = _.verilog.v2001 + // join-any / join-none can dynamically spawn threads and are never lowered: a join-all in the + // same design is lowered while the others are left as native fork blocks (rejected later by any + // backend without native support). + test("forkJoinAny / forkJoinNone are left untouched under VHDL"): + given options.CompilerOptions.Backend = _.vhdl class FJ extends EDDesign: val a = Bit <> OUT val b = Bit <> OUT @@ -230,43 +144,99 @@ class DropForkJoinsSpec extends StageSpec: a :== 0 locally: b :== 0 + val fkNone = forkJoinNone: + locally: + a :== 1 + locally: + b :== 0 end FJ - val fj = (new FJ).dropForkJoins + val fj = (new FJ).dropForkJoinsED assertCodeString( fj, """|class FJ extends EDDesign: | val a = Bit <> OUT | val b = Bit <> OUT - | val fkAny_start_0 = Bit <> VAR - | val fkAny_start_1 = Bit <> VAR - | val fkAny_done_0 = Bit <> VAR - | val fkAny_done_1 = Bit <> VAR - | process: - | val fkAll = forkJoin: + | val fkAll_start_0 = Bit <> VAR + | val fkAll_start_1 = Bit <> VAR + | val fkAll_done_0 = Bit <> VAR + | val fkAll_done_1 = Bit <> VAR + | process: + | fkAll_start_0 :== 1 + | fkAll_start_1 :== 1 + | waitUntil(fkAll_done_0 && fkAll_done_1) + | fkAll_start_0 :== 0 + | fkAll_start_1 :== 0 + | val fkAny = forkJoinAny: + | locally: + | a :== 0 + | locally: + | b :== 0 + | val fkNone = forkJoinNone: | locally: | a :== 1 | locally: - | b :== 1 - | fkAny_start_0 :== 1 - | fkAny_start_1 :== 1 - | waitUntil(fkAny_done_0 || fkAny_done_1) - | fkAny_start_0 :== 0 - | fkAny_start_1 :== 0 + | b :== 0 + | process: + | waitUntil(fkAll_start_0) + | locally: + | a :== 1 + | fkAll_done_0 :== 1 + | waitWhile(fkAll_start_0) + | fkAll_done_0 :== 0 + | process: + | waitUntil(fkAll_start_1) + | locally: + | b :== 1 + | fkAll_done_1 :== 1 + | waitWhile(fkAll_start_1) + | fkAll_done_1 :== 0 + |end FJ + |""".stripMargin + ) + + test("forkJoin (all) lowering under RT"): + class FJ extends RTDesign: + val a = Bit <> OUT.REG + val b = Bit <> OUT.REG + process: + val fk = forkJoin: + locally: + a.din := 1 + locally: + b.din := 1 + end FJ + val fj = (new FJ).dropForkJoinsRT + assertCodeString( + fj, + """|class FJ extends RTDesign: + | val a = Bit <> OUT.REG + | val b = Bit <> OUT.REG + | val fk_start_0 = Bit <> VAR.REG + | val fk_start_1 = Bit <> VAR.REG + | val fk_done_0 = Bit <> VAR.REG + | val fk_done_1 = Bit <> VAR.REG + | process: + | fk_start_0.din := 1 + | fk_start_1.din := 1 + | waitUntil(fk_done_0 && fk_done_1) + | fk_start_0.din := 0 + | fk_start_1.din := 0 | process: - | waitUntil(fkAny_start_0) + | waitUntil(fk_start_0) | locally: - | a :== 0 - | fkAny_done_0 :== 1 - | waitWhile(fkAny_start_0) - | fkAny_done_0 :== 0 + | a.din := 1 + | fk_done_0.din := 1 + | waitWhile(fk_start_0) + | fk_done_0.din := 0 | process: - | waitUntil(fkAny_start_1) + | waitUntil(fk_start_1) | locally: - | b :== 0 - | fkAny_done_1 :== 1 - | waitWhile(fkAny_start_1) - | fkAny_done_1 :== 0 + | b.din := 1 + | fk_done_1.din := 1 + | waitWhile(fk_start_1) + | fk_done_1.din := 0 |end FJ |""".stripMargin ) + end DropForkJoinsSpec diff --git a/compiler/stages/src/test/scala/StagesSpec/DropLocalBlocksSpec.scala b/compiler/stages/src/test/scala/StagesSpec/DropLocalBlocksSpec.scala index 4b4e42f1b..db23256b0 100644 --- a/compiler/stages/src/test/scala/StagesSpec/DropLocalBlocksSpec.scala +++ b/compiler/stages/src/test/scala/StagesSpec/DropLocalBlocksSpec.scala @@ -1,7 +1,7 @@ package StagesSpec import dfhdl.* -import dfhdl.compiler.stages.dropLocalBlocks +import dfhdl.compiler.stages.{dropLocalBlocksED, dropLocalBlocksRT} // scalafmt: { align.tokens = [{code = "<>"}, {code = "="}, {code = "=>"}, {code = ":="}]} class DropLocalBlocksSpec extends StageSpec: @@ -16,7 +16,7 @@ class DropLocalBlocksSpec extends StageSpec: b :== 1 a :== 0 end LB - val lb = (new LB).dropLocalBlocks + val lb = (new LB).dropLocalBlocksED assertCodeString( lb, """|class LB extends EDDesign: @@ -43,7 +43,7 @@ class DropLocalBlocksSpec extends StageSpec: locally: c :== 1 end LB - val lb = (new LB).dropLocalBlocks + val lb = (new LB).dropLocalBlocksED assertCodeString( lb, """|class LB extends EDDesign: @@ -69,7 +69,7 @@ class DropLocalBlocksSpec extends StageSpec: b :== 1 a :== 0 end LB - val lb = (new LB).dropLocalBlocks + val lb = (new LB).dropLocalBlocksED assertCodeString( lb, """|class LB extends EDDesign: @@ -83,4 +83,55 @@ class DropLocalBlocksSpec extends StageSpec: |end LB |""".stripMargin ) + + test("standalone local block flattening under RT"): + class LB extends RTDesign: + val a = Bit <> OUT.REG + val b = Bit <> OUT.REG + process: + a.din := 1 + locally: + b.din := 1 + a.din := 0 + end LB + val lb = (new LB).dropLocalBlocksRT + assertCodeString( + lb, + """|class LB extends RTDesign: + | val a = Bit <> OUT.REG + | val b = Bit <> OUT.REG + | process: + | a.din := 1 + | b.din := 1 + | a.din := 0 + |end LB + |""".stripMargin + ) + + test("nested local blocks flattening under RT"): + class LB extends RTDesign: + val a = Bit <> OUT.REG + val b = Bit <> OUT.REG + val c = Bit <> OUT.REG + process: + a.din := 1 + locally: + b.din := 1 + locally: + c.din := 1 + end LB + val lb = (new LB).dropLocalBlocksRT + assertCodeString( + lb, + """|class LB extends RTDesign: + | val a = Bit <> OUT.REG + | val b = Bit <> OUT.REG + | val c = Bit <> OUT.REG + | process: + | a.din := 1 + | b.din := 1 + | c.din := 1 + |end LB + |""".stripMargin + ) end DropLocalBlocksSpec diff --git a/compiler/stages/src/test/scala/StagesSpec/PrintVHDLCodeSpec.scala b/compiler/stages/src/test/scala/StagesSpec/PrintVHDLCodeSpec.scala index 34fc36d61..60dc28d71 100644 --- a/compiler/stages/src/test/scala/StagesSpec/PrintVHDLCodeSpec.scala +++ b/compiler/stages/src/test/scala/StagesSpec/PrintVHDLCodeSpec.scala @@ -1674,28 +1674,21 @@ class PrintVHDLCodeSpec extends StageSpec: |end Foo_arch;""".stripMargin ) } - test("fork-join lowering to processes + handshake signals") { + // join-all is the only mode lowered for VHDL (no native fork-join). join-any / join-none can + // dynamically spawn threads and are rejected by the VHDL backend (see DropForkJoins). + test("ED forkJoin (wait-all) lowering to processes + handshake signals") { given options.PrinterOptions.Align = false - class FJ3 extends EDDesign: + class FJ extends EDDesign: val a = Bit <> OUT val b = Bit <> OUT - val c = Bit <> OUT process: val jAll = forkJoin: locally: a :== 0 locally: b :== 0 - val jAny = forkJoinAny: - locally: - a :== 1 - locally: - c :== 1 - val jNone = forkJoinNone: - locally: - c :== 0 - end FJ3 - val fj = (new FJ3).getCompiledCodeString + end FJ + val fj = (new FJ).getCompiledCodeString assertNoDiff( fj, """|library ieee; @@ -1703,25 +1696,18 @@ class PrintVHDLCodeSpec extends StageSpec: |use ieee.numeric_std.all; |use work.dfhdl_pkg.all; | - |entity FJ3 is + |entity FJ is |port ( | a : out std_logic; - | b : out std_logic; - | c : out std_logic + | b : out std_logic |); - |end FJ3; + |end FJ; | - |architecture FJ3_arch of FJ3 is + |architecture FJ_arch of FJ is | signal jAll_start_0 : std_logic; | signal jAll_start_1 : std_logic; | signal jAll_done_0 : std_logic; | signal jAll_done_1 : std_logic; - | signal jAny_start_0 : std_logic; - | signal jAny_start_1 : std_logic; - | signal jAny_done_0 : std_logic; - | signal jAny_done_1 : std_logic; - | signal jNone_start_0 : std_logic; - | signal jNone_done_0 : std_logic; |begin | process | begin @@ -1730,12 +1716,6 @@ class PrintVHDLCodeSpec extends StageSpec: | wait until jAll_done_0 and jAll_done_1; | jAll_start_0 <= '0'; | jAll_start_1 <= '0'; - | jAny_start_0 <= '1'; - | jAny_start_1 <= '1'; - | wait until jAny_done_0 or jAny_done_1; - | jAny_start_0 <= '0'; - | jAny_start_1 <= '0'; - | jNone_start_0 <= '1'; | end process; | process | begin @@ -1753,31 +1733,109 @@ class PrintVHDLCodeSpec extends StageSpec: | wait until not jAll_start_1; | jAll_done_1 <= '0'; | end process; - | process - | begin - | wait until jAny_start_0; - | a <= '1'; - | jAny_done_0 <= '1'; - | wait until not jAny_start_0; - | jAny_done_0 <= '0'; - | end process; - | process - | begin - | wait until jAny_start_1; - | c <= '1'; - | jAny_done_1 <= '1'; - | wait until not jAny_start_1; - | jAny_done_1 <= '0'; - | end process; - | process + |end FJ_arch;""".stripMargin + ) + } + // a register-transfer forkJoin lowers all the way to a clocked FSM (each branch + the parent + // handshake becomes its own state-register process). + test("RT forkJoin lowers to a clocked FSM") { + given options.PrinterOptions.Align = false + class ForkJoinFSM extends RTDesign: + val a = Bit <> OUT.REG init 0 + val b = Bit <> OUT.REG init 0 + process: + val fk = forkJoin: + locally: + a.din := 1 + locally: + b.din := 1 + end ForkJoinFSM + val top = (new ForkJoinFSM).getCompiledCodeString + assertNoDiff( + top, + """|library ieee; + |use ieee.std_logic_1164.all; + |use ieee.numeric_std.all; + |use work.dfhdl_pkg.all; + | + |entity ForkJoinFSM is + |port ( + | clk : in std_logic; + | rst : in std_logic; + | a : out std_logic; + | b : out std_logic + |); + |end ForkJoinFSM; + | + |architecture ForkJoinFSM_arch of ForkJoinFSM is + | type t_enum_State is ( + | State_S_0, State_S_1 + | ); + | signal fk_start_0 : std_logic; + | signal fk_start_1 : std_logic; + | signal fk_done_0 : std_logic; + | signal fk_done_1 : std_logic; + | signal state_0 : t_enum_State; + | signal state_1 : t_enum_State; + | signal state_2 : t_enum_State; + |begin + | process (clk) | begin - | wait until jNone_start_0; - | c <= '0'; - | jNone_done_0 <= '1'; - | wait until not jNone_start_0; - | jNone_done_0 <= '0'; + | if rising_edge(clk) then + | if rst = '1' then + | a <= '0'; + | b <= '0'; + | state_0 <= State_S_0; + | state_1 <= State_S_0; + | state_2 <= State_S_0; + | else + | case state_0 is + | when State_S_0 => + | fk_start_0 <= '1'; + | fk_start_1 <= '1'; + | state_0 <= State_S_1; + | when State_S_1 => + | if not (fk_done_0 and fk_done_1) then state_0 <= State_S_1; + | else + | fk_start_0 <= '0'; + | fk_start_1 <= '0'; + | state_0 <= State_S_0; + | end if; + | end case; + | case state_1 is + | when State_S_0 => + | if not fk_start_0 then state_1 <= State_S_0; + | else + | a <= '1'; + | fk_done_0 <= '1'; + | state_1 <= State_S_1; + | end if; + | when State_S_1 => + | if fk_start_0 then state_1 <= State_S_1; + | else + | fk_done_0 <= '0'; + | state_1 <= State_S_0; + | end if; + | end case; + | case state_2 is + | when State_S_0 => + | if not fk_start_1 then state_2 <= State_S_0; + | else + | b <= '1'; + | fk_done_1 <= '1'; + | state_2 <= State_S_1; + | end if; + | when State_S_1 => + | if fk_start_1 then state_2 <= State_S_1; + | else + | fk_done_1 <= '0'; + | state_2 <= State_S_0; + | end if; + | end case; + | end if; + | end if; | end process; - |end FJ3_arch;""".stripMargin + |end ForkJoinFSM_arch;""".stripMargin ) } end PrintVHDLCodeSpec diff --git a/compiler/stages/src/test/scala/StagesSpec/PrintVerilogCodeSpec.scala b/compiler/stages/src/test/scala/StagesSpec/PrintVerilogCodeSpec.scala index 651ad20c3..6d501f42e 100644 --- a/compiler/stages/src/test/scala/StagesSpec/PrintVerilogCodeSpec.scala +++ b/compiler/stages/src/test/scala/StagesSpec/PrintVerilogCodeSpec.scala @@ -1809,10 +1809,10 @@ class PrintVerilogCodeSpec extends StageSpec: |endmodule""".stripMargin ) } - test("forkJoinAny is lowered to handshake processes under old Verilog (v2001)") { + test("forkJoinAny is unsupported under old Verilog (v2001)") { given options.CompilerOptions.Backend = _.verilog.v2001 - // v2001 lacks join_any/join_none, so forkJoinAny is lowered to multiple always blocks with - // start/done handshake signals (parent waits for ANY done). Local blocks stay native. + // v2001 lacks join_any/join_none and they are never lowered (they may dynamically spawn + // threads, e.g. a fork inside a loop), so forkJoinAny is rejected by the backend. class FJvAny extends EDDesign: val a = Bit <> OUT val b = Bit <> OUT @@ -1823,48 +1823,105 @@ class PrintVerilogCodeSpec extends StageSpec: locally: b :== 0 end FJvAny - val fj = (new FJvAny).getCompiledCodeString + intercept[IllegalArgumentException]((new FJvAny).getCompiledCodeString) + } + // a register-transfer forkJoin lowers all the way to a clocked FSM (each branch + the parent + // handshake becomes its own state-register process). + test("RT forkJoin lowers to a clocked FSM") { + class ForkJoinFSM extends RTDesign: + val a = Bit <> OUT.REG init 0 + val b = Bit <> OUT.REG init 0 + process: + val fk = forkJoin: + locally: + a.din := 1 + locally: + b.din := 1 + end ForkJoinFSM + val top = (new ForkJoinFSM).getCompiledCodeString assertNoDiff( - fj, + top, """|`default_nettype none |`timescale 1ns/1ps | - |module FJvAny( - | output reg a, - | output reg b + |module ForkJoinFSM( + | input wire logic clk, + | input wire logic rst, + | output logic a, + | output logic b |); - | `include "dfhdl_defs.vh" - | reg j_start_0; - | reg j_start_1; - | reg j_done_0; - | reg j_done_1; - | always - | begin - | j_start_0 <= 1'b1; - | j_start_1 <= 1'b1; - | wait(j_done_0 | j_done_1); - | j_start_0 <= 1'b0; - | j_start_1 <= 1'b0; - | end - | always + | `include "dfhdl_defs.svh" + | typedef enum logic [0:0] { + | State_S_0 = 0, + | State_S_1 = 1 + | } t_enum_State; + | logic fk_start_0; + | logic fk_start_1; + | logic fk_done_0; + | logic fk_done_1; + | t_enum_State state_0; + | t_enum_State state_1; + | t_enum_State state_2; + | always_ff @(posedge clk) | begin - | wait(j_start_0); - | begin + | if (rst == 1'b1) begin | a <= 1'b0; - | end - | j_done_0 <= 1'b1; - | wait(~j_start_0); - | j_done_0 <= 1'b0; - | end - | always - | begin - | wait(j_start_1); - | begin | b <= 1'b0; + | state_0 <= State_S_0; + | state_1 <= State_S_0; + | state_2 <= State_S_0; + | end + | else begin + | unique case (state_0) + | State_S_0: begin + | fk_start_0 <= 1'b1; + | fk_start_1 <= 1'b1; + | state_0 <= State_S_1; + | end + | State_S_1: begin + | if (~(fk_done_0 & fk_done_1)) state_0 <= State_S_1; + | else begin + | fk_start_0 <= 1'b0; + | fk_start_1 <= 1'b0; + | state_0 <= State_S_0; + | end + | end + | endcase + | unique case (state_1) + | State_S_0: begin + | if (~fk_start_0) state_1 <= State_S_0; + | else begin + | a <= 1'b1; + | fk_done_0 <= 1'b1; + | state_1 <= State_S_1; + | end + | end + | State_S_1: begin + | if (fk_start_0) state_1 <= State_S_1; + | else begin + | fk_done_0 <= 1'b0; + | state_1 <= State_S_0; + | end + | end + | endcase + | unique case (state_2) + | State_S_0: begin + | if (~fk_start_1) state_2 <= State_S_0; + | else begin + | b <= 1'b1; + | fk_done_1 <= 1'b1; + | state_2 <= State_S_1; + | end + | end + | State_S_1: begin + | if (fk_start_1) state_2 <= State_S_1; + | else begin + | fk_done_1 <= 1'b0; + | state_2 <= State_S_0; + | end + | end + | endcase | end - | j_done_1 <= 1'b1; - | wait(~j_start_1); - | j_done_1 <= 1'b0; | end |endmodule""".stripMargin ) diff --git a/core/src/main/scala/dfhdl/core/Fork.scala b/core/src/main/scala/dfhdl/core/Fork.scala index 8aff654ff..e981097e2 100644 --- a/core/src/main/scala/dfhdl/core/Fork.scala +++ b/core/src/main/scala/dfhdl/core/Fork.scala @@ -15,9 +15,18 @@ object Fork: end Block object Ops: + // forkJoinAny / forkJoinNone can dynamically spawn threads (e.g. a fork inside a loop), which + // cannot be represented as a static set of synthesized processes. For now they are restricted + // to event-driven (ED) domains, where they map to native SystemVerilog `join_any` / `join_none`. protected type EDDomainOnly[A] = AssertGiven[ A <:< DomainType.ED, - "Fork-join is only allowed under event-driven (ED) domains (RT support is planned)." + "forkJoinAny and forkJoinNone are only allowed under event-driven (ED) domains." + ] + // forkJoin (join-all) blocks the parent until all branches complete, so it is safe in any + // non-dataflow domain (including register-transfer, where it lowers to a clocked FSM). + protected type NotDFDomain[A] = AssertGiven[ + util.NotGiven[A <:< DomainType.DF], + "Fork-join is not supported under dataflow (DF) domains." ] protected type InProcess = AssertGiven[ DFC.Scope.Process, @@ -30,20 +39,17 @@ object Fork: dfc.enterOwner(owner) block(using DFC.Scope.Process) dfc.exitOwner() - object forkJoin: - def apply(block: DFC.Scope.Process ?=> Unit)(using - dt: DomainType - )(using EDDomainOnly[dt.type], InProcess, DFC): Unit = - forkBlock(ir.ForkBlock.Join.All)(block) - object forkJoinAny: - def apply(block: DFC.Scope.Process ?=> Unit)(using - dt: DomainType - )(using EDDomainOnly[dt.type], InProcess, DFC): Unit = - forkBlock(ir.ForkBlock.Join.Any)(block) - object forkJoinNone: - def apply(block: DFC.Scope.Process ?=> Unit)(using - dt: DomainType - )(using EDDomainOnly[dt.type], InProcess, DFC): Unit = - forkBlock(ir.ForkBlock.Join.None)(block) + def forkJoin(block: DFC.Scope.Process ?=> Unit)(using + dt: DomainType + )(using NotDFDomain[dt.type], InProcess, DFC): Unit = + forkBlock(ir.ForkBlock.Join.All)(block) + def forkJoinAny(block: DFC.Scope.Process ?=> Unit)(using + dt: DomainType + )(using EDDomainOnly[dt.type], InProcess, DFC): Unit = + forkBlock(ir.ForkBlock.Join.Any)(block) + def forkJoinNone(block: DFC.Scope.Process ?=> Unit)(using + dt: DomainType + )(using EDDomainOnly[dt.type], InProcess, DFC): Unit = + forkBlock(ir.ForkBlock.Join.None)(block) end Ops end Fork diff --git a/core/src/test/scala/CoreSpec/ForkSpec.scala b/core/src/test/scala/CoreSpec/ForkSpec.scala new file mode 100644 index 000000000..1a5667b85 --- /dev/null +++ b/core/src/test/scala/CoreSpec/ForkSpec.scala @@ -0,0 +1,37 @@ +package CoreSpec +import dfhdl.* +import munit.* + +class ForkSpec extends DFSpec: + test("forkJoinAny is rejected under RT"): + assertCompileError( + "forkJoinAny and forkJoinNone are only allowed under event-driven (ED) domains." + )( + """ + class FJ extends RTDesign: + val a = Bit <> OUT.REG + process: + forkJoinAny: + locally: + a.din := 1 + locally: + a.din := 0 + """ + ) + + test("forkJoinNone is rejected under RT"): + assertCompileError( + "forkJoinAny and forkJoinNone are only allowed under event-driven (ED) domains." + )( + """ + class FJ extends RTDesign: + val a = Bit <> OUT.REG + process: + forkJoinNone: + locally: + a.din := 1 + locally: + a.din := 0 + """ + ) +end ForkSpec From 96d570693be018295763a8704a224a221576f2c7 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Mon, 8 Jun 2026 14:42:11 +0300 Subject: [PATCH 05/72] minimize references to process.forever --- .../dfhdl/compiler/stages/DropForkJoins.scala | 4 ++-- .../scala/StagesSpec/PrintVerilogCodeSpec.scala | 2 +- docs/user-guide/processes/index.md | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropForkJoins.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropForkJoins.scala index ce3ffb2b5..8b2410fd9 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropForkJoins.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropForkJoins.scala @@ -29,7 +29,7 @@ private type BitFE = dfhdl.core.DFValOf[dfhdl.core.DFBit] * natively, so nothing is lowered there. Handshake signals are plain `Bit <> VAR` driven with * non-blocking assignments. * - [[DropForkJoinsRT]] lowers join-all fork-joins in the register-transfer (RT) domain into the - * per-branch `process.forever` + handshake form, which the RT->FSM pipeline (`SimplifyRTOps` + * per-branch `process` + handshake form, which the RT->FSM pipeline (`SimplifyRTOps` * -> `DropRTWaits` -> `DropRTProcess`) then turns into a clocked state-machine. Handshake * signals must hold value across clock edges, so they are registered (`Bit <> VAR.REG`) and * driven with blocking assignments (equivalent to `.din :=`). @@ -40,7 +40,7 @@ private type BitFE = dfhdl.core.DFValOf[dfhdl.core.DFBit] * 2. replaces the fork in the parent process with: drive every `start_i` high, a join wait * (All: wait for all dones; Any: wait for any done; None: no wait), then re-arm the `start_i` * signals (All/Any only); - * 3. emits one `process.forever` per branch (placed right *after* the parent process) that waits + * 3. emits one `process` per branch (placed right *after* the parent process) that waits * for its `start_i`, runs the branch body, raises `done_i`, then waits for `start_i` to drop * and clears `done_i`. * diff --git a/compiler/stages/src/test/scala/StagesSpec/PrintVerilogCodeSpec.scala b/compiler/stages/src/test/scala/StagesSpec/PrintVerilogCodeSpec.scala index 6d501f42e..2034241d9 100644 --- a/compiler/stages/src/test/scala/StagesSpec/PrintVerilogCodeSpec.scala +++ b/compiler/stages/src/test/scala/StagesSpec/PrintVerilogCodeSpec.scala @@ -1816,7 +1816,7 @@ class PrintVerilogCodeSpec extends StageSpec: class FJvAny extends EDDesign: val a = Bit <> OUT val b = Bit <> OUT - process.forever: + process: val j = forkJoinAny: locally: a :== 0 diff --git a/docs/user-guide/processes/index.md b/docs/user-guide/processes/index.md index e5355904a..6ebcca526 100644 --- a/docs/user-guide/processes/index.md +++ b/docs/user-guide/processes/index.md @@ -11,9 +11,9 @@ Processes are not available in the dataflow (DF) domain. Processes cannot be nes In an [RT design][design-domains], a process is used to describe a finite-state machine that is **clock-bound**: it advances on the domain clock and is compiled to a state register plus combinational next-state and output logic. -### Syntax: `process` / `process.forever` +### Syntax: `process:` -Use the shorthand `process:` (or `process.forever`) inside an `RTDesign` or `RTDomain`. The block contains either plain combinational logic (assignments, no steps) or step definitions that form an FSM. +Use the argument-less `process:` inside an `RTDesign` or `RTDomain`. The block contains either plain combinational logic (assignments, no steps) or step definitions that form an FSM. ### Step-based FSM @@ -145,9 +145,9 @@ process(all): This applies to every process form (`process(sig)`, `process(all)`, `process(clk)`, etc.). /// -### Forever process: `process.forever` / `process` +### Forever process: `process:` -A process with no sensitivity list runs continuously. It is allowed in RT and ED, but **not** in DF. The shorthand `process:` (no arguments) is rewritten by the compiler to `process.forever`. +A process with no sensitivity list runs continuously. It is allowed in RT and ED, but **not** in DF. - **In RT**: `process:` is the [clock-bound FSM process](#rt-domain-clock-bound-fsm-process) described above (steps, waits, etc.). - **In ED**: Use it for testbenches or clock generation (e.g. toggling a clock with `wait`). @@ -155,7 +155,7 @@ A process with no sensitivity list runs continuously. It is allowed in RT and ED ```scala class Testbench extends EDDesign: val clk = Bit <> VAR - process.forever: + process: clk := !clk 5.ns.wait ``` @@ -312,8 +312,8 @@ If you need a purely combinational intermediate inside a clocked process, use a | Domain | Processes | |--------|-----------| | **DF** | No processes. Behavior is expressed with dataflow and `.prev`; the compiler introduces registers and eventually ED processes. | -| **RT** | **Clock-bound FSM process**: `process:` (or `process.forever`) with optional step definitions (`def Name: Step = ...`), `onEntry`/`onExit`, and waits. Compiled to a state register and match logic. Plain RT register code (no process) is also lowered to ED processes by the compiler. | -| **ED** | **Sensitivity-driven**: `process(sig1, sig2, ...)`, `process(all)`, and `process.forever` / `process`. Full control over sensitivity and blocking vs non-blocking assignment. | +| **RT** | **Clock-bound FSM process**: `process:` with optional step definitions (`def Name: Step = ...`), `onEntry`/`onExit`, and waits. Compiled to a state register and match logic. Plain RT register code (no process) is also lowered to ED processes by the compiler. | +| **ED** | **Sensitivity-driven**: `process(sig1, sig2, ...)`, `process(all)`, and `process`. Full control over sensitivity and blocking vs non-blocking assignment. | See [Design Domains][design-domains] for the overall flow from DF → RT → ED and how processes fit into compilation. From ad1b4598fd090153c05b5898afae094a6c36bf50 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Tue, 9 Jun 2026 12:30:22 +0300 Subject: [PATCH 06/72] prepare for Scala 3.9.0 --- .../dfhdl/compiler/analysis/StateAnalysis.scala | 4 ++-- .../ir/src/main/scala/dfhdl/compiler/ir/DB.scala | 2 +- .../dfhdl/compiler/stages/NameRegAliases.scala | 2 +- core/src/main/scala/dfhdl/core/DFDecimal.scala | 3 ++- core/src/main/scala/dfhdl/core/r__For_Plugin.scala | 2 +- lib/src/main/scala/dfhdl/app/DesignArgs.scala | 14 ++++++++------ .../main/scala/dfhdl/tools/toolsCore/Diamond.scala | 2 +- .../dfhdl/tools/toolsCore/GowinDesigner.scala | 2 +- .../scala/dfhdl/tools/toolsCore/QuartusPrime.scala | 2 +- .../main/scala/dfhdl/tools/toolsCore/Tool.scala | 2 +- 10 files changed, 19 insertions(+), 16 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/analysis/StateAnalysis.scala b/compiler/ir/src/main/scala/dfhdl/compiler/analysis/StateAnalysis.scala index d503b7949..888420ec2 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/analysis/StateAnalysis.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/analysis/StateAnalysis.scala @@ -153,7 +153,7 @@ object StateAnalysis: given DFBlock = currentBlock remaining match case (nextBlock: DFBlock) :: rs if nextBlock.getOwnerBlock == currentBlock => // entering child block - val (updatedSet, updatedScopeMap): (Set[DFVal], AssignMap) = nextBlock match + val (updatedSet, updatedScopeMap) = nextBlock match case cb: DFConditional.Block => cb.guardRef.get match case dfVal: DFVal => consumeFrom(dfVal, scopeMap, currentSet) @@ -167,7 +167,7 @@ object StateAnalysis: if r.getOwnerBlock == currentBlock && checkedDomain( currentBlock.getThisOrOwnerDomain.domainType ) => // checking member consumers - val (updatedSet, updatedScopeMap): (Set[DFVal], AssignMap) = r match + val (updatedSet, updatedScopeMap) = r match case net @ DFNet.Assignment(toVal, fromVal) => (consumeFrom(fromVal, scopeMap, currentSet), assignTo(toVal, scopeMap)) case net @ DFNet.Connection(toVal: DFVal, fromVal: DFVal, _) => diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index fcdab73cc..d30c6a7bf 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -823,7 +823,7 @@ final case class DB private ( // that explicitly declares "this group has no reset" — we suppress the default // reset that would otherwise be merged in. val explicitNoRst = userClkOpt.isDefined && userRstOpt.isEmpty - val (baseClkOpt, baseRstOpt): ClkRstTiming = + val (baseClkOpt, baseRstOpt) = if (isDeviceTop) val clk = domainOwner.getTimingConstraintClkRateOpt match case Some(rate) => defaultTag.clk.copy(rate = rate) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/NameRegAliases.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/NameRegAliases.scala index dace065d2..a31ad7196 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/NameRegAliases.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/NameRegAliases.scala @@ -118,7 +118,7 @@ case object NameRegAliases extends HierarchyStage: // we keep in mind that the declaration may have anonymous value // representing the initialized values among them, before the declarations, // so we go from the bottom to the top searching for the first declaration. - val (posMember, addCfg): (DFMember, Patch.Add.Config) = members.view.reverse.dropWhile { + val (posMember, addCfg) = members.view.reverse.dropWhile { case dcl: DFVal.Dcl if dcl.getOwnerDomain == domainOwner => false case _ => true }.headOption match diff --git a/core/src/main/scala/dfhdl/core/DFDecimal.scala b/core/src/main/scala/dfhdl/core/DFDecimal.scala index 9e3348443..6c3d70d2f 100644 --- a/core/src/main/scala/dfhdl/core/DFDecimal.scala +++ b/core/src/main/scala/dfhdl/core/DFDecimal.scala @@ -396,7 +396,7 @@ object DFDecimal: case '{ Some($expr) } => Some(expr.asTerm.tpe) case _ => None val signedForced = opExpr.value.get == "sd" - val (signedTpe, interpWidthTpe, fractionWidthTpe): (TypeRepr, TypeRepr, TypeRepr) = + val (signedTpe, interpWidthTpe, fractionWidthTpe) = fullTerm match case Literal(StringConstant(t)) => fromDecString(t, signedForced) match @@ -1822,6 +1822,7 @@ object DFSInt: .applyDFXInt(lhs, updatedWidth - 1, 0) .asValTP[DFUInt[RW], P] } + end extension extension [P](lhs: DFValTP[DFInt32, P]) @targetName("negateDFInt32") def unary_-(using DFCG): DFValTP[DFInt32, P] = trydf { diff --git a/core/src/main/scala/dfhdl/core/r__For_Plugin.scala b/core/src/main/scala/dfhdl/core/r__For_Plugin.scala index 8c89e8c2e..cd9fbbfec 100644 --- a/core/src/main/scala/dfhdl/core/r__For_Plugin.scala +++ b/core/src/main/scala/dfhdl/core/r__For_Plugin.scala @@ -144,7 +144,7 @@ object r__For_Plugin: val inputs = args.map { (arg, argMeta) => DFVal.Dcl(arg.dfType, Modifier.IN)(using dfc.setMeta(argMeta)) } - val (isDuplicate, ret): (Boolean, V) = + val (isDuplicate, ret) = dfc.mutableDB.DesignContext.runFuncWithInputs(func, inputs) val paramEntries = Design.Inst.collectParamEntries def exitAndConnectInputs() = diff --git a/lib/src/main/scala/dfhdl/app/DesignArgs.scala b/lib/src/main/scala/dfhdl/app/DesignArgs.scala index 5022010ab..3f6428301 100644 --- a/lib/src/main/scala/dfhdl/app/DesignArgs.scala +++ b/lib/src/main/scala/dfhdl/app/DesignArgs.scala @@ -53,11 +53,11 @@ case class DesignArg(name: String, value: Any, desc: String)(using dfc: DFC): val data = value match case dfConst: DFValAny => dfConst.asIR.dfType.runtimeChecked match - case ir.DFBool => dfConst.asConstOf[DFBool].toScalaBoolean - case ir.DFInt32 => dfConst.asConstOf[DFInt32].toScalaInt - case ir.DFDouble => dfConst.asConstOf[DFDouble].toScalaDouble - case ir.DFString => dfConst.asConstOf[DFString].toScalaString - case _ => + case ir.DFBool => dfConst.asConstOf[DFBool].toScalaBoolean + case ir.DFInt32 => dfConst.asConstOf[DFInt32].toScalaInt + case ir.DFDouble => dfConst.asConstOf[DFDouble].toScalaDouble + case ir.DFString => dfConst.asConstOf[DFString].toScalaString + case _ => // Bit / Bits / UInt / SInt: show the DFHDL literal form the user // would type at the CLI. formatDFHDLLiteral(dfConst) @@ -65,6 +65,7 @@ case class DesignArg(name: String, value: Any, desc: String)(using dfc: DFC): data match case bigInt: BigInt => bigInt.toInt case _ => data + end getScalaValue // DFHDL-literal display of the default value for help output. def defaultDisplay: String = @@ -121,6 +122,7 @@ case class DesignArg(name: String, value: Any, desc: String)(using dfc: DFC): case ir.DFSInt(_) => parseDecimalLiteral(updatedScalaValue.toString, dfConst, signedForced = true) case _ => dfConst + end match case _ => updatedScalaValue match case i: Int if value.isInstanceOf[BigInt] => BigInt(i) @@ -164,7 +166,7 @@ case class DesignArg(name: String, value: Any, desc: String)(using dfc: DFC): ) ) case _ => throw new IllegalArgumentException(s"Design argument $name is not a Bits type.") - val (payload, explicitWidth): (String, Option[Int]) = body match + val (payload, explicitWidth) = body match case s"$w'$rest" if w.nonEmpty && w.forall(_.isDigit) => (rest, Some(w.toInt)) case s => (s, None) val dataOrErr = op match diff --git a/lib/src/main/scala/dfhdl/tools/toolsCore/Diamond.scala b/lib/src/main/scala/dfhdl/tools/toolsCore/Diamond.scala index 50474ee79..325dfcf4d 100644 --- a/lib/src/main/scala/dfhdl/tools/toolsCore/Diamond.scala +++ b/lib/src/main/scala/dfhdl/tools/toolsCore/Diamond.scala @@ -69,7 +69,7 @@ class DiamondProjectTclConfigPrinter(using ): val designDB: DB = getSet.designDB val topName: String = getSet.topName - val (part, deviceVersion): (String, String) = + val (part, deviceVersion) = designDB.top.dclMeta.annotations.collectFirst { case annotation: constraints.DeviceID => (annotation.partName, annotation.deviceVersion) }.getOrElse(throw new IllegalArgumentException("No device constraint found")) diff --git a/lib/src/main/scala/dfhdl/tools/toolsCore/GowinDesigner.scala b/lib/src/main/scala/dfhdl/tools/toolsCore/GowinDesigner.scala index 88f00edbe..e49dc7e4e 100644 --- a/lib/src/main/scala/dfhdl/tools/toolsCore/GowinDesigner.scala +++ b/lib/src/main/scala/dfhdl/tools/toolsCore/GowinDesigner.scala @@ -70,7 +70,7 @@ class GowinDesignerProjectTclConfigPrinter(using val targetLanguage: String = co.backend match case _: backends.verilog => "verilog" case _: backends.vhdl => "vhdl" - val (part, deviceVersion): (String, String) = + val (part, deviceVersion) = designDB.top.dclMeta.annotations.collectFirst { case annotation: constraints.DeviceID => (annotation.partName, annotation.deviceVersion) }.getOrElse(throw new IllegalArgumentException("No device constraint found")) diff --git a/lib/src/main/scala/dfhdl/tools/toolsCore/QuartusPrime.scala b/lib/src/main/scala/dfhdl/tools/toolsCore/QuartusPrime.scala index e26d706d0..01e6db30c 100644 --- a/lib/src/main/scala/dfhdl/tools/toolsCore/QuartusPrime.scala +++ b/lib/src/main/scala/dfhdl/tools/toolsCore/QuartusPrime.scala @@ -84,7 +84,7 @@ class QuartusPrimeProjectTclConfigPrinter(using val targetLanguage: String = co.backend match case _: backends.verilog => "verilog" case _: backends.vhdl => "vhdl" - val (part, deviceVersion): (String, String) = + val (part, deviceVersion) = designDB.top.dclMeta.annotations.collectFirst { case annotation: constraints.DeviceID => (annotation.partName, annotation.deviceVersion) }.getOrElse(throw new IllegalArgumentException("No device constraint found")) diff --git a/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala b/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala index cc2c648bd..6c0137dbc 100644 --- a/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala +++ b/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala @@ -31,7 +31,7 @@ trait Tool: Nil protected[dfhdl] def cleanUpBeforeFileRestore()(using MemberGetSet, CompilerOptions): Unit = {} - private[dfhdl] lazy val (runExecFullPath, installedVersion): (String, Option[String]) = + private[dfhdl] lazy val (runExecFullPath, installedVersion) = var runExecFullPathRet: String = "" val installedVersionRet = programFullPaths(runExec).view.flatMap { runExecFullPath => runExecFullPathRet = runExecFullPath From d9a896693f4d263dfeceb70dc9b90a6ee88f9a1a Mon Sep 17 00:00:00 2001 From: Oron Port Date: Wed, 10 Jun 2026 02:05:45 +0300 Subject: [PATCH 07/72] improve error positioning and messaging in more nested error cases --- core/src/main/scala/dfhdl/core/DFVal.scala | 187 +++++++------- .../internals/ControlledMacroError.scala | 11 +- .../main/scala/dfhdl/internals/Exact.scala | 238 +++++++++--------- .../src/main/scala/plugin/PreTyperPhase.scala | 8 +- 4 files changed, 235 insertions(+), 209 deletions(-) diff --git a/core/src/main/scala/dfhdl/core/DFVal.scala b/core/src/main/scala/dfhdl/core/DFVal.scala index 76f8bc51c..76960f856 100644 --- a/core/src/main/scala/dfhdl/core/DFVal.scala +++ b/core/src/main/scala/dfhdl/core/DFVal.scala @@ -149,19 +149,21 @@ def DFValConversionMacro[T <: DFTypeAny, P, R]( from: Expr[R] )(dfc: Expr[DFCG])(using Quotes, Type[T], Type[P], Type[R]): Expr[DFValTP[T, P]] = import quotes.reflect.* - val fromExactInfo = from.exactInfo - lazy val nonConstTermOpt = from.asTerm.getNonConstTerm - if (TypeRepr.of[P] =:= TypeRepr.of[CONST] && nonConstTermOpt.nonEmpty) - nonConstTermOpt.get.compiletimeErrorPosExpr("Applied argument must be a constant.") - else - val tStr = Expr(s"implicit conversion to type ${TypeRepr.of[T].showDFType}") - '{ - val tc = compiletime.summonInline[DFVal.TCConv[T, fromExactInfo.Underlying]] - trydf { - tc(${ fromExactInfo.exactExpr })(using $dfc).asValTP[T, P] - }(using $dfc, CTName($tStr)) - } - end if + from.mapExprOrError { from => + val fromExactInfo = from.exactInfo + lazy val nonConstTermOpt = from.asTerm.getNonConstTerm + if (TypeRepr.of[P] =:= TypeRepr.of[CONST] && nonConstTermOpt.nonEmpty) + nonConstTermOpt.get.compiletimeErrorPosExpr("Applied argument must be a constant.") + else + val tStr = Expr(s"implicit conversion to type ${TypeRepr.of[T].showDFType}") + '{ + val tc = compiletime.summonInline[DFVal.TCConv[T, fromExactInfo.Underlying]] + trydf { + tc(${ fromExactInfo.exactExpr })(using $dfc).asValTP[T, P] + }(using $dfc, CTName($tStr)) + } + end if + } end DFValConversionMacro sealed protected trait DFValLP: @@ -431,20 +433,22 @@ object DFVal extends DFValLP: value: Expr[V] )(using Quotes, Type[T], Type[V]): Expr[InitValue[T]] = import quotes.reflect.* - val (argExpr, enableExpr) = value match - case '{ Conditional.Ops.@@[t]($x)($y) } => (x, y) - case _ => (value, '{ true }) - argExpr.asTerm.getNonConstTerm match - case Some(term) => term.compiletimeErrorPosExpr("Init value must be a constant.") - case None => - val exactInfo = argExpr.exactInfo - '{ - val tc = compiletime.summonInline[DFVal.TC[T, exactInfo.Underlying]] - new InitValue[T]: - def enable: Boolean = $enableExpr - def apply(dfType: T)(using dfc: DFC): DFConstOf[T] = - tc(dfType, ${ exactInfo.exactExpr })(using dfc).asConstOf[T] - } + value.mapExprOrError { value => + val (argExpr, enableExpr) = value match + case '{ Conditional.Ops.@@[t]($x)($y) } => (x, y) + case _ => (value, '{ true }) + argExpr.asTerm.getNonConstTerm match + case Some(term) => term.compiletimeErrorPosExpr("Init value must be a constant.") + case None => + val exactInfo = argExpr.exactInfo + '{ + val tc = compiletime.summonInline[DFVal.TC[T, exactInfo.Underlying]] + new InitValue[T]: + def enable: Boolean = $enableExpr + def apply(dfType: T)(using dfc: DFC): DFConstOf[T] = + tc(dfType, ${ exactInfo.exactExpr })(using dfc).asConstOf[T] + } + } end fromValueMacro end InitValue @@ -461,70 +465,75 @@ object DFVal extends DFValLP: value: Expr[V] )(using Quotes, Type[T], Type[V]): Expr[InitTupleValues[T]] = import quotes.reflect.* - val (argExpr, enableExpr) = value match - case '{ Conditional.Ops.@@[t]($x)($y) } => (x, y) - case _ => (value, '{ true }) - val term = argExpr.asTerm.underlyingArgument - term.getNonConstTerm match - case Some(term) => term.compiletimeErrorPosExpr("Init value must be a constant.") - case _ => - val tTpe = TypeRepr.of[T] - extension (lhs: TypeRepr) - def tupleSigMatch( - rhs: TypeRepr, - tupleAndNonTupleMatch: Boolean - ): Boolean = - import quotes.reflect.* - (lhs.asType, rhs.asType) match - case ('[DFTuple[t]], '[Any]) => - TypeRepr.of[t].tupleSigMatch(rhs, tupleAndNonTupleMatch) - case ('[Tuple], '[Tuple]) => - val lArgs = lhs.getTupleArgs - val rArgs = rhs.getTupleArgs - if (lArgs.length != rArgs.length) false - else - (lArgs lazyZip rArgs).forall((l, r) => l.tupleSigMatch(r, true)) - case ('[Tuple], '[Any]) => tupleAndNonTupleMatch - case ('[Any], '[Tuple]) => tupleAndNonTupleMatch - case _ => true - end tupleSigMatch - end extension - - val vTpe = term.tpe - val multiElements = vTpe.asTypeOf[Any] match - case '[NonEmptyTuple] => - vTpe.getTupleArgs.forall(va => tTpe.tupleSigMatch(va, false)) - case _ => false - // In the case we have a multiple elements in the tuple value that match the signature - // of the DFHDL type, then each element is considered as a candidate - if (multiElements) - val Apply(_, vArgsTerm) = term.runtimeChecked - def inits(dfType: Expr[DFTuple[T]], dfc: Expr[DFC]): List[Expr[DFConstOf[DFTuple[T]]]] = - vArgsTerm.map { a => - val aExactInfo = a.exactInfo - '{ - val tc = compiletime.summonInline[DFVal.TC[DFTuple[T], aExactInfo.Underlying]] - tc($dfType, ${ aExactInfo.exactExpr })(using $dfc).asConstOf[DFTuple[T]] + value.mapExprOrError { value => + val (argExpr, enableExpr) = value match + case '{ Conditional.Ops.@@[t]($x)($y) } => (x, y) + case _ => (value, '{ true }) + val term = argExpr.asTerm.underlyingArgument + term.getNonConstTerm match + case Some(term) => term.compiletimeErrorPosExpr("Init value must be a constant.") + case _ => + val tTpe = TypeRepr.of[T] + extension (lhs: TypeRepr) + def tupleSigMatch( + rhs: TypeRepr, + tupleAndNonTupleMatch: Boolean + ): Boolean = + import quotes.reflect.* + (lhs.asType, rhs.asType) match + case ('[DFTuple[t]], '[Any]) => + TypeRepr.of[t].tupleSigMatch(rhs, tupleAndNonTupleMatch) + case ('[Tuple], '[Tuple]) => + val lArgs = lhs.getTupleArgs + val rArgs = rhs.getTupleArgs + if (lArgs.length != rArgs.length) false + else + (lArgs lazyZip rArgs).forall((l, r) => l.tupleSigMatch(r, true)) + case ('[Tuple], '[Any]) => tupleAndNonTupleMatch + case ('[Any], '[Tuple]) => tupleAndNonTupleMatch + case _ => true + end tupleSigMatch + end extension + + val vTpe = term.tpe + val multiElements = vTpe.asTypeOf[Any] match + case '[NonEmptyTuple] => + vTpe.getTupleArgs.forall(va => tTpe.tupleSigMatch(va, false)) + case _ => false + // In the case we have a multiple elements in the tuple value that match the signature + // of the DFHDL type, then each element is considered as a candidate + if (multiElements) + val Apply(_, vArgsTerm) = term.runtimeChecked + def inits( + dfType: Expr[DFTuple[T]], + dfc: Expr[DFC] + ): List[Expr[DFConstOf[DFTuple[T]]]] = + vArgsTerm.map { a => + val aExactInfo = a.exactInfo + '{ + val tc = compiletime.summonInline[DFVal.TC[DFTuple[T], aExactInfo.Underlying]] + tc($dfType, ${ aExactInfo.exactExpr })(using $dfc).asConstOf[DFTuple[T]] + } } + '{ + new InitTupleValues[T]: + def enable: Boolean = $enableExpr + def apply(dfType: DFTuple[T])(using dfc: DFC): List[DFConstOf[DFTuple[T]]] = + List(${ Expr.ofList(inits('dfType, 'dfc)) }*) } - '{ - new InitTupleValues[T]: - def enable: Boolean = $enableExpr - def apply(dfType: DFTuple[T])(using dfc: DFC): List[DFConstOf[DFTuple[T]]] = - List(${ Expr.ofList(inits('dfType, 'dfc)) }*) - } - // otherwise the entire tuple is considered as a candidate. - else - val vExactInfo = term.exactInfo - '{ - val tc = compiletime.summonInline[DFVal.TC[DFTuple[T], vExactInfo.Underlying]] - new InitTupleValues[T]: - def enable: Boolean = $enableExpr - def apply(dfType: DFTuple[T])(using dfc: DFC): List[DFConstOf[DFTuple[T]]] = - List(tc(dfType, ${ vExactInfo.exactExpr })(using dfc).asConstOf[DFTuple[T]]) - } - end if - end match + // otherwise the entire tuple is considered as a candidate. + else + val vExactInfo = term.exactInfo + '{ + val tc = compiletime.summonInline[DFVal.TC[DFTuple[T], vExactInfo.Underlying]] + new InitTupleValues[T]: + def enable: Boolean = $enableExpr + def apply(dfType: DFTuple[T])(using dfc: DFC): List[DFConstOf[DFTuple[T]]] = + List(tc(dfType, ${ vExactInfo.exactExpr })(using dfc).asConstOf[DFTuple[T]]) + } + end if + end match + } end fromValueMacro end InitTupleValues diff --git a/internals/src/main/scala/dfhdl/internals/ControlledMacroError.scala b/internals/src/main/scala/dfhdl/internals/ControlledMacroError.scala index 9a21417d5..f2a56cb92 100644 --- a/internals/src/main/scala/dfhdl/internals/ControlledMacroError.scala +++ b/internals/src/main/scala/dfhdl/internals/ControlledMacroError.scala @@ -50,14 +50,17 @@ object ControlledMacroError: Some(msg) case Apply(fun, args) => searchList(fun :: args) case _ => None - if (getLastCompiletimeError.nonEmpty) search(expr.asTerm) - else None + search(expr.asTerm) end getLastErrorInExpr end ControlledMacroError extension [T](expr: Expr[T])(using Quotes, Type[T]) def mapExprOrError[R](f: Expr[T] => Expr[R]): Expr[R] = + import quotes.reflect.* ControlledMacroError.getLastErrorInExpr(expr) match - case Some(msg) => '{ compiletime.error(${ Expr(msg) }) } - case None => f(expr) + case Some(msg) => + if (ControlledMacroError.getLastCompiletimeError.nonEmpty) + '{ compiletime.error(${ Expr(msg) }) } + else report.errorAndAbort(msg, expr.asTerm.underlying.pos) + case None => f(expr) end mapExprOrError diff --git a/internals/src/main/scala/dfhdl/internals/Exact.scala b/internals/src/main/scala/dfhdl/internals/Exact.scala index f5b763c78..96fa0502a 100644 --- a/internals/src/main/scala/dfhdl/internals/Exact.scala +++ b/internals/src/main/scala/dfhdl/internals/Exact.scala @@ -314,20 +314,22 @@ private def exactOp1Macro[Op, Ctx, OutUB](lhs: Expr[Any])(ctx: Expr[Ctx])(using Type[OutUB] ): Expr[OutUB] = import quotes.reflect.* - val lhsExactInfo = lhs.exactInfo - val (lhsBindings, lhsInner) = flattenInlined(lhsExactInfo.exactExpr.asTerm) - Expr.summon[ExactOp1[Op, Ctx, OutUB, lhsExactInfo.Underlying]] match - case Some(expr) => - val appTerm = ascribeWidenedType('{ $expr(${ lhsInner.asExpr })(using $ctx) }.asTerm) - if lhsBindings.isEmpty then appTerm.asExprOf[OutUB] - else - val innerTerm = appTerm match - case Inlined(_, Nil, inner) => inner - case t => t - Block(lhsBindings, innerTerm).asExprOf[OutUB] - case None => - ControlledMacroError.report("Unsupported argument type for this operation.") - end match + lhs.mapExprOrError { lhs => + val lhsExactInfo = lhs.exactInfo + val (lhsBindings, lhsInner) = flattenInlined(lhsExactInfo.exactExpr.asTerm) + Expr.summon[ExactOp1[Op, Ctx, OutUB, lhsExactInfo.Underlying]] match + case Some(expr) => + val appTerm = ascribeWidenedType('{ $expr(${ lhsInner.asExpr })(using $ctx) }.asTerm) + if lhsBindings.isEmpty then appTerm.asExprOf[OutUB] + else + val innerTerm = appTerm match + case Inlined(_, Nil, inner) => inner + case t => t + Block(lhsBindings, innerTerm).asExprOf[OutUB] + case None => + ControlledMacroError.report("Unsupported argument type for this operation.") + end match + } end exactOp1Macro ///////////////////////////////////////////////////////////////////////////////// @@ -357,74 +359,78 @@ private def exactOp2Macro[Op, Ctx, OutUB]( Type[OutUB] ): Expr[OutUB] = import quotes.reflect.* - val lhsExactInfo = lhs.exactInfo - val rhsExactInfo = rhs.exactInfo - val exactOp2ExprOrError = - try - Expr.summonOrError[ExactOp2[ - Op, - Ctx, - OutUB, - lhsExactInfo.Underlying, - rhsExactInfo.Underlying - ]] - catch - // TODO: this is a workaround for a Scala compiler bug that is not minimized yet. - // It throws an exception which somehow disappears when we widen the types and run show. - // Regression test is in platforms/src/test/scala/PlatformSpec.scala - case e: Throwable => - lhsExactInfo.exactTpe.widen.show - rhsExactInfo.exactTpe.widen.show - Expr.summonOrError[ExactOp2[ - Op, - Ctx, - OutUB, - lhsExactInfo.Underlying, - rhsExactInfo.Underlying - ]] - end try - end exactOp2ExprOrError - def buildFlattened(lhsTerm: Term, rhsTerm: Term, expr: Expr[?]): Expr[OutUB] = - val (lhsBindings, lhsInner) = flattenInlined(lhsTerm) - val (rhsBindings, rhsInner) = flattenInlined(rhsTerm) - val allBindings = lhsBindings ++ rhsBindings - val appTerm = ascribeWidenedType('{ - ${ - expr.asInstanceOf[Expr[ExactOp2[ - Op, - Ctx, - OutUB, - lhsExactInfo.Underlying, - rhsExactInfo.Underlying - ]]] - }(${ lhsInner.asExpr }, ${ rhsInner.asExpr })(using $ctx) - }.asTerm) - if allBindings.isEmpty then appTerm.asExprOf[OutUB] - else - val innerTerm = appTerm match - case Inlined(_, Nil, inner) => inner - case t => t - Block(allBindings, innerTerm).asExprOf[OutUB] - end buildFlattened - exactOp2ExprOrError match - case Right(expr) => - buildFlattened(lhsExactInfo.exactExpr.asTerm, rhsExactInfo.exactExpr.asTerm, expr) - case Left(msg) => - if (bothWays.value.getOrElse(false)) - Expr.summonOrError[ExactOp2[ - Op, - Ctx, - OutUB, - rhsExactInfo.Underlying, - lhsExactInfo.Underlying - ]] match - case Right(expr) => - buildFlattened(rhsExactInfo.exactExpr.asTerm, lhsExactInfo.exactExpr.asTerm, expr) - case Left(msg) => + lhs.mapExprOrError { lhs => + rhs.mapExprOrError { rhs => + val lhsExactInfo = lhs.exactInfo + val rhsExactInfo = rhs.exactInfo + val exactOp2ExprOrError = + try + Expr.summonOrError[ExactOp2[ + Op, + Ctx, + OutUB, + lhsExactInfo.Underlying, + rhsExactInfo.Underlying + ]] + catch + // TODO: this is a workaround for a Scala compiler bug that is not minimized yet. + // It throws an exception which somehow disappears when we widen the types and run show. + // Regression test is in platforms/src/test/scala/PlatformSpec.scala + case e: Throwable => + lhsExactInfo.exactTpe.widen.show + rhsExactInfo.exactTpe.widen.show + Expr.summonOrError[ExactOp2[ + Op, + Ctx, + OutUB, + lhsExactInfo.Underlying, + rhsExactInfo.Underlying + ]] + end try + end exactOp2ExprOrError + def buildFlattened(lhsTerm: Term, rhsTerm: Term, expr: Expr[?]): Expr[OutUB] = + val (lhsBindings, lhsInner) = flattenInlined(lhsTerm) + val (rhsBindings, rhsInner) = flattenInlined(rhsTerm) + val allBindings = lhsBindings ++ rhsBindings + val appTerm = ascribeWidenedType('{ + ${ + expr.asInstanceOf[Expr[ExactOp2[ + Op, + Ctx, + OutUB, + lhsExactInfo.Underlying, + rhsExactInfo.Underlying + ]]] + }(${ lhsInner.asExpr }, ${ rhsInner.asExpr })(using $ctx) + }.asTerm) + if allBindings.isEmpty then appTerm.asExprOf[OutUB] + else + val innerTerm = appTerm match + case Inlined(_, Nil, inner) => inner + case t => t + Block(allBindings, innerTerm).asExprOf[OutUB] + end buildFlattened + exactOp2ExprOrError match + case Right(expr) => + buildFlattened(lhsExactInfo.exactExpr.asTerm, rhsExactInfo.exactExpr.asTerm, expr) + case Left(msg) => + if (bothWays.value.getOrElse(false)) + Expr.summonOrError[ExactOp2[ + Op, + Ctx, + OutUB, + rhsExactInfo.Underlying, + lhsExactInfo.Underlying + ]] match + case Right(expr) => + buildFlattened(rhsExactInfo.exactExpr.asTerm, lhsExactInfo.exactExpr.asTerm, expr) + case Left(msg) => + ControlledMacroError.report("Unsupported argument types for this operation.") + else ControlledMacroError.report("Unsupported argument types for this operation.") - else - ControlledMacroError.report("Unsupported argument types for this operation.") - end match + end match + } + } end exactOp2Macro ///////////////////////////////////////////////////////////////////////////////// @@ -452,36 +458,42 @@ private def exactOp3Macro[Op, Ctx, OutUB]( Type[OutUB] ): Expr[OutUB] = import quotes.reflect.* - val lhsExactInfo = lhs.exactInfo - val mhsExactInfo = mhs.exactInfo - val rhsExactInfo = rhs.exactInfo - val (lhsBindings, lhsInner) = flattenInlined(lhsExactInfo.exactExpr.asTerm) - val (mhsBindings, mhsInner) = flattenInlined(mhsExactInfo.exactExpr.asTerm) - val (rhsBindings, rhsInner) = flattenInlined(rhsExactInfo.exactExpr.asTerm) - val allBindings = lhsBindings ++ mhsBindings ++ rhsBindings - Expr.summon[ExactOp3[ - Op, - Ctx, - OutUB, - lhsExactInfo.Underlying, - mhsExactInfo.Underlying, - rhsExactInfo.Underlying - ]] match - case Some(expr) => - val appTerm = ascribeWidenedType('{ - $expr( - ${ lhsInner.asExpr }, - ${ mhsInner.asExpr }, - ${ rhsInner.asExpr } - )(using $ctx) - }.asTerm) - if allBindings.isEmpty then appTerm.asExprOf[OutUB] - else - val innerTerm = appTerm match - case Inlined(_, Nil, inner) => inner - case t => t - Block(allBindings, innerTerm).asExprOf[OutUB] - case None => - ControlledMacroError.report("Unsupported argument types for this operation.") - end match + lhs.mapExprOrError { lhs => + mhs.mapExprOrError { mhs => + rhs.mapExprOrError { rhs => + val lhsExactInfo = lhs.exactInfo + val mhsExactInfo = mhs.exactInfo + val rhsExactInfo = rhs.exactInfo + val (lhsBindings, lhsInner) = flattenInlined(lhsExactInfo.exactExpr.asTerm) + val (mhsBindings, mhsInner) = flattenInlined(mhsExactInfo.exactExpr.asTerm) + val (rhsBindings, rhsInner) = flattenInlined(rhsExactInfo.exactExpr.asTerm) + val allBindings = lhsBindings ++ mhsBindings ++ rhsBindings + Expr.summon[ExactOp3[ + Op, + Ctx, + OutUB, + lhsExactInfo.Underlying, + mhsExactInfo.Underlying, + rhsExactInfo.Underlying + ]] match + case Some(expr) => + val appTerm = ascribeWidenedType('{ + $expr( + ${ lhsInner.asExpr }, + ${ mhsInner.asExpr }, + ${ rhsInner.asExpr } + )(using $ctx) + }.asTerm) + if allBindings.isEmpty then appTerm.asExprOf[OutUB] + else + val innerTerm = appTerm match + case Inlined(_, Nil, inner) => inner + case t => t + Block(allBindings, innerTerm).asExprOf[OutUB] + case None => + ControlledMacroError.report("Unsupported argument types for this operation.") + end match + } + } + } end exactOp3Macro diff --git a/plugin/src/main/scala/plugin/PreTyperPhase.scala b/plugin/src/main/scala/plugin/PreTyperPhase.scala index 1a32b020e..4b617d276 100644 --- a/plugin/src/main/scala/plugin/PreTyperPhase.scala +++ b/plugin/src/main/scala/plugin/PreTyperPhase.scala @@ -29,7 +29,8 @@ class CustomReporter( override def flush()(using ctx: Context): Unit = orig.flush() override def doReport(dia: Diagnostic)(using ctx: Context): Unit = val updatedMsg = dia.msg.toString - val updatedDia = Diagnostic(dia.msg.mapMsg(x => updatedMsg), dia.pos, dia.level) + val diaPos = dia.pos.copy(outer = null) // disable inline stack error printing + val updatedDia = Diagnostic(dia.msg.mapMsg(x => updatedMsg), diaPos, dia.level) orig.doReport(updatedDia) end doReport end CustomReporter @@ -193,14 +194,15 @@ class PreTyperPhase(setting: Setting) extends CommonPhase: def apply(x: Boolean, tree: Tree)(using Context): Boolean = if (x) true else tree match - case InfixOp(_, Ident(op), _) if op.toString == "<>" => true + case InfixOp(_, Ident(op), _) if op.toString == "<>" => true case _: Template | _: DefDef | _: Function | _: Block | _: TypeDef => x - case _ => foldOver(x, tree) + case _ => foldOver(x, tree) body.exists { case vd: ValDef if !vd.rhs.isEmpty => acc(false, vd.rhs) case InfixOp(_, Ident(op), _) if op.toString == "<>" => true case _ => false } + end bodyUsesConnect private def hasDfhdlWildcardImport(stats: List[Tree]): Boolean = stats.exists { From 2fef908995f839e28d546de995f0d431c1f5c91d Mon Sep 17 00:00:00 2001 From: Oron Port Date: Wed, 10 Jun 2026 02:42:09 +0300 Subject: [PATCH 08/72] add checks for error positioning --- .../test/scala/CoreSpec/DFDecimalSpec.scala | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/core/src/test/scala/CoreSpec/DFDecimalSpec.scala b/core/src/test/scala/CoreSpec/DFDecimalSpec.scala index 5f6a31af8..e52d7bab5 100644 --- a/core/src/test/scala/CoreSpec/DFDecimalSpec.scala +++ b/core/src/test/scala/CoreSpec/DFDecimalSpec.scala @@ -1076,12 +1076,32 @@ class DFDecimalSpec extends DFSpec: s"Error message should not contain ExactOp2Aux projection types:\n$allMessages" ) } - // TODO: there is a problem in position error. need to minimize and report to Scala bug tracker. - // test("Error positions") { - // val cnt = Bits[8] <> VAR - // assertCompileErrorPos( - // "The wildcard `Int` value width (14) is larger than the bit-accurate value width (8).", - // 0 - // )("""val x = cnt := cnt + 10000""") - // } + + test("Error positions") { + val cnt = Bits[8] <> VAR + val err1 = compiletime.testing.typeCheckErrors("cnt := cnt + 10000").last + val err2 = compiletime.testing.typeCheckErrors("cnt := cnt + (cnt + 10000)").last + val err3 = compiletime.testing.typeCheckErrors("cnt := cnt + 10000 + cnt").last + val err4 = compiletime.testing.typeCheckErrors("val x: Bits[8] <> VAL = cnt + 10000").last + assertEquals( + err1.message, + "The wildcard `Int` value width (14) is larger than the bit-accurate value width (8)." + ) + assertEquals(err1.column, 7) + assertEquals( + err2.message, + "The wildcard `Int` value width (14) is larger than the bit-accurate value width (8)." + ) + assertEquals(err2.column, 14) + assertEquals( + err3.message, + "The wildcard `Int` value width (14) is larger than the bit-accurate value width (8)." + ) + assertEquals(err3.column, 7) + assertEquals( + err4.message, + "The wildcard `Int` value width (14) is larger than the bit-accurate value width (8)." + ) + assertEquals(err4.column, 24) + } end DFDecimalSpec From 6933ade35bd4798201bb79cde4c624377d3cebbe Mon Sep 17 00:00:00 2001 From: Oron Port Date: Thu, 11 Jun 2026 02:36:55 +0300 Subject: [PATCH 09/72] fix runtime positions --- .../test/scala/CoreSpec/DFDecimalSpec.scala | 10 +++++ core/src/test/scala/DFSpec.scala | 17 +++++++- .../scala/plugin/MetaContextGenPhase.scala | 40 ++++++++++++++++++- 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/core/src/test/scala/CoreSpec/DFDecimalSpec.scala b/core/src/test/scala/CoreSpec/DFDecimalSpec.scala index e52d7bab5..aeca28321 100644 --- a/core/src/test/scala/CoreSpec/DFDecimalSpec.scala +++ b/core/src/test/scala/CoreSpec/DFDecimalSpec.scala @@ -1104,4 +1104,14 @@ class DFDecimalSpec extends DFSpec: ) assertEquals(err4.column, 24) } + test("Runtime error positions") { + val cnt = Bits[8] <> VAR + val arg = 10000 + val errMsg = + "Wildcard `Int` value width (14) is larger than the bit-accurate value width (8)." + assertRuntimeErrorLog(errMsg, 43, 59)(cnt := cnt + arg) + assertRuntimeErrorLog(errMsg, 43, 67)(cnt := cnt + (cnt + arg)) + assertRuntimeErrorLog(errMsg, 43, 65)(cnt := cnt + arg + cnt) + assertRuntimeErrorLog(errMsg, 69, 78) { val x: Bits[8] <> VAL = cnt + arg } + } end DFDecimalSpec diff --git a/core/src/test/scala/DFSpec.scala b/core/src/test/scala/DFSpec.scala index 0f9c132c0..d2abfdb37 100644 --- a/core/src/test/scala/DFSpec.scala +++ b/core/src/test/scala/DFSpec.scala @@ -33,14 +33,27 @@ abstract class DFSpec extends NoDFCSpec, HasTypeName, HasDFC: dfc.enterOwner(owner) private val noErrMsg = "No error found" - inline def assertRuntimeErrorLog(expectedErr: String)(runTimeCode: => Unit): Unit = + inline def assertRuntimeErrorLog( + expectedErr: String, + colStart: Int = -1, + colEnd: Int = -1 + )(runTimeCode: => Unit): Unit = val currentEvents = dfc.getEvents dfc.clearEvents() runTimeCode - val err = dfc.getErrors.headOption.map(_.dfMsg).getOrElse(noErrMsg) + val errOpt = dfc.getErrors.headOption + val err = errOpt.map(_.dfMsg).getOrElse(noErrMsg) dfc.clearEvents() dfc.injectEvents(currentEvents) assertNoDiff(err, expectedErr) + // optionally also verify the error position's column span + if (colStart >= 0 || colEnd >= 0) + val position = errOpt match + case Some(err: core.DFError.Basic) => err.position + case _ => Position.unknown + assertEquals(position.columnStart, colStart) + assertEquals(position.columnEnd, colEnd) + end assertRuntimeErrorLog def assertEquals[T <: DFType, L <: DFConstOf[T], R <: DFConstOf[T]](l: L, r: R)(using DFC): Unit = assert((l == r).toScalaBoolean) diff --git a/plugin/src/main/scala/plugin/MetaContextGenPhase.scala b/plugin/src/main/scala/plugin/MetaContextGenPhase.scala index ff6b10b43..659edaa77 100755 --- a/plugin/src/main/scala/plugin/MetaContextGenPhase.scala +++ b/plugin/src/main/scala/plugin/MetaContextGenPhase.scala @@ -90,6 +90,18 @@ class MetaContextGenPhase(setting: Setting) extends CommonPhase: end setMeta end extension + extension (tree: Tree)(using Context) + // A context argument that is a synthetic inline proxy (e.g. `x$3$proxy1`) + // generated by macro inlining. Such proxies already wrap a positioned context. + // The reference is often wrapped in `Inlined`/`Typed` nodes (whose own symbol is + // NoSymbol), so we strip the wrappers to reach the proxy reference and test its + // `InlineProxy` flag. + def isProxyContext: Boolean = + tree match + case Inlined(_, _, expansion) => expansion.isProxyContext + case Typed(expr, _) => expr.isProxyContext + case _ => tree.symbol.is(InlineProxy) + extension (sym: Symbol) def fixedFullName(using Context): String = sym.fullName.toString.replace("._$", ".") @@ -163,7 +175,24 @@ class MetaContextGenPhase(setting: Setting) extends CommonPhase: case Some(ownerTree, srcPos) => getMetaInfo(ownerTree, srcPos) match case Some(metaInfo) => - fixedApply.replaceArg(argTree, argTree.setMeta(metaInfo)) + // When an anonymous (nameless) meta is stamped onto a synthetic proxy + // context (e.g. `x$N$proxyM`) produced by macro inlining, and its only + // position leaks outside the compilation unit (e.g. into the macro's own + // internals/Exact.scala), that position is bogus for user-facing reporting. + // We override it with the nearest enclosing user-code position when one + // exists. The `setMeta` itself must stay (it preserves value grouping), so + // if no better position is found we keep the original one unchanged. + val pos = + if ( + metaInfo.nameOpt.isEmpty && argTree.isProxyContext && + metaInfo.srcPos.startPos.source != ctx.compilationUnit.source + ) + enclosingUserSrcPos.getOrElse(metaInfo.srcPos) + else metaInfo.srcPos + fixedApply.replaceArg( + argTree, + argTree.setMeta(metaInfo.nameOpt, pos, metaInfo.docOpt, metaInfo.annotations) + ) case None => val sym = argTree.symbol contextDefs.get(sym.fixedFullName) match @@ -188,6 +217,15 @@ class MetaContextGenPhase(setting: Setting) extends CommonPhase: else fixedApply end transformApply + // The nearest enclosing apply (currently on the stack) whose position lies in the + // compilation unit being compiled. Used to recover a user-code position when a + // macro-synthesized apply only carries macro-internal positions. + private def enclosingUserSrcPos(using Context): Option[util.SrcPos] = + applyStack.collectFirst { + case a if a.span.exists && a.srcPos.startPos.source == ctx.compilationUnit.source => + a.srcPos + } + override def prepareForBlock(tree: Block)(using Context): Context = tree.stats match case _ :+ (cls @ TypeDef(_, template: Template)) if cls.symbol.isAnonymousClass => From 7970d2747afc7d01f71647f5a9aacb93dede519b Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 13 Jun 2026 15:04:32 +0300 Subject: [PATCH 10/72] Migrate RT clk/rst domain analyses and 3 stages to hierarchical DB Add hierarchical (root-DB) clones of the cross-design RT clk/rst analyses that previously required the flat DB, computed via design-tree navigation (subDBs.get(ownerRef) down, parentSubDBOpt up) with no global member index: new_relatedAnnotMap, new_dependentRTDomainOwners, new_resolvedClkRstMap, new_isDependentOn. Gated against the flat versions by new_clkRstEquivalenceCheck (run from SanityCheck): flat XYZ == oldToNew.new_XYZ across the test corpus. Migrate three stages off the flat DB using these clones: - ExplicitClkRstCfg: drop the rebindGetSet=false hack - AddClkRst: flat Stage -> GlobalStage (per-sub-DB patching; pre-pass for cross-design clk/rst OUT tracking) - ToED: flat Stage -> HierarchyStage Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 401 ++++++++++++++++ .../dfhdl/compiler/stages/AddClkRst.scala | 446 ++++++++++-------- .../compiler/stages/ExplicitClkRstCfg.scala | 14 +- .../dfhdl/compiler/stages/SanityCheck.scala | 3 + .../scala/dfhdl/compiler/stages/ToED.scala | 28 +- 5 files changed, 684 insertions(+), 208 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index d30c6a7bf..a50786edb 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -957,6 +957,407 @@ final case class DB private ( fillDomainMap(rtDomainOwners, Nil, domainMap) domainMap.toMap + // =========================================================================== + // New-style (hierarchical) clones of the RT clk/rst domain analyses. They are + // invoked on the new-style ROOT DB and mirror the flat versions above, but + // route every ref resolution / per-design table lookup to the OWNING sub-DB's + // getSet (the root getSet throws). On sub-DBs and old-style flat DBs they + // delegate to the root. Gated against the flat versions by + // `new_clkRstEquivalenceCheck` (run from SanityCheck) until the consuming + // stages (ExplicitClkRstCfg, AddClkRst) are migrated, after which the flat + // versions are dropped and these lose the `new_` prefix. + // =========================================================================== + + // Cross-design reaches navigate the DESIGN TREE (no global member index): + // `subDBs.get(d.ownerRef)` goes DOWN to a child design's sub-DB; + // `subDB.parentSubDBOpt` goes UP to the parent sub-DB where that design is + // instantiated. Each new_* analysis processes one sub-DB's own members at a + // time, under that sub-DB's getSet, hopping between neighbors via the tree. + + lazy val new_relatedAnnotMap: Map[DFDomainOwner, DFDomainOwner] = + if (!isRoot) rootDB.new_relatedAnnotMap + else + subDBs.view.values.flatMap { sub => + sub.atGetSet { + sub.domainOwnerMemberList.view.flatMap { (owner, _) => + owner.meta.annotations.collectFirst { + case rel: constraints.Timing.Related => rel.ref.get + }.map(owner -> _) + } + } + }.toMap + + // Resolves a PortByNameSelect (living in `ctxSub`) to its underlying Dcl by + // navigating DOWN to the targeted child design's sub-DB and walking its + // namedOwnerMemberTable there. Returns the port with the child sub-DB that + // owns it, so callers can resolve the port's domain in the right getSet. + private def new_pbnsToPort( + pbns: DFVal.PortByNameSelect, + ctxSub: DB + ): Option[(DFVal.Dcl, DB)] = + val childDesign = ctxSub.atGetSet(pbns.designInstRef.get.getDesignBlock) + subDBs.get(childDesign.ownerRef).flatMap { childSub => + childSub.atGetSet { + val pathParts = pbns.portNamePath.split('.').toList + @tailrec def walk(owner: DFOwnerNamed, parts: List[String]): Option[DFMember] = + parts match + case Nil => None + case head :: rest => + childSub.namedOwnerMemberTable.getOrElse(owner, Nil).collectFirst { + case n: DFMember.Named if !n.isAnonymous && n.getName == head => n + } match + case Some(m) if rest.isEmpty => Some(m) + case Some(o: DFOwnerNamed) => walk(o, rest) + case _ => None + walk(childDesign, pathParts) match + case Some(dcl: DFVal.Dcl) => Some(dcl -> childSub) + case _ => None + } + } + end new_pbnsToPort + + // The domain that encloses `design`'s instantiation: navigate UP to `design`'s + // parent sub-DB and read the owning domain of `design`'s inst there. Returns + // the domain owner with the parent sub-DB that owns it. + private def new_designEnclosingDomain(design: DFDesignBlock): Option[(DFDomainOwner, DB)] = + subDBs.get(design.ownerRef).flatMap(_.parentSubDBOpt).flatMap { parentSub => + parentSub.atGetSet { + parentSub.membersNoGlobals.view.collectFirst { + case inst: DFDesignInst if inst.getDesignBlock eq design => inst.getOwnerDomain + } + }.map(_ -> parentSub) + } + + // Routed clone of the local `getRTOwnerOption` from `dependentRTDomainOwners`. + // `member` lives in `ctxSub`; returns its RT domain owner with the sub-DB that + // owns that domain (None if the resolved domain is not RT). + private def new_getRTOwnerWithSub( + member: DFMember, + ctxSub: DB + ): Option[(DFDomainOwner, DB)] = + val ownerAndSub: Option[(DFDomainOwner, DB)] = member match + case design: DFDesignBlock => new_designEnclosingDomain(design) + case pbns: DFVal.PortByNameSelect => + new_pbnsToPort(pbns, ctxSub) match + case Some((port, portSub)) => Some(portSub.atGetSet(port.getOwnerDomain) -> portSub) + case None => Some(ctxSub.atGetSet(pbns.getOwnerDomain) -> ctxSub) + case _ => Some(ctxSub.atGetSet(member.getOwnerDomain) -> ctxSub) + ownerAndSub.flatMap { case (o, sub) => + o.domainType match + case DomainType.RT => Some(o -> sub) + case _ => None + } + end new_getRTOwnerWithSub + + // Routed, best-effort clone of `fullNameViaInst` (error messages only). + // `owner` lives in `ownerSub`; instance paths are recovered by navigating UP. + private def new_fullNameViaInst(owner: DFDomainOwner, ownerSub: DB): String = + def instFullName(d: DFDesignBlock, dSub: DB): Option[String] = + dSub.parentSubDBOpt.flatMap { parentSub => + parentSub.atGetSet { + parentSub.membersNoGlobals.view.collectFirst { + case inst: DFDesignInst if inst.getDesignBlock eq d => inst.getFullName + } + } + } + val design = ownerSub.atGetSet(owner.getThisOrOwnerDesign) + if (design eq topDB.top) ownerSub.atGetSet(owner.getFullName) + else + owner match + case d: DFDesignBlock => + instFullName(d, ownerSub).getOrElse(ownerSub.atGetSet(owner.getFullName)) + case _ => + instFullName(design, ownerSub) match + case Some(instName) => + s"$instName.${ownerSub.atGetSet(owner.getRelativeName(design))}" + case None => ownerSub.atGetSet(owner.getFullName) + end new_fullNameViaInst + + lazy val new_dependentRTDomainOwners: Map[DFDomainOwner, DFDomainOwner] = + if (!isRoot) rootDB.new_dependentRTDomainOwners + else + subDBs.view.values.flatMap { subDB => + subDB.domainOwnerMemberList.view.flatMap { case (domainOwner, domainMembers) => + subDB.atGetSet { + domainOwner.domainType match + case DomainType.RT => + new_relatedAnnotMap.get(domainOwner) match + case Some(relatedOwner) => Some(domainOwner -> relatedOwner) + case None => + val hasClkOrRst = domainOwner.meta.annotations.exists { + case _: constraints.Timing.Clock => true + case _: constraints.Timing.Reset => true + case _ => false + } + if (hasClkOrRst) None + else + domainOwner match + case design: DFDesignBlock => + // The absolute top is the root's top-top design; a + // sub-DB's own `isTopTop`/`isTop` can't tell because + // every design block has an Empty ownerRef. + if (design eq topDB.top) None + else new_getRTOwnerWithSub(design, subDB).map { case (o, _) => + design -> o + } + case domain: DomainBlock => + val ed = domain.getOwnerDesign + val portRelNames = domainMembers.collect { + case dcl: DFVal.Dcl + if dcl.isPortIn && !dcl.isClkDcl && !dcl.isRstDcl => + dcl -> dcl.getRelativeName(ed) + } + // External drivers come from `ed`'s instantiations in + // each PARENT sub-DB — navigate the design tree up. + val parentDesigns = designBlockOwnershipMap.getOrElse(ed, Set.empty) + val inSources: Set[(DFDomainOwner, DB)] = + portRelNames.view.flatMap { case (port, portRelName) => + val externalNets: List[(DFNet, DB)] = + parentDesigns.toList.flatMap { parentDesign => + subDBs.get(parentDesign.ownerRef).toList.flatMap { parentSub => + parentSub.atGetSet { + parentSub.membersNoGlobals.collect { + case inst: DFDesignInst + if inst.getDesignBlock eq ed => inst + }.flatMap { inst => + parentSub.designInstPBNS.getOrElse(inst, Nil) + .filter(_.portNamePath == portRelName) + .flatMap(parentSub.connectionTable.getNets(_)) + } + }.map(_ -> parentSub) + } + } + val localNets: List[(DFNet, DB)] = + subDB.connectionTable.getNets(port).toList.map(_ -> subDB) + (localNets ++ externalNets).headOption.flatMap { case (net, netSub) => + netSub.atGetSet { + net match + case DFNet.Connection(_, from, _) => + new_getRTOwnerWithSub(from, netSub) + case _ => None + } + } + }.toSet + val inSourceDomains = inSources.map { case (o, _) => o } + if (inSourceDomains.isEmpty) + new_getRTOwnerWithSub(domain, subDB).map { case (o, _) => + domain -> o + } + else if (inSourceDomains.size > 1) + throw new IllegalArgumentException( + s"""|Found ambiguous source RT configurations for the domain: + |${new_fullNameViaInst(domain, subDB)} + |Sources: + |${inSources.map { case (o, s) => new_fullNameViaInst(o, s) } + .mkString("\n")} + |Possible solution: + |Either explicitly define a configuration for the domain or drive it from a single source domain. + |""".stripMargin + ) + else Some(domain -> inSourceDomains.head) + case ifc: DFInterfaceOwner => ??? + case _ => None + } + } + }.toMap + + // Hierarchical equivalent of the `isDependentOn` analysis: does `domainOwner` + // transitively depend on `thatDomainOwner` per `new_dependentRTDomainOwners`? + // Invoked on the root DB. + @tailrec final def new_isDependentOn( + domainOwner: DFDomainOwner, + thatDomainOwner: DFDomainOwner + ): Boolean = + new_dependentRTDomainOwners.get(domainOwner) match + case Some(dependency) => + if (dependency == thatDomainOwner) true + else new_isDependentOn(dependency, thatDomainOwner) + case None => false + + // Structural map: every domain owner -> its sub-DB. Keys are domain owners + // only (designs + domain blocks), built from each sub-DB's own + // domainOwnerMemberList — the same category as designBlockOwnershipMap, NOT a + // member index. Routes a domain owner's getResolvedClkRst / usesClkRst to its + // own getSet, including reverse-dependent domains reached by usesClk/usesRst. + private lazy val domainOwnerToSubDB: Map[DFDomainOwner, DB] = + if (isRoot) + subDBs.view.values.flatMap { sub => + sub.domainOwnerMemberList.view.map { case (owner, _) => owner -> sub } + }.toMap + else rootDB.domainOwnerToSubDB + + lazy val new_resolvedClkRstMap: Map[DFDomainOwner, ClkRstTiming] = + if (!isRoot) rootDB.new_resolvedClkRstMap + else + val reversedDependents: Map[DFDomainOwner, Set[DFDomainOwner]] = + new_dependentRTDomainOwners.invert + val designUsesClkRst = + mutable.Map.empty[String, (usesClk: Boolean, usesRst: Boolean)] + val domainOwnerUsesClkRst = + mutable.Map.empty[DFDomainOwner, (usesClk: Boolean, usesRst: Boolean)] + // run `f` under the getSet of `owner`'s sub-DB (looked up structurally) + def atOwner[T](owner: DFDomainOwner)(f: MemberGetSet ?=> T): T = + domainOwnerToSubDB(owner).atGetSet(f) + // getSet-threaded clones of the clk/rst resolver extensions: the originals + // bind to the class's `this.getSet` (which throws on the root), so they + // can't be reused here — these take an explicit `using MemberGetSet` that + // `atOwner` supplies from the owner's sub-DB. + def domainClkConstraints(owner: DFDomainOwner)(using + MemberGetSet + ): collection.View[constraints.Constraint] = + owner.getConstraints.view ++ + domainOwnerMemberTable(owner).view.collectFirst { + case dcl: DFVal.Dcl if dcl.isPortIn && dcl.isClkDcl => dcl.getConstraints + }.getOrElse(Nil) + def timingClkRateOpt(owner: DFDomainOwner)(using MemberGetSet): Option[RateNumber] = + domainClkConstraints(owner).collectFirst { + case constraints.Timing.Clock(rate = rate: RateNumber @unchecked) => rate + } + def resolvedClkRst(owner: DFDomainOwner)(using MemberGetSet): ClkRstTiming = + val defaultTag = globalTags.getTagOf[DefaultRTDomainCfgTag].get + val userClkOpt = owner.meta.annotations.collectFirst { + case c: constraints.Timing.Clock => c + } + val userRstOpt = owner.meta.annotations.collectFirst { + case r: constraints.Timing.Reset => r + } + val isDeviceTop = owner.getThisOrOwnerDesign.isDeviceTop + val explicitNoRst = userClkOpt.isDefined && userRstOpt.isEmpty + val (baseClkOpt, baseRstOpt) = + if (isDeviceTop) + val clk = timingClkRateOpt(owner) match + case Some(rate) => defaultTag.clk.copy(rate = rate) + case None => defaultTag.clk + (Some(clk), None) + else if (explicitNoRst) (Some(defaultTag.clk), None) + else (Some(defaultTag.clk), Some(defaultTag.rst)) + def mergeClk( + base: Option[constraints.Timing.Clock], + user: Option[constraints.Timing.Clock] + ): Option[constraints.Timing.Clock] = (base, user) match + case (Some(b), Some(u)) => + Some(b.merge(u, withPriority = true).get.asInstanceOf[constraints.Timing.Clock]) + case (Some(b), None) => Some(b) + case (None, Some(u)) => Some(u) + case (None, None) => None + def mergeRst( + base: Option[constraints.Timing.Reset], + user: Option[constraints.Timing.Reset] + ): Option[constraints.Timing.Reset] = (base, user) match + case (Some(b), Some(u)) => + Some(b.merge(u, withPriority = true).get.asInstanceOf[constraints.Timing.Reset]) + case (Some(b), None) => Some(b) + case (None, Some(u)) => Some(u) + case (None, None) => None + (mergeClk(baseClkOpt, userClkOpt), mergeRst(baseRstOpt, userRstOpt)) + end resolvedClkRst + def isAlwaysAtTopClk(owner: DFDomainOwner)(using MemberGetSet): Boolean = + resolvedClkRst(owner)._1 match + case Some(clk) => + clk.inclusionPolicy match + case ClkRstInclusionPolicy.AlwaysAtTop => true + case _ => false + case None => false + def isAlwaysAtTopRst(owner: DFDomainOwner)(using MemberGetSet): Boolean = + resolvedClkRst(owner)._2 match + case Some(rst) => + rst.inclusionPolicy match + case ClkRstInclusionPolicy.AlwaysAtTop => true + case _ => false + case None => false + def usesClkRst(owner: DFDomainOwner): (usesClk: Boolean, usesRst: Boolean) = + owner match + case design: DFDesignBlock => + designUsesClkRst.getOrElseUpdate(design.dclName, (usesClk(design), usesRst(design))) + case _ => + domainOwnerUsesClkRst.getOrElseUpdate(owner, (usesClk(owner), usesRst(owner))) + def usesClk(owner: DFDomainOwner): Boolean = + atOwner(owner) { + domainOwnerMemberTable(owner).exists { + case dcl: DFVal.Dcl => dcl.isReg || dcl.isClkDcl + case reg: DFVal.Alias.History => true + case pb: ProcessBlock if pb.isInRTDomain => true + case inst: DFDesignInst => usesClkRst(inst.getDesignBlock).usesClk + case _ => false + } + } || reversedDependents.getOrElse(owner, Set()).exists(d => usesClkRst(d).usesClk) || + (owner eq topDB.top) && atOwner(owner)(isAlwaysAtTopClk(owner)) || + owner.hasClkAnnot + def usesRst(owner: DFDomainOwner): Boolean = + atOwner(owner) { + domainOwnerMemberTable(owner).exists { + case dcl: DFVal.Dcl => + (dcl.isReg && dcl.hasNonBubbleInit) || dcl.isRstDcl + case reg: DFVal.Alias.History => reg.hasNonBubbleInit + case pb: ProcessBlock if pb.isInRTDomain => true + case inst: DFDesignInst => usesClkRst(inst.getDesignBlock).usesRst + case _ => false + } + } || reversedDependents.getOrElse(owner, Set()).exists(d => usesClkRst(d).usesRst) || + (owner eq topDB.top) && atOwner(owner)(isAlwaysAtTopRst(owner)) || + owner.hasRstAnnot + def relaxed(resolved: ClkRstTiming, atDomain: DFDomainOwner): ClkRstTiming = + val (uClk, uRst) = usesClkRst(atDomain) + val (clk, rst) = resolved + (if (uClk) clk else None, if (uRst) rst else None) + @tailrec def fillDomainMap( + domains: List[DFDomainOwner], + stack: List[DFDomainOwner], + domainMap: mutable.Map[DFDomainOwner, ClkRstTiming] + ): Unit = + domains match + case domain :: rest if domainMap.contains(domain) => + fillDomainMap(rest, stack, domainMap) + case domain :: rest => + new_dependentRTDomainOwners.get(domain) match + case Some(dependencyDomain) => + domainMap.get(dependencyDomain) match + case Some(dependencyResolved) => + domainMap += domain -> relaxed(dependencyResolved, domain) + fillDomainMap(rest, stack, domainMap) + case None => fillDomainMap(rest, domain :: stack, domainMap) + case _ => + val resolved = atOwner(domain)(resolvedClkRst(domain)) + domainMap += domain -> relaxed(resolved, domain) + fillDomainMap(rest, stack, domainMap) + case Nil if stack.nonEmpty => fillDomainMap(stack, Nil, domainMap) + case _ => + val domainMap = mutable.Map.empty[DFDomainOwner, ClkRstTiming] + val rtDomainOwners: List[DFDomainOwner] = + subDBs.view.values.flatMap { sub => + sub.domainOwnerMemberList.view.map { case (owner, _) => owner } + }.filter { owner => + owner.domainType match + case DomainType.RT => true + case _ => false + }.toList + fillDomainMap(rtDomainOwners, Nil, domainMap) + domainMap.toMap + + // Temporary equivalence gate: on an old-style flat DB, assert the flat RT + // clk/rst analyses match their new-style clones computed on `oldToNew`. + // Dropped once the consuming stages are migrated and the flat versions removed. + def new_clkRstEquivalenceCheck(): Unit = + if (isOldStyleFlatDB) + val newRoot = this.oldToNew + def fail(name: String, flat: Any, neu: Any): Nothing = + throw new IllegalArgumentException( + s"""|new_$name mismatch between flat and hierarchical computation. + |flat: $flat + |new: $neu""".stripMargin + ) + if (this.relatedAnnotMap != newRoot.new_relatedAnnotMap) + fail("relatedAnnotMap", this.relatedAnnotMap, newRoot.new_relatedAnnotMap) + if (this.dependentRTDomainOwners != newRoot.new_dependentRTDomainOwners) + fail( + "dependentRTDomainOwners", + this.dependentRTDomainOwners, + newRoot.new_dependentRTDomainOwners + ) + if (this.resolvedClkRstMap != newRoot.new_resolvedClkRstMap) + fail("resolvedClkRstMap", this.resolvedClkRstMap, newRoot.new_resolvedClkRstMap) + end new_clkRstEquivalenceCheck + /** Checks that device top design domains all have timing clock rate constraints. Additionally, if * there is an explicit clock rate configuration, it must match the timing constraint rate. */ diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddClkRst.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddClkRst.scala index 302c38f10..9570b6d5b 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddClkRst.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddClkRst.scala @@ -6,6 +6,7 @@ import dfhdl.compiler.patching.* import dfhdl.internals.* import dfhdl.options.CompilerOptions import scala.collection.mutable +import scala.collection.immutable.ListMap import dfhdl.core.DFOpaque as coreDFOpaque import dfhdl.core.{asFE, DFCG} @@ -18,12 +19,17 @@ import dfhdl.core.{asFE, DFCG} * configurations share the same opaque type). * - Generates the sim-driver block for top-level simulation designs. */ -case object AddClkRst extends Stage: +case object AddClkRst extends GlobalStage: def dependencies: List[Stage] = List(ToRT, ExplicitClkRstCfg) def nullifies: Set[Stage] = Set(ViaConnection) - def transform(designDB: DB)(using MemberGetSet, CompilerOptions): DB = - given RefGen = RefGen.fromGetSet + def transformGlobal(designDB: DB)(using + co: CompilerOptions, + refGen: RefGen + ): DB = val defaultTag = designDB.globalTags.getTagOf[DefaultRTDomainCfgTag].get + // The absolute top design; replaces the per-sub-DB-unreliable `isTopTop` + // (every new-style design block has an Empty ownerRef). + val topTop = designDB.top // saves (design, clkContent) pairs that are outputting clk, and // (design, clkGrpName, rstContent) triples for outputting rst. Rst is keyed together // with its paired clock's grpName so two rsts with identical mode/active but belonging @@ -51,7 +57,7 @@ case object AddClkRst extends Stage: // `@timing.clock` / `@timing.reset` stay on the owner (the domain is the canonical // carrier of the timing configuration; the opaque Clk_ / Rst_ port type // encodes the same info into the clk/rst port's static type). - def getClkConstraints: List[constraints.Constraint] = + def getClkConstraints(using MemberGetSet): List[constraints.Constraint] = domainOwner.getConstraints.collect { case c: constraints.IO => c } @@ -76,200 +82,262 @@ case object AddClkRst extends Stage: (clk, rst) end resolvedAnnots - val patchList: List[(DFMember, Patch)] = designDB.domainOwnerMemberList.flatMap { - case (owner, members) => - val ownerClkConstraints = owner.getClkConstraints - val design = owner.getThisOrOwnerDesign - val ownerDomainPatchOption = owner.domainType match - case DomainType.RT => - val (clkAnnotOpt, rstAnnotOpt) = resolvedAnnots(owner) - // skip if no clk/rst slots (relaxed combinational or Related) - if (clkAnnotOpt.isEmpty && rstAnnotOpt.isEmpty) None - else - // check for existing clk/rst dcls - val existingClk = members.collectFirst { - case clk: DFVal.Dcl if clk.isClkDcl => - clkAnnotOpt.foreach { clkAnnot => - if (clk.modifier.dir == DFVal.Modifier.OUT && !design.isTopTop) - designDB.designBlockOwnershipMap.getOrElse(design, Set.empty) - .foreach(parent => designClkOut += ((parent, clkAnnot))) - } - clk - } - val existingRst = members.collectFirst { - case rst: DFVal.Dcl if rst.isRstDcl => - (rstAnnotOpt, clkAnnotOpt) match - case (Some(rstAnnot), Some(clkAnnot)) - if rst.modifier.dir == DFVal.Modifier.OUT && !design.isTopTop => - designDB.designBlockOwnershipMap.getOrElse(design, Set.empty) - .foreach(parent => designRstOut += ((parent, grpName(clkAnnot), rstAnnot))) - case _ => - rst - } - // "already handled by an internal design that has an OUT of the same config" check - val clkAlreadyHandled = clkAnnotOpt.exists(c => designClkOut.contains((design, c))) - val rstAlreadyHandled = (clkAnnotOpt, rstAnnotOpt) match - case (Some(c), Some(r)) => designRstOut.contains((design, grpName(c), r)) - case _ => false + // Build the patch list for a single sub-DB's domain owners (run under that + // sub-DB's getSet). The shared mutable state above accumulates across all + // sub-DBs in elaboration order, so cross-design clk/rst output tracking and + // opaque-type memoization stay global; only the patches are partitioned per + // sub-DB so each can be applied to its own DB. + def subDBPatches(subDB: DB)(using MemberGetSet): List[(DFMember, Patch)] = + given dfhdl.core.DFC = dfhdl.core.DFC.emptyNoEO + subDB.domainOwnerMemberList.flatMap { + case (owner, members) => + val ownerClkConstraints = owner.getClkConstraints + val design = owner.getThisOrOwnerDesign + val ownerDomainPatchOption = owner.domainType match + case DomainType.RT => + val (clkAnnotOpt, rstAnnotOpt) = resolvedAnnots(owner) + // skip if no clk/rst slots (relaxed combinational or Related) + if (clkAnnotOpt.isEmpty && rstAnnotOpt.isEmpty) None + else + // check for existing clk/rst dcls + val existingClk = members.collectFirst { + case clk: DFVal.Dcl if clk.isClkDcl => + clkAnnotOpt.foreach { clkAnnot => + if (clk.modifier.dir == DFVal.Modifier.OUT && !(design eq topTop)) + designDB.designBlockOwnershipMap.getOrElse(design, Set.empty) + .foreach(parent => designClkOut += ((parent, clkAnnot))) + } + clk + } + val existingRst = members.collectFirst { + case rst: DFVal.Dcl if rst.isRstDcl => + (rstAnnotOpt, clkAnnotOpt) match + case (Some(rstAnnot), Some(clkAnnot)) + if rst.modifier.dir == DFVal.Modifier.OUT && !(design eq topTop) => + designDB.designBlockOwnershipMap.getOrElse(design, Set.empty) + .foreach(parent => + designRstOut += ((parent, grpName(clkAnnot), rstAnnot)) + ) + case _ => + rst + } + // "already handled by an internal design that has an OUT of the same config" check + val clkAlreadyHandled = clkAnnotOpt.exists(c => designClkOut.contains((design, c))) + val rstAlreadyHandled = (clkAnnotOpt, rstAnnotOpt) match + case (Some(c), Some(r)) => designRstOut.contains((design, grpName(c), r)) + case _ => false - // required names from the resolved annotations - val requiredClkName = clkAnnotOpt.collect { - case c if !clkAlreadyHandled => c.portName.get - } - val requiredRstName = rstAnnotOpt.collect { - case r if !rstAlreadyHandled => r.portName.get - } - // whether to add new clk/rst ports - val addClk = requiredClkName.nonEmpty && existingClk.isEmpty - val addRst = requiredRstName.nonEmpty && existingRst.isEmpty + // required names from the resolved annotations + val requiredClkName = clkAnnotOpt.collect { + case c if !clkAlreadyHandled => c.portName.get + } + val requiredRstName = rstAnnotOpt.collect { + case r if !rstAlreadyHandled => r.portName.get + } + // whether to add new clk/rst ports + val addClk = requiredClkName.nonEmpty && existingClk.isEmpty + val addRst = requiredRstName.nonEmpty && existingRst.isEmpty - val opaqueDFC = DFCG() - val clkTypeOpt: Option[coreDFOpaque[coreDFOpaque.Clk]] = clkAnnotOpt.map { clkAnnot => - val name = grpName(clkAnnot) - class Unique: - case class Clk() extends coreDFOpaque.Clk: - override lazy val typeName: String = s"Clk_${name}" - clkTypeMap.getOrElseUpdate(clkAnnot, coreDFOpaque(Unique().Clk())(using opaqueDFC)) - } - val rstTypeOpt: Option[coreDFOpaque[coreDFOpaque.Rst]] = rstAnnotOpt.map { rstAnnot => - // rst is paired with its domain's clk — use the clk's grpName for naming - // and memoization keying. If no clk slot (rare — reset without clock), - // fall back to a synthetic "nogrp" suffix to avoid a name collision. - val name = clkAnnotOpt.map(grpName).getOrElse("nogrp") - class Unique: - case class Rst() extends coreDFOpaque.Rst: - override lazy val typeName: String = s"Rst_${name}" - rstTypeMap.getOrElseUpdate( - (name, rstAnnot), - coreDFOpaque(Unique().Rst())(using opaqueDFC) - ) - } - // saving changed opaques to change in all relevant members later - (existingClk, clkTypeOpt) match - case (Some(dcl), Some(clkType)) => - opaqueReplaceMap += dcl.dfType.asInstanceOf[DFOpaque] -> clkType.asIR - case _ => - (existingRst, rstTypeOpt) match - case (Some(dcl), Some(rstType)) => - opaqueReplaceMap += dcl.dfType.asInstanceOf[DFOpaque] -> rstType.asIR - case _ => + val opaqueDFC = DFCG() + val clkTypeOpt: Option[coreDFOpaque[coreDFOpaque.Clk]] = clkAnnotOpt.map { + clkAnnot => + val name = grpName(clkAnnot) + class Unique: + case class Clk() extends coreDFOpaque.Clk: + override lazy val typeName: String = s"Clk_${name}" + clkTypeMap.getOrElseUpdate( + clkAnnot, + coreDFOpaque(Unique().Clk())(using opaqueDFC) + ) + } + val rstTypeOpt: Option[coreDFOpaque[coreDFOpaque.Rst]] = rstAnnotOpt.map { + rstAnnot => + // rst is paired with its domain's clk — use the clk's grpName for naming + // and memoization keying. If no clk slot (rare — reset without clock), + // fall back to a synthetic "nogrp" suffix to avoid a name collision. + val name = clkAnnotOpt.map(grpName).getOrElse("nogrp") + class Unique: + case class Rst() extends coreDFOpaque.Rst: + override lazy val typeName: String = s"Rst_${name}" + rstTypeMap.getOrElseUpdate( + (name, rstAnnot), + coreDFOpaque(Unique().Rst())(using opaqueDFC) + ) + } + // saving changed opaques to change in all relevant members later + (existingClk, clkTypeOpt) match + case (Some(dcl), Some(clkType)) => + opaqueReplaceMap += dcl.dfType.asInstanceOf[DFOpaque] -> clkType.asIR + case _ => + (existingRst, rstTypeOpt) match + case (Some(dcl), Some(rstType)) => + opaqueReplaceMap += dcl.dfType.asInstanceOf[DFOpaque] -> rstType.asIR + case _ => - // add missing clk/rst ports - if (addClk || addRst) - val simGen = designDB.inSimulation && design.isTopTop - val dsn = new MetaDesign(owner, Patch.Add.Config.InsideFirst): - val selfDFC = dfc - if (simGen) - val clk = (clkTypeOpt.get <> VAR)(using dfc.setName(requiredClkName.get)) - lazy val rst = (rstTypeOpt.get <> VAR)(using dfc.setName(requiredRstName.get)) - if (addRst) rst - val clkRstSimGen = new EDDomain: - override protected def __dfc: DFC = - selfDFC.setName("clkRstSimGen") - .setAnnotations(List(annotation.FlattenMode.Transparent)) - locally { - given DFC = selfDFC.anonymize.setAnnotations(Nil) - val clkAnnot = clkAnnotOpt.get - val clkRate: RateNumber = clkAnnot.rate.get - val clkActive = clkAnnot.edge.get match - case ClkCfg.Edge.Rising => true - case ClkCfg.Edge.Falling => false - val clkPeriodHalf = clkRate.to_period / 2 - lazy val rstActive = - rstAnnotOpt.get.active.get match - case RstCfg.Active.Low => false - case RstCfg.Active.High => true - def clkPeriodHalfConst(using - DFC - ) - : dfhdl.core.DFConstOf[dfhdl.core.DFTime] = - dfhdl.core.DFVal.Const(dfhdl.core.DFTime, clkPeriodHalf) - process.forever { - if (addRst) - rst.actual :== dfhdl.core.DFVal.Const(dfhdl.core.DFBit, Some(rstActive)) - val cond: Boolean <> VAL = true - dfhdl.core.DFWhile.plugin(cond) { - clk.actual :== dfhdl.core.DFVal.Const( - dfhdl.core.DFBit, - Some(!clkActive) - ) - wait(clkPeriodHalfConst) - clk.actual :== dfhdl.core.DFVal.Const( - dfhdl.core.DFBit, - Some(clkActive) - ) - wait(clkPeriodHalfConst) + // add missing clk/rst ports + if (addClk || addRst) + val simGen = designDB.inSimulation && (design eq topTop) + val dsn = new MetaDesign(owner, Patch.Add.Config.InsideFirst): + val selfDFC = dfc + if (simGen) + val clk = (clkTypeOpt.get <> VAR)(using dfc.setName(requiredClkName.get)) + lazy val rst = (rstTypeOpt.get <> VAR)(using dfc.setName(requiredRstName.get)) + if (addRst) rst + val clkRstSimGen = new EDDomain: + override protected def __dfc: DFC = + selfDFC.setName("clkRstSimGen") + .setAnnotations(List(annotation.FlattenMode.Transparent)) + locally { + given DFC = selfDFC.anonymize.setAnnotations(Nil) + val clkAnnot = clkAnnotOpt.get + val clkRate: RateNumber = clkAnnot.rate.get + val clkActive = clkAnnot.edge.get match + case ClkCfg.Edge.Rising => true + case ClkCfg.Edge.Falling => false + val clkPeriodHalf = clkRate.to_period / 2 + lazy val rstActive = + rstAnnotOpt.get.active.get match + case RstCfg.Active.Low => false + case RstCfg.Active.High => true + def clkPeriodHalfConst(using + DFC + ) + : dfhdl.core.DFConstOf[dfhdl.core.DFTime] = + dfhdl.core.DFVal.Const(dfhdl.core.DFTime, clkPeriodHalf) + process.forever { if (addRst) - rst.actual :== - dfhdl.core.DFVal.Const(dfhdl.core.DFBit, Some(!rstActive)) + rst.actual :== dfhdl.core.DFVal.Const( + dfhdl.core.DFBit, + Some(rstActive) + ) + val cond: Boolean <> VAL = true + dfhdl.core.DFWhile.plugin(cond) { + clk.actual :== dfhdl.core.DFVal.Const( + dfhdl.core.DFBit, + Some(!clkActive) + ) + wait(clkPeriodHalfConst) + clk.actual :== dfhdl.core.DFVal.Const( + dfhdl.core.DFBit, + Some(clkActive) + ) + wait(clkPeriodHalfConst) + if (addRst) + rst.actual :== + dfhdl.core.DFVal.Const(dfhdl.core.DFBit, Some(!rstActive)) + } } } - } - else - lazy val clk = (clkTypeOpt.get <> IN)(using - dfc.setName(requiredClkName.get).setAnnotations(ownerClkConstraints) - ) - if (addClk) clk - lazy val rst = (rstTypeOpt.get <> IN)(using - dfc.setName(requiredRstName.get) - ) - if (addRst) rst - end if - Some(dsn.patch) - else None + else + lazy val clk = (clkTypeOpt.get <> IN)(using + dfc.setName(requiredClkName.get).setAnnotations(ownerClkConstraints) + ) + if (addClk) clk + lazy val rst = (rstTypeOpt.get <> IN)(using + dfc.setName(requiredRstName.get) + ) + if (addRst) rst + end if + Some(dsn.patch) + else None + end if end if - end if - case _ => None - // replace clk/rst value DFTypes with updated ones - val opaqueTypeReplacePatches = members.view.flatMap { - case dfVal: DFVal => - dfVal.dfType match - case dfType @ DFOpaque(kind = (DFOpaque.Kind.Clk | DFOpaque.Kind.Rst)) - if opaqueReplaceMap.contains(dfType) => - val updatedDFVal = dfVal match - case clk: DFVal.Dcl if clk.isClkDcl => - val updatedAnnotations = (ownerClkConstraints ++ clk.meta.annotations).distinct - clk.copy( - dfType = opaqueReplaceMap(dfType), - meta = clk.meta.copy(annotations = updatedAnnotations) - ) - case _ => dfVal.updateDFType(opaqueReplaceMap(dfType)) - Some(dfVal -> Patch.Replace(updatedDFVal, Patch.Replace.Config.FullReplacement)) - case _ => None - case _ => None + case _ => None + // replace clk/rst value DFTypes with updated ones + val opaqueTypeReplacePatches = members.view.flatMap { + case dfVal: DFVal => + dfVal.dfType match + case dfType @ DFOpaque(kind = (DFOpaque.Kind.Clk | DFOpaque.Kind.Rst)) + if opaqueReplaceMap.contains(dfType) => + val updatedDFVal = dfVal match + case clk: DFVal.Dcl if clk.isClkDcl => + val updatedAnnotations = + (ownerClkConstraints ++ clk.meta.annotations).distinct + clk.copy( + dfType = opaqueReplaceMap(dfType), + meta = clk.meta.copy(annotations = updatedAnnotations) + ) + case _ => dfVal.updateDFType(opaqueReplaceMap(dfType)) + Some(dfVal -> Patch.Replace(updatedDFVal, Patch.Replace.Config.FullReplacement)) + case _ => None + case _ => None + } + // Strip only IO constraints from the owner (they moved to the clk port). Keep the + // resolved `@timing.clock` / `@timing.reset` on the owner so that subsequent re-runs + // of the pipeline (e.g. idempotent `.addClkRst.addClkRst`) see the resolved state + // and don't re-derive defaults that conflict with ports already created. + val hasOwnerIO = + owner.meta.annotations.exists { case _: constraints.IO => true; case _ => false } + val ownerConstraintRemovalPatchOption = + if (hasOwnerIO) + def updateMeta(meta: Meta): Meta = + meta.copy(annotations = meta.annotations.flatMap { + case _: constraints.IO => None + case c => Some(c) + }) + val updatedOwner = owner match + case design: DFDesignBlock => + design.copy(meta = updateMeta(design.meta)) + case interface: DFInterfaceOwner => + interface.copy( + dclMeta = updateMeta(interface.dclMeta), + meta = updateMeta(interface.meta) + ) + case _ => owner.setMeta(updateMeta) + Some(owner -> Patch.Replace(updatedOwner, Patch.Replace.Config.FullReplacement)) + else None + List( + ownerConstraintRemovalPatchOption, + opaqueTypeReplacePatches, + ownerDomainPatchOption + ).flatten + } + end subDBPatches + + // Pre-pass: populate the cross-design clk/rst OUT tracking from ALL designs + // before any per-owner "already handled" check. The flat DB visited domain + // owners child-before-parent, so a parent always saw its children's OUT + // contributions; sub-DBs iterate top (parent) first, so without this pre-pass + // a parent would be checked before its child's OUT was recorded. + designDB.subDBs.foreach { case (_, subDB) => + subDB.atGetSet { + subDB.domainOwnerMemberList.foreach { case (owner, members) => + owner.domainType match + case DomainType.RT => + val (clkAnnotOpt, rstAnnotOpt) = resolvedAnnots(owner) + val design = owner.getThisOrOwnerDesign + if (!(design eq topTop)) + members.foreach { + case clk: DFVal.Dcl if clk.isClkDcl && clk.modifier.dir == DFVal.Modifier.OUT => + clkAnnotOpt.foreach { clkAnnot => + designDB.designBlockOwnershipMap.getOrElse(design, Set.empty) + .foreach(parent => designClkOut += ((parent, clkAnnot))) + } + case rst: DFVal.Dcl if rst.isRstDcl && rst.modifier.dir == DFVal.Modifier.OUT => + (rstAnnotOpt, clkAnnotOpt) match + case (Some(rstAnnot), Some(clkAnnot)) => + designDB.designBlockOwnershipMap.getOrElse(design, Set.empty) + .foreach(parent => + designRstOut += ((parent, grpName(clkAnnot), rstAnnot)) + ) + case _ => + case _ => + } + case _ => } - // Strip only IO constraints from the owner (they moved to the clk port). Keep the - // resolved `@timing.clock` / `@timing.reset` on the owner so that subsequent re-runs - // of the pipeline (e.g. idempotent `.addClkRst.addClkRst`) see the resolved state - // and don't re-derive defaults that conflict with ports already created. - val hasOwnerIO = - owner.meta.annotations.exists { case _: constraints.IO => true; case _ => false } - val ownerConstraintRemovalPatchOption = - if (hasOwnerIO) - def updateMeta(meta: Meta): Meta = - meta.copy(annotations = meta.annotations.flatMap { - case _: constraints.IO => None - case c => Some(c) - }) - val updatedOwner = owner match - case design: DFDesignBlock => - design.copy(meta = updateMeta(design.meta)) - case interface: DFInterfaceOwner => - interface.copy( - dclMeta = updateMeta(interface.dclMeta), - meta = updateMeta(interface.meta) - ) - case _ => owner.setMeta(updateMeta) - Some(owner -> Patch.Replace(updatedOwner, Patch.Replace.Config.FullReplacement)) - else None - List( - ownerConstraintRemovalPatchOption, - opaqueTypeReplacePatches, - ownerDomainPatchOption - ).flatten + } + } + + // Iterate sub-DBs in elaboration order (top first) so the shared cross-design + // state accumulates deterministically; build and apply each sub-DB's patches + // under its own getSet. + val newSubDBs = mutable.ListBuffer.empty[(DFOwner.Ref, DB)] + designDB.subDBs.foreach { case (subKey, subDB) => + val patchList = subDB.atGetSet(subDBPatches(subDB)) + newSubDBs += subKey -> (if (patchList.isEmpty) subDB else subDB.patch(patchList)) } - designDB.patch(patchList) - end transform + designDB.update(subDBs = ListMap.from(newSubDBs)) + end transformGlobal end AddClkRst extension [T: HasDB](t: T) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ExplicitClkRstCfg.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ExplicitClkRstCfg.scala index 22680f7c5..825aa708c 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ExplicitClkRstCfg.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ExplicitClkRstCfg.scala @@ -17,16 +17,14 @@ import dfhdl.core.{refTW, DFC} case object ExplicitClkRstCfg extends HierarchyStage: def dependencies: List[Stage] = List(UniqueDesigns, NamedAnonMultiref) def nullifies: Set[Stage] = Set(DropUnreferencedAnons) - // resolvedClkRstMap / dependentRTDomainOwners / getOwnerDomain walk the full - // hierarchy across sub-DB boundaries, so resolve via the outer flat-DB getSet - // rather than per-sub-DB getSet. - override def rebindGetSet: Boolean = false - def transformSubDB(subDB: DB)(using + // Cross-design clk/rst resolution (`new_resolvedClkRstMap` / `new_isDependentOn`) + // is read from the root DB; everything else is per-sub-DB, so the default + // `rebindGetSet = true` applies and `subDB` is this stage's current sub-DB. + def transformSubDB(rootDB: DB)(using getSet: MemberGetSet, co: CompilerOptions, rg: RefGen ): DB = - val designDB = getSet.designDB val relatedCfgRefs = mutable.Map.empty[DFRefAny, DFMember] given dfc: DFC = DFC.emptyNoEO // Strip any @timing.clock / @timing.reset / @timing.related from the annotation @@ -46,7 +44,7 @@ case object ExplicitClkRstCfg extends HierarchyStage: owner.domainType match case DomainType.RT => val (resolvedClk, resolvedRst) = - designDB.resolvedClkRstMap.getOrElse(owner, (None, None)) + rootDB.new_resolvedClkRstMap.getOrElse(owner, (None, None)) // Preserve any existing user-authored @timing.related annotation. For a domain // related to a sibling or to its enclosing owner the downstream pipeline uses the // annotation to skip clk/rst port insertion. @@ -61,7 +59,7 @@ case object ExplicitClkRstCfg extends HierarchyStage: owner match case domain: DomainBlock => val domainOwner = domain.getOwnerDomain - if (domain.isDependentOn(domainOwner)) + if (rootDB.new_isDependentOn(domain, domainOwner)) val ref = domainOwner.asInstanceOf[DomainBlock | DFDesignBlock].refTW[DomainBlock] relatedCfgRefs += ref -> domainOwner diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala index d5ea09c81..375c70df0 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala @@ -274,6 +274,7 @@ case class SanityCheck(skipAnonRefCheck: Boolean) extends Stage: ) require(false, "Failed ownership check!") ownerStack.pop() + end match end while end ownershipCheck @@ -348,6 +349,7 @@ case class SanityCheck(skipAnonRefCheck: Boolean) extends Stage: lhs.globalTags == rhs.globalTags && lhs.srcFiles == rhs.srcFiles, "Hierarchical DB round-trip globalTags or srcFiles mismatch." ) + end hierarchicalDBRoundTripCheck def transform(designDB: DB)(using MemberGetSet, CompilerOptions): DB = refCheck() @@ -355,6 +357,7 @@ case class SanityCheck(skipAnonRefCheck: Boolean) extends Stage: ownershipCheck(designDB.top, designDB.membersNoGlobals.drop(1)) orderCheck() designDB.check() + designDB.new_clkRstEquivalenceCheck() hierarchicalDBRoundTripCheck(designDB) designDB end SanityCheck diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ToED.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ToED.scala index ffdb85132..a4aa644ea 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ToED.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ToED.scala @@ -19,17 +19,23 @@ import scala.collection.mutable * - Pick the reset mode (sync vs async) and active polarity for the reset branch. * - Walk `@timing.related(ref)` to the related target's clk/rst Dcls. * - * Strips the resolved timing annotations on RT→ED conversion — by that point the configuration - * is fully baked into the generated `Clk_` / `Rst_` opaque port types and the - * annotations are redundant. + * Strips the resolved timing annotations on RT→ED conversion — by that point the configuration is + * fully baked into the generated `Clk_` / `Rst_` opaque port types and the annotations + * are redundant. */ -case object ToED extends Stage: +case object ToED extends HierarchyStage: def dependencies: List[Stage] = List(DropUnreferencedAnons, ToRT, DropRTProcess, NameRegAliases, ExplicitNamedVars, ExplicitCondExprAssign, AddClkRst, SimpleOrderMembers) def nullifies: Set[Stage] = Set(DropUnreferencedAnons) - def transform(designDB: DB)(using getSet: MemberGetSet, co: CompilerOptions): DB = - given RefGen = RefGen.fromGetSet + // ToED is per-design: every domain owner it transforms, and the clk/rst Dcls + // it reads, live in the current sub-DB. The default `rebindGetSet = true` gives + // the sub-DB's getSet, and the `subDB` helper is that current design's DB. + def transformSubDB(rootDB: DB)(using + getSet: MemberGetSet, + co: CompilerOptions, + rg: RefGen + ): DB = // Annotation-based mirror of `DomainAnalysis.designDomains`. For an RT owner returns // its (clkOpt, rstOpt) Dcl pair; if the owner carries `@timing.related(ref)`, the @@ -46,7 +52,7 @@ case object ToED extends Stage: relatedTarget(owner) match case Some(target) => lookupClkRst(target) case None => - val members = designDB.domainOwnerMemberTable(owner) + val members = subDB.domainOwnerMemberTable(owner) val clkOpt = members.collectFirst { case clk: DFVal.Dcl if clk.isClkDcl => clk } @@ -60,10 +66,10 @@ case object ToED extends Stage: // the handledDesignDcls set (saving as top for initial since transforming bottom-up, // and this guarantees to work at any case and not required if we only have a single design top // with no hierarchies) - var handledDesign: DFDesignBlock = designDB.top + var handledDesign: DFDesignBlock = subDB.top // save handled REG dcls for a given design at any domain level val handledDesignREGDclSet = mutable.Set.empty[DFVal.Dcl] - val patchList: List[(DFMember, Patch)] = designDB.domainOwnerMemberList.flatMap { + val patchList: List[(DFMember, Patch)] = subDB.domainOwnerMemberList.flatMap { // for all domain owners that are also blocks (RTDesign, RTDomain) case (domainOwner: (DFDomainOwner & DFBlock), members) => val design = domainOwner.getThisOrOwnerDesign @@ -346,7 +352,7 @@ case object ToED extends Stage: // other owners case _ => None } - val firstPart = designDB.patch(patchList) + val firstPart = subDB.patch(patchList) locally { import firstPart.getSet val patchList = firstPart.members.collect { @@ -403,7 +409,7 @@ case object ToED extends Stage: } firstPart.patch(patchList) } - end transform + end transformSubDB end ToED extension [T: HasDB](t: T) From fbee3357dbf82630885779c28f364eec21f2596b Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 13 Jun 2026 15:44:52 +0300 Subject: [PATCH 11/72] Add hierarchical new_magnetConnectionMap (cross-design magnet matching) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MagnetMap.getHierarchical resolves each magnet ConnectPoint's design context (owner + container design) once under its owning sub-DB getSet, then runs the distance / inside-owner matching purely on the root-aware design tree (designBlockOwnershipMap) — no flattening, no ref-resolving global getSet. Rename the temporary equivalence gate new_clkRstEquivalenceCheck -> new_hierEquivalenceCheck and extend it to also assert magnetConnectionMap == oldToNew.new_magnetConnectionMap. Validated across the full unit suite (incl. AES's cross-design magnet hierarchies). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 20 +- .../scala/dfhdl/compiler/ir/MagnetMap.scala | 215 ++++++++++++++++++ .../dfhdl/compiler/stages/SanityCheck.scala | 2 +- 3 files changed, 231 insertions(+), 6 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index a50786edb..776f500df 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -602,6 +602,14 @@ final case class DB private ( // To From lazy val magnetConnectionMap: Map[ConnectPoint, ConnectPoint] = MagnetMap.get + // Hierarchical clone of `magnetConnectionMap`, invoked on the root DB. The + // magnet matching is intrinsically cross-design; `MagnetMap.getHierarchical` + // precomputes each magnet point's design context per sub-DB, then matches on + // the root-aware design tree (no flattening). + lazy val new_magnetConnectionMap: Map[ConnectPoint, ConnectPoint] = + if (!isRoot) rootDB.new_magnetConnectionMap + else MagnetMap.getHierarchical(this) + def checkDanglingPorts(): Unit = val assignmentsDclTable = assignmentsTable.keys @@ -963,9 +971,9 @@ final case class DB private ( // route every ref resolution / per-design table lookup to the OWNING sub-DB's // getSet (the root getSet throws). On sub-DBs and old-style flat DBs they // delegate to the root. Gated against the flat versions by - // `new_clkRstEquivalenceCheck` (run from SanityCheck) until the consuming - // stages (ExplicitClkRstCfg, AddClkRst) are migrated, after which the flat - // versions are dropped and these lose the `new_` prefix. + // `new_hierEquivalenceCheck` (run from SanityCheck) until the consuming + // stages are migrated, after which the flat versions are dropped and these + // lose the `new_` prefix. // =========================================================================== // Cross-design reaches navigate the DESIGN TREE (no global member index): @@ -1337,7 +1345,7 @@ final case class DB private ( // Temporary equivalence gate: on an old-style flat DB, assert the flat RT // clk/rst analyses match their new-style clones computed on `oldToNew`. // Dropped once the consuming stages are migrated and the flat versions removed. - def new_clkRstEquivalenceCheck(): Unit = + def new_hierEquivalenceCheck(): Unit = if (isOldStyleFlatDB) val newRoot = this.oldToNew def fail(name: String, flat: Any, neu: Any): Nothing = @@ -1356,7 +1364,9 @@ final case class DB private ( ) if (this.resolvedClkRstMap != newRoot.new_resolvedClkRstMap) fail("resolvedClkRstMap", this.resolvedClkRstMap, newRoot.new_resolvedClkRstMap) - end new_clkRstEquivalenceCheck + if (this.magnetConnectionMap != newRoot.new_magnetConnectionMap) + fail("magnetConnectionMap", this.magnetConnectionMap, newRoot.new_magnetConnectionMap) + end new_hierEquivalenceCheck /** Checks that device top design domains all have timing clock rate constraints. Additionally, if * there is an explicit clock rate configuration, it must match the timing constraint rate. diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/MagnetMap.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/MagnetMap.scala index 00d5c3872..698759799 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/MagnetMap.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/MagnetMap.scala @@ -2,6 +2,7 @@ package dfhdl.compiler.ir import dfhdl.internals.* import dfhdl.compiler.analysis.* import scala.collection.View +import scala.annotation.tailrec enum ConnectPoint(_dfType: DFType, _dir: DFVal.Modifier.Dir) derives CanEqual: case Via( @@ -236,4 +237,218 @@ object MagnetMap: ) ret end get + + // Hierarchical (new-style root DB) clone of `get`. Magnet matching is intrinsically + // cross-design: it pairs sources to targets across the whole hierarchy by distance. + // Approach: resolve each magnet ConnectPoint's design context (owner + container + // design) ONCE under its owning sub-DB getSet, then run the distance / inside-owner + // matching purely on the root-aware design tree (designBlockOwnershipMap) — no ref + // resolution during matching, so the throwing root getSet is fine. + def getHierarchical(rootDB: DB): MagnetMap = + // a magnet ConnectPoint with its design context precomputed under the + // owning sub-DB getSet (so the matching never resolves refs) + final case class RMP( + cp: ConnectPoint, + ownerDesign: DFDesignBlock, // cp.getOwnerDesign + containerDesign: DFDesignBlock, // design directly containing the point's member + ownerIsBlackBox: Boolean, + fullName: String, + position: Position + ): + def dfType: DFType = cp.dfType + def isPortIn: Boolean = cp.isPortIn + def isPortOut: Boolean = cp.isPortOut + def isVar: Boolean = cp.isVar + + var errors = List.empty[String] + def newError(errMsg: String): Option[RMP] = + errors = errMsg :: errors + None + + // Via magnet points: a design instance's magnet ports. The inst lives in a + // parent sub-DB; its magnet ports live in the instantiated child design's + // sub-DB — resolve each under the right getSet. + val viaRMPs: List[RMP] = rootDB.subDBs.values.iterator.flatMap { parentSub => + parentSub.atGetSet { + parentSub.membersNoGlobals.iterator.collect { case inst: DFDesignInst => inst }.flatMap { + inst => + val childDesign = inst.getDesignBlock + val containerDesign = inst.getOwnerDesign + val instFullName = inst.getFullName + val instPos = inst.meta.position + rootDB.subDBs.get(childDesign.ownerRef).iterator.flatMap { childSub => + childSub.atGetSet { + childDesign.members(MemberView.Folded).iterator.collect { + case dcl @ MagnetDcl(_) => + val cp = ConnectPoint.Via(inst, dcl) + RMP( + cp, + childDesign, + containerDesign, + childDesign.isBlackBox, + s"$instFullName.${dcl.getRelativeName(childDesign)}", + instPos + ) + } + } + } + } + } + }.toList + + // Direct magnet points: magnet ports/vars declared directly in a design. + val directRMPs: List[RMP] = rootDB.subDBs.values.iterator.flatMap { subDB => + subDB.atGetSet { + subDB.membersNoGlobals.iterator.collect { + case dcl @ MagnetDcl(_) => + val cp = ConnectPoint.Direct(dcl) + val ownerDesign = dcl.getOwnerDesign + RMP(cp, ownerDesign, ownerDesign, ownerDesign.isBlackBox, dcl.getFullName, + dcl.meta.position) + } + } + }.toList + + val allRMPs: List[RMP] = viaRMPs ++ directRMPs + + // magnet ports already explicitly connected/assigned (per sub-DB connectivity, unioned) + val alreadyConnectedOrAssignedDcls: Set[DFVal.Dcl] = + rootDB.subDBs.values.iterator.flatMap { subDB => + subDB.atGetSet { + subDB.assignmentsTable.keys.flatMap(_.dealias).collect { + case dcl @ MagnetDcl(_) if dcl.isPort => dcl + } ++ subDB.connectionTable.connectToVals.collect { + case dcl @ MagnetDcl(_) if dcl.isPort => dcl + } + } + }.toSet + val alreadyConnectedMPVias: Set[ConnectPoint.Via] = + rootDB.subDBs.values.iterator.flatMap { subDB => + subDB.atGetSet { + subDB.connectionTable.connectToVals.flatMap { + case dfVal @ Magnet(_) => + dfVal match + case pbns: DFVal.PortByNameSelect => Some(ConnectPoint.Via(pbns)) + case _ => None + case _ => None + } + } + }.toSet + + // is `x` the same design as `anc`, or a descendant of it, in the design tree + @tailrec def reaches( + frontier: Set[DFDesignBlock], + anc: DFDesignBlock, + seen: Set[DFDesignBlock] + ): Boolean = + if (frontier.isEmpty) false + else if (frontier.contains(anc)) true + else + reaches( + frontier.flatMap(rootDB.designBlockOwnershipMap.getOrElse(_, Set.empty)) -- seen, + anc, + seen ++ frontier + ) + def isDescendantOrSelf(x: DFDesignBlock, anc: DFDesignBlock): Boolean = + (x == anc) || reaches(rootDB.designBlockOwnershipMap.getOrElse(x, Set.empty), anc, Set(x)) + + // root getSet: the distance helpers only read designBlockOwnershipMap (root-aware) + // and never resolve refs on a DFDesignBlock, so this never throws. + given MemberGetSet = rootDB.getSet + + val groups: List[List[RMP]] = allRMPs.groupBy(_.dfType).values.map(_.toList).toList + val ret = groups.flatMap { grp => + grp.view.filter { rmp => + rmp.cp match + case ConnectPoint.Direct(dcl) + if rmp.isPortIn || rmp.isPortOut && rmp.ownerIsBlackBox || + alreadyConnectedOrAssignedDcls.contains(dcl) => + false + case via: ConnectPoint.Via if rmp.isPortOut || alreadyConnectedMPVias.contains(via) => + false + case _ => true + }.flatMap { targetRMP => + val targetDsn = targetRMP.ownerDesign + val sourceRMP: Option[RMP] = + if (targetRMP.isPortIn) + val sourceInCandidates = grp.filter { c => + c.cp match + case ConnectPoint.Direct(_) + if (c.isPortIn || c.isVar) && + isDescendantOrSelf(targetRMP.containerDesign, c.ownerDesign) => + true + case _ => false + }.map(src => (src, targetDsn.getDistanceFromOwnerDesign(src.ownerDesign))) + .sortBy(_._2) + val sourceOutCandidates = grp.filter { c => + c.cp match + case _: ConnectPoint.Via if c.isPortOut => true + case _ => false + }.map { src => + val mpDsn = src.ownerDesign + val commonDesign = targetDsn.getCommonDesignWith(mpDsn) + ( + src, + targetDsn.getDistanceFromOwnerDesign(commonDesign), + mpDsn.getDistanceFromOwnerDesign(commonDesign) + ) + }.sortBy(_._3).sortBy(_._2) + (sourceInCandidates, sourceOutCandidates) match + case (Nil, Nil) => None + case (Nil, (src, _, _) :: _) => Some(src) + case ((src, _) :: _, Nil) => Some(src) + case ((srcIn, distIn) :: _, (srcOut, distOut, _) :: _) => + if (distIn < distOut) Some(srcIn) + else + newError( + s"""|Found two possible magnet sources for a target magnet. + |Target Position: ${targetRMP.position} + |Target Path: ${targetRMP.fullName} + |Source1 Position: ${srcIn.position} + |Source1 Path: ${srcIn.fullName} + |Source2 Position: ${srcOut.position} + |Source2 Path: ${srcOut.fullName}""".stripMargin + ) + end match + else + val sourceOutCandidates = grp.filter { c => + c.cp match + case _: ConnectPoint.Via + if c.isPortOut && isDescendantOrSelf(c.containerDesign, targetDsn) => + true + case _: ConnectPoint.Direct + if c.isVar && isDescendantOrSelf(c.containerDesign, targetDsn) || + c.isPortIn && (c.ownerDesign == targetDsn) => + true + case _ => false + }.map(src => (src, src.ownerDesign.getDistanceFromOwnerDesign(targetDsn))) + .sortBy(_._2) + sourceOutCandidates match + case Nil => None + case (src, ld) :: otherCandidates => + var lastDistance: Int = ld + var lastSrc: RMP = src + otherCandidates.foreach { case (s, distance) => + if (distance == lastDistance) + newError( + s"""|Found two possible magnet sources for a target magnet. + |Target Position: ${targetRMP.position} + |Target Path: ${targetRMP.fullName} + |Source1 Position: ${lastSrc.position} + |Source1 Path: ${lastSrc.fullName} + |Source2 Position: ${s.position} + |Source2 Path: ${s.fullName}""".stripMargin + ) + lastDistance = distance + lastSrc = s + } + Some(src) + end match + sourceRMP.map(s => targetRMP.cp -> s.cp) + } + }.toMap + if (errors.nonEmpty) + throw new IllegalArgumentException(errors.view.reverse.mkString("\n\n")) + ret + end getHierarchical end MagnetMap diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala index 375c70df0..0e3b7102e 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala @@ -357,7 +357,7 @@ case class SanityCheck(skipAnonRefCheck: Boolean) extends Stage: ownershipCheck(designDB.top, designDB.membersNoGlobals.drop(1)) orderCheck() designDB.check() - designDB.new_clkRstEquivalenceCheck() + designDB.new_hierEquivalenceCheck() hierarchicalDBRoundTripCheck(designDB) designDB end SanityCheck From 6845e7101e03b4dcad63061d715d86813d66a241 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 13 Jun 2026 15:53:00 +0300 Subject: [PATCH 12/72] Migrate magnet stages off the flat DB Expose new_magnetPointInfo (each magnet ConnectPoint -> its owner design + name), precomputed cross-design by MagnetMap.getHierarchical, so migrating consumers never re-resolve a ConnectPoint that lives in another sub-DB. - ConnectMagnets: drop the rebindGetSet=false hack; classify connections via new_magnetConnectionMap + new_magnetPointInfo and the root-aware design tree (parentOf via designBlockOwnershipMap), create connections per sub-DB. - AddMagnets: flat Stage -> GlobalStage; accumulate missing magnets once over new_magnetConnectionMap (climbing designBlockOwnershipMap), then add each design's ports under its own sub-DB getSet. No stage sets rebindGetSet=false anymore. Full unit suite green (incl. AES). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 11 ++- .../scala/dfhdl/compiler/ir/MagnetMap.scala | 12 ++-- .../dfhdl/compiler/stages/AddMagnets.scala | 68 +++++++++++-------- .../compiler/stages/ConnectMagnets.scala | 35 ++++------ 4 files changed, 71 insertions(+), 55 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index 776f500df..4ef8052d7 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -605,10 +605,15 @@ final case class DB private ( // Hierarchical clone of `magnetConnectionMap`, invoked on the root DB. The // magnet matching is intrinsically cross-design; `MagnetMap.getHierarchical` // precomputes each magnet point's design context per sub-DB, then matches on - // the root-aware design tree (no flattening). - lazy val new_magnetConnectionMap: Map[ConnectPoint, ConnectPoint] = - if (!isRoot) rootDB.new_magnetConnectionMap + // the root-aware design tree (no flattening). It also returns each magnet + // point's (owner design, name) so migrating consumers don't re-resolve a + // cross-design ConnectPoint (which would need a flat member index). + private lazy val new_magnetData + : (Map[ConnectPoint, ConnectPoint], Map[ConnectPoint, (DFDesignBlock, String)]) = + if (!isRoot) rootDB.new_magnetData else MagnetMap.getHierarchical(this) + lazy val new_magnetConnectionMap: Map[ConnectPoint, ConnectPoint] = new_magnetData._1 + lazy val new_magnetPointInfo: Map[ConnectPoint, (DFDesignBlock, String)] = new_magnetData._2 def checkDanglingPorts(): Unit = val assignmentsDclTable = diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/MagnetMap.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/MagnetMap.scala index 698759799..92f3ff940 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/MagnetMap.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/MagnetMap.scala @@ -244,7 +244,7 @@ object MagnetMap: // design) ONCE under its owning sub-DB getSet, then run the distance / inside-owner // matching purely on the root-aware design tree (designBlockOwnershipMap) — no ref // resolution during matching, so the throwing root getSet is fine. - def getHierarchical(rootDB: DB): MagnetMap = + def getHierarchical(rootDB: DB): (MagnetMap, Map[ConnectPoint, (DFDesignBlock, String)]) = // a magnet ConnectPoint with its design context precomputed under the // owning sub-DB getSet (so the matching never resolves refs) final case class RMP( @@ -252,6 +252,7 @@ object MagnetMap: ownerDesign: DFDesignBlock, // cp.getOwnerDesign containerDesign: DFDesignBlock, // design directly containing the point's member ownerIsBlackBox: Boolean, + name: String, fullName: String, position: Position ): @@ -286,6 +287,7 @@ object MagnetMap: childDesign, containerDesign, childDesign.isBlackBox, + cp.getName, s"$instFullName.${dcl.getRelativeName(childDesign)}", instPos ) @@ -303,8 +305,8 @@ object MagnetMap: case dcl @ MagnetDcl(_) => val cp = ConnectPoint.Direct(dcl) val ownerDesign = dcl.getOwnerDesign - RMP(cp, ownerDesign, ownerDesign, ownerDesign.isBlackBox, dcl.getFullName, - dcl.meta.position) + RMP(cp, ownerDesign, ownerDesign, ownerDesign.isBlackBox, dcl.getName, + dcl.getFullName, dcl.meta.position) } } }.toList @@ -449,6 +451,8 @@ object MagnetMap: }.toMap if (errors.nonEmpty) throw new IllegalArgumentException(errors.view.reverse.mkString("\n\n")) - ret + val pointInfo: Map[ConnectPoint, (DFDesignBlock, String)] = + allRMPs.iterator.map(rmp => rmp.cp -> (rmp.ownerDesign, rmp.name)).toMap + (ret, pointInfo) end getHierarchical end MagnetMap diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddMagnets.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddMagnets.scala index ba68a36a3..9fda2639a 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddMagnets.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddMagnets.scala @@ -6,6 +6,7 @@ import dfhdl.compiler.patching.* import dfhdl.internals.* import dfhdl.options.CompilerOptions import scala.collection.mutable +import scala.collection.immutable.ListMap import dfhdl.core.DFOpaque as coreDFOpaque import dfhdl.core.{asFE, ModifierAny} import DFVal.Modifier.Dir.{IN, OUT} @@ -13,29 +14,36 @@ import DFVal.Modifier.Dir.{IN, OUT} /** This stage adds missing magnet ports across the entire design. These will be connected at a * later stage. */ -case object AddMagnets extends Stage: +case object AddMagnets extends GlobalStage: def dependencies: List[Stage] = List(AddClkRst) def nullifies: Set[Stage] = Set(ViaConnection, SimpleOrderMembers) - def transform(designDB: DB)(using MemberGetSet, CompilerOptions): DB = - given RefGen = RefGen.fromGetSet + def transformGlobal(designDB: DB)(using co: CompilerOptions, refGen: RefGen): DB = + // root getSet: only `getCommonDesignWith` uses it, and only on DFDesignBlocks + // (reads designBlockOwnershipMap, never resolves a ref), so it never throws. + given MemberGetSet = designDB.getSet + // owner design + name of each magnet point, precomputed cross-design by the + // analysis so a ConnectPoint living in another sub-DB is never re-resolved. + def ownerOf(cp: ConnectPoint): DFDesignBlock = designDB.new_magnetPointInfo(cp)._1 + def nameOf(cp: ConnectPoint): String = designDB.new_magnetPointInfo(cp)._2 // Populating a missing magnets map with the suggested port names and direction val missingMagnets = mutable.Map.empty[DFDesignBlock, Map[DFType, (String, DFVal.Modifier.Dir)]] - designDB.magnetConnectionMap.foreach { (toMP, fromMP) => - val toDsn = toMP.getOwnerDesign - val fromDsn = fromMP.getOwnerDesign + designDB.new_magnetConnectionMap.foreach { (toMP, fromMP) => + val toDsn = ownerOf(toMP) + val fromDsn = ownerOf(fromMP) + val fromName = nameOf(fromMP) val dfType = toMP.dfType def anotherMissingMagnet(dsn: DFDesignBlock, dir: DFVal.Modifier.Dir): Unit = missingMagnets.get(dsn) match - case None => missingMagnets += dsn -> Map(dfType -> (fromMP.getName, dir)) + case None => missingMagnets += dsn -> Map(dfType -> (fromName, dir)) case Some(dfTypeNameMap) if !dfTypeNameMap.contains(dfType) => - missingMagnets += dsn -> (dfTypeNameMap + (dfType -> (fromMP.getName, dir))) + missingMagnets += dsn -> (dfTypeNameMap + (dfType -> (fromName, dir))) case _ => // do nothing // climb from a bottom design to a top design, while memoizing missing // magnets between the designs. With DFDesignBlock.ownerRef == Empty the // lexical parent is no longer reachable via getOwnerDesign — walk up - // through `designBlockOwnershipMap` (parents-via-instances) instead. - // Multiple parents at any level are all visited; iteration stops once - // we reach `topDsn`. Both endpoints are excluded from the registration. + // through `designBlockOwnershipMap` (parents-via-instances, root-aware) + // instead. Multiple parents at any level are all visited; iteration stops + // once we reach `topDsn`. Both endpoints are excluded from the registration. def climbUpDsn( bottomDsn: DFDesignBlock, topDsn: DFDesignBlock, @@ -50,7 +58,7 @@ case object AddMagnets extends Stage: anotherMissingMagnet(dsn, dir) queue ++= designDB.designBlockOwnershipMap.getOrElse(dsn, Set.empty) def climbUp(bottomPort: ConnectPoint, topDsn: DFDesignBlock): Unit = - climbUpDsn(bottomPort.getOwnerDesign, topDsn, bottomPort.dir) + climbUpDsn(ownerOf(bottomPort), topDsn, bottomPort.dir) (toMP.dir, fromMP.dir) match // climbing up to the source input port case (IN, IN) => climbUp(toMP, fromDsn) @@ -66,21 +74,27 @@ case object AddMagnets extends Stage: case _ => // do nothing end match } - val patchList: List[(DFMember, Patch)] = designDB.designMemberList.flatMap { - // for all designs - case (design, _) if missingMagnets.contains(design) => - // sorting added magnets for consistent port addition order - val magnets = missingMagnets(design).toList.sortBy(_._2._1) - val dsn = new MetaDesign(design, Patch.Add.Config.InsideFirst): - for ((dfType, (name, dir)) <- magnets) - val modFE = new ModifierAny(DFVal.Modifier(dir, DFVal.Modifier.Special.Ordinary)) - (dfType.asFE[DFType] <> modFE)(using dfc.setName(name)) - // the ports are added as first members - Some(dsn.patch) - case _ => Nil - } - designDB.patch(patchList) - end transform + // Add each design's missing magnet ports under that design's own sub-DB getSet. + val newSubDBs = ListMap.from( + designDB.subDBs.iterator.map { case (key, subDB) => + val design = subDB.top + missingMagnets.get(design) match + case Some(magnetsMap) => + // sorting added magnets for consistent port addition order + val magnets = magnetsMap.toList.sortBy(_._2._1) + val dsn = subDB.atGetSet { + new MetaDesign(design, Patch.Add.Config.InsideFirst): + for ((dfType, (name, dir)) <- magnets) + val modFE = new ModifierAny(DFVal.Modifier(dir, DFVal.Modifier.Special.Ordinary)) + // the ports are added as first members + (dfType.asFE[DFType] <> modFE)(using dfc.setName(name)) + } + key -> subDB.patch(List(dsn.patch)) + case None => key -> subDB + } + ) + designDB.update(subDBs = newSubDBs) + end transformGlobal end AddMagnets extension [T: HasDB](t: T) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ConnectMagnets.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ConnectMagnets.scala index bffd20cc2..8cc593ac1 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ConnectMagnets.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ConnectMagnets.scala @@ -14,34 +14,27 @@ import dfhdl.core.{asFE, ModifierAny} case object ConnectMagnets extends HierarchyStage: def dependencies: List[Stage] = List(AddMagnets) def nullifies: Set[Stage] = Set(ViaConnection, SimpleOrderMembers) - // `toPort.asDclAny <> fromPort.asDclAny` uses refTW → getOwnerDesign on - // Dcls from potentially any design; magnetConnectionMap is also a - // global (flat) analysis. Run with the outer flat getSet. - override def rebindGetSet: Boolean = false - def transformSubDB(subDB: DB)(using MemberGetSet, CompilerOptions, RefGen): DB = + def transformSubDB(rootDB: DB)(using MemberGetSet, CompilerOptions, RefGen): DB = val design = subDB.top - val outer = getSet.designDB - // Collect magnet connections targeting this design (mirrors the - // classification rules of the flat version, but scoped). - // For (IN, OUT): connection lives in the design containing both - // ports' designs — i.e., the parent of `toMP.getOwnerDesign`. - // DFDesignBlock.ownerRef is Empty under the new model, so walk up - // via designBlockInstMap (first inst's owner). Singleton hierarchies - // resolve cleanly; multi-instance picks an arbitrary parent. + // owner design + name of each magnet point, precomputed cross-design by the + // analysis so a ConnectPoint living in another sub-DB is never re-resolved. + def ownerOf(cp: ConnectPoint): DFDesignBlock = rootDB.new_magnetPointInfo(cp)._1 + // a design's parent design via the root-aware design tree (no ref resolution). + // For (IN, OUT) the connection lives in the design containing both ports' + // designs — i.e. the parent of `toMP`'s design. def parentOf(d: DFDesignBlock): Option[DFDesignBlock] = - outer.designBlockInstMap.get(d).flatMap(_.headOption).map(_.getOwnerDesign) - val connsForDesign = outer.magnetConnectionMap.iterator.flatMap { case (toMP, fromMP) => + rootDB.designBlockOwnershipMap.get(d).flatMap(_.headOption) + val connsForDesign = rootDB.new_magnetConnectionMap.iterator.flatMap { case (toMP, fromMP) => val targetDsn = - if (toMP.isPortIn && (fromMP.isPortIn || fromMP.isVar)) Some(fromMP.getOwnerDesign) - else if (toMP.isPortOut && fromMP.isPortOut) Some(toMP.getOwnerDesign) - else if (toMP.isPortIn && fromMP.isPortOut) parentOf(toMP.getOwnerDesign) - else if (toMP.isPortOut && (fromMP.isPortIn || fromMP.isVar)) - Some(fromMP.getOwnerDesign) + if (toMP.isPortIn && (fromMP.isPortIn || fromMP.isVar)) Some(ownerOf(fromMP)) + else if (toMP.isPortOut && fromMP.isPortOut) Some(ownerOf(toMP)) + else if (toMP.isPortIn && fromMP.isPortOut) parentOf(ownerOf(toMP)) + else if (toMP.isPortOut && (fromMP.isPortIn || fromMP.isVar)) Some(ownerOf(fromMP)) else None if (targetDsn.contains(design)) Some((toMP, fromMP)) else None }.toList if (connsForDesign.nonEmpty) - val magnets = connsForDesign.sortBy(_._1.getName) + val magnets = connsForDesign.sortBy { case (toMP, _) => rootDB.new_magnetPointInfo(toMP)._2 } val dsn = new MetaDesign(design, Patch.Add.Config.InsideLast): extension (mp: ConnectPoint) def toDclAny(using MemberGetSet) = mp match From e72a8f0ccb7a573e1c1e2ec4ea8f074aa4fb81e6 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 13 Jun 2026 15:59:13 +0300 Subject: [PATCH 13/72] Remove the rebindGetSet knob from HierarchyStage No stage overrides rebindGetSet to false anymore (ExplicitClkRstCfg and ConnectMagnets were migrated to read the new_* root-DB analyses), so drop the knob and the outer-flat-getSet escape hatch: HierarchyStage.transformSubDB now always sees the root DB and the current sub-DB's getSet. Full unit suite green. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../dfhdl/compiler/stages/ExplicitClkRstCfg.scala | 4 ++-- .../main/scala/dfhdl/compiler/stages/Stage.scala | 14 +++++--------- .../main/scala/dfhdl/compiler/stages/ToED.scala | 3 +-- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ExplicitClkRstCfg.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ExplicitClkRstCfg.scala index 825aa708c..59eceaf39 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ExplicitClkRstCfg.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ExplicitClkRstCfg.scala @@ -18,8 +18,8 @@ case object ExplicitClkRstCfg extends HierarchyStage: def dependencies: List[Stage] = List(UniqueDesigns, NamedAnonMultiref) def nullifies: Set[Stage] = Set(DropUnreferencedAnons) // Cross-design clk/rst resolution (`new_resolvedClkRstMap` / `new_isDependentOn`) - // is read from the root DB; everything else is per-sub-DB, so the default - // `rebindGetSet = true` applies and `subDB` is this stage's current sub-DB. + // is read from the root DB; everything else is per-sub-DB via the `subDB` + // helper (this stage's current sub-DB). def transformSubDB(rootDB: DB)(using getSet: MemberGetSet, co: CompilerOptions, diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/Stage.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/Stage.scala index 18e47c73e..9a7ad3c7e 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/Stage.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/Stage.scala @@ -66,14 +66,11 @@ end GlobalStage * is returned by reference too. This lets iterative stages (e.g. `BreakOps`, * `DropUnreferencedAnons`) terminate via `result eq designDB`. * - * Configuration knob: - * - `rebindGetSet` (default `true`): rebind the implicit `MemberGetSet` to each DB's own getSet - * while `transformSubDB` runs. Set to `false` when the body needs full-hierarchy resolution - * (reverse lookups like `memberTable` / `getReadDeps`, or cross-design tables like - * `connectionTable` / `resolvedClkRstMap`) — those need the outer flat-DB getSet. + * `transformSubDB` always runs with the implicit `MemberGetSet` rebound to the current sub-DB's + * own getSet (so the `subDB` helper resolves that design's members); the root DB is passed as the + * parameter for cross-design needs (e.g. `rootDB.new_resolvedClkRstMap`). */ trait HierarchyStage extends Stage: - def rebindGetSet: Boolean = true final protected def subDB(using MemberGetSet): DB = getSet.designDB def transformSubDB(rootDB: DB)(using MemberGetSet, CompilerOptions, RefGen): DB @@ -83,9 +80,8 @@ trait HierarchyStage extends Stage: val newDB = designDB.oldToNew var changed = false def run(subDB: DB): DB = - val rebindDB = if (rebindGetSet) newDB else subDB - val rebindGS = if (rebindGetSet) subDB.getSet else designDB.getSet - val result = transformSubDB(rebindDB)(using rebindGS, co, refGen) + // `transformSubDB` always sees the root DB and the current sub-DB's getSet. + val result = transformSubDB(newDB)(using subDB.getSet, co, refGen) if (!(result eq subDB)) changed = true result val transformedSubs: ListMap[DFOwner.Ref, DB] = diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ToED.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ToED.scala index a4aa644ea..fca236d45 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ToED.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ToED.scala @@ -29,8 +29,7 @@ case object ToED extends HierarchyStage: ExplicitCondExprAssign, AddClkRst, SimpleOrderMembers) def nullifies: Set[Stage] = Set(DropUnreferencedAnons) // ToED is per-design: every domain owner it transforms, and the clk/rst Dcls - // it reads, live in the current sub-DB. The default `rebindGetSet = true` gives - // the sub-DB's getSet, and the `subDB` helper is that current design's DB. + // it reads, live in the current sub-DB (the `subDB` helper). def transformSubDB(rootDB: DB)(using getSet: MemberGetSet, co: CompilerOptions, From 49f64790fd1bdcf6254ad35a733f6f20f1d49761 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 13 Jun 2026 16:58:34 +0300 Subject: [PATCH 14/72] Migrate DropTimedRTWaits off flat resolvedClkRstMap; drop dead isDependentOn DropTimedRTWaits now reads the clk rate from the hierarchical rootDB.new_resolvedClkRstMap (lookup hoisted out of the MetaDesign into the sub-DB getSet context) instead of the flat resolvedClkRstMap. With that, the flat isDependentOn extension in DFOwnerAnalysis has no remaining caller (ExplicitClkRstCfg moved to new_isDependentOn earlier), so it is removed along with its now-unused tailrec import. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../dfhdl/compiler/analysis/DFOwnerAnalysis.scala | 10 ---------- .../dfhdl/compiler/stages/DropTimedRTWaits.scala | 13 ++++++++----- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/analysis/DFOwnerAnalysis.scala b/compiler/ir/src/main/scala/dfhdl/compiler/analysis/DFOwnerAnalysis.scala index 67580d18f..c731ab081 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/analysis/DFOwnerAnalysis.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/analysis/DFOwnerAnalysis.scala @@ -2,7 +2,6 @@ package dfhdl.compiler package analysis import dfhdl.internals.* import ir.* -import scala.annotation.tailrec import scala.collection.mutable extension (owner: DFOwner) def members(memberView: MemberView)(using MemberGetSet): List[DFMember] = @@ -22,15 +21,6 @@ extension (owner: DFOwner) case x => x end extension -extension (domainOwner: DFDomainOwner) - // true if the domainOwner is dependent at any level of thatDomainOwner's configuration - @tailrec def isDependentOn(thatDomainOwner: DFDomainOwner)(using getSet: MemberGetSet): Boolean = - getSet.designDB.dependentRTDomainOwners.get(domainOwner) match - case Some(dependency) => - if (dependency == thatDomainOwner) true - else dependency.isDependentOn(thatDomainOwner) - case None => false - extension (design: DFDesignBlock) // collect all local parameters that are used in IOs def getIOLocalParams(using getSet: MemberGetSet): List[DFVal.CanBeExpr] = diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropTimedRTWaits.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropTimedRTWaits.scala index 0d8ac9345..bb5c32878 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropTimedRTWaits.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropTimedRTWaits.scala @@ -29,17 +29,20 @@ case object DropTimedRTWaits extends HierarchyStage: _, _ ) if waitMember.isInRTDomain => + // The resolved clk rate comes from the root DB's hierarchical clk/rst map + // (cross-design resolution); the owner domain is resolved under this + // sub-DB's getSet. + val clkRate: RateNumber = rootDB.new_resolvedClkRstMap + .get(waitMember.getOwnerDomain) + .flatMap(_._1) + .flatMap(_.rate.toOption) + .get val dsn = new MetaDesign( waitMember, Patch.Add.Config.ReplaceWithLast(Patch.Replace.Config.FullReplacement), dfhdl.core.DomainType.RT ): val waitTime = duration.getConstData[TimeNumber].toOption.get - val clkRate: RateNumber = getSet.designDB.resolvedClkRstMap - .get(waitMember.getOwnerDomain) - .flatMap(_._1) - .flatMap(_.rate.toOption) - .get val cycles = (waitTime / clkRate.to_ps).value.toLong cycles.cy.wait(using dfc.setMeta(waitMember.meta)) dsn.patch From 3cce0fbc8b0f338af0a744dd08519ea5a1786d6b Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 13 Jun 2026 17:50:48 +0300 Subject: [PATCH 15/72] Split DB.check into representation-aware subDBCheck + rootDBCheck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check is now a lazy val that dispatches on the DB representation: - hierarchical root: run each sub-DB's subDBCheck, then rootDBCheck once; - in-hierarchy sub-DB: subDBCheck only; - old-style flat DB: both groups on itself (preserves today's behavior). subDBCheck holds the per-design structural checks (name, connectivity, direct-ref); rootDBCheck holds the whole-tree checks (magnet connectivity, dangling ports, derived-domain cycles, clk/rst rate, wait) plus the device-top port location/direction checks. rootDBCheck still uses the flat implementations and is dormant on the root for now — it will be rewired to the hierarchical new_* analyses and the call sites switched to oldToNew.check in a follow-up. The two callers (SanityCheck, Design) now read the lazy val. directRefCheck runs slightly earlier (before the magnet/dangling checks); the full suite confirms no check-ordering test depends on that. The device-top location/direction checks stay after domainClkRateCheck so the reported error is unchanged when a design lacks both a clock rate and a clock location. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 35 +++++++++++++++++-- .../dfhdl/compiler/stages/SanityCheck.scala | 2 +- core/src/main/scala/dfhdl/core/Design.scala | 2 +- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index 4ef8052d7..67ed7efb6 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -1713,18 +1713,47 @@ final case class DB private ( ) end portResourceDirCheck - def check(): Unit = + // Uniform entry point, representation-aware: + // - hierarchical root: run each sub-DB's per-design checks, then the + // cross-design root checks once on the root; + // - in-hierarchy sub-DB: run only its per-design checks; + // - old-style flat DB: the whole design lives in one DB, so run BOTH groups + // on itself (preserves legacy behavior). This is the path callers use + // today; the root path is dormant until `rootDBCheck` is rewired to the + // hierarchical `new_*` analyses and callers switch to `oldToNew.check`. + lazy val check: Unit = + if (isRoot) + subDBs.view.values.foreach(_.subDBCheck) + rootDBCheck + else if (isOldStyleFlatDB) + subDBCheck + rootDBCheck + else subDBCheck + + // Per-design structural checks: each validates a single design's own members + // and references in isolation. Run on each sub-DB (and on the flat DB, on the + // whole design). + private lazy val subDBCheck: Unit = nameCheck() connectionTable // causes connectivity checks + directRefCheck() + + // Whole-tree checks: cross-design connectivity / RT-domain correlation + // (dangling ports vs connections made at parent instantiation sites; + // derived-domain cycles; clk/rst rates) plus the device-top port + // location/direction checks (the device top is a single root-identified + // design). These consume the genuinely-global analyses (magnetConnectionMap, + // dependentRTDomainOwners, resolvedClkRstMap). Currently the flat + // implementations (correct on a flat DB); to be rewired to the hierarchical + // `new_*` analyses so they also run on the root. + private lazy val rootDBCheck: Unit = magnetConnectionMap // causes magnet connectivity checks checkDanglingPorts() - directRefCheck() circularDerivedDomainsCheck() domainClkRateCheck() waitCheck() portLocationCheck() portResourceDirCheck() - end check // There can only be a single connection to a value in a given range // (multiple assignments are possible) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala index 0e3b7102e..bec55d13a 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala @@ -356,7 +356,7 @@ case class SanityCheck(skipAnonRefCheck: Boolean) extends Stage: memberExistenceCheck() ownershipCheck(designDB.top, designDB.membersNoGlobals.drop(1)) orderCheck() - designDB.check() + designDB.check designDB.new_hierEquivalenceCheck() hierarchicalDBRoundTripCheck(designDB) designDB diff --git a/core/src/main/scala/dfhdl/core/Design.scala b/core/src/main/scala/dfhdl/core/Design.scala index f20d73559..7a6dc0026 100644 --- a/core/src/main/scala/dfhdl/core/Design.scala +++ b/core/src/main/scala/dfhdl/core/Design.scala @@ -151,7 +151,7 @@ trait Design extends Container, HasClsMetaArgs: try import Design.latchesCheck val designDB = dfc.mutableDB.immutable - designDB.check() // various checks post initial elaboration + designDB.check // various checks post initial elaboration designDB.latchesCheck() customTopChecks() // custom user/library checks catch From c95a6613208f45694501f5bb308437fff8db7a80 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 13 Jun 2026 18:09:49 +0300 Subject: [PATCH 16/72] Add hierarchical new_circularDerivedDomainsCheck (Step 2b) First rootDBCheck member rewired to run on the hierarchical root: same DFS as the flat check but over new_dependentRTDomainOwners, naming cycle members via new_fullNameViaInst routed through each owner's sub-DB (domainOwnerToSubDB). Wired into new_hierEquivalenceCheck so it runs on every design that passes the flat check, asserting it does not falsely flag a valid design. Error-message exactness for the cycle case is validated by ElaborationChecksSpec once callers switch to oldToNew.check (Step 2c). rootDBCheck itself is unchanged (still flat, still the production path) until all members are rewired. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index 67ed7efb6..0f9adae94 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -1371,6 +1371,11 @@ final case class DB private ( fail("resolvedClkRstMap", this.resolvedClkRstMap, newRoot.new_resolvedClkRstMap) if (this.magnetConnectionMap != newRoot.new_magnetConnectionMap) fail("magnetConnectionMap", this.magnetConnectionMap, newRoot.new_magnetConnectionMap) + // rootDBCheck members already rewired to hierarchical form: assert they do + // not falsely flag this design, which just passed the flat check. (Error + // tests validate that failing designs still throw the right message once + // callers switch to oldToNew.check.) + newRoot.new_circularDerivedDomainsCheck() end new_hierEquivalenceCheck /** Checks that device top design domains all have timing clock rate constraints. Additionally, if @@ -1496,6 +1501,30 @@ final case class DB private ( dfs(node, Set.empty, Set.empty) end circularDerivedDomainsCheck + // Hierarchical clone of `circularDerivedDomainsCheck`, invoked on the root DB. + // Same DFS, over `new_dependentRTDomainOwners`; the cycle error names each + // owner via `new_fullNameViaInst` routed through that owner's sub-DB. + def new_circularDerivedDomainsCheck(): Unit = + @tailrec def dfs( + node: DFDomainOwner, + visited: Set[DFDomainOwner], + stack: Set[DFDomainOwner] + ): Unit = + if (stack.contains(node)) + throw new IllegalArgumentException( + s"""|Circular derived RT configuration detected. Involved in the cycle: + |${stack.map(o => new_fullNameViaInst(o, domainOwnerToSubDB(o))).mkString("\n")} + |""".stripMargin + ) + if (!visited.contains(node)) + new_dependentRTDomainOwners.get(node) match + case Some(dependentNode) => dfs(dependentNode, visited + node, stack + node) + case None => + end dfs + for (node <- new_dependentRTDomainOwners.keys) + dfs(node, Set.empty, Set.empty) + end new_circularDerivedDomainsCheck + def nameCheck(): Unit = // We use a Set since meta programming is usually the cause and can result in // multiple anonymous members with the same position. The top can be anonymous. From 95e4e61fc8a672b28d5661944aa6a48dfc5c634e Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 13 Jun 2026 18:16:55 +0300 Subject: [PATCH 17/72] Add hierarchical new_domainClkRateCheck (Step 2b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second rootDBCheck member rewired for the root. The flat usesClk filter is cross-design and flat-only, but is equivalent to "the resolver produced a clock" — new_resolvedClkRstMap(owner)._1.isDefined — so the resolved-clock map drives both the device-top filter and the explicit rate. Per-owner local reads (getThisOrOwnerDesign.isDeviceTop, isTop position, getFullName, the clk timing constraint) are routed through the owner's sub-DB getSet via atOwner, mirroring new_resolvedClkRstMap. Device-top domains live in the toptop's sub-DB, so their getFullName is the correct full path. Wired into new_hierEquivalenceCheck so every passing design confirms it does not falsely flag a valid design; the error message is validated by ElaborationChecksSpec's "clock missing timing constraint check" at Step 2c. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index 0f9adae94..b39c5bf37 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -1376,6 +1376,7 @@ final case class DB private ( // tests validate that failing designs still throw the right message once // callers switch to oldToNew.check.) newRoot.new_circularDerivedDomainsCheck() + newRoot.new_domainClkRateCheck() end new_hierEquivalenceCheck /** Checks that device top design domains all have timing clock rate constraints. Additionally, if @@ -1429,6 +1430,65 @@ final case class DB private ( throw new IllegalArgumentException(errors.mkString("\n")) end domainClkRateCheck + // Hierarchical clone of `domainClkRateCheck`, invoked on the root DB. The flat + // `usesClk` filter (cross-design, flat-only) is equivalent to "the resolver + // produced a clock" (new_resolvedClkRstMap(owner)._1.isDefined), so the + // resolved-clock map drives both the filter and the explicit rate. Per-owner + // local reads (isDeviceTop, isTop position, getFullName, the clk timing + // constraint) are routed through the owner's sub-DB getSet via `atOwner`. + def new_domainClkRateCheck(): Unit = + def atOwner[T](owner: DFDomainOwner)(f: MemberGetSet ?=> T): T = + domainOwnerToSubDB(owner).atGetSet(f) + val errors = collection.mutable.ArrayBuffer[String]() + domainOwnerMemberList.view.map(_._1).foreach { domainOwner => + val resolvedClkOpt = new_resolvedClkRstMap.get(domainOwner).flatMap(_._1) + if ( + resolvedClkOpt.isDefined && + atOwner(domainOwner)(domainOwner.getThisOrOwnerDesign.isDeviceTop) + ) + def waitError(msg: String): Unit = + val pos = atOwner(domainOwner) { + if (domainOwner.isTop) domainOwner.asInstanceOf[DFDesignBlock].dclMeta.position + else domainOwner.meta.position + } + errors += s"""|DFiant HDL domain clock rate error! + |Position: ${pos} + |Hierarchy: ${atOwner(domainOwner)(domainOwner.getFullName)} + |Message: $msg""".stripMargin + val explicitRateOpt = resolvedClkOpt.flatMap(_.rate.toOption) + val timingConstraintRateOpt = + atOwner(domainOwner)(domainOwner.getTimingConstraintClkRateOpt) + (explicitRateOpt, timingConstraintRateOpt) match + case (Some(explicitRate), Some(timingConstraintRate)) => + if (explicitRate.to_freq.to_hz != timingConstraintRate.to_freq.to_hz) + waitError( + s"""|Mismatch between domain clock rate configuration ($explicitRate) and timing constraint rate ($timingConstraintRate). + |To fix, do one of the following: + |* Connect a different clock resource to the domain to match your configuration. + |* Explicitly set the clock rate configuration to $timingConstraintRate. + |* Remove the domain clock rate configuration and let it be derived from the timing constraint.""".stripMargin + ) + case (Some(explicitRate), None) => + waitError( + s"""|Missing clock rate timing constraint. + |To Fix: + |Connect a $explicitRate clock resource to the domain to match your configuration.""".stripMargin + ) + case (None, None) => + waitError( + s"""|Missing clock rate timing constraint. + |To Fix: + |Connect the wanted clock resource to the domain. + |(the domain will automatically derive the clock rate from the resource).""".stripMargin + ) + case _ => + end match + end if + } + if (errors.nonEmpty) + throw new IllegalArgumentException(errors.mkString("\n")) + end new_domainClkRateCheck + def waitCheck(): Unit = val errors = collection.mutable.ArrayBuffer[String]() for From 220b4493d911e3dc1c05fde2a8995c3682e44515 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 13 Jun 2026 18:20:13 +0300 Subject: [PATCH 18/72] Add hierarchical new_waitCheck (Step 2b) Third rootDBCheck member rewired. Iterates each sub-DB's RT waits under that sub-DB's getSet (the root has no members of its own) and reads the clock rate from new_resolvedClkRstMap. The error hierarchy uses new_fullNameViaInst so a wait in a nested design reports its full instance path (plain getFullName on a sub-DB would be relative). Wired into new_hierEquivalenceCheck for passing-design validation; the wait error messages are validated by ElaborationChecksSpec at Step 2c. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index b39c5bf37..28a155016 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -1377,6 +1377,7 @@ final case class DB private ( // callers switch to oldToNew.check.) newRoot.new_circularDerivedDomainsCheck() newRoot.new_domainClkRateCheck() + newRoot.new_waitCheck() end new_hierEquivalenceCheck /** Checks that device top design domains all have timing clock rate constraints. Additionally, if @@ -1536,6 +1537,54 @@ final case class DB private ( throw new IllegalArgumentException(errors.mkString("\n")) end waitCheck + // Hierarchical clone of `waitCheck`, invoked on the root DB. Iterates each + // sub-DB's RT waits under that sub-DB's getSet (the root has no members of its + // own); the clock rate comes from new_resolvedClkRstMap. The error hierarchy + // uses new_fullNameViaInst so a wait in a nested design reports its full + // instance path (plain getFullName on a sub-DB would be relative). + def new_waitCheck(): Unit = + val errors = collection.mutable.ArrayBuffer[String]() + subDBs.view.values.foreach { sub => + sub.atGetSet { + for + wait <- sub.members.collect { case w: Wait if w.isInRTDomain => w } + trigger = wait.triggerRef.get + if trigger.dfType == DFTime + do + def waitError(msg: String): Unit = + errors += s"""|DFiant HDL wait error! + |Position: ${wait.meta.position} + |Hierarchy: ${new_fullNameViaInst(wait.getOwnerDesign, sub)} + |Message: $msg""".stripMargin + val ownerDomain = wait.getOwnerDomain + trigger.getConstData[TimeNumber].toOption match + case Some(waitTime) => + new_resolvedClkRstMap.get(ownerDomain).flatMap(_._1).flatMap(_.rate.toOption) match + case Some(rate) => + val clockPeriodPs = rate.to_ps.value + val desc = rate match + case time: TimeNumber => s"period ${time}" + case freq: FreqNumber => s"frequency ${freq}" + val waitDurationPs = waitTime.to_ps.value + if (!(waitDurationPs / clockPeriodPs).isWhole) + waitError( + s"Wait duration ${waitTime} is not exactly divisible by the clock $desc." + ) + case _ => + waitError( + s"Wait statement is missing an explicit clock configuration in its domain." + ) + end match + case _ => + waitError(s"Wait duration is not constant.") + end match + end for + } + } + if (errors.nonEmpty) + throw new IllegalArgumentException(errors.mkString("\n")) + end new_waitCheck + def circularDerivedDomainsCheck(): Unit = // Helper function to perform DFS and detect cycles @tailrec def dfs( From 0ede1f5f6244713db0e69318c8b9f8f53594e692 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 13 Jun 2026 18:23:28 +0300 Subject: [PATCH 19/72] Add hierarchical new_portLocationCheck + new_portResourceDirCheck (Step 2b) Two device-top-only checks rewired for the root. Each iterates designMemberList (root-aware), and for the device-top design resolves its members under that design's sub-DB getSet via domainOwnerToSubDB(design).atGetSet. Device top == toptop, so getFullName yields the correct full path. Bodies are otherwise identical to the flat checks. Wired into new_hierEquivalenceCheck (the Platform device-top design exercises them); error messages validated at Step 2c. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index 28a155016..7fbd8035b 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -1378,6 +1378,8 @@ final case class DB private ( newRoot.new_circularDerivedDomainsCheck() newRoot.new_domainClkRateCheck() newRoot.new_waitCheck() + newRoot.new_portLocationCheck() + newRoot.new_portResourceDirCheck() end new_hierEquivalenceCheck /** Checks that device top design domains all have timing clock rate constraints. Additionally, if @@ -1851,6 +1853,127 @@ final case class DB private ( ) end portResourceDirCheck + // Hierarchical clone of `portLocationCheck`, invoked on the root DB. Only the + // device-top design is examined; its members are resolved under that design's + // sub-DB getSet (device top == toptop, so getFullName is the full path). + def new_portLocationCheck(): Unit = + val errors = mutable.ListBuffer.empty[String] + val locationCollisions = mutable.ListBuffer.empty[String] + designMemberList.foreach { + case (design, members) if design.isDeviceTop => + domainOwnerToSubDB(design).atGetSet { + val locationMap = mutable.Map.empty[String, String] // loc -> portName(idx) + (design :: members).foreach { + case designInstance: DFDesignBlock if designInstance != design => + case domainOwner: DFDomainOwner => + domainOwner.domainType match + case DomainType.RT => + var foundLoc = false + domainOwner.getDomainClkConstraintsView.foreach { + case constraints.IO(loc = loc: String) => + locationMap.get(loc).foreach { prevPort => + locationCollisions += + s"${prevPort} and ${domainOwner.getFullName} are both assigned to location `${loc}`" + } + locationMap += loc -> domainOwner.getFullName + foundLoc = true + case _ => + } + val clkIsVar = domainOwnerMemberTable(domainOwner).view.collectFirst { + case dcl: DFVal.Dcl if dcl.isClkDcl => dcl.isVar + }.getOrElse(false) + if (!foundLoc && !clkIsVar) + errors += s"${domainOwner.getFullName} is missing a clock location constraint" + case _ => + end match + case clkPort: DFVal.Dcl if clkPort.isPortIn && clkPort.isClkDcl => + case port: DFVal.Dcl if port.isPort => + val bitSet = port.widthIntOpt match + case Some(width) => mutable.BitSet((0 until width)*) + case None => mutable.BitSet.empty + port.meta.annotations.foreach { + case constraints.IO(bitIdx = None, loc = loc: String) => + bitSet.clear() + locationMap.get(loc).foreach { prevPort => + locationCollisions += + s"${prevPort} and ${port.getFullName} are both assigned to location `${loc}`" + } + locationMap += loc -> port.getFullName + if (port.widthIntOpt.get != 1) + locationCollisions += + s"${port.getFullName} has mutliple bits assigned to location `${loc}`" + case constraints.IO(bitIdx = bitIdx: Int, loc = loc: String) => + locationMap.get(loc).foreach { prevPort => + locationCollisions += + s"${prevPort} and ${port.getFullName}(${bitIdx}) are both assigned to location `${loc}`" + } + locationMap += loc -> s"${port.getFullName}(${bitIdx})" + bitSet -= bitIdx + case _ => + } + if (bitSet.nonEmpty) + if (port.widthIntOpt.get == 1) + errors += s"${port.getFullName}" + else + errors += s"${port.getFullName} with bits ${bitSet.mkString(", ")}" + case _ => + } + } + case _ => + } + if (errors.nonEmpty) + throw new IllegalArgumentException( + s"""|The following top device design ports or domains are missing location constraints: + | ${errors.mkString("\n ")} + |To Fix: + |Add a location constraint to the ports by connecting them to a located resource or + |by using the `@io` constraint. + |""".stripMargin + ) + if (locationCollisions.nonEmpty) + throw new IllegalArgumentException( + s"""|The following location constraints have collisions: + | ${locationCollisions.mkString("\n ")} + |To Fix: + |Ensure each location is used by a single port bit. + |""".stripMargin + ) + end new_portLocationCheck + + // Hierarchical clone of `portResourceDirCheck`, invoked on the root DB. Same + // device-top-only check, members resolved under the device-top sub-DB getSet. + def new_portResourceDirCheck(): Unit = + import DFVal.Modifier.Dir + val errors = mutable.ListBuffer.empty[String] + designMemberList.foreach { + case (design, members) if design.isDeviceTop => + domainOwnerToSubDB(design).atGetSet { + members.foreach { + case port: DFVal.Dcl if port.isPort => + port.meta.annotations.foreach { + case constraints.IO(dir = dir: Dir) => + (dir, port.modifier.dir) match + case (Dir.IN, Dir.OUT) | (Dir.OUT, Dir.IN) => + errors += + s"${port.getFullName} direction (${port.modifier.dir}) has a resource direction ($dir) mismatch." + case _ => + case _ => + } + case _ => + } + } + case _ => + } + if (errors.nonEmpty) + throw new IllegalArgumentException( + s"""|The following top device design ports have resource direction mismatches: + | ${errors.mkString("\n ")} + |To Fix: + |Make sure you connect the resource to the port with the correct direction. + |""".stripMargin + ) + end new_portResourceDirCheck + // Uniform entry point, representation-aware: // - hierarchical root: run each sub-DB's per-design checks, then the // cross-design root checks once on the root; From 1567c3e3297d8379a5eb209234dfd0b6bd905e17 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 13 Jun 2026 18:41:48 +0300 Subject: [PATCH 20/72] Add hierarchical new_checkDanglingPorts (Step 2b complete) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Last rootDBCheck member rewired. Aggregates assignment coverage and the connected-point set across all sub-DBs (each design's assignments/connections live in its own sub-DB) plus the cross-design magnet connections. Input ports are checked from each parent sub-DB (which owns the instance + its connections), reading the child design's port list via the root-aware designMemberTable; output ports per instantiated design (designBlockInstMap keys, matching the flat version — the toptop's own ports are device IO, not dangling) under its own sub-DB getSet. The dcl width is resolved inside the sub-DB getSet (a DFBits width-param ref can't resolve against the root getSet). Wired into new_hierEquivalenceCheck; no false positives across the suite. All seven rootDBCheck members now have hierarchical clones (magnet is covered by the existing map comparison). Error messages validated at Step 2c. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index 7fbd8035b..e4b3931fb 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -667,6 +667,83 @@ final case class DB private ( ) end checkDanglingPorts + // Hierarchical clone of `checkDanglingPorts`, invoked on the root DB. The + // assignment coverage and the connected-point set are aggregated across all + // sub-DBs (each design's assignments/connections live in its own sub-DB) plus + // the cross-design magnet connections. Input ports are checked from each + // parent sub-DB (which owns the instance and its connections), reading the + // child design's port list via the root-aware designMemberTable; output ports + // are checked per instantiated design under its own sub-DB getSet. Only + // instantiated designs (designBlockInstMap keys) are checked, matching the + // flat version (the toptop's own ports are device IO, not dangling). + def new_checkDanglingPorts(): Unit = + val assignmentsDclTable: Map[DFVal.Dcl, Coverage] = + subDBs.view.values.flatMap { sub => + // resolve the dcl width inside the sub-DB's getSet (a DFBits width-param + // ref can't be resolved against the root getSet) + sub.atGetSet { + sub.assignmentsTable.keys.flatMap(_.departialDcl).map { case (dcl, slice) => + (dcl, slice, dcl.dfType.widthIntOpt) + }.toList + } + }.foldLeft(Map.empty[DFVal.Dcl, Coverage]) { case (acc, (dcl, slice, widthOpt)) => + acc.updated(dcl, acc.getOrElse(dcl, Coverage.empty).assign(slice, widthOpt)) + } + val alreadyConnectedPoints: Set[ConnectPoint] = + subDBs.view.values.flatMap { sub => + sub.atGetSet { + sub.connectionTable.connectToVals.view.collect { + case dcl: DFVal.Dcl => ConnectPoint.Direct(dcl) + case pbns: DFVal.PortByNameSelect => ConnectPoint.Via(pbns) + }.toList + } + }.toSet ++ new_magnetConnectionMap.keySet + // input ports: checked from the parent sub-DB that owns the instance + val danglingInputs = subDBs.view.values.flatMap { parentSub => + parentSub.atGetSet { + parentSub.membersNoGlobals.view.collect { case inst: DFDesignInst => inst }.flatMap { + designInst => + val childDesign = designInst.getDesignBlock + val instFullName = designInst.getFullName + val instPos = designInst.meta.position + domainOwnerToSubDB(childDesign).atGetSet { + designMemberTable(childDesign).view.collect { + case port: DFVal.Dcl if port.isPortIn && !port.isClkDcl && !port.isRstDcl => + (ConnectPoint.Via(designInst, port), port.getName) + }.toList + }.flatMap { case (via, portName) => + if (alreadyConnectedPoints.contains(via)) None + else + Some( + s"""|DFiant HDL connectivity error! + |Position: ${instPos} + |Hierarchy: ${instFullName} + |Message: Found a dangling (unconnected) input port `${portName}`.""".stripMargin + ) + } + }.toList + } + } + // output ports: checked per instantiated design under its own sub-DB getSet + val danglingOutputs = designBlockInstMap.keys.view.flatMap { design => + domainOwnerToSubDB(design).atGetSet { + designMemberTable(design).view.collect { + case port: DFVal.Dcl + if port.isPortOut && !design.isBlackBox && !port.hasNonBubbleInit && + !assignmentsDclTable.contains(port) && + !alreadyConnectedPoints.contains(ConnectPoint.Direct(port)) => + s"""|DFiant HDL connectivity error! + |Position: ${port.meta.position} + |Hierarchy: ${design.getFullName} + |Message: Found a dangling (unconnected/unassigned and uninitialized) output port `${port.getName}`.""".stripMargin + }.toList + } + } + val danglingPorts = (danglingInputs ++ danglingOutputs).toList + if (danglingPorts.nonEmpty) + throw new IllegalArgumentException(danglingPorts.mkString("\n")) + end new_checkDanglingPorts + extension (domainOwner: DFDomainOwner) // Aggregates `@hw.constraints.IO` / `@timing.clock` annotations applied at // the domain owner and on its (single) clk-port-in declaration. Used by the @@ -1380,6 +1457,7 @@ final case class DB private ( newRoot.new_waitCheck() newRoot.new_portLocationCheck() newRoot.new_portResourceDirCheck() + newRoot.new_checkDanglingPorts() end new_hierEquivalenceCheck /** Checks that device top design domains all have timing clock rate constraints. Additionally, if From 16b873e30aa912b35bc4add3307c88b5f11828a2 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 13 Jun 2026 19:02:43 +0300 Subject: [PATCH 21/72] Run the hierarchical rootDBCheck on the root; flip call sites (Step 2c) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check's isRoot branch now runs new_rootDBCheck (the new_* clones) instead of the flat rootDBCheck, and both callers (SanityCheck.transform, Design post-elaboration) call designDB.oldToNew.check, so the production connectivity/RT/device-top checks run on the hierarchical root. The flat rootDBCheck remains only on the (now unreached) isOldStyleFlatDB branch — dead, to be removed in Step 3. Two fixes surfaced by the error tests once the hierarchical path went live: - new_fullNameViaInst now mirrors the flat naming (design CLASS name, e.g. Internal2.dmn) by resolving the enclosing design in its own sub-DB rather than via the parent instance path. - new_portLocationCheck iterates `members` directly: the root-aware designMemberList already includes the design block as the head, so the flat code's `design :: members` would double-count it (spurious Top-vs-Top collisions / doubled "missing" errors). Removed the now-redundant new_* check calls from new_hierEquivalenceCheck (the production root path exercises them); the gate keeps the analysis comparisons. Full suite green (StagesSpec 434, CoreSpec 74, lib 152, platforms 1), ElaborationChecksSpec error messages match. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 63 +++++++++---------- .../dfhdl/compiler/stages/SanityCheck.scala | 2 +- core/src/main/scala/dfhdl/core/Design.scala | 2 +- 3 files changed, 30 insertions(+), 37 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index e4b3931fb..db2a63a60 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -1142,25 +1142,18 @@ final case class DB private ( // Routed, best-effort clone of `fullNameViaInst` (error messages only). // `owner` lives in `ownerSub`; instance paths are recovered by navigating UP. private def new_fullNameViaInst(owner: DFDomainOwner, ownerSub: DB): String = - def instFullName(d: DFDesignBlock, dSub: DB): Option[String] = - dSub.parentSubDBOpt.flatMap { parentSub => - parentSub.atGetSet { - parentSub.membersNoGlobals.view.collectFirst { - case inst: DFDesignInst if inst.getDesignBlock eq d => inst.getFullName - } - } - } val design = ownerSub.atGetSet(owner.getThisOrOwnerDesign) if (design eq topDB.top) ownerSub.atGetSet(owner.getFullName) else + // Mirror the flat `fullNameViaInst`, whose `inst.getFullName` yields the + // design CLASS name (e.g. `Internal2`), not the instance path: resolve the + // enclosing design by its own full name in its sub-DB (where it is the + // top), then append the owner's design-relative name. owner match - case d: DFDesignBlock => - instFullName(d, ownerSub).getOrElse(ownerSub.atGetSet(owner.getFullName)) - case _ => - instFullName(design, ownerSub) match - case Some(instName) => - s"$instName.${ownerSub.atGetSet(owner.getRelativeName(design))}" - case None => ownerSub.atGetSet(owner.getFullName) + case d: DFDesignBlock => ownerSub.atGetSet(d.getFullName) + case _ => + val designName = ownerSub.atGetSet(design.getFullName) + s"$designName.${ownerSub.atGetSet(owner.getRelativeName(design))}" end new_fullNameViaInst lazy val new_dependentRTDomainOwners: Map[DFDomainOwner, DFDomainOwner] = @@ -1448,16 +1441,6 @@ final case class DB private ( fail("resolvedClkRstMap", this.resolvedClkRstMap, newRoot.new_resolvedClkRstMap) if (this.magnetConnectionMap != newRoot.new_magnetConnectionMap) fail("magnetConnectionMap", this.magnetConnectionMap, newRoot.new_magnetConnectionMap) - // rootDBCheck members already rewired to hierarchical form: assert they do - // not falsely flag this design, which just passed the flat check. (Error - // tests validate that failing designs still throw the right message once - // callers switch to oldToNew.check.) - newRoot.new_circularDerivedDomainsCheck() - newRoot.new_domainClkRateCheck() - newRoot.new_waitCheck() - newRoot.new_portLocationCheck() - newRoot.new_portResourceDirCheck() - newRoot.new_checkDanglingPorts() end new_hierEquivalenceCheck /** Checks that device top design domains all have timing clock rate constraints. Additionally, if @@ -1941,7 +1924,10 @@ final case class DB private ( case (design, members) if design.isDeviceTop => domainOwnerToSubDB(design).atGetSet { val locationMap = mutable.Map.empty[String, String] // loc -> portName(idx) - (design :: members).foreach { + // the root-aware designMemberList already includes the design block as + // the head of its member list, so iterate `members` directly (the flat + // version prepended `design ::` to a children-only list). + members.foreach { case designInstance: DFDesignBlock if designInstance != design => case domainOwner: DFDomainOwner => domainOwner.domainType match @@ -2063,7 +2049,7 @@ final case class DB private ( lazy val check: Unit = if (isRoot) subDBs.view.values.foreach(_.subDBCheck) - rootDBCheck + new_rootDBCheck else if (isOldStyleFlatDB) subDBCheck rootDBCheck @@ -2077,14 +2063,9 @@ final case class DB private ( connectionTable // causes connectivity checks directRefCheck() - // Whole-tree checks: cross-design connectivity / RT-domain correlation - // (dangling ports vs connections made at parent instantiation sites; - // derived-domain cycles; clk/rst rates) plus the device-top port - // location/direction checks (the device top is a single root-identified - // design). These consume the genuinely-global analyses (magnetConnectionMap, - // dependentRTDomainOwners, resolvedClkRstMap). Currently the flat - // implementations (correct on a flat DB); to be rewired to the hierarchical - // `new_*` analyses so they also run on the root. + // Whole-tree checks, FLAT implementation. Used only on an old-style flat DB + // (the `isOldStyleFlatDB` branch of `check`); the hierarchical root path uses + // `new_rootDBCheck`. Dead once the flat DB is gone (Step 3). private lazy val rootDBCheck: Unit = magnetConnectionMap // causes magnet connectivity checks checkDanglingPorts() @@ -2094,6 +2075,18 @@ final case class DB private ( portLocationCheck() portResourceDirCheck() + // Whole-tree checks, HIERARCHICAL implementation. Run once on the root: the + // same cross-design connectivity / RT-domain / device-top checks as + // `rootDBCheck`, via the `new_*` clones that navigate the sub-DB tree. + private lazy val new_rootDBCheck: Unit = + new_magnetConnectionMap // causes magnet connectivity checks + new_checkDanglingPorts() + new_circularDerivedDomainsCheck() + new_domainClkRateCheck() + new_waitCheck() + new_portLocationCheck() + new_portResourceDirCheck() + // There can only be a single connection to a value in a given range // (multiple assignments are possible) lazy val connectionTable: ConnectToMap = diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala index bec55d13a..bbff7af25 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala @@ -356,7 +356,7 @@ case class SanityCheck(skipAnonRefCheck: Boolean) extends Stage: memberExistenceCheck() ownershipCheck(designDB.top, designDB.membersNoGlobals.drop(1)) orderCheck() - designDB.check + designDB.oldToNew.check designDB.new_hierEquivalenceCheck() hierarchicalDBRoundTripCheck(designDB) designDB diff --git a/core/src/main/scala/dfhdl/core/Design.scala b/core/src/main/scala/dfhdl/core/Design.scala index 7a6dc0026..1177b87b4 100644 --- a/core/src/main/scala/dfhdl/core/Design.scala +++ b/core/src/main/scala/dfhdl/core/Design.scala @@ -151,7 +151,7 @@ trait Design extends Container, HasClsMetaArgs: try import Design.latchesCheck val designDB = dfc.mutableDB.immutable - designDB.check // various checks post initial elaboration + designDB.oldToNew.check // various checks post initial elaboration designDB.latchesCheck() customTopChecks() // custom user/library checks catch From 27546431c9fc92e7880717deed335fa807ffa651 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 13 Jun 2026 19:23:12 +0300 Subject: [PATCH 22/72] Delete dead flat SanityCheck methods (Step 3a) Production runs the hierarchical new_rootDBCheck on the root via oldToNew.check; the flat rootDBCheck and the six flat check methods were only reachable through the now-unreached isOldStyleFlatDB branch of `check`. Remove that branch and the dead flat checks (checkDanglingPorts, circularDerivedDomainsCheck, domainClkRateCheck, waitCheck, portLocationCheck, portResourceDirCheck). The flat clk/rst analyses they consumed stay alive via the equivalence gate and the DFMember CLK_FREQ folder until Steps 3b/3c. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 323 +----------------- 1 file changed, 5 insertions(+), 318 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index db2a63a60..10e1efbdc 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -615,58 +615,6 @@ final case class DB private ( lazy val new_magnetConnectionMap: Map[ConnectPoint, ConnectPoint] = new_magnetData._1 lazy val new_magnetPointInfo: Map[ConnectPoint, (DFDesignBlock, String)] = new_magnetData._2 - def checkDanglingPorts(): Unit = - val assignmentsDclTable = - assignmentsTable.keys - .flatMap(_.departialDcl) - .foldLeft(Map.empty[DFVal.Dcl, Coverage]) { case (acc, (dcl, slice)) => - acc.updated( - dcl, - acc.getOrElse(dcl, Coverage.empty).assign(slice, dcl.dfType.widthIntOpt) - ) - } - val alreadyConnectedPoints = connectionTable.connectToVals.view.collect { - case dcl: DFVal.Dcl => ConnectPoint.Direct(dcl) - case pbns: DFVal.PortByNameSelect => ConnectPoint.Via(pbns) - }.toSet ++ magnetConnectionMap.keySet - - // go through all designs and their instances - val danglingPorts = designBlockInstMap.view.flatMap { (design, insts) => - designMemberTable(design).flatMap { - // all input ports that are not clock/reset and not already connected - case port: DFVal.Dcl if port.isPortIn && !port.isClkDcl && !port.isRstDcl => - // all design instances - insts.flatMap { designInst => - val cp = ConnectPoint.Via(designInst, port) - if (alreadyConnectedPoints.contains(ConnectPoint.Via(designInst, port))) None - else Some( - s"""|DFiant HDL connectivity error! - |Position: ${designInst.meta.position} - |Hierarchy: ${designInst.getFullName} - |Message: Found a dangling (unconnected) input port `${port.getName}`.""".stripMargin - ) - } - // all output ports that are not blackbox and not already assigned/connected/initialized - case port: DFVal.Dcl - if port.isPortOut && !design.isBlackBox && !port.hasNonBubbleInit && - !assignmentsDclTable.contains(port) && - !alreadyConnectedPoints.contains(ConnectPoint.Direct(port)) => - Some( - s"""|DFiant HDL connectivity error! - |Position: ${port.meta.position} - |Hierarchy: ${design.getFullName} - |Message: Found a dangling (unconnected/unassigned and uninitialized) output port `${port.getName}`.""".stripMargin - ) - case _ => None - } - } - - if (danglingPorts.nonEmpty) - throw new IllegalArgumentException( - danglingPorts.mkString("\n") - ) - end checkDanglingPorts - // Hierarchical clone of `checkDanglingPorts`, invoked on the root DB. The // assignment coverage and the connected-point set are aggregated across all // sub-DBs (each design's assignments/connections live in its own sub-DB) plus @@ -1443,57 +1391,6 @@ final case class DB private ( fail("magnetConnectionMap", this.magnetConnectionMap, newRoot.new_magnetConnectionMap) end new_hierEquivalenceCheck - /** Checks that device top design domains all have timing clock rate constraints. Additionally, if - * there is an explicit clock rate configuration, it must match the timing constraint rate. - */ - def domainClkRateCheck(): Unit = - val errors = collection.mutable.ArrayBuffer[String]() - domainOwnerMemberList.view.map(_._1).foreach { - case domainOwner if domainOwner.getThisOrOwnerDesign.isDeviceTop && domainOwner.usesClk => - def waitError(msg: String): Unit = - val pos = - if (domainOwner.isTop) domainOwner.asInstanceOf[DFDesignBlock].dclMeta.position - else domainOwner.meta.position - errors += s"""|DFiant HDL domain clock rate error! - |Position: ${pos} - |Hierarchy: ${domainOwner.getFullName} - |Message: $msg""".stripMargin - val explicitRateOpt = resolvedClkRstMap.get(domainOwner) - .flatMap(_._1) - .flatMap(_.rate.toOption) - val timingConstraintRateOpt = domainOwner.getTimingConstraintClkRateOpt - - (explicitRateOpt, timingConstraintRateOpt) match - case (Some(explicitRate), Some(timingConstraintRate)) => - if (explicitRate.to_freq.to_hz != timingConstraintRate.to_freq.to_hz) - waitError( - s"""|Mismatch between domain clock rate configuration ($explicitRate) and timing constraint rate ($timingConstraintRate). - |To fix, do one of the following: - |* Connect a different clock resource to the domain to match your configuration. - |* Explicitly set the clock rate configuration to $timingConstraintRate. - |* Remove the domain clock rate configuration and let it be derived from the timing constraint.""".stripMargin - ) - case (Some(explicitRate), None) => - waitError( - s"""|Missing clock rate timing constraint. - |To Fix: - |Connect a $explicitRate clock resource to the domain to match your configuration.""".stripMargin - ) - case (None, None) => - waitError( - s"""|Missing clock rate timing constraint. - |To Fix: - |Connect the wanted clock resource to the domain. - |(the domain will automatically derive the clock rate from the resource).""".stripMargin - ) - case _ => - end match - case _ => - } - if (errors.nonEmpty) - throw new IllegalArgumentException(errors.mkString("\n")) - end domainClkRateCheck - // Hierarchical clone of `domainClkRateCheck`, invoked on the root DB. The flat // `usesClk` filter (cross-design, flat-only) is equivalent to "the resolver // produced a clock" (new_resolvedClkRstMap(owner)._1.isDefined), so the @@ -1553,53 +1450,6 @@ final case class DB private ( throw new IllegalArgumentException(errors.mkString("\n")) end new_domainClkRateCheck - def waitCheck(): Unit = - val errors = collection.mutable.ArrayBuffer[String]() - for - wait <- members.collect { case w: Wait if w.isInRTDomain => w } - trigger = wait.triggerRef.get - if trigger.dfType == DFTime - do - def waitError(msg: String): Unit = - errors += s"""|DFiant HDL wait error! - |Position: ${wait.meta.position} - |Hierarchy: ${wait.getOwnerDesign.getFullName} - |Message: $msg""".stripMargin - val ownerDomain = wait.getOwnerDomain - trigger.getConstData[TimeNumber].toOption match - case Some(waitTime) => - // Check if the wait statement is in a domain with a clock rate configuration - resolvedClkRstMap.get(ownerDomain).flatMap(_._1).flatMap(_.rate.toOption) match - case Some(rate) => - - // Get the clock period in picoseconds - val clockPeriodPs = rate.to_ps.value - val desc = rate match - case time: TimeNumber => s"period ${time}" - case freq: FreqNumber => s"frequency ${freq}" - - // Get wait duration in picoseconds - val waitDurationPs = waitTime.to_ps.value - - // Check if wait duration is exactly divisible by clock period - if (!(waitDurationPs / clockPeriodPs).isWhole) - waitError( - s"Wait duration ${waitTime} is not exactly divisible by the clock $desc." - ) - case _ => - waitError( - s"Wait statement is missing an explicit clock configuration in its domain." - ) - end match - case _ => - waitError(s"Wait duration is not constant.") - end match - end for - - if (errors.nonEmpty) - throw new IllegalArgumentException(errors.mkString("\n")) - end waitCheck - // Hierarchical clone of `waitCheck`, invoked on the root DB. Iterates each // sub-DB's RT waits under that sub-DB's getSet (the root has no members of its // own); the clock rate comes from new_resolvedClkRstMap. The error hierarchy @@ -1648,31 +1498,6 @@ final case class DB private ( throw new IllegalArgumentException(errors.mkString("\n")) end new_waitCheck - def circularDerivedDomainsCheck(): Unit = - // Helper function to perform DFS and detect cycles - @tailrec def dfs( - node: DFDomainOwner, - visited: Set[DFDomainOwner], - stack: Set[DFDomainOwner] - ): Unit = - if (stack.contains(node)) - throw new IllegalArgumentException( - s"""|Circular derived RT configuration detected. Involved in the cycle: - |${stack.map(fullNameViaInst).mkString("\n")} - |""".stripMargin - ) - if (!visited.contains(node)) - val newVisited = visited + node - val newStack = stack + node - dependentRTDomainOwners.get(node) match - case Some(dependentNode) => dfs(dependentNode, newVisited, newStack) - case None => // No dependency, end of this path - end dfs - // Iterate over all nodes in the map and perform DFS - for (node <- dependentRTDomainOwners.keys) - dfs(node, Set.empty, Set.empty) - end circularDerivedDomainsCheck - // Hierarchical clone of `circularDerivedDomainsCheck`, invoked on the root DB. // Same DFS, over `new_dependentRTDomainOwners`; the cycle error names each // owner via `new_fullNameViaInst` routed through that owner's sub-DB. @@ -1794,126 +1619,6 @@ final case class DB private ( ) end directRefCheck - def portLocationCheck(): Unit = - val errors = mutable.ListBuffer.empty[String] - val locationCollisions = mutable.ListBuffer.empty[String] - - designMemberList.foreach { - case (design, members) if design.isDeviceTop => - // Collect all location constraints to check for collisions - val locationMap = mutable.Map.empty[String, String] // loc -> portName(idx) - (design :: members).foreach { - case designInstance: DFDesignBlock if designInstance != design => // no need to check for location constraints in nested designs - case domainOwner: DFDomainOwner => - domainOwner.domainType match - case DomainType.RT => - var foundLoc = false - domainOwner.getDomainClkConstraintsView.foreach { - case constraints.IO(loc = loc: String) => - locationMap.get(loc).foreach { prevPort => - locationCollisions += - s"${prevPort} and ${domainOwner.getFullName} are both assigned to location `${loc}`" - } - locationMap += loc -> domainOwner.getFullName - foundLoc = true - case _ => - } - val clkIsVar = domainOwnerMemberTable(domainOwner).view.collectFirst { - case dcl: DFVal.Dcl if dcl.isClkDcl => dcl.isVar - }.getOrElse(false) - - // for internal domains (indicated by a clock variable) we don't need to check for location constraints - if (!foundLoc && !clkIsVar) - errors += s"${domainOwner.getFullName} is missing a clock location constraint" - case _ => - end match - case clkPort: DFVal.Dcl if clkPort.isPortIn && clkPort.isClkDcl => // do nothing (checked in the domain itself) - case port: DFVal.Dcl if port.isPort => - val bitSet = port.widthIntOpt match - case Some(width) => mutable.BitSet((0 until width)*) - case None => mutable.BitSet.empty - port.meta.annotations.foreach { - case constraints.IO(bitIdx = None, loc = loc: String) => - bitSet.clear() - locationMap.get(loc).foreach { prevPort => - locationCollisions += - s"${prevPort} and ${port.getFullName} are both assigned to location `${loc}`" - } - locationMap += loc -> port.getFullName - if (port.widthIntOpt.get != 1) - locationCollisions += - s"${port.getFullName} has mutliple bits assigned to location `${loc}`" - case constraints.IO(bitIdx = bitIdx: Int, loc = loc: String) => - locationMap.get(loc).foreach { prevPort => - locationCollisions += - s"${prevPort} and ${port.getFullName}(${bitIdx}) are both assigned to location `${loc}`" - } - locationMap += loc -> s"${port.getFullName}(${bitIdx})" - bitSet -= bitIdx - case _ => - } - if (bitSet.nonEmpty) - if (port.widthIntOpt.get == 1) - errors += s"${port.getFullName}" - else - errors += s"${port.getFullName} with bits ${bitSet.mkString(", ")}" - case _ => - } - case _ => - } - - if (errors.nonEmpty) - throw new IllegalArgumentException( - s"""|The following top device design ports or domains are missing location constraints: - | ${errors.mkString("\n ")} - |To Fix: - |Add a location constraint to the ports by connecting them to a located resource or - |by using the `@io` constraint. - |""".stripMargin - ) - - if (locationCollisions.nonEmpty) - throw new IllegalArgumentException( - s"""|The following location constraints have collisions: - | ${locationCollisions.mkString("\n ")} - |To Fix: - |Ensure each location is used by a single port bit. - |""".stripMargin - ) - end portLocationCheck - - def portResourceDirCheck(): Unit = - import DFVal.Modifier.Dir - val errors = mutable.ListBuffer.empty[String] - - designMemberList.foreach { - case (design, members) if design.isDeviceTop => - members.foreach { - case port: DFVal.Dcl if port.isPort => - port.meta.annotations.foreach { - case constraints.IO(dir = dir: Dir) => - (dir, port.modifier.dir) match - case (Dir.IN, Dir.OUT) | (Dir.OUT, Dir.IN) => - errors += - s"${port.getFullName} direction (${port.modifier.dir}) has a resource direction ($dir) mismatch." - case _ => - case _ => - } - case _ => - } - case _ => - } - - if (errors.nonEmpty) - throw new IllegalArgumentException( - s"""|The following top device design ports have resource direction mismatches: - | ${errors.mkString("\n ")} - |To Fix: - |Make sure you connect the resource to the port with the correct direction. - |""".stripMargin - ) - end portResourceDirCheck - // Hierarchical clone of `portLocationCheck`, invoked on the root DB. Only the // device-top design is examined; its members are resolved under that design's // sub-DB getSet (device top == toptop, so getFullName is the full path). @@ -2041,18 +1746,12 @@ final case class DB private ( // Uniform entry point, representation-aware: // - hierarchical root: run each sub-DB's per-design checks, then the // cross-design root checks once on the root; - // - in-hierarchy sub-DB: run only its per-design checks; - // - old-style flat DB: the whole design lives in one DB, so run BOTH groups - // on itself (preserves legacy behavior). This is the path callers use - // today; the root path is dormant until `rootDBCheck` is rewired to the - // hierarchical `new_*` analyses and callers switch to `oldToNew.check`. + // - in-hierarchy sub-DB: run only its per-design checks. + // Callers always invoke this on the root (via `oldToNew.check`). lazy val check: Unit = if (isRoot) subDBs.view.values.foreach(_.subDBCheck) new_rootDBCheck - else if (isOldStyleFlatDB) - subDBCheck - rootDBCheck else subDBCheck // Per-design structural checks: each validates a single design's own members @@ -2063,21 +1762,9 @@ final case class DB private ( connectionTable // causes connectivity checks directRefCheck() - // Whole-tree checks, FLAT implementation. Used only on an old-style flat DB - // (the `isOldStyleFlatDB` branch of `check`); the hierarchical root path uses - // `new_rootDBCheck`. Dead once the flat DB is gone (Step 3). - private lazy val rootDBCheck: Unit = - magnetConnectionMap // causes magnet connectivity checks - checkDanglingPorts() - circularDerivedDomainsCheck() - domainClkRateCheck() - waitCheck() - portLocationCheck() - portResourceDirCheck() - - // Whole-tree checks, HIERARCHICAL implementation. Run once on the root: the - // same cross-design connectivity / RT-domain / device-top checks as - // `rootDBCheck`, via the `new_*` clones that navigate the sub-DB tree. + // Whole-tree checks, run once on the root: the cross-design connectivity / + // RT-domain / device-top checks, via the `new_*` clones that navigate the + // sub-DB tree. private lazy val new_rootDBCheck: Unit = new_magnetConnectionMap // causes magnet connectivity checks new_checkDanglingPorts() From e76798bd8fd73ae9d81866ad61906c2fb503293d Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 13 Jun 2026 19:26:44 +0300 Subject: [PATCH 23/72] Remove the flat<->hierarchical equivalence gate (Step 3b) new_hierEquivalenceCheck was temporary scaffolding that asserted the flat RT clk/rst + magnet analyses matched their hierarchical clones on every design passing SanityCheck. The hierarchical path is now the sole production path (Steps 2c/3a) and the equivalence is locked in by the test suite, so drop the gate and its SanityCheck call site. The flat magnetConnectionMap was only consumed by the gate and the (deleted) flat checks, so remove it too. MagnetMap.get (flat) is now dead and will be removed with the getHierarchical->get rename in Step 3d. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 29 ------------------- .../dfhdl/compiler/stages/SanityCheck.scala | 1 - 2 files changed, 30 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index 10e1efbdc..1d3cdea51 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -599,9 +599,6 @@ final case class DB private ( end match end getConnToMap - // To From - lazy val magnetConnectionMap: Map[ConnectPoint, ConnectPoint] = MagnetMap.get - // Hierarchical clone of `magnetConnectionMap`, invoked on the root DB. The // magnet matching is intrinsically cross-design; `MagnetMap.getHierarchical` // precomputes each magnet point's design context per sub-DB, then matches on @@ -1365,32 +1362,6 @@ final case class DB private ( fillDomainMap(rtDomainOwners, Nil, domainMap) domainMap.toMap - // Temporary equivalence gate: on an old-style flat DB, assert the flat RT - // clk/rst analyses match their new-style clones computed on `oldToNew`. - // Dropped once the consuming stages are migrated and the flat versions removed. - def new_hierEquivalenceCheck(): Unit = - if (isOldStyleFlatDB) - val newRoot = this.oldToNew - def fail(name: String, flat: Any, neu: Any): Nothing = - throw new IllegalArgumentException( - s"""|new_$name mismatch between flat and hierarchical computation. - |flat: $flat - |new: $neu""".stripMargin - ) - if (this.relatedAnnotMap != newRoot.new_relatedAnnotMap) - fail("relatedAnnotMap", this.relatedAnnotMap, newRoot.new_relatedAnnotMap) - if (this.dependentRTDomainOwners != newRoot.new_dependentRTDomainOwners) - fail( - "dependentRTDomainOwners", - this.dependentRTDomainOwners, - newRoot.new_dependentRTDomainOwners - ) - if (this.resolvedClkRstMap != newRoot.new_resolvedClkRstMap) - fail("resolvedClkRstMap", this.resolvedClkRstMap, newRoot.new_resolvedClkRstMap) - if (this.magnetConnectionMap != newRoot.new_magnetConnectionMap) - fail("magnetConnectionMap", this.magnetConnectionMap, newRoot.new_magnetConnectionMap) - end new_hierEquivalenceCheck - // Hierarchical clone of `domainClkRateCheck`, invoked on the root DB. The flat // `usesClk` filter (cross-design, flat-only) is equivalent to "the resolver // produced a clock" (new_resolvedClkRstMap(owner)._1.isDefined), so the diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala index bbff7af25..c32b13e15 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala @@ -357,7 +357,6 @@ case class SanityCheck(skipAnonRefCheck: Boolean) extends Stage: ownershipCheck(designDB.top, designDB.membersNoGlobals.drop(1)) orderCheck() designDB.oldToNew.check - designDB.new_hierEquivalenceCheck() hierarchicalDBRoundTripCheck(designDB) designDB end SanityCheck From 7deb2b94ba794bc7791b01c1e15b68d280710a09 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 13 Jun 2026 19:37:49 +0300 Subject: [PATCH 24/72] Merge resolvedClkRstMap; delete the flat clk/rst analysis chain (Step 3c) resolvedClkRstMap is now a single representation-dispatched lazy val: the hierarchical body runs on the root, a sub-DB delegates to its root, and an old-style flat DB is converted via oldToNew first. The only flat entry point is the CLK_FREQ const-folder (DFMember.Special), which only runs on a complete immutable DB (disabled during elaboration), so the oldToNew conversion is always valid there. This lets the entire flat clk/rst chain go: relatedAnnotMap, pbnsToPort, fullNameViaInst, dependentRTDomainOwners, reversedDependents, the getResolvedClkRst/usesClkRst/usesClk/usesRst/relaxed extensions, and fillDomainMap. hasClkAnnot/hasRstAnnot (representation-agnostic) move up to the surviving constraints extension. new_resolvedClkRstMap loses its prefix here (callers in ExplicitClkRstCfg/DropTimedRTWaits updated); the remaining new_* analyses are renamed in Step 3d. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 297 +----------------- .../compiler/stages/DropTimedRTWaits.scala | 2 +- .../compiler/stages/ExplicitClkRstCfg.scala | 4 +- .../scala/dfhdl/compiler/stages/Stage.scala | 2 +- 4 files changed, 17 insertions(+), 288 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index 1d3cdea51..8d7483b61 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -702,221 +702,10 @@ final case class DB private ( getDomainClkConstraintsView.collectFirst { case constraints.Timing.Clock(rate = rate: RateNumber @unchecked) => rate } - end extension - - // ========================================================================= - // Annotation-based RT domain clock/reset resolver. - // - // For each RT domain owner, resolvedClkRstMap holds the resolved - // Timing.Clock / Timing.Reset (Some) or None when the corresponding slot - // has been stripped by relaxation ("no clock needed" / "no reset needed"). - // ========================================================================= - - private lazy val relatedAnnotMap: Map[DFDomainOwner, DFDomainOwner] = - domainOwnerMemberList.view.flatMap { case (owner, _) => - owner.meta.annotations.collectFirst { - case rel: constraints.Timing.Related => rel.ref.get - }.map(owner -> _) - }.toMap - - // Resolves a PortByNameSelect to its underlying DFVal.Dcl by walking the - // dotted `portNamePath` from the targeted design block. Cross-design refs - // go through PBNS (the target port lives in a different sub-DB), so this - // is needed whenever an analysis needs the actual port (e.g. to read its - // owning RT domain). - private def pbnsToPort(pbns: DFVal.PortByNameSelect): Option[DFVal.Dcl] = - val designBlock: DFDesignBlock = pbns.designInstRef.get.getDesignBlock - val pathParts = pbns.portNamePath.split('.').toList - @tailrec def walk(owner: DFOwnerNamed, parts: List[String]): Option[DFMember] = - parts match - case Nil => None - case head :: rest => - namedOwnerMemberTable.getOrElse(owner, Nil).collectFirst { - case n: DFMember.Named if !n.isAnonymous && n.getName == head => n - } match - case Some(m) if rest.isEmpty => Some(m) - case Some(o: DFOwnerNamed) => walk(o, rest) - case _ => None - walk(designBlock, pathParts) match - case Some(dcl: DFVal.Dcl) => Some(dcl) - case _ => None - end pbnsToPort - - // Full path for a domain owner that prefers the per-instance name over the - // canonical design-block class name. With dedup, a non-top DFDesignBlock - // is reached through one or more DFDesignInsts; we walk through the head - // inst so that error messages show user-visible instance paths - // ("Top.internal1.dmn1") rather than class-name paths ("Top.Internal1.dmn1"). - // For design blocks that have multiple instances, the choice picks the - // first inst — callers that need every instance path must enumerate them - // separately. - private def fullNameViaInst(owner: DFDomainOwner): String = - val design = owner.getThisOrOwnerDesign - if (design.isTop) owner.getFullName - else - designBlockInstMap.get(design).flatMap(_.headOption) match - case Some(inst) => - owner match - case _: DFDesignBlock => inst.getFullName - case _ => - val relPath = owner.getRelativeName(design) - s"${inst.getFullName}.$relPath" - case None => owner.getFullName - end fullNameViaInst - - lazy val dependentRTDomainOwners: Map[DFDomainOwner, DFDomainOwner] = - extension (member: DFMember) - def getRTOwnerOption: Option[DFDomainOwner] = - // A DFDesignBlock no longer carries a lexical parent in `ownerRef`, - // so resolve its enclosing domain via the (first) DFDesignInst - // instead of `getOwnerDomain` (which would throw on the empty ref). - val owner = member match - case design: DFDesignBlock => - designBlockInstMap.get(design).flatMap(_.headOption).map(_.getOwnerDomain) - case pbns: DFVal.PortByNameSelect => - // Resolve through PBNS so we get the RT domain of the underlying - // port rather than the parent design that owns the PBNS net. - Some(pbnsToPort(pbns).map(_.getOwnerDomain).getOrElse(member.getOwnerDomain)) - case _ => Some(member.getOwnerDomain) - owner.flatMap(_.domainType match - case DomainType.RT => owner - case _ => None) - end extension - domainOwnerMemberList.view.flatMap { (domainOwner, domainMembers) => - domainOwner.domainType match - case DomainType.RT => - relatedAnnotMap.get(domainOwner) match - case Some(relatedOwner) => Some(domainOwner -> relatedOwner) - case None => - val hasClkOrRst = domainOwner.meta.annotations.exists { - case _: constraints.Timing.Clock => true - case _: constraints.Timing.Reset => true - case _ => false - } - if (hasClkOrRst) None - else - domainOwner match - case design: DFDesignBlock => - if (design.isTopTop) None - else design.getRTOwnerOption.map(design -> _) - case domain: DomainBlock => - val inPorts = domainMembers.collect { - case dcl: DFVal.Dcl if dcl.isPortIn && !dcl.isClkDcl && !dcl.isRstDcl => dcl - } - // Cross-design connections to an internal RT-domain input - // are keyed in the connectionTable by the PBNS at the - // parent scope, not by the inner port — collect both so - // the source-domain analysis covers external drivers. - val enclosingDesign = domain.getOwnerDesign - val designInsts = designBlockInstMap.getOrElse(enclosingDesign, Nil) - val inSourceDomains = inPorts.view.flatMap { port => - val portRelName = port.getRelativeName(enclosingDesign) - val pbnsNets = designInsts.view.flatMap { inst => - designInstPBNS.getOrElse(inst, Nil) - .filter(_.portNamePath == portRelName) - .flatMap(connectionTable.getNets(_)) - }.toSet - val allNets = connectionTable.getNets(port) ++ pbnsNets - allNets.headOption match - case Some(DFNet.Connection(_, from, _)) => from.getRTOwnerOption - case _ => None - }.toSet - if (inSourceDomains.isEmpty) domain.getRTOwnerOption.map(domain -> _) - else if (inSourceDomains.size > 1) - throw new IllegalArgumentException( - s"""|Found ambiguous source RT configurations for the domain: - |${fullNameViaInst(domain)} - |Sources: - |${inSourceDomains.map(fullNameViaInst).mkString("\n")} - |Possible solution: - |Either explicitly define a configuration for the domain or drive it from a single source domain. - |""".stripMargin - ) - else Some(domain -> inSourceDomains.head) - case ifc: DFInterfaceOwner => ??? - case _ => None - }.toMap - end dependentRTDomainOwners - - private lazy val designUsesClkRst = - mutable.Map.empty[String, (usesClk: Boolean, usesRst: Boolean)] - private lazy val domainOwnerUsesClkRst = - mutable.Map.empty[DFDomainOwner, (usesClk: Boolean, usesRst: Boolean)] - private lazy val reversedDependents = dependentRTDomainOwners.invert - - extension (domainOwner: DFDomainOwner) - private def getResolvedClkRst: ClkRstTiming = - val defaultTag = globalTags.getTagOf[DefaultRTDomainCfgTag].get - val userClkOpt = domainOwner.meta.annotations.collectFirst { - case c: constraints.Timing.Clock => c - } - val userRstOpt = domainOwner.meta.annotations.collectFirst { - case r: constraints.Timing.Reset => r - } - val isDeviceTop = domainOwner.getThisOrOwnerDesign.isDeviceTop - // No-reset opt-out: if the user specifies @timing.clock without @timing.reset, - // that explicitly declares "this group has no reset" — we suppress the default - // reset that would otherwise be merged in. - val explicitNoRst = userClkOpt.isDefined && userRstOpt.isEmpty - val (baseClkOpt, baseRstOpt) = - if (isDeviceTop) - val clk = domainOwner.getTimingConstraintClkRateOpt match - case Some(rate) => defaultTag.clk.copy(rate = rate) - case None => defaultTag.clk - (Some(clk), None) - else if (explicitNoRst) (Some(defaultTag.clk), None) - else (Some(defaultTag.clk), Some(defaultTag.rst)) - def mergeClk( - base: Option[constraints.Timing.Clock], - user: Option[constraints.Timing.Clock] - ): Option[constraints.Timing.Clock] = (base, user) match - case (Some(b), Some(u)) => - Some(b.merge(u, withPriority = true).get.asInstanceOf[constraints.Timing.Clock]) - case (Some(b), None) => Some(b) - case (None, Some(u)) => Some(u) - case (None, None) => None - def mergeRst( - base: Option[constraints.Timing.Reset], - user: Option[constraints.Timing.Reset] - ): Option[constraints.Timing.Reset] = (base, user) match - case (Some(b), Some(u)) => - Some(b.merge(u, withPriority = true).get.asInstanceOf[constraints.Timing.Reset]) - case (Some(b), None) => Some(b) - case (None, Some(u)) => Some(u) - case (None, None) => None - (mergeClk(baseClkOpt, userClkOpt), mergeRst(baseRstOpt, userRstOpt)) - end getResolvedClkRst - - private def usesClkRst: (usesClk: Boolean, usesRst: Boolean) = domainOwner match - case design: DFDesignBlock => - designUsesClkRst.getOrElseUpdate( - design.dclName, - (design.usesClk, design.usesRst) - ) - case _ => - domainOwnerUsesClkRst.getOrElseUpdate( - domainOwner, - (domainOwner.usesClk, domainOwner.usesRst) - ) - - private def isAlwaysAtTopClk: Boolean = - domainOwner.getResolvedClkRst._1 match - case Some(clk) => clk.inclusionPolicy match - case ClkRstInclusionPolicy.AlwaysAtTop => true - case _ => false - case None => false - - private def isAlwaysAtTopRst: Boolean = - domainOwner.getResolvedClkRst._2 match - case Some(rst) => rst.inclusionPolicy match - case ClkRstInclusionPolicy.AlwaysAtTop => true - case _ => false - case None => false - // Plan-mandated "forcing" semantics: an explicit `@timing.clock` / `@timing.reset` // annotation on the owner counts as "this slot is required" even when no member - // generates the usage (covers bare-annotation forcing on blackbox / combinational owners - // and partial-override forms). + // generates the usage (covers bare-annotation forcing on blackbox / combinational + // owners and partial-override forms). private def hasClkAnnot: Boolean = domainOwner.meta.annotations.exists { case _: constraints.Timing.Clock => true case _ => false @@ -925,73 +714,8 @@ final case class DB private ( case _: constraints.Timing.Reset => true case _ => false } - - private def usesClk: Boolean = domainOwnerMemberTable(domainOwner).exists { - case dcl: DFVal.Dcl => dcl.isReg || dcl.isClkDcl - case reg: DFVal.Alias.History => true - case pb: ProcessBlock if pb.isInRTDomain => true - case inst: DFDesignInst => inst.getDesignBlock.usesClkRst.usesClk - case _ => false - } || reversedDependents.getOrElse(domainOwner, Set()).exists(_.usesClkRst.usesClk) || - domainOwner.isTopTop && domainOwner.isAlwaysAtTopClk || - domainOwner.hasClkAnnot - - private def usesRst: Boolean = domainOwnerMemberTable(domainOwner).exists { - case dcl: DFVal.Dcl => - (dcl.isReg && dcl.hasNonBubbleInit) || dcl.isRstDcl - case reg: DFVal.Alias.History => reg.hasNonBubbleInit - case pb: ProcessBlock if pb.isInRTDomain => true - case inst: DFDesignInst => inst.getDesignBlock.usesClkRst.usesRst - case _ => false - } || reversedDependents.getOrElse(domainOwner, Set()).exists(_.usesClkRst.usesRst) || - domainOwner.isTopTop && domainOwner.isAlwaysAtTopRst || - domainOwner.hasRstAnnot end extension - extension (resolved: ClkRstTiming) - private def relaxed(atDomain: DFDomainOwner): ClkRstTiming = - val (usesClk, usesRst) = atDomain.usesClkRst - val (clk, rst) = resolved - val updatedClk = if (usesClk) clk else None - val updatedRst = if (usesRst) rst else None - (updatedClk, updatedRst) - end extension - - @tailrec private def fillDomainMap( - domains: List[DFDomainOwner], - stack: List[DFDomainOwner], - domainMap: mutable.Map[DFDomainOwner, ClkRstTiming] - ): Unit = - domains match - case domain :: rest if domainMap.contains(domain) => - fillDomainMap(rest, stack, domainMap) - case domain :: rest => - dependentRTDomainOwners.get(domain) match - case Some(dependencyDomain) => - domainMap.get(dependencyDomain) match - case Some(dependencyResolved) => - domainMap += domain -> dependencyResolved.relaxed(domain) - fillDomainMap(rest, stack, domainMap) - case None => fillDomainMap(rest, domain :: stack, domainMap) - end match - case _ => - val resolved = domain.getResolvedClkRst - domainMap += domain -> resolved.relaxed(domain) - fillDomainMap(rest, stack, domainMap) - end match - case Nil if stack.nonEmpty => fillDomainMap(domains = stack, Nil, domainMap) - case _ => - end match - end fillDomainMap - - lazy val resolvedClkRstMap: Map[DFDomainOwner, ClkRstTiming] = - val domainMap = mutable.Map.empty[DFDomainOwner, ClkRstTiming] - val rtDomainOwners = domainOwnerMemberList.view.map(_._1).filter(_.domainType match - case DomainType.RT => true - case _ => false).toList - fillDomainMap(rtDomainOwners, Nil, domainMap) - domainMap.toMap - // =========================================================================== // New-style (hierarchical) clones of the RT clk/rst domain analyses. They are // invoked on the new-style ROOT DB and mirror the flat versions above, but @@ -1214,8 +938,13 @@ final case class DB private ( }.toMap else rootDB.domainOwnerToSubDB - lazy val new_resolvedClkRstMap: Map[DFDomainOwner, ClkRstTiming] = - if (!isRoot) rootDB.new_resolvedClkRstMap + // Resolved @timing.clock / @timing.reset per RT domain owner. Computed on the + // hierarchical root; a sub-DB delegates to its root; an old-style flat DB is + // converted to the hierarchy first (`oldToNew`) — this is the entry point the + // CLK_FREQ const-folder (DFMember.Special) hits on a flat stage DB. + lazy val resolvedClkRstMap: Map[DFDomainOwner, ClkRstTiming] = + if (isOldStyleFlatDB) oldToNew.resolvedClkRstMap + else if (!isRoot) rootDB.resolvedClkRstMap else val reversedDependents: Map[DFDomainOwner, Set[DFDomainOwner]] = new_dependentRTDomainOwners.invert @@ -1364,7 +1093,7 @@ final case class DB private ( // Hierarchical clone of `domainClkRateCheck`, invoked on the root DB. The flat // `usesClk` filter (cross-design, flat-only) is equivalent to "the resolver - // produced a clock" (new_resolvedClkRstMap(owner)._1.isDefined), so the + // produced a clock" (resolvedClkRstMap(owner)._1.isDefined), so the // resolved-clock map drives both the filter and the explicit rate. Per-owner // local reads (isDeviceTop, isTop position, getFullName, the clk timing // constraint) are routed through the owner's sub-DB getSet via `atOwner`. @@ -1373,7 +1102,7 @@ final case class DB private ( domainOwnerToSubDB(owner).atGetSet(f) val errors = collection.mutable.ArrayBuffer[String]() domainOwnerMemberList.view.map(_._1).foreach { domainOwner => - val resolvedClkOpt = new_resolvedClkRstMap.get(domainOwner).flatMap(_._1) + val resolvedClkOpt = resolvedClkRstMap.get(domainOwner).flatMap(_._1) if ( resolvedClkOpt.isDefined && atOwner(domainOwner)(domainOwner.getThisOrOwnerDesign.isDeviceTop) @@ -1423,7 +1152,7 @@ final case class DB private ( // Hierarchical clone of `waitCheck`, invoked on the root DB. Iterates each // sub-DB's RT waits under that sub-DB's getSet (the root has no members of its - // own); the clock rate comes from new_resolvedClkRstMap. The error hierarchy + // own); the clock rate comes from resolvedClkRstMap. The error hierarchy // uses new_fullNameViaInst so a wait in a nested design reports its full // instance path (plain getFullName on a sub-DB would be relative). def new_waitCheck(): Unit = @@ -1443,7 +1172,7 @@ final case class DB private ( val ownerDomain = wait.getOwnerDomain trigger.getConstData[TimeNumber].toOption match case Some(waitTime) => - new_resolvedClkRstMap.get(ownerDomain).flatMap(_._1).flatMap(_.rate.toOption) match + resolvedClkRstMap.get(ownerDomain).flatMap(_._1).flatMap(_.rate.toOption) match case Some(rate) => val clockPeriodPs = rate.to_ps.value val desc = rate match diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropTimedRTWaits.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropTimedRTWaits.scala index bb5c32878..13b4aa01c 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropTimedRTWaits.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropTimedRTWaits.scala @@ -32,7 +32,7 @@ case object DropTimedRTWaits extends HierarchyStage: // The resolved clk rate comes from the root DB's hierarchical clk/rst map // (cross-design resolution); the owner domain is resolved under this // sub-DB's getSet. - val clkRate: RateNumber = rootDB.new_resolvedClkRstMap + val clkRate: RateNumber = rootDB.resolvedClkRstMap .get(waitMember.getOwnerDomain) .flatMap(_._1) .flatMap(_.rate.toOption) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ExplicitClkRstCfg.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ExplicitClkRstCfg.scala index 59eceaf39..3a2690a55 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ExplicitClkRstCfg.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ExplicitClkRstCfg.scala @@ -17,7 +17,7 @@ import dfhdl.core.{refTW, DFC} case object ExplicitClkRstCfg extends HierarchyStage: def dependencies: List[Stage] = List(UniqueDesigns, NamedAnonMultiref) def nullifies: Set[Stage] = Set(DropUnreferencedAnons) - // Cross-design clk/rst resolution (`new_resolvedClkRstMap` / `new_isDependentOn`) + // Cross-design clk/rst resolution (`resolvedClkRstMap` / `new_isDependentOn`) // is read from the root DB; everything else is per-sub-DB via the `subDB` // helper (this stage's current sub-DB). def transformSubDB(rootDB: DB)(using @@ -44,7 +44,7 @@ case object ExplicitClkRstCfg extends HierarchyStage: owner.domainType match case DomainType.RT => val (resolvedClk, resolvedRst) = - rootDB.new_resolvedClkRstMap.getOrElse(owner, (None, None)) + rootDB.resolvedClkRstMap.getOrElse(owner, (None, None)) // Preserve any existing user-authored @timing.related annotation. For a domain // related to a sibling or to its enclosing owner the downstream pipeline uses the // annotation to skip clk/rst port insertion. diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/Stage.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/Stage.scala index 9a7ad3c7e..5e85c7b0c 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/Stage.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/Stage.scala @@ -68,7 +68,7 @@ end GlobalStage * * `transformSubDB` always runs with the implicit `MemberGetSet` rebound to the current sub-DB's * own getSet (so the `subDB` helper resolves that design's members); the root DB is passed as the - * parameter for cross-design needs (e.g. `rootDB.new_resolvedClkRstMap`). + * parameter for cross-design needs (e.g. `rootDB.resolvedClkRstMap`). */ trait HierarchyStage extends Stage: final protected def subDB(using MemberGetSet): DB = getSet.designDB From d66beae1808c1e60c16495b042786f8f9da5f186 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 13 Jun 2026 19:42:50 +0300 Subject: [PATCH 25/72] Drop the new_ prefix from the hierarchical analyses (Step 3d) With every flat counterpart deleted (Steps 3a-3c), the hierarchical analyses are the sole implementations, so rename them to canonical names: magnetConnectionMap, magnetPointInfo, magnetData, relatedAnnotMap, pbnsToPort, designEnclosingDomain, getRTOwnerWithSub, fullNameViaInst, dependentRTDomainOwners, isDependentOn, checkDanglingPorts, circularDerivedDomainsCheck, domainClkRateCheck, waitCheck, portLocationCheck, portResourceDirCheck, rootDBCheck. Callers in ExplicitClkRstCfg/ConnectMagnets/AddMagnets updated; the stale new-style/equivalence-gate comment block is rewritten. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 135 +++++++++--------- .../dfhdl/compiler/stages/AddMagnets.scala | 6 +- .../compiler/stages/ConnectMagnets.scala | 6 +- .../compiler/stages/ExplicitClkRstCfg.scala | 4 +- 4 files changed, 74 insertions(+), 77 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index 8d7483b61..4910c1da5 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -605,12 +605,12 @@ final case class DB private ( // the root-aware design tree (no flattening). It also returns each magnet // point's (owner design, name) so migrating consumers don't re-resolve a // cross-design ConnectPoint (which would need a flat member index). - private lazy val new_magnetData + private lazy val magnetData : (Map[ConnectPoint, ConnectPoint], Map[ConnectPoint, (DFDesignBlock, String)]) = - if (!isRoot) rootDB.new_magnetData + if (!isRoot) rootDB.magnetData else MagnetMap.getHierarchical(this) - lazy val new_magnetConnectionMap: Map[ConnectPoint, ConnectPoint] = new_magnetData._1 - lazy val new_magnetPointInfo: Map[ConnectPoint, (DFDesignBlock, String)] = new_magnetData._2 + lazy val magnetConnectionMap: Map[ConnectPoint, ConnectPoint] = magnetData._1 + lazy val magnetPointInfo: Map[ConnectPoint, (DFDesignBlock, String)] = magnetData._2 // Hierarchical clone of `checkDanglingPorts`, invoked on the root DB. The // assignment coverage and the connected-point set are aggregated across all @@ -621,7 +621,7 @@ final case class DB private ( // are checked per instantiated design under its own sub-DB getSet. Only // instantiated designs (designBlockInstMap keys) are checked, matching the // flat version (the toptop's own ports are device IO, not dangling). - def new_checkDanglingPorts(): Unit = + def checkDanglingPorts(): Unit = val assignmentsDclTable: Map[DFVal.Dcl, Coverage] = subDBs.view.values.flatMap { sub => // resolve the dcl width inside the sub-DB's getSet (a DFBits width-param @@ -642,7 +642,7 @@ final case class DB private ( case pbns: DFVal.PortByNameSelect => ConnectPoint.Via(pbns) }.toList } - }.toSet ++ new_magnetConnectionMap.keySet + }.toSet ++ magnetConnectionMap.keySet // input ports: checked from the parent sub-DB that owns the instance val danglingInputs = subDBs.view.values.flatMap { parentSub => parentSub.atGetSet { @@ -687,7 +687,7 @@ final case class DB private ( val danglingPorts = (danglingInputs ++ danglingOutputs).toList if (danglingPorts.nonEmpty) throw new IllegalArgumentException(danglingPorts.mkString("\n")) - end new_checkDanglingPorts + end checkDanglingPorts extension (domainOwner: DFDomainOwner) // Aggregates `@hw.constraints.IO` / `@timing.clock` annotations applied at @@ -717,24 +717,21 @@ final case class DB private ( end extension // =========================================================================== - // New-style (hierarchical) clones of the RT clk/rst domain analyses. They are - // invoked on the new-style ROOT DB and mirror the flat versions above, but - // route every ref resolution / per-design table lookup to the OWNING sub-DB's - // getSet (the root getSet throws). On sub-DBs and old-style flat DBs they - // delegate to the root. Gated against the flat versions by - // `new_hierEquivalenceCheck` (run from SanityCheck) until the consuming - // stages are migrated, after which the flat versions are dropped and these - // lose the `new_` prefix. + // RT clk/rst domain analyses, computed on the hierarchical ROOT DB. Each + // routes ref resolution / per-design table lookups to the OWNING sub-DB's + // getSet (the root getSet throws); a sub-DB delegates up to its root. The + // public entry point `resolvedClkRstMap` additionally accepts a bare + // old-style flat DB by converting it via `oldToNew` first. // =========================================================================== // Cross-design reaches navigate the DESIGN TREE (no global member index): // `subDBs.get(d.ownerRef)` goes DOWN to a child design's sub-DB; // `subDB.parentSubDBOpt` goes UP to the parent sub-DB where that design is - // instantiated. Each new_* analysis processes one sub-DB's own members at a - // time, under that sub-DB's getSet, hopping between neighbors via the tree. + // instantiated. Each analysis processes one sub-DB's own members at a time, + // under that sub-DB's getSet, hopping between neighbors via the tree. - lazy val new_relatedAnnotMap: Map[DFDomainOwner, DFDomainOwner] = - if (!isRoot) rootDB.new_relatedAnnotMap + lazy val relatedAnnotMap: Map[DFDomainOwner, DFDomainOwner] = + if (!isRoot) rootDB.relatedAnnotMap else subDBs.view.values.flatMap { sub => sub.atGetSet { @@ -750,7 +747,7 @@ final case class DB private ( // navigating DOWN to the targeted child design's sub-DB and walking its // namedOwnerMemberTable there. Returns the port with the child sub-DB that // owns it, so callers can resolve the port's domain in the right getSet. - private def new_pbnsToPort( + private def pbnsToPort( pbns: DFVal.PortByNameSelect, ctxSub: DB ): Option[(DFVal.Dcl, DB)] = @@ -773,12 +770,12 @@ final case class DB private ( case _ => None } } - end new_pbnsToPort + end pbnsToPort // The domain that encloses `design`'s instantiation: navigate UP to `design`'s // parent sub-DB and read the owning domain of `design`'s inst there. Returns // the domain owner with the parent sub-DB that owns it. - private def new_designEnclosingDomain(design: DFDesignBlock): Option[(DFDomainOwner, DB)] = + private def designEnclosingDomain(design: DFDesignBlock): Option[(DFDomainOwner, DB)] = subDBs.get(design.ownerRef).flatMap(_.parentSubDBOpt).flatMap { parentSub => parentSub.atGetSet { parentSub.membersNoGlobals.view.collectFirst { @@ -790,14 +787,14 @@ final case class DB private ( // Routed clone of the local `getRTOwnerOption` from `dependentRTDomainOwners`. // `member` lives in `ctxSub`; returns its RT domain owner with the sub-DB that // owns that domain (None if the resolved domain is not RT). - private def new_getRTOwnerWithSub( + private def getRTOwnerWithSub( member: DFMember, ctxSub: DB ): Option[(DFDomainOwner, DB)] = val ownerAndSub: Option[(DFDomainOwner, DB)] = member match - case design: DFDesignBlock => new_designEnclosingDomain(design) + case design: DFDesignBlock => designEnclosingDomain(design) case pbns: DFVal.PortByNameSelect => - new_pbnsToPort(pbns, ctxSub) match + pbnsToPort(pbns, ctxSub) match case Some((port, portSub)) => Some(portSub.atGetSet(port.getOwnerDomain) -> portSub) case None => Some(ctxSub.atGetSet(pbns.getOwnerDomain) -> ctxSub) case _ => Some(ctxSub.atGetSet(member.getOwnerDomain) -> ctxSub) @@ -806,11 +803,11 @@ final case class DB private ( case DomainType.RT => Some(o -> sub) case _ => None } - end new_getRTOwnerWithSub + end getRTOwnerWithSub // Routed, best-effort clone of `fullNameViaInst` (error messages only). // `owner` lives in `ownerSub`; instance paths are recovered by navigating UP. - private def new_fullNameViaInst(owner: DFDomainOwner, ownerSub: DB): String = + private def fullNameViaInst(owner: DFDomainOwner, ownerSub: DB): String = val design = ownerSub.atGetSet(owner.getThisOrOwnerDesign) if (design eq topDB.top) ownerSub.atGetSet(owner.getFullName) else @@ -823,17 +820,17 @@ final case class DB private ( case _ => val designName = ownerSub.atGetSet(design.getFullName) s"$designName.${ownerSub.atGetSet(owner.getRelativeName(design))}" - end new_fullNameViaInst + end fullNameViaInst - lazy val new_dependentRTDomainOwners: Map[DFDomainOwner, DFDomainOwner] = - if (!isRoot) rootDB.new_dependentRTDomainOwners + lazy val dependentRTDomainOwners: Map[DFDomainOwner, DFDomainOwner] = + if (!isRoot) rootDB.dependentRTDomainOwners else subDBs.view.values.flatMap { subDB => subDB.domainOwnerMemberList.view.flatMap { case (domainOwner, domainMembers) => subDB.atGetSet { domainOwner.domainType match case DomainType.RT => - new_relatedAnnotMap.get(domainOwner) match + relatedAnnotMap.get(domainOwner) match case Some(relatedOwner) => Some(domainOwner -> relatedOwner) case None => val hasClkOrRst = domainOwner.meta.annotations.exists { @@ -849,7 +846,7 @@ final case class DB private ( // sub-DB's own `isTopTop`/`isTop` can't tell because // every design block has an Empty ownerRef. if (design eq topDB.top) None - else new_getRTOwnerWithSub(design, subDB).map { case (o, _) => + else getRTOwnerWithSub(design, subDB).map { case (o, _) => design -> o } case domain: DomainBlock => @@ -885,22 +882,22 @@ final case class DB private ( netSub.atGetSet { net match case DFNet.Connection(_, from, _) => - new_getRTOwnerWithSub(from, netSub) + getRTOwnerWithSub(from, netSub) case _ => None } } }.toSet val inSourceDomains = inSources.map { case (o, _) => o } if (inSourceDomains.isEmpty) - new_getRTOwnerWithSub(domain, subDB).map { case (o, _) => + getRTOwnerWithSub(domain, subDB).map { case (o, _) => domain -> o } else if (inSourceDomains.size > 1) throw new IllegalArgumentException( s"""|Found ambiguous source RT configurations for the domain: - |${new_fullNameViaInst(domain, subDB)} + |${fullNameViaInst(domain, subDB)} |Sources: - |${inSources.map { case (o, s) => new_fullNameViaInst(o, s) } + |${inSources.map { case (o, s) => fullNameViaInst(o, s) } .mkString("\n")} |Possible solution: |Either explicitly define a configuration for the domain or drive it from a single source domain. @@ -914,16 +911,16 @@ final case class DB private ( }.toMap // Hierarchical equivalent of the `isDependentOn` analysis: does `domainOwner` - // transitively depend on `thatDomainOwner` per `new_dependentRTDomainOwners`? + // transitively depend on `thatDomainOwner` per `dependentRTDomainOwners`? // Invoked on the root DB. - @tailrec final def new_isDependentOn( + @tailrec final def isDependentOn( domainOwner: DFDomainOwner, thatDomainOwner: DFDomainOwner ): Boolean = - new_dependentRTDomainOwners.get(domainOwner) match + dependentRTDomainOwners.get(domainOwner) match case Some(dependency) => if (dependency == thatDomainOwner) true - else new_isDependentOn(dependency, thatDomainOwner) + else isDependentOn(dependency, thatDomainOwner) case None => false // Structural map: every domain owner -> its sub-DB. Keys are domain owners @@ -947,7 +944,7 @@ final case class DB private ( else if (!isRoot) rootDB.resolvedClkRstMap else val reversedDependents: Map[DFDomainOwner, Set[DFDomainOwner]] = - new_dependentRTDomainOwners.invert + dependentRTDomainOwners.invert val designUsesClkRst = mutable.Map.empty[String, (usesClk: Boolean, usesRst: Boolean)] val domainOwnerUsesClkRst = @@ -1066,7 +1063,7 @@ final case class DB private ( case domain :: rest if domainMap.contains(domain) => fillDomainMap(rest, stack, domainMap) case domain :: rest => - new_dependentRTDomainOwners.get(domain) match + dependentRTDomainOwners.get(domain) match case Some(dependencyDomain) => domainMap.get(dependencyDomain) match case Some(dependencyResolved) => @@ -1097,7 +1094,7 @@ final case class DB private ( // resolved-clock map drives both the filter and the explicit rate. Per-owner // local reads (isDeviceTop, isTop position, getFullName, the clk timing // constraint) are routed through the owner's sub-DB getSet via `atOwner`. - def new_domainClkRateCheck(): Unit = + def domainClkRateCheck(): Unit = def atOwner[T](owner: DFDomainOwner)(f: MemberGetSet ?=> T): T = domainOwnerToSubDB(owner).atGetSet(f) val errors = collection.mutable.ArrayBuffer[String]() @@ -1148,14 +1145,14 @@ final case class DB private ( } if (errors.nonEmpty) throw new IllegalArgumentException(errors.mkString("\n")) - end new_domainClkRateCheck + end domainClkRateCheck // Hierarchical clone of `waitCheck`, invoked on the root DB. Iterates each // sub-DB's RT waits under that sub-DB's getSet (the root has no members of its // own); the clock rate comes from resolvedClkRstMap. The error hierarchy - // uses new_fullNameViaInst so a wait in a nested design reports its full + // uses fullNameViaInst so a wait in a nested design reports its full // instance path (plain getFullName on a sub-DB would be relative). - def new_waitCheck(): Unit = + def waitCheck(): Unit = val errors = collection.mutable.ArrayBuffer[String]() subDBs.view.values.foreach { sub => sub.atGetSet { @@ -1167,7 +1164,7 @@ final case class DB private ( def waitError(msg: String): Unit = errors += s"""|DFiant HDL wait error! |Position: ${wait.meta.position} - |Hierarchy: ${new_fullNameViaInst(wait.getOwnerDesign, sub)} + |Hierarchy: ${fullNameViaInst(wait.getOwnerDesign, sub)} |Message: $msg""".stripMargin val ownerDomain = wait.getOwnerDomain trigger.getConstData[TimeNumber].toOption match @@ -1196,12 +1193,12 @@ final case class DB private ( } if (errors.nonEmpty) throw new IllegalArgumentException(errors.mkString("\n")) - end new_waitCheck + end waitCheck // Hierarchical clone of `circularDerivedDomainsCheck`, invoked on the root DB. - // Same DFS, over `new_dependentRTDomainOwners`; the cycle error names each - // owner via `new_fullNameViaInst` routed through that owner's sub-DB. - def new_circularDerivedDomainsCheck(): Unit = + // Same DFS, over `dependentRTDomainOwners`; the cycle error names each + // owner via `fullNameViaInst` routed through that owner's sub-DB. + def circularDerivedDomainsCheck(): Unit = @tailrec def dfs( node: DFDomainOwner, visited: Set[DFDomainOwner], @@ -1210,17 +1207,17 @@ final case class DB private ( if (stack.contains(node)) throw new IllegalArgumentException( s"""|Circular derived RT configuration detected. Involved in the cycle: - |${stack.map(o => new_fullNameViaInst(o, domainOwnerToSubDB(o))).mkString("\n")} + |${stack.map(o => fullNameViaInst(o, domainOwnerToSubDB(o))).mkString("\n")} |""".stripMargin ) if (!visited.contains(node)) - new_dependentRTDomainOwners.get(node) match + dependentRTDomainOwners.get(node) match case Some(dependentNode) => dfs(dependentNode, visited + node, stack + node) case None => end dfs - for (node <- new_dependentRTDomainOwners.keys) + for (node <- dependentRTDomainOwners.keys) dfs(node, Set.empty, Set.empty) - end new_circularDerivedDomainsCheck + end circularDerivedDomainsCheck def nameCheck(): Unit = // We use a Set since meta programming is usually the cause and can result in @@ -1322,7 +1319,7 @@ final case class DB private ( // Hierarchical clone of `portLocationCheck`, invoked on the root DB. Only the // device-top design is examined; its members are resolved under that design's // sub-DB getSet (device top == toptop, so getFullName is the full path). - def new_portLocationCheck(): Unit = + def portLocationCheck(): Unit = val errors = mutable.ListBuffer.empty[String] val locationCollisions = mutable.ListBuffer.empty[String] designMemberList.foreach { @@ -1407,11 +1404,11 @@ final case class DB private ( |Ensure each location is used by a single port bit. |""".stripMargin ) - end new_portLocationCheck + end portLocationCheck // Hierarchical clone of `portResourceDirCheck`, invoked on the root DB. Same // device-top-only check, members resolved under the device-top sub-DB getSet. - def new_portResourceDirCheck(): Unit = + def portResourceDirCheck(): Unit = import DFVal.Modifier.Dir val errors = mutable.ListBuffer.empty[String] designMemberList.foreach { @@ -1441,7 +1438,7 @@ final case class DB private ( |Make sure you connect the resource to the port with the correct direction. |""".stripMargin ) - end new_portResourceDirCheck + end portResourceDirCheck // Uniform entry point, representation-aware: // - hierarchical root: run each sub-DB's per-design checks, then the @@ -1451,7 +1448,7 @@ final case class DB private ( lazy val check: Unit = if (isRoot) subDBs.view.values.foreach(_.subDBCheck) - new_rootDBCheck + rootDBCheck else subDBCheck // Per-design structural checks: each validates a single design's own members @@ -1463,16 +1460,16 @@ final case class DB private ( directRefCheck() // Whole-tree checks, run once on the root: the cross-design connectivity / - // RT-domain / device-top checks, via the `new_*` clones that navigate the + // RT-domain / device-top checks, via the `*` clones that navigate the // sub-DB tree. - private lazy val new_rootDBCheck: Unit = - new_magnetConnectionMap // causes magnet connectivity checks - new_checkDanglingPorts() - new_circularDerivedDomainsCheck() - new_domainClkRateCheck() - new_waitCheck() - new_portLocationCheck() - new_portResourceDirCheck() + private lazy val rootDBCheck: Unit = + magnetConnectionMap // causes magnet connectivity checks + checkDanglingPorts() + circularDerivedDomainsCheck() + domainClkRateCheck() + waitCheck() + portLocationCheck() + portResourceDirCheck() // There can only be a single connection to a value in a given range // (multiple assignments are possible) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddMagnets.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddMagnets.scala index 9fda2639a..da4e68deb 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddMagnets.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddMagnets.scala @@ -23,11 +23,11 @@ case object AddMagnets extends GlobalStage: given MemberGetSet = designDB.getSet // owner design + name of each magnet point, precomputed cross-design by the // analysis so a ConnectPoint living in another sub-DB is never re-resolved. - def ownerOf(cp: ConnectPoint): DFDesignBlock = designDB.new_magnetPointInfo(cp)._1 - def nameOf(cp: ConnectPoint): String = designDB.new_magnetPointInfo(cp)._2 + def ownerOf(cp: ConnectPoint): DFDesignBlock = designDB.magnetPointInfo(cp)._1 + def nameOf(cp: ConnectPoint): String = designDB.magnetPointInfo(cp)._2 // Populating a missing magnets map with the suggested port names and direction val missingMagnets = mutable.Map.empty[DFDesignBlock, Map[DFType, (String, DFVal.Modifier.Dir)]] - designDB.new_magnetConnectionMap.foreach { (toMP, fromMP) => + designDB.magnetConnectionMap.foreach { (toMP, fromMP) => val toDsn = ownerOf(toMP) val fromDsn = ownerOf(fromMP) val fromName = nameOf(fromMP) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ConnectMagnets.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ConnectMagnets.scala index 8cc593ac1..0940fb23a 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ConnectMagnets.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ConnectMagnets.scala @@ -18,13 +18,13 @@ case object ConnectMagnets extends HierarchyStage: val design = subDB.top // owner design + name of each magnet point, precomputed cross-design by the // analysis so a ConnectPoint living in another sub-DB is never re-resolved. - def ownerOf(cp: ConnectPoint): DFDesignBlock = rootDB.new_magnetPointInfo(cp)._1 + def ownerOf(cp: ConnectPoint): DFDesignBlock = rootDB.magnetPointInfo(cp)._1 // a design's parent design via the root-aware design tree (no ref resolution). // For (IN, OUT) the connection lives in the design containing both ports' // designs — i.e. the parent of `toMP`'s design. def parentOf(d: DFDesignBlock): Option[DFDesignBlock] = rootDB.designBlockOwnershipMap.get(d).flatMap(_.headOption) - val connsForDesign = rootDB.new_magnetConnectionMap.iterator.flatMap { case (toMP, fromMP) => + val connsForDesign = rootDB.magnetConnectionMap.iterator.flatMap { case (toMP, fromMP) => val targetDsn = if (toMP.isPortIn && (fromMP.isPortIn || fromMP.isVar)) Some(ownerOf(fromMP)) else if (toMP.isPortOut && fromMP.isPortOut) Some(ownerOf(toMP)) @@ -34,7 +34,7 @@ case object ConnectMagnets extends HierarchyStage: if (targetDsn.contains(design)) Some((toMP, fromMP)) else None }.toList if (connsForDesign.nonEmpty) - val magnets = connsForDesign.sortBy { case (toMP, _) => rootDB.new_magnetPointInfo(toMP)._2 } + val magnets = connsForDesign.sortBy { case (toMP, _) => rootDB.magnetPointInfo(toMP)._2 } val dsn = new MetaDesign(design, Patch.Add.Config.InsideLast): extension (mp: ConnectPoint) def toDclAny(using MemberGetSet) = mp match diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ExplicitClkRstCfg.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ExplicitClkRstCfg.scala index 3a2690a55..cf25b81a0 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ExplicitClkRstCfg.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ExplicitClkRstCfg.scala @@ -17,7 +17,7 @@ import dfhdl.core.{refTW, DFC} case object ExplicitClkRstCfg extends HierarchyStage: def dependencies: List[Stage] = List(UniqueDesigns, NamedAnonMultiref) def nullifies: Set[Stage] = Set(DropUnreferencedAnons) - // Cross-design clk/rst resolution (`resolvedClkRstMap` / `new_isDependentOn`) + // Cross-design clk/rst resolution (`resolvedClkRstMap` / `isDependentOn`) // is read from the root DB; everything else is per-sub-DB via the `subDB` // helper (this stage's current sub-DB). def transformSubDB(rootDB: DB)(using @@ -59,7 +59,7 @@ case object ExplicitClkRstCfg extends HierarchyStage: owner match case domain: DomainBlock => val domainOwner = domain.getOwnerDomain - if (rootDB.new_isDependentOn(domain, domainOwner)) + if (rootDB.isDependentOn(domain, domainOwner)) val ref = domainOwner.asInstanceOf[DomainBlock | DFDesignBlock].refTW[DomainBlock] relatedCfgRefs += ref -> domainOwner From b240594887937bd59c543585662059b59ecbf51b Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 13 Jun 2026 19:45:02 +0300 Subject: [PATCH 26/72] Delete the flat MagnetMap.get; rename getHierarchical->get (Step 3d) The flat MagnetMap.get (full-member-index magnet matching) lost its only caller when the flat magnetConnectionMap was removed in Step 3b. Drop it and rename the surviving hierarchical getHierarchical to the canonical get (its sole call site is DB.magnetData). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 12 +- .../scala/dfhdl/compiler/ir/MagnetMap.scala | 183 +----------------- 2 files changed, 16 insertions(+), 179 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index 4910c1da5..7c3e65ba1 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -599,16 +599,16 @@ final case class DB private ( end match end getConnToMap - // Hierarchical clone of `magnetConnectionMap`, invoked on the root DB. The - // magnet matching is intrinsically cross-design; `MagnetMap.getHierarchical` + // Magnet connection map + per-point (owner design, name) info, computed on the + // root DB. Magnet matching is intrinsically cross-design; `MagnetMap.get` // precomputes each magnet point's design context per sub-DB, then matches on - // the root-aware design tree (no flattening). It also returns each magnet - // point's (owner design, name) so migrating consumers don't re-resolve a - // cross-design ConnectPoint (which would need a flat member index). + // the root-aware design tree (no flattening). The point info lets consumers + // avoid re-resolving a cross-design ConnectPoint (which would need a flat + // member index). private lazy val magnetData : (Map[ConnectPoint, ConnectPoint], Map[ConnectPoint, (DFDesignBlock, String)]) = if (!isRoot) rootDB.magnetData - else MagnetMap.getHierarchical(this) + else MagnetMap.get(this) lazy val magnetConnectionMap: Map[ConnectPoint, ConnectPoint] = magnetData._1 lazy val magnetPointInfo: Map[ConnectPoint, (DFDesignBlock, String)] = magnetData._2 diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/MagnetMap.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/MagnetMap.scala index 92f3ff940..94ca78b16 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/MagnetMap.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/MagnetMap.scala @@ -73,178 +73,15 @@ end ConnectPoint type MagnetMap = Map[ConnectPoint, ConnectPoint] object MagnetMap: - def get(using MemberGetSet): MagnetMap = - var errors = List.empty[String] - def newError(errMsg: String): Option[ConnectPoint] = - errors = errMsg :: errors - None - val magnetPointView: View[ConnectPoint] = - // include the magnet points that are declared via design instances - getSet.designDB.members.view.flatMap { - case designInst: DFDesignInst => - designInst.getDesignBlock.members(MemberView.Folded).view.flatMap { - case dcl @ MagnetDcl(_) => Some(ConnectPoint.Via(designInst, dcl)) - case _ => None - } - case _ => Nil - } ++ - // also include the magnet points that are directly declared in the design, - // not via a design instance - getSet.designDB.designMemberList.view.flatMap { (design, members) => - members.view.flatMap { - case dcl @ MagnetDcl(_) => Some(ConnectPoint.Direct(dcl)) - case _ => None - } - } - val magnetPointGrps: List[List[ConnectPoint]] = magnetPointView.groupBy { - case cp => cp.dfType - }.view.values.map(_.toList).toList - - // set of magnet ports that are explicitly connected/assigned - val alreadyConnectedOrAssignedDcls: Set[DFVal.Dcl] = - getSet.designDB.assignmentsTable.keys.flatMap(_.dealias).collect { - case dcl @ MagnetDcl(_) if dcl.isPort => dcl - }.toSet ++ getSet.designDB.connectionTable.connectToVals.collect { - case dcl @ MagnetDcl(_) if dcl.isPort => dcl - } - - val alreadyConnectedMPVias: Set[ConnectPoint.Via] = - getSet.designDB.connectionTable.connectToVals.flatMap { - case dfVal @ Magnet(_) => dfVal match - case pbns: DFVal.PortByNameSelect => Some(ConnectPoint.Via(pbns)) - case _ => None - case _ => None - } - // TODO: what to do with missing clk/rst definitions in RTDomains when they are not declared? - // Option 1: create a dedicated check for clk/rst - // Option 2: always add clk/rst in RTDomains in elaboration using injection, if the user did not construct them. - // this will remove the need for AddClkRst stage. - // Option 3: apply AddClkRst stage after elaboration and before elaboration checks. this is interesting since we could - // use this mechanism to apply various design fixes from elaboration meta-programming. - def missingSourceError(targetMP: ConnectPoint): Option[ConnectPoint] = - // newError( - // s"""|Missing magnet source for target port ${targetPort.getName} - // |Position: ${targetPort.meta.position} - // |Hierarchy: ${targetPort.getOwnerNamed.getFullName}""".stripMargin - // ) - None - - val ret = magnetPointGrps.flatMap { mpGrp => - mpGrp.view - // first rejecting inviable magnet targets - .filter { - // rejecting design block inputs or outputs of blackbox design blocks or - // already connected/assigned direct design blocks variables/ports - case ConnectPoint.Direct(dcl) - if dcl.isPortIn || dcl.isPortOut && dcl.getOwnerDesign.isBlackBox || - alreadyConnectedOrAssignedDcls.contains(dcl) => false - // rejecting connected port vias that are outputs or already connected - case via: ConnectPoint.Via if via.isPortOut || alreadyConnectedMPVias.contains(via) => - false - // the rest of the points are viable magnet targets - case _ => true - } - // finding the magnet source point for each target point - .flatMap { targetMP => - val targetDsn = targetMP.getOwnerDesign - val sourceMP: Option[ConnectPoint] = - // target is a via port in (direct port in cannot be a target) - if (targetMP.isPortIn) - // sorted source in port candidates according to the distance - val sourceInCandidates = mpGrp.filter { - case ConnectPoint.Direct(dcl) - if (dcl.isPortIn || dcl.isVar) && targetMP.isInsideOwner(dcl.getOwnerDesign) => - true - case _ => false - }.map { srcMP => - (srcMP, targetDsn.getDistanceFromOwnerDesign(srcMP.getOwnerDesign)) - }.toList.sortBy(_._2) - // sorted source out port candidates according to the distance - val sourceOutCandidates = mpGrp.filter { - case via: ConnectPoint.Via if via.isPortOut => true - case _ => false - }.map { srcMP => - val mpDsn = srcMP.getOwnerDesign - val commonDesign = targetDsn.getCommonDesignWith(mpDsn) - ( - srcMP, - targetDsn.getDistanceFromOwnerDesign(commonDesign), - mpDsn.getDistanceFromOwnerDesign(commonDesign) - ) - }.toList.sortBy(_._3).sortBy(_._2) - (sourceInCandidates, sourceOutCandidates) match - case (Nil, Nil) => - missingSourceError(targetMP) - case (Nil, (src, _, _) :: _) => - Some(src) - case ((src, _) :: _, Nil) => - Some(src) - case ((srcIn, distIn) :: _, (srcOut, distOut, _) :: _) => - if (distIn < distOut) Some(srcIn) - else - newError( - s"""|Found two possible magnet sources for a target magnet. - |Target Position: ${targetMP.position} - |Target Path: ${targetMP.getFullName} - |Source1 Position: ${srcIn.position} - |Source1 Path: ${srcIn.getFullName} - |Source2 Position: ${srcOut.position} - |Source2 Path: ${srcOut.getFullName}""".stripMargin - ) - end match - // target is direct output port - else - // sorted source candidates according to the distance - val sourceOutCandidates = mpGrp.filter { - case via: ConnectPoint.Via if via.isPortOut && via.isInsideOwner(targetDsn) => true - case direct: ConnectPoint.Direct - if direct.isVar && direct.isInsideOwner(targetDsn) || - direct.isPortIn && direct.getOwnerDesign == targetDsn => true - case _ => false - }.map { srcMP => - (srcMP, srcMP.getOwnerDesign.getDistanceFromOwnerDesign(targetDsn)) - }.toList.sortBy(_._2) - sourceOutCandidates match - case Nil => - missingSourceError(targetMP) - case (src, ld) :: otherCandidates => - var lastDistance: Int = ld - var lastSrc: ConnectPoint = src - otherCandidates.foreach { case (src, distance) => - if (distance == lastDistance) - newError( - s"""|Found two possible magnet sources for a target magnet. - |Target Position: ${targetMP.position} - |Target Path: ${targetMP.getFullName} - |Source1 Position: ${lastSrc.position} - |Source1 Path: ${lastSrc.getFullName} - |Source2 Position: ${src.position} - |Source2 Path: ${src.getFullName}""".stripMargin - ) - lastDistance = distance - lastSrc = src - } - Some(src) - end match - end if - end sourceMP - sourceMP.map(targetMP -> _) - } - }.toMap - if (errors.nonEmpty) - throw new IllegalArgumentException( - errors.view.reverse.mkString("\n\n") - ) - ret - end get - - // Hierarchical (new-style root DB) clone of `get`. Magnet matching is intrinsically - // cross-design: it pairs sources to targets across the whole hierarchy by distance. - // Approach: resolve each magnet ConnectPoint's design context (owner + container - // design) ONCE under its owning sub-DB getSet, then run the distance / inside-owner - // matching purely on the root-aware design tree (designBlockOwnershipMap) — no ref - // resolution during matching, so the throwing root getSet is fine. - def getHierarchical(rootDB: DB): (MagnetMap, Map[ConnectPoint, (DFDesignBlock, String)]) = + // Computes the magnet connection map on the hierarchical ROOT DB. Magnet + // matching is intrinsically cross-design: it pairs sources to targets across + // the whole hierarchy by distance. Approach: resolve each magnet ConnectPoint's + // design context (owner + container design) ONCE under its owning sub-DB getSet, + // then run the distance / inside-owner matching purely on the root-aware design + // tree (designBlockOwnershipMap) — no ref resolution during matching, so the + // throwing root getSet is fine. Also returns each magnet point's (owner design, + // name) so consumers don't re-resolve a cross-design ConnectPoint. + def get(rootDB: DB): (MagnetMap, Map[ConnectPoint, (DFDesignBlock, String)]) = // a magnet ConnectPoint with its design context precomputed under the // owning sub-DB getSet (so the matching never resolves refs) final case class RMP( @@ -454,5 +291,5 @@ object MagnetMap: val pointInfo: Map[ConnectPoint, (DFDesignBlock, String)] = allRMPs.iterator.map(rmp => rmp.cp -> (rmp.ownerDesign, rmp.name)).toMap (ret, pointInfo) - end getHierarchical + end get end MagnetMap From e8d5eadfd62c35b199d8701238eec64769991fbc Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 13 Jun 2026 19:47:51 +0300 Subject: [PATCH 27/72] Polish now-self-referential analysis/check comments (Step 3d) After dropping the new_ prefix, the doc comments that described each method as a "Hierarchical clone of " / "Routed clone of " now name the method itself, and a couple referenced deleted flat helpers (getRTOwnerOption, the flat usesClk filter). Reword them to describe the methods directly. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index 7c3e65ba1..631ae287a 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -612,15 +612,15 @@ final case class DB private ( lazy val magnetConnectionMap: Map[ConnectPoint, ConnectPoint] = magnetData._1 lazy val magnetPointInfo: Map[ConnectPoint, (DFDesignBlock, String)] = magnetData._2 - // Hierarchical clone of `checkDanglingPorts`, invoked on the root DB. The - // assignment coverage and the connected-point set are aggregated across all - // sub-DBs (each design's assignments/connections live in its own sub-DB) plus - // the cross-design magnet connections. Input ports are checked from each - // parent sub-DB (which owns the instance and its connections), reading the - // child design's port list via the root-aware designMemberTable; output ports - // are checked per instantiated design under its own sub-DB getSet. Only - // instantiated designs (designBlockInstMap keys) are checked, matching the - // flat version (the toptop's own ports are device IO, not dangling). + // Dangling-port check, run on the root DB. The assignment coverage and the + // connected-point set are aggregated across all sub-DBs (each design's + // assignments/connections live in its own sub-DB) plus the cross-design + // magnet connections. Input ports are checked from each parent sub-DB (which + // owns the instance and its connections), reading the child design's port + // list via the root-aware designMemberTable; output ports are checked per + // instantiated design under its own sub-DB getSet. Only instantiated designs + // (designBlockInstMap keys) are checked — the toptop's own ports are device + // IO, not dangling. def checkDanglingPorts(): Unit = val assignmentsDclTable: Map[DFVal.Dcl, Coverage] = subDBs.view.values.flatMap { sub => @@ -784,9 +784,8 @@ final case class DB private ( }.map(_ -> parentSub) } - // Routed clone of the local `getRTOwnerOption` from `dependentRTDomainOwners`. - // `member` lives in `ctxSub`; returns its RT domain owner with the sub-DB that - // owns that domain (None if the resolved domain is not RT). + // `member` lives in `ctxSub`; returns its RT domain owner together with the + // sub-DB that owns that domain (None if the resolved domain is not RT). private def getRTOwnerWithSub( member: DFMember, ctxSub: DB @@ -805,14 +804,14 @@ final case class DB private ( } end getRTOwnerWithSub - // Routed, best-effort clone of `fullNameViaInst` (error messages only). - // `owner` lives in `ownerSub`; instance paths are recovered by navigating UP. + // Best-effort full instance path for `owner` (error messages only). `owner` + // lives in `ownerSub`; instance paths are recovered by navigating UP. private def fullNameViaInst(owner: DFDomainOwner, ownerSub: DB): String = val design = ownerSub.atGetSet(owner.getThisOrOwnerDesign) if (design eq topDB.top) ownerSub.atGetSet(owner.getFullName) else - // Mirror the flat `fullNameViaInst`, whose `inst.getFullName` yields the - // design CLASS name (e.g. `Internal2`), not the instance path: resolve the + // The enclosing design's `inst.getFullName` yields the design CLASS name + // (e.g. `Internal2`), not the instance path: resolve the // enclosing design by its own full name in its sub-DB (where it is the // top), then append the owner's design-relative name. owner match @@ -1088,10 +1087,10 @@ final case class DB private ( fillDomainMap(rtDomainOwners, Nil, domainMap) domainMap.toMap - // Hierarchical clone of `domainClkRateCheck`, invoked on the root DB. The flat - // `usesClk` filter (cross-design, flat-only) is equivalent to "the resolver - // produced a clock" (resolvedClkRstMap(owner)._1.isDefined), so the - // resolved-clock map drives both the filter and the explicit rate. Per-owner + // Device-top clock-rate check, run on the root DB. A device-top RT domain + // "uses a clock" exactly when the resolver produced a clock + // (resolvedClkRstMap(owner)._1.isDefined), so the resolved-clock map drives + // both the filter and the explicit rate. Per-owner // local reads (isDeviceTop, isTop position, getFullName, the clk timing // constraint) are routed through the owner's sub-DB getSet via `atOwner`. def domainClkRateCheck(): Unit = @@ -1147,7 +1146,7 @@ final case class DB private ( throw new IllegalArgumentException(errors.mkString("\n")) end domainClkRateCheck - // Hierarchical clone of `waitCheck`, invoked on the root DB. Iterates each + // Timed-wait divisibility check, run on the root DB. Iterates each // sub-DB's RT waits under that sub-DB's getSet (the root has no members of its // own); the clock rate comes from resolvedClkRstMap. The error hierarchy // uses fullNameViaInst so a wait in a nested design reports its full @@ -1195,8 +1194,8 @@ final case class DB private ( throw new IllegalArgumentException(errors.mkString("\n")) end waitCheck - // Hierarchical clone of `circularDerivedDomainsCheck`, invoked on the root DB. - // Same DFS, over `dependentRTDomainOwners`; the cycle error names each + // Circular-derived-domain check, run on the root DB. DFS over + // `dependentRTDomainOwners`; the cycle error names each // owner via `fullNameViaInst` routed through that owner's sub-DB. def circularDerivedDomainsCheck(): Unit = @tailrec def dfs( @@ -1316,7 +1315,7 @@ final case class DB private ( ) end directRefCheck - // Hierarchical clone of `portLocationCheck`, invoked on the root DB. Only the + // Device-top port location-constraint check, run on the root DB. Only the // device-top design is examined; its members are resolved under that design's // sub-DB getSet (device top == toptop, so getFullName is the full path). def portLocationCheck(): Unit = @@ -1406,8 +1405,8 @@ final case class DB private ( ) end portLocationCheck - // Hierarchical clone of `portResourceDirCheck`, invoked on the root DB. Same - // device-top-only check, members resolved under the device-top sub-DB getSet. + // Device-top port resource-direction check, run on the root DB. Members + // resolved under the device-top sub-DB getSet. def portResourceDirCheck(): Unit = import DFVal.Modifier.Dir val errors = mutable.ListBuffer.empty[String] From aed6baace4c9fe67d025774a517f5d75408d433f Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 13 Jun 2026 19:50:31 +0300 Subject: [PATCH 28/72] Build oldToNew once per SanityCheck call (Step 3e) SanityCheck used oldToNew twice per invocation (once for the checks, once for the round-trip), and oldToNew is O(members) while SanityCheck runs after every stage. Compute the hierarchical DB once and thread it into both the check and hierarchicalDBRoundTripCheck. (The gate removed in Step 3b had been a third oldToNew call.) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../scala/dfhdl/compiler/stages/SanityCheck.scala | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala index c32b13e15..51ece2a82 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala @@ -315,8 +315,8 @@ case class SanityCheck(skipAnonRefCheck: Boolean) extends Stage: // not preserved across the round-trip. Compare globals by identity-set and // the rest of the members as an ordered list. RefTable, globalTags, and // srcFiles still must match exactly. - private def hierarchicalDBRoundTripCheck(designDB: DB): Unit = - val lhs = designDB.oldToNew.newToOld.canonicalForm + private def hierarchicalDBRoundTripCheck(designDB: DB, hierDB: DB): Unit = + val lhs = hierDB.newToOld.canonicalForm val rhs = designDB.canonicalForm def partition(db: DB): (Set[DFMember], List[DFMember]) = given MemberGetSet = db.getSet @@ -356,8 +356,11 @@ case class SanityCheck(skipAnonRefCheck: Boolean) extends Stage: memberExistenceCheck() ownershipCheck(designDB.top, designDB.membersNoGlobals.drop(1)) orderCheck() - designDB.oldToNew.check - hierarchicalDBRoundTripCheck(designDB) + // Build the hierarchical DB once and reuse it for both the checks and the + // round-trip (oldToNew is O(members) and SanityCheck runs after every stage). + val hierDB = designDB.oldToNew + hierDB.check + hierarchicalDBRoundTripCheck(designDB, hierDB) designDB end SanityCheck From 06650fda4b21e028aeceb085470726b60330b6b2 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 13 Jun 2026 20:45:30 +0300 Subject: [PATCH 29/72] Run SanityCheck structural checks per sub-DB SanityCheck.transform now builds the hierarchical DB once, runs the four structural checks (refCheck, memberExistenceCheck, ownershipCheck, orderCheck) per sub-DB under each sub-DB's own getSet, then runs the design checks via hierDB.check. The temporary hierarchicalDBRoundTripCheck is removed. Investigation (instrumenting each check independently across all 434 StagesSpec tests) showed refCheck was the only check that failed on sub-DB scope; the other three are per-sub-DB-safe. refCheck had two cross-design assumptions, both fixed by fetching the child design's sub-DB via rootDB.subDBs and reading it under that sub-DB's getSet: * instPortsByNameSet read child ports via inst.getDesignBlock.members(Folded), which threw NoSuchElementException because the child design isn't an owner in the parent sub-DB. * the "removed member" refTable scan flagged inst.designRef -> childBlock (a legitimate cross-sub-DB reference); it now whitelists DFDesignBlock targets that are live subDBs keys. Both carry a TODO for the future unification of DFDesignInst.designRef with the child DFDesignBlock.ownerRef (the subDBs key), which collapses the lookups to rootDB.subDBs.get(inst.designRef). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../dfhdl/compiler/stages/SanityCheck.scala | 98 ++++++++----------- 1 file changed, 42 insertions(+), 56 deletions(-) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala index 51ece2a82..18f9ae73f 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala @@ -20,13 +20,27 @@ case class SanityCheck(skipAnonRefCheck: Boolean) extends Stage: hasViolations = true System.err.println(msg) - // for quick lookup of by-name port selections during ref checks + // For quick lookup of by-name port selections during ref checks. refCheck + // runs per sub-DB, so an instantiated child design's ports live in the + // child's own sub-DB (not the current one). Fetch the child sub-DB through + // the design tree and read its folded ports under the child's getSet — the + // resulting (inst, relativeName) tuples are consumed below under the current + // (parent) sub-DB's getSet. + val rootDB = getSet.designDB.rootDB val instPortsByNameSet = getSet.designDB.members.view.flatMap { case inst: DFDesignInst => - val design = inst.getDesignBlock - design.members(MemberView.Folded).view.collect { - case port: DFVal.Dcl if port.isPort => (inst, port.getRelativeName(design)) - } + val childBlock = inst.getDesignBlock + // TODO: once DFDesignInst.designRef is unified with the child + // DFDesignBlock.ownerRef (the `subDBs` key), this simplifies to + // `rootDB.subDBs.get(inst.designRef)` with no block resolution first. + rootDB.subDBs.get(childBlock.ownerRef) match + case Some(childSub) => + childSub.atGetSet { + childBlock.members(MemberView.Folded).view.collect { + case port: DFVal.Dcl if port.isPort => (inst, port.getRelativeName(childBlock)) + }.toList + } + case None => Nil case _ => Nil }.toSet @@ -140,7 +154,16 @@ case class SanityCheck(skipAnonRefCheck: Boolean) extends Stage: } // checks for all references refTable.foreach { (r, m) => - if (!m.isInstanceOf[DFMember.Empty] && !memberSet.contains(m)) + // A DFDesignInst.designRef targets its child DFDesignBlock, which lives in + // the child's own sub-DB rather than this one — a legitimate cross-sub-DB + // reference, not a removed member (refCheck runs per sub-DB). + // TODO: once DFDesignInst.designRef is unified with the child + // DFDesignBlock.ownerRef (the `subDBs` key), the whitelist key `d.ownerRef` + // is exactly that unified ref. + val isLiveChildDesign = m match + case d: DFDesignBlock => rootDB.subDBs.contains(d.ownerRef) + case _ => false + if (!m.isInstanceOf[DFMember.Empty] && !isLiveChildDesign && !memberSet.contains(m)) reportViolation(s"Ref $r exists for a removed member: $m") r match case r: DFRef.TwoWayAny if !originRefTable.contains(r) => @@ -308,60 +331,23 @@ case class SanityCheck(skipAnonRefCheck: Boolean) extends Stage: require(!hasViolations, "Failed member order check!") end orderCheck - // Temporary Phase 1 check: exercise the old<->new DB conversion round-trip - // against every design the test suite exercises via sanityCheck. Under - // B-pure, globals no longer live at root in the new-style DB and are - // partitioned per sub-DB by closure; the elaboration order of globals is - // not preserved across the round-trip. Compare globals by identity-set and - // the rest of the members as an ordered list. RefTable, globalTags, and - // srcFiles still must match exactly. - private def hierarchicalDBRoundTripCheck(designDB: DB, hierDB: DB): Unit = - val lhs = hierDB.newToOld.canonicalForm - val rhs = designDB.canonicalForm - def partition(db: DB): (Set[DFMember], List[DFMember]) = - given MemberGetSet = db.getSet - val globals = mutable.Set.empty[DFMember] - val nonGlobals = mutable.ListBuffer.empty[DFMember] - db.members.foreach { - case dfVal: DFVal.CanBeGlobal if dfVal.isGlobal => globals += dfVal - case m => nonGlobals += m - } - (globals.toSet, nonGlobals.toList) - val (lhsGlobals, lhsNonGlobals) = partition(lhs) - val (rhsGlobals, rhsNonGlobals) = partition(rhs) - if (lhsGlobals != rhsGlobals) - val onlyLhs = lhsGlobals -- rhsGlobals - val onlyRhs = rhsGlobals -- lhsGlobals - throw new IllegalArgumentException( - s"""Hierarchical DB round-trip globals identity-set mismatch. - |Only in lhs (round-trip): ${onlyLhs.mkString(", ")} - |Only in rhs (input): ${onlyRhs.mkString(", ")}""".stripMargin - ) - require( - lhsNonGlobals == rhsNonGlobals, - "Hierarchical DB round-trip non-globals member list mismatch." - ) - require( - lhs.refTable == rhs.refTable, - "Hierarchical DB round-trip refTable mismatch." - ) - require( - lhs.globalTags == rhs.globalTags && lhs.srcFiles == rhs.srcFiles, - "Hierarchical DB round-trip globalTags or srcFiles mismatch." - ) - end hierarchicalDBRoundTripCheck - def transform(designDB: DB)(using MemberGetSet, CompilerOptions): DB = - refCheck() - memberExistenceCheck() - ownershipCheck(designDB.top, designDB.membersNoGlobals.drop(1)) - orderCheck() - // Build the hierarchical DB once and reuse it for both the checks and the - // round-trip (oldToNew is O(members) and SanityCheck runs after every stage). + // Build the hierarchical DB once (oldToNew is O(members) and SanityCheck + // runs after every stage). Run the per-design structural checks under each + // sub-DB's own getSet, then the design checks (per-sub-DB `subDBCheck` plus + // the cross-design root checks) once on the root via `check`. val hierDB = designDB.oldToNew + hierDB.subDBs.view.values.foreach { subDB => + subDB.atGetSet { + refCheck() + memberExistenceCheck() + ownershipCheck(subDB.top, subDB.membersNoGlobals.drop(1)) + orderCheck() + } + } hierDB.check - hierarchicalDBRoundTripCheck(designDB, hierDB) designDB + end transform end SanityCheck extension [T: HasDB](t: T) From a0d8609c4691ab7b084daa463447d7850e2df7c7 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 13 Jun 2026 21:54:34 +0300 Subject: [PATCH 30/72] Make PrintCodeString a BundleStage and the printers hierarchical-DB capable PrintCodeString was a no-op dependency anchor that happened to `extends Stage`; it is now a `BundleStage`, removing it from the flat-DB stage list. The printer layer (shared `Printer`/`AbstractPrinter` traits, inherited by DFPrinter, VerilogPrinter and VHDLPrinter) is now able to render a hierarchical root DB, not just a flat one: * `withGetSet` constructs a same-typed sub-printer bound to a sub-DB's getSet; `csDB`/`printedDB` render each design under its owning sub-DB (the root's own getSet throws on ref resolution). * designs are rendered in post-order DFS of the design tree, matching the flat `designMemberList` order (the `subDBs` ListMap is pre-order). * const globals are aggregated across sub-DBs with first-occurrence dedup. * `DB.hierGlobalNamedDFTypes` promotes a named type to global when any sub-DB marks it global (a sub-DB sees only one design, so a type used as IO in a child but a plain local in a parent would otherwise be printed twice); these are excluded from each design's local type declarations. * `printerForDesign` routes a blackbox component/module declaration to the referenced blackbox design's own sub-DB (its port list lives there). Validated byte-identical against the flat path across all of StagesSpec (DFHDL 434, Verilog+VHDL 78). HierarchicalPrintSpec is a permanent round-trip guard, since the pipeline still feeds the printer a flat DB for now. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 14 ++++ .../compiler/printing/DFTypePrinter.scala | 66 ++++++++++++++----- .../dfhdl/compiler/printing/Printer.scala | 62 +++++++++++++++-- .../compiler/stages/PrintCodeString.scala | 11 ++-- .../stages/verilog/VerilogPrinter.scala | 2 + .../stages/vhdl/VHDLOwnerPrinter.scala | 2 +- .../compiler/stages/vhdl/VHDLPrinter.scala | 2 + .../StagesSpec/HierarchicalPrintSpec.scala | 34 ++++++++++ 8 files changed, 164 insertions(+), 29 deletions(-) create mode 100644 compiler/stages/src/test/scala/StagesSpec/HierarchicalPrintSpec.scala diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index 631ae287a..7f9593158 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -188,6 +188,20 @@ final case class DB private ( def getLocalNamedDFTypes(design: DFDesignBlock): Set[NamedDFType] = localNamedDFTypes.getOrElse(design, Set()) + // Cross-design global named types for the hierarchical root. Each sub-DB only + // sees one design, so a type used as IO in a child but as a plain local in a + // parent gets classified global by the child and local by the parent — the + // union of every sub-DB's `getGlobalNamedDFTypes` is the authoritative global + // set. The printer prints these once globally and excludes them from every + // design's local type declarations. Empty on non-root DBs (a flat DB already + // classifies named types directly). + // TODO: a named type used as a plain local in more than one design (never as + // IO/global) is global in the flat model but not captured here; add the + // multi-design count if such a case arises. + lazy val hierGlobalNamedDFTypes: ListSet[NamedDFType] = + if (!isRoot) ListSet.empty + else subDBs.view.values.flatMap(_.getGlobalNamedDFTypes).to(ListSet) + @tailrec private def OMLGen[O <: DFOwner: ClassTag]( getOwnerFunc: DFMember => O )( diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/printing/DFTypePrinter.scala b/compiler/ir/src/main/scala/dfhdl/compiler/printing/DFTypePrinter.scala index ea6f50447..593a9e719 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/printing/DFTypePrinter.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/printing/DFTypePrinter.scala @@ -2,6 +2,7 @@ package dfhdl.compiler package printing import ir.* import dfhdl.internals.* +import scala.collection.mutable trait AbstractTypePrinter extends AbstractPrinter: def csDFBoolOrBit(dfType: DFBoolOrBit, typeCS: Boolean): String @@ -19,25 +20,59 @@ trait AbstractTypePrinter extends AbstractPrinter: case DFInt32 => true case _ => false case _ => false + // Per-sub-printer fresh global members for a hierarchical root: each sub-DB's + // global members not yet emitted by an earlier sub-DB (first-occurrence dedup + // in sub-DB iteration order), printed under that sub-DB's getSet. A flat DB is + // a single (this, all globals) group. + private def globalConstGroups: List[(TPrinter, List[DFMember])] = + val designDB = getSet.designDB + if (designDB.isRoot) + val seen = mutable.HashSet.empty[DFMember] + designDB.subDBs.view.values.flatMap { sub => + val fresh = sub.membersGlobals.filter(seen.add) + Option.when(fresh.nonEmpty)(withGetSet(sub.getSet) -> fresh) + }.toList + else List(printer -> designDB.membersGlobals) final def csGlobalConstIntDcls: String = - printer.csDFMembers(getSet.designDB.membersGlobals.filter(isInt32Val)) + globalConstGroups.iterator + .map((p, gs) => p.csDFMembers(gs.filter(isInt32Val))) + .filter(_.nonEmpty).mkString("\n") final def csGlobalConstNonIntDcls: String = - printer.csDFMembers(getSet.designDB.membersGlobals.filterNot(isInt32Val)) + globalConstGroups.iterator + .map((p, gs) => p.csDFMembers(gs.filterNot(isInt32Val))) + .filter(_.nonEmpty).mkString("\n") final def csGlobalTypeDcls: String = - getSet.designDB.getGlobalNamedDFTypes.view - .filter { - // show tuple structures only if tuple support is disabled - case dfType: DFStruct if dfType.isTuple && tupleSupportEnable => false - // skipping unknown clock and reset definitions (they are unknown because - // they lack additional name suffix that belongs to their configuration) - case DFOpaque(name = "Clk", kind = DFOpaque.Kind.Clk) => false - case DFOpaque(name = "Rst", kind = DFOpaque.Kind.Rst) => false - case _ => true - } - .map(x => printer.csNamedDFTypeDcl(x, global = true)) - .mkString("\n") + val designDB = getSet.designDB + val typeGroups: List[(TPrinter, List[NamedDFType])] = + if (designDB.isRoot) + val seen = mutable.HashSet.empty[NamedDFType] + designDB.subDBs.view.values.flatMap { sub => + val fresh = sub.getGlobalNamedDFTypes.iterator.filter(seen.add).toList + Option.when(fresh.nonEmpty)(withGetSet(sub.getSet) -> fresh) + }.toList + else List(printer -> designDB.getGlobalNamedDFTypes.toList) + typeGroups.iterator.flatMap { (p, types) => + types.view + .filter { + // show tuple structures only if tuple support is disabled + case dfType: DFStruct if dfType.isTuple && tupleSupportEnable => false + // skipping unknown clock and reset definitions (they are unknown because + // they lack additional name suffix that belongs to their configuration) + case DFOpaque(name = "Clk", kind = DFOpaque.Kind.Clk) => false + case DFOpaque(name = "Rst", kind = DFOpaque.Kind.Rst) => false + case _ => true + } + .map(x => p.csNamedDFTypeDcl(x, global = true)) + }.mkString("\n") + end csGlobalTypeDcls final def csLocalTypeDcls(design: DFDesignBlock): String = - getSet.designDB.getLocalNamedDFTypes(design).view + val designDB = getSet.designDB + // Exclude types promoted to global across the hierarchy: a sub-DB sees only + // its one design and may mis-classify a cross-design global type as local + // (empty for a flat DB, which classifies named types directly). + val hierGlobal = designDB.rootDB.hierGlobalNamedDFTypes + designDB.getLocalNamedDFTypes(design).view + .filterNot(hierGlobal) .filter { // show tuple structures only if tuple support is disabled case dfType: DFStruct if dfType.isTuple && tupleSupportEnable => false @@ -49,6 +84,7 @@ trait AbstractTypePrinter extends AbstractPrinter: } .map(x => printer.csNamedDFTypeDcl(x, global = false)) .mkString("\n") + end csLocalTypeDcls def csDFEnumDcl(dfType: DFEnum, global: Boolean): String def csDFEnum(dfType: DFEnum, typeCS: Boolean): String def csDFVector(dfType: DFVector, typeCS: Boolean): String diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/printing/Printer.scala b/compiler/ir/src/main/scala/dfhdl/compiler/printing/Printer.scala index 82b2750c0..2369dd957 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/printing/Printer.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/printing/Printer.scala @@ -17,6 +17,22 @@ protected trait AbstractPrinter: given getSet: MemberGetSet given printerOptions: PrinterOptions val tupleSupportEnable: Boolean + // Construct a printer of the same concrete type bound to `subGetSet`. Used to + // render each sub-DB's design (and its globals) under that sub-DB's own getSet + // when the DB is a hierarchical root — the root's own getSet throws on ref + // resolution, so the trait-level print methods dispatch through one of these + // per sub-DB. + protected def withGetSet(subGetSet: MemberGetSet): TPrinter + // The printer to use when rendering `design`'s own members: for a hierarchical + // root, a sub-printer bound to that design's sub-DB getSet (its members live + // there); for a flat DB, `this` printer (every design shares one getSet). Used + // for cross-design renders such as a blackbox component/module declaration, + // where a design's body references another design's port list. + protected final def printerForDesign(design: DFDesignBlock): TPrinter = + getSet.designDB.rootDB.subDBs.get(design.ownerRef) match + case Some(sub) => withGetSet(sub.getSet) + case None => printer +end AbstractPrinter trait Printer extends AbstractTypePrinter, @@ -121,7 +137,12 @@ trait Printer def designFileName(designName: String): String def globalFileName: String protected def hasGlobalContentCheck: Boolean = - getSet.designDB.membersGlobals.exists(!_.isAnonymous) || csGlobalTypeDcls.nonEmpty + val designDB = getSet.designDB + val anyNamedGlobal = + if (designDB.isRoot) + designDB.subDBs.view.values.exists(_.membersGlobals.exists(!_.isAnonymous)) + else designDB.membersGlobals.exists(!_.isAnonymous) + anyNamedGlobal || csGlobalTypeDcls.nonEmpty lazy val hasGlobalContent: Boolean = hasGlobalContentCheck def csGlobalFileContent: String = sn"""|$csGlobalConstIntDcls @@ -175,7 +196,7 @@ trait Printer val compiledFiles = Iterable( dfhdlSourceFile, globalSourceFile, - designDB.designMemberList.view.map { case (block: DFDesignBlock, _) => + designPrinters.view.map { case (block, p) => val sourceType = block.instMode match case _: DFDesignBlock.InstMode.BlackBox => SourceType.BlackBox case _ => SourceType.Design @@ -183,7 +204,7 @@ trait Printer SourceOrigin.Compiled, sourceType, hdlFolderName + separatorChar + designFileName(block.dclName), - formatCode(csFile(block), withColor = false) + formatCode(p.csFile(block), withColor = false) ) } ).flatten @@ -197,13 +218,38 @@ trait Printer val printVendorIPBlackbox: Boolean = false - final def csDB: String = + // The (design block, printer-bound-to-its-getSet) pairs to render, in order. + // Flat DB: every design under `this` printer. Hierarchical root: each sub-DB's + // design under a sub-printer bound to that sub-DB's getSet (the root's own + // getSet throws on ref resolution). + protected final def designPrinters: List[(DFDesignBlock, TPrinter)] = val designDB = getSet.designDB - val csFileList = designDB.designMemberList.collect { - case (block: DFDesignBlock, _) + if (designDB.isRoot) + // Flat `designMemberList` prints designs in post-order DFS of the design + // tree (children in instantiation order, then the parent); the `subDBs` + // ListMap is pre-order (parent first). Reorder to post-order so the + // hierarchical output matches the flat output design-for-design. + val childrenOf = mutable.LinkedHashMap.empty[DFOwner.Ref, mutable.ListBuffer[DB]] + designDB.subDBs.values.foreach { sub => + sub.parentSubDBOpt.foreach { parent => + childrenOf.getOrElseUpdate(parent.top.ownerRef, mutable.ListBuffer.empty) += sub + } + } + def postOrder(sub: DB): List[DB] = + childrenOf.getOrElse(sub.top.ownerRef, mutable.ListBuffer.empty).toList + .flatMap(postOrder) :+ sub + postOrder(designDB.topDB).map(sub => sub.top -> withGetSet(sub.getSet)) + else + designDB.designMemberList.collect { case (block: DFDesignBlock, _) => block -> printer } + end if + end designPrinters + + final def csDB: String = + val csFileList = designPrinters.collect { + case (block, p) if printerOptions.designPrintFilter(block) && (!block.isVendorIPBlackbox || printVendorIPBlackbox) => - formatCode(csFile(block)) + formatCode(p.csFile(block)) } val globals = formatCode( sn"""|$csGlobalConstIntDcls @@ -272,6 +318,8 @@ class DFPrinter(using val getSet: MemberGetSet, val printerOptions: PrinterOptio DFOwnerPrinter: type TPrinter = DFPrinter given printer: TPrinter = this + protected def withGetSet(subGetSet: MemberGetSet): DFPrinter = + new DFPrinter(using subGetSet, printerOptions) override val printVendorIPBlackbox: Boolean = true val tupleSupportEnable: Boolean = true def csViaConnectionSep: String = "" diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/PrintCodeString.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/PrintCodeString.scala index 02db1564d..d83f323bc 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/PrintCodeString.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/PrintCodeString.scala @@ -4,12 +4,11 @@ import dfhdl.compiler.analysis.* import dfhdl.compiler.printing.* import dfhdl.options.{CompilerOptions, PrinterOptions} -case object PrintCodeString extends Stage: - def dependencies: List[Stage] = - List(DropUnreferencedAnons, NamedAnonMultiref, DFHDLUniqueNames) - def nullifies: Set[Stage] = Set() - def transform(designDB: DB)(using MemberGetSet, CompilerOptions): DB = designDB -end PrintCodeString +// A no-op dependency anchor: running it forces DropUnreferencedAnons, +// NamedAnonMultiref, and DFHDLUniqueNames to run so the DB is in printable +// shape, then the `getCodeString`/`printCodeString` extensions run the printer. +case object PrintCodeString + extends BundleStage(DropUnreferencedAnons, NamedAnonMultiref, DFHDLUniqueNames) extension [T: HasDB](t: T) def getCodeString(align: Boolean)(using CompilerOptions): String = diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogPrinter.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogPrinter.scala index 65a39c1c3..abc90e06b 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogPrinter.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogPrinter.scala @@ -17,6 +17,8 @@ class VerilogPrinter(val dialect: VerilogDialect)(using VerilogOwnerPrinter: type TPrinter = VerilogPrinter given printer: TPrinter = this + protected def withGetSet(subGetSet: MemberGetSet): VerilogPrinter = + new VerilogPrinter(dialect)(using subGetSet, printerOptions) def unsupported: Nothing = throw new IllegalArgumentException( "Unsupported member for this VerilogPrinter." ) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLOwnerPrinter.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLOwnerPrinter.scala index 4d0cc9a57..5f1537c25 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLOwnerPrinter.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLOwnerPrinter.scala @@ -153,7 +153,7 @@ protected trait VHDLOwnerPrinter extends AbstractOwnerPrinter: .mkString("\n") val components = designMembers.view.collect { case inst: DFDesignInst if inst.getDesignBlock.isBlackBox => inst.getDesignBlock - }.map(printer.csEntityDcl(_, asComponent = true)).mkString("\n") + }.map(bb => printerForDesign(bb).csEntityDcl(bb, asComponent = true)).mkString("\n") val declarations = sn"""|$constIntDcls |$namedTypeConvFuncsDcl diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLPrinter.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLPrinter.scala index afb6da599..24f608a98 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLPrinter.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLPrinter.scala @@ -18,6 +18,8 @@ class VHDLPrinter(val dialect: VHDLDialect)(using VHDLOwnerPrinter: type TPrinter = VHDLPrinter given printer: TPrinter = this + protected def withGetSet(subGetSet: MemberGetSet): VHDLPrinter = + new VHDLPrinter(dialect)(using subGetSet, printerOptions) val inVHDL93: Boolean = dialect match case VHDLDialect.v93 => true case _ => false diff --git a/compiler/stages/src/test/scala/StagesSpec/HierarchicalPrintSpec.scala b/compiler/stages/src/test/scala/StagesSpec/HierarchicalPrintSpec.scala new file mode 100644 index 000000000..e7df3a189 --- /dev/null +++ b/compiler/stages/src/test/scala/StagesSpec/HierarchicalPrintSpec.scala @@ -0,0 +1,34 @@ +package StagesSpec + +import dfhdl.* +import dfhdl.compiler.printing.DefaultPrinter +// scalafmt: { align.tokens = [{code = "<>"}, {code = "="}, {code = "=>"}, {code = ":="}] } + +// Validates that the printer produces identical output whether given a flat DB +// or its hierarchical (root + sub-DBs) form via `oldToNew`. This exercises the +// root-aware printer path (post-order `designPrinters`, per-sub-DB getSet +// routing, cross-design globals/named-types). The compiler pipeline still feeds +// the printer a flat DB, so without this the root path would be unexercised. +class HierarchicalPrintSpec extends StageSpec: + private def assertSamePrintFlatVsHier(dsn: core.Design): Unit = + val db = dsn.getDB + val flat = DefaultPrinter(using db.getSet).csDB + val hier = DefaultPrinter(using db.oldToNew.getSet).csDB + assertNoDiff(hier, flat) + + test("nested hierarchy: flat and hierarchical printing match") { + class SubDesign extends DFDesign: + val x = SInt(16) <> IN + val y = SInt(16) <> OUT + y := x + + class TopDesign extends DFDesign: + val x = SInt(16) <> IN + val y = SInt(16) <> OUT + val sub = new SubDesign() + sub.x <> x + y <> sub.y + + assertSamePrintFlatVsHier(new TopDesign) + } +end HierarchicalPrintSpec From 04889b9ed283a10817cf6549af8bfbc91615aea1 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sun, 14 Jun 2026 12:59:00 +0300 Subject: [PATCH 31/72] migrate DropDesignDefs to GlobalStage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert DropDesignDefs from a flat-DB `Stage` to a hierarchical `GlobalStage`. The stage's work is inherently cross-sub-DB: each def-mode design body lives in its own sub-DB while its instances live in parent sub-DBs. The new `transformGlobal` buckets patches by target sub-DB key — instance renames land in the parent sub-DB, the def-design conversion (instMode -> Normal, redundant return-ident removal, output-port reordering) lands in the def design's own sub-DB — then applies each bucket and reassembles the root. `newToOld`'s canonicalize fixes the parents' now-stale cross-sub-DB designRefs onto the converted Normal block. The per-def dedup that the flat version did via `handledDesigns` is now keyed by `designBlock.ownerRef` (the sub-DB key); the output-port `suggestName` prefix is confined to the def design's own members (external connections reference the instance via PortByNameSelect string paths, not the def ports), so it resolves identically under the sub-DB getSet. StagesSpec (435, incl. DropDesignDefsSpec) and lib (152, incl. AES CipherSpecs) all pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../compiler/stages/DropDesignDefs.scala | 153 +++++++++++------- 1 file changed, 96 insertions(+), 57 deletions(-) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropDesignDefs.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropDesignDefs.scala index 50ebc9319..f87f2140d 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropDesignDefs.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropDesignDefs.scala @@ -4,71 +4,110 @@ import dfhdl.compiler.analysis.* import dfhdl.compiler.ir.* import dfhdl.compiler.patching.* import dfhdl.compiler.ir.DFDesignBlock.InstMode +import dfhdl.internals.* import dfhdl.options.CompilerOptions import scala.collection.mutable +import scala.collection.immutable.ListMap -case object DropDesignDefs extends Stage: +case object DropDesignDefs extends GlobalStage: def dependencies: List[Stage] = List() def nullifies: Set[Stage] = Set(DFHDLUniqueNames, DropLocalDcls, DropUnreferencedAnons) - def transform(designDB: DB)(using MemberGetSet, CompilerOptions): DB = - val handledDesigns = mutable.Set.empty[DFDesignBlock] - val patchList = designDB.members.view.flatMap { - // only going after design definitions - case designInst @ DFDesignInst( - designRef = DFRef( - design @ DFDesignBlock(domainType = DomainType.DF, instMode = InstMode.Def) - ) - ) => - val members = design.members(MemberView.Folded) - var outPortOpt: Option[DFVal.Dcl] = None - // we remove redundant ident that is wrapped around the return value - val identRemovePatch = members.view.reverse.collectFirst { - case DFNet.Connection(port @ DclOut(), ident @ Ident(retVal), _) => - outPortOpt = Some(port) - ident -> Patch.Replace(retVal, Patch.Replace.Config.FullReplacement) - }.toList - val updatedName = - // design definition instances may be anonymous, so we name them - if (designInst.isAnonymous) - // the output port is connected and used in the function and from that - // we know the target name using `suggestName` - outPortOpt - .flatMap(_.suggestName.map(x => x + "_")) - .getOrElse("") + s"${design.dclName}_inst" - else designInst.getName + def transformGlobal(designDB: DB)(using + co: CompilerOptions, + refGen: RefGen + ): DB = + // Patches bucketed by the sub-DB they target (keyed by `designBlock.ownerRef`). + // The instance rename lands in the instance's parent sub-DB; the def-design + // conversion (instMode -> Normal, redundant-ident removal, output-port move) + // lands in the def design's own sub-DB. `newToOld` canonicalizes the parents' + // cross-sub-DB `designRef`s onto the now-Normal block. + val patchesByKey = + mutable.LinkedHashMap.empty[DFOwner.Ref, mutable.ListBuffer[(DFMember, Patch)]] + def addPatch(key: DFOwner.Ref, patch: (DFMember, Patch)): Unit = + patchesByKey.getOrElseUpdate(key, mutable.ListBuffer.empty) += patch + // For each def design (computed once), the name prefix derived from its output + // port's `suggestName`, reused to name every anonymous instance of it. + val suggestPrefixByDef = mutable.Map.empty[DFOwner.Ref, String] - // we need to move the output port to the end of the inputs, to prevent - // malformed ordering in future stages when referencing the output port. - val lastInputOpt = members.dropWhile { - case DclIn() => false - case _ => true - }.takeWhile { - case DclIn() => true - case _ => false - }.lastOption + designDB.subDBs.foreach { (parentKey, parentSubDB) => + parentSubDB.members.foreach { + // only going after design definition instances + case designInst: DFDesignInst => + parentSubDB.refTable.get(designInst.designRef) match + case Some(design @ DFDesignBlock( + domainType = DomainType.DF, + instMode = InstMode.Def + )) => + val defKey = design.ownerRef + // On the first instance of a given def design, stage its conversion + // patches (in the def design's own sub-DB) and cache the output-port- + // derived name prefix reused by every anonymous instance. + if (!suggestPrefixByDef.contains(defKey)) + val prefix = designDB.subDBs(defKey).atGetSet { + val members = design.members(MemberView.Folded) + var outPortOpt: Option[DFVal.Dcl] = None + // we remove redundant ident that is wrapped around the return value + val identRemovePatch = members.view.reverse.collectFirst { + case DFNet.Connection(port @ DclOut(), ident @ Ident(retVal), _) => + outPortOpt = Some(port) + ident -> Patch.Replace(retVal, Patch.Replace.Config.FullReplacement) + }.toList + // we need to move the output port to the end of the inputs, to + // prevent malformed ordering in future stages when referencing + // the output port. + val lastInputOpt = members.dropWhile { + case DclIn() => false + case _ => true + }.takeWhile { + case DclIn() => true + case _ => false + }.lastOption + val outPortPatch = outPortOpt.map { o => + lastInputOpt match + case Some(posMember) => posMember -> Patch.Move(o, Patch.Move.Config.After) + // if there are no inputs, we move the output port to the beginning + case None => members.head -> Patch.Move(o, Patch.Move.Config.Before) + } + val designPatch = design -> Patch.Replace( + design.copy(instMode = InstMode.Normal), + Patch.Replace.Config.FullReplacement + ) + addPatch(defKey, designPatch) + identRemovePatch.foreach(addPatch(defKey, _)) + outPortPatch.foreach(addPatch(defKey, _)) + outPortOpt.flatMap(_.suggestName.map(x => x + "_")).getOrElse("") + } + suggestPrefixByDef(defKey) = prefix + end if + // design definition instances may be anonymous, so we name them using + // the output port's `suggestName` (see above) plus the design class name. + val renamedInst = parentSubDB.atGetSet { + val updatedName = + if (designInst.isAnonymous) + suggestPrefixByDef(defKey) + s"${design.dclName}_inst" + else designInst.getName + designInst.setName(updatedName) + } + addPatch( + parentKey, + designInst -> Patch.Replace(renamedInst, Patch.Replace.Config.FullReplacement) + ) + case _ => + case _ => + } + } - val outPortPatch = outPortOpt.map { o => - lastInputOpt match - case Some(posMember) => posMember -> Patch.Move(o, Patch.Move.Config.After) - // if there are no inputs, we move the output port to the beginning - case None => members.head -> Patch.Move(o, Patch.Move.Config.Before) + if (patchesByKey.isEmpty) designDB + else + val newSubDBs = ListMap.from( + designDB.subDBs.iterator.map { (key, subDB) => + patchesByKey.get(key) match + case Some(patches) => key -> subDB.patch(patches.toList) + case None => key -> subDB } - val designInstPatch = designInst -> Patch.Replace( - designInst.setName(updatedName), - Patch.Replace.Config.FullReplacement - ) - val designPatch = design -> Patch.Replace( - design.copy(instMode = InstMode.Normal), - Patch.Replace.Config.FullReplacement - ) - if (handledDesigns.contains(design)) Some(designInstPatch) - else - handledDesigns += design - designPatch :: designInstPatch :: identRemovePatch ++ outPortPatch - case _ => None - }.toList - designDB.patch(patchList) - end transform + ) + designDB.update(subDBs = newSubDBs) + end transformGlobal end DropDesignDefs //turns design definitions into normal designs, and set their instance names From d93c436840133b49ddd87324c3b9b7fa21e83f7d Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sun, 14 Jun 2026 13:28:48 +0300 Subject: [PATCH 32/72] migrate DropStructsVecs to GlobalStage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert DropStructsVecs from a flat-DB `Stage` to a hierarchical `GlobalStage`. The stage flattens structs/non-Bit vectors to Bits in two phases sharing a `replacementMap` (stage 1 replaces the values, stage 2 rewrites partial selections into Bits slices). A first attempt as a per-design `HierarchyStage` is incorrect: a global struct/vec constant referenced from more than one design lives (by identity) in every referencing sub-DB's closure, so each sub-DB would build its OWN replacement object. `newToOld` dedups globals by identity, not structure, so the distinct replacements survive as duplicate, divergent globals — and the refTable merge corrupts one into referencing itself. A new "Cross-design global vector" test characterizes this (a global vector used by two designs). GlobalStage fixes it: stage 1 builds each global value's replacement patch ONCE (`globalStage1Patch`, keyed by the global's identity) and reuses that same patch for every sub-DB that holds the global, so all of them receive the same replacement member and `newToOld` dedups to a single global. Locals are still flattened per-sub-DB; the `replacementMap`/`handledPartials` are shared across sub-DBs so the stage-2 partial rewrite resolves cross-design related values uniformly. The big MetaDesign bodies are factored verbatim into `stage1Patch` /`stage2Patch` helpers. StagesSpec 436 (incl. the new cross-design test) and lib 152 (incl. AES CipherSpec, which compiles real multi-design designs with global tables under v95/v2001) all pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../compiler/stages/DropStructsVecs.scala | 387 ++++++++++-------- .../StagesSpec/DropStructsVecsSpec.scala | 32 ++ 2 files changed, 242 insertions(+), 177 deletions(-) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropStructsVecs.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropStructsVecs.scala index f737b7ca1..0725c0258 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropStructsVecs.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropStructsVecs.scala @@ -10,12 +10,13 @@ import dfhdl.core.{DFTypeAny, widthIntParam, IntParam, get, DFValAny} import dfhdl.compiler.ir.DFType as irDFType import dfhdl.compiler.ir.DFVector as irDFVector import scala.collection.mutable +import scala.collection.immutable.ListMap import dfhdl.core.DFVal.Func.Op as FuncOp /** This stage drops all structs and (non-Bit) vectors that are not standard block-ram accesses. It * drops them by flattening them into Bits. */ -case object DropStructsVecs extends Stage: +case object DropStructsVecs extends GlobalStage: override def runCondition(using co: CompilerOptions): Boolean = co.backend match case be: dfhdl.backends.verilog => @@ -25,8 +26,7 @@ case object DropStructsVecs extends Stage: case _ => false override def dependencies: List[Stage] = List(ExplicitRomVar) override def nullifies: Set[Stage] = Set(DropUnreferencedAnons) - def transform(designDB: DB)(using MemberGetSet, CompilerOptions): DB = - given RefGen = RefGen.fromGetSet + def transformGlobal(designDB: DB)(using co: CompilerOptions, refGen: RefGen): DB = object StructOrVecVal: def unapply(dfVal: DFVal)(using MemberGetSet): Boolean = dfVal.dfType match // all structs are dropped @@ -41,195 +41,228 @@ case object DropStructsVecs extends Stage: case _ => true case _ => false end StructOrVecVal + // replacementMap maps the updated (Bits) DFVal back to the original struct/vec + // DFVal. Shared across every sub-DB so the stage-2 partial rewrite resolves a + // partial's (possibly cross-design global) related value uniformly. val replacementMap = mutable.Map.empty[DFVal, DFVal] val handledPartials = mutable.Set.empty[DFVal] + // A global struct/vec value lives (by identity) in every referencing sub-DB's + // closure. Its stage-1 replacement patch is built ONCE and reused for every + // such sub-DB, so all of them get the SAME replacement member object and + // `newToOld` dedups it to a single global (building a fresh replacement per + // sub-DB would emit duplicate, divergent globals). + val globalStage1Patch = mutable.Map.empty[DFVal, (DFMember, Patch)] object PartialSel: import DFVal.Alias.* def unapply(partial: ApplyIdx | ApplyRange | SelectField)(using MemberGetSet): Boolean = replacementMap.contains(partial.relValRef.get) + /////////////////////////////////////////////////////////////////////////////// // Stage 1: Replace structs and vectors with Bits /////////////////////////////////////////////////////////////////////////////// - // replacementMap maps the updated DFVal to the original DFVal - val patchList: List[(DFMember, Patch)] = designDB.members.collect { - case dfVal @ StructOrVecVal() => - val dsn = new MetaDesign( - dfVal, - Patch.Add.Config.ReplaceWithLast(Patch.Replace.Config.FullReplacement) - ): - def updateArg(arg: DFVal): DFValAny = arg.dfType match - // Structs and Vectors will be replaced with Bits in a different patch - case _: (DFStruct | DFVector | DFBits) => arg.asValAny - case _ if !arg.isAnonymous => arg.asValAny.bits - case _ => arg.asValAny.bits - def typeToBits(dfType: irDFType): DFTypeAny = - val width = dfType.asFE[DFTypeAny].widthIntParam - DFBits(width.ref).asFE[DFTypeAny] - def recurToBits(dfVal: DFVal): DFVal = - val updatedDFType = typeToBits(dfVal.dfType) - dfVal match - // update constant data to bits - case const: DFVal.Const => - val updatedData = - const.dfType.dataToBitsData(const.data.asInstanceOf[const.dfType.Data]) - dfhdl.core.DFVal.Const.forced(updatedDFType, updatedData)(using - dfc.setMeta(const.meta) - ).asIR - // update vector concatenation arguments to bits as well - case concat @ DFVal.Func(op = FuncOp.++, args = args) => - val updatedArgs = args.map(a => updateArg(a.get)) - dfhdl.core.DFVal.Func(updatedDFType, FuncOp.++, updatedArgs)(using - dfc.setMeta(concat.meta) - ).asIR - // update vector repeated argument to bits as well - case repeat @ DFVal.Func( - op = FuncOp.repeat, - args = repeatedArgRef :: repeatCntArgRef :: Nil - ) => - val repeatedArg = repeatedArgRef.get - val updatedArgs = List(updateArg(repeatedArg), repeatCntArgRef.get.asValAny) - dfhdl.core.DFVal.Func(updatedDFType, FuncOp.repeat, updatedArgs)(using - dfc.setMeta(repeat.meta) - ).asIR - // for other values, just update the DFType - case _ => plantMember(dfVal.updateDFType(updatedDFType.asIR)) - end match - end recurToBits - // memoize the block ram variable check - val isBlockRamVar = dfVal match - case BlockRamVar() => true - case _ => false - val updatedDFVal = dfVal match - // declarations are special cased to handle the initial value - case dcl: DFVal.Dcl if dcl.initRefList.nonEmpty || isBlockRamVar => - val updatedDclType = - // for block ram variables, we keep the type as-is, unless the cell type is a struct or vector, - // in which case we convert it to bits. - if (isBlockRamVar) dcl.dfType match - case dfType @ irDFVector(cellType = cellType: (DFVector | DFStruct)) => - dfType.copy(cellType = typeToBits(cellType).asIR) - case dfType => dfType - else typeToBits(dcl.dfType).asIR - // block ram variables are not replaced with bits, because they are handled - // by the verilog backend, but the initial value is converted to bits. so we - // need to cast it back to the block ram vector type for the IR to be legal, - // and later ignore the casting in the verilog backend. - def toVector(initVal: DFVal): DFVal = - if (isBlockRamVar) dfhdl.core.DFVal.Alias.AsIs.forced(updatedDclType, initVal) - else initVal - val updatedInits = dcl.initRefList.view.map(_.get).map { - // for block ram variables, this initial value appears to be already casting from - // bits to vector, so it is already in the correct type. - case asIs: DFVal.Alias.AsIs if asIs.isAnonymous && isBlockRamVar => asIs - // anonymous initial values are converted to bits, and maybe cast back to vector, - // if the declaration is a block ram variable (see `toVector`). - case initVal if initVal.isAnonymous => toVector(recurToBits(initVal)) - // named initial values only may need to be cast to vector (see `toVector`). - case initVal => toVector(initVal) - }.map(_.asConstAny).toList - val modifier = new dfhdl.core.Modifier(dcl.modifier) - dfhdl.core.DFVal.Dcl(updatedDclType.asFE[DFTypeAny], modifier, updatedInits)(using - dfc.setMeta(dcl.meta) + def stage1Patch(dfVal: DFVal)(using MemberGetSet): (DFMember, Patch) = + val dsn = new MetaDesign( + dfVal, + Patch.Add.Config.ReplaceWithLast(Patch.Replace.Config.FullReplacement) + ): + def updateArg(arg: DFVal): DFValAny = arg.dfType match + // Structs and Vectors will be replaced with Bits in a different patch + case _: (DFStruct | DFVector | DFBits) => arg.asValAny + case _ if !arg.isAnonymous => arg.asValAny.bits + case _ => arg.asValAny.bits + def typeToBits(dfType: irDFType): DFTypeAny = + val width = dfType.asFE[DFTypeAny].widthIntParam + DFBits(width.ref).asFE[DFTypeAny] + def recurToBits(dfVal: DFVal): DFVal = + val updatedDFType = typeToBits(dfVal.dfType) + dfVal match + // update constant data to bits + case const: DFVal.Const => + val updatedData = + const.dfType.dataToBitsData(const.data.asInstanceOf[const.dfType.Data]) + dfhdl.core.DFVal.Const.forced(updatedDFType, updatedData)(using + dfc.setMeta(const.meta) + ).asIR + // update vector concatenation arguments to bits as well + case concat @ DFVal.Func(op = FuncOp.++, args = args) => + val updatedArgs = args.map(a => updateArg(a.get)) + dfhdl.core.DFVal.Func(updatedDFType, FuncOp.++, updatedArgs)(using + dfc.setMeta(concat.meta) ).asIR - case _ => recurToBits(dfVal) - // block ram variables are not replaced with bits, because they are handled - // by the verilog backend - if (!isBlockRamVar) - replacementMap += (updatedDFVal -> dfVal) - dsn.patch - } - val stage1 = designDB.patch(patchList) + // update vector repeated argument to bits as well + case repeat @ DFVal.Func( + op = FuncOp.repeat, + args = repeatedArgRef :: repeatCntArgRef :: Nil + ) => + val repeatedArg = repeatedArgRef.get + val updatedArgs = List(updateArg(repeatedArg), repeatCntArgRef.get.asValAny) + dfhdl.core.DFVal.Func(updatedDFType, FuncOp.repeat, updatedArgs)(using + dfc.setMeta(repeat.meta) + ).asIR + // for other values, just update the DFType + case _ => plantMember(dfVal.updateDFType(updatedDFType.asIR)) + end match + end recurToBits + // memoize the block ram variable check + val isBlockRamVar = dfVal match + case BlockRamVar() => true + case _ => false + val updatedDFVal = dfVal match + // declarations are special cased to handle the initial value + case dcl: DFVal.Dcl if dcl.initRefList.nonEmpty || isBlockRamVar => + val updatedDclType = + // for block ram variables, we keep the type as-is, unless the cell type is a struct or vector, + // in which case we convert it to bits. + if (isBlockRamVar) dcl.dfType match + case dfType @ irDFVector(cellType = cellType: (DFVector | DFStruct)) => + dfType.copy(cellType = typeToBits(cellType).asIR) + case dfType => dfType + else typeToBits(dcl.dfType).asIR + // block ram variables are not replaced with bits, because they are handled + // by the verilog backend, but the initial value is converted to bits. so we + // need to cast it back to the block ram vector type for the IR to be legal, + // and later ignore the casting in the verilog backend. + def toVector(initVal: DFVal): DFVal = + if (isBlockRamVar) dfhdl.core.DFVal.Alias.AsIs.forced(updatedDclType, initVal) + else initVal + val updatedInits = dcl.initRefList.view.map(_.get).map { + // for block ram variables, this initial value appears to be already casting from + // bits to vector, so it is already in the correct type. + case asIs: DFVal.Alias.AsIs if asIs.isAnonymous && isBlockRamVar => asIs + // anonymous initial values are converted to bits, and maybe cast back to vector, + // if the declaration is a block ram variable (see `toVector`). + case initVal if initVal.isAnonymous => toVector(recurToBits(initVal)) + // named initial values only may need to be cast to vector (see `toVector`). + case initVal => toVector(initVal) + }.map(_.asConstAny).toList + val modifier = new dfhdl.core.Modifier(dcl.modifier) + dfhdl.core.DFVal.Dcl(updatedDclType.asFE[DFTypeAny], modifier, updatedInits)(using + dfc.setMeta(dcl.meta) + ).asIR + case _ => recurToBits(dfVal) + // block ram variables are not replaced with bits, because they are handled + // by the verilog backend + if (!isBlockRamVar) + replacementMap += (updatedDFVal -> dfVal) + dsn.patch + end stage1Patch + + val stage1Subs: ListMap[DFOwner.Ref, DB] = ListMap.from( + designDB.subDBs.iterator.map { (key, sub) => + val patchList = sub.atGetSet { + sub.members.collect { + case dfVal @ StructOrVecVal() => + // a global value is replaced once and the same patch reused across + // every sub-DB that holds it (see `globalStage1Patch`) + if (dfVal.isGlobal) globalStage1Patch.getOrElseUpdate(dfVal, stage1Patch(dfVal)) + else stage1Patch(dfVal) + } + } + key -> sub.patch(patchList) + } + ) + val stage1Root = designDB.update(subDBs = stage1Subs) + /////////////////////////////////////////////////////////////////////////////// // Stage 2: Replace partial references with Bits /////////////////////////////////////////////////////////////////////////////// - locally { - import stage1.getSet - given RefGen = RefGen.fromGetSet - // we need to reverse the list because we want to handle the innermost partial first - val patchList2: List[(DFMember, Patch)] = stage1.members.view.reverse.collect { - case partial @ PartialSel() if !handledPartials.contains(partial) => - val dsn = new MetaDesign( - partial, - Patch.Add.Config.ReplaceWithLast(Patch.Replace.Config.FullReplacement) - ): - // looping through the partial references to find the outermost related value and its index - var currentPartial = partial - var relVal = currentPartial.relValRef.get - var idxLow: IntParam[Int] = 0 - var explore: Boolean = true - while (explore) - currentPartial match - case elemSel: DFVal.Alias.ApplyIdx => - val elemIdxVal = elemSel.relIdx.get - val elemIdx = elemIdxVal.getConstData[Option[BigInt]].toOption match - case Some(Some(idx: BigInt)) if elemIdxVal.isAnonymous => - idx.toInt.asInstanceOf[IntParam[Int]] - case _ => elemIdxVal.asValAny.asInstanceOf[IntParam[Int]] - val elemWidth = elemSel.asValAny.widthIntParam - val relValWidth = relVal.asValAny.widthIntParam - idxLow = (relValWidth - elemWidth * (elemIdx + 1)) + - idxLow - .asInstanceOf[IntParam[Int]] - case rangeSel: DFVal.Alias.ApplyRange => - val elemWidth = - replacementMap(relVal).dfType.asInstanceOf[DFVector] - .cellType.asFE[DFTypeAny].widthIntParam - val relValWidth = relVal.asValAny.widthIntParam - idxLow = (relValWidth - elemWidth * (rangeSel.idxHighRef.get + 1)) + - idxLow - .asInstanceOf[IntParam[Int]] - case fieldSel: DFVal.Alias.SelectField => - var relBitLow: IntParam[Int] = idxLow - val dfType = replacementMap(relVal).dfType.asInstanceOf[DFStruct] - dfType.fieldMap.toList.reverse.exists((fieldName, fieldType) => - val relWidth = fieldType.asFE[DFTypeAny].widthIntParam - val relBitHigh = ((relWidth + relBitLow) - 1).asInstanceOf[IntParam[Int]] - if (fieldName == fieldSel.fieldName) - idxLow = relBitLow - true - else - relBitLow = relBitLow + relWidth - false - ) - end match - relVal match - case nextPartial @ PartialSel() if nextPartial.isAnonymous => - handledPartials += nextPartial - currentPartial = nextPartial - relVal = currentPartial.relValRef.get - explore = true - case _ => - explore = false - end while - val requireCast = partial.dfType match - case _: DFBits => false - case _: DFVector => false - case _: DFStruct => false - case _ => true - val bitsMeta = if (requireCast) partial.meta.anonymize else partial.meta - val idxHigh: IntParam[ - Int - ] = (partial.asValAny.widthIntParam + idxLow - 1).asInstanceOf[IntParam[Int]] - val bitsVal = - dfhdl.core.DFVal.Alias.ApplyRange( - relVal.asValOf[Bits[Int]], - idxHigh.cloneAnonValueAndDepsHere, - idxLow.cloneAnonValueAndDepsHere - )(using - dfc.setMeta(bitsMeta) + def stage2Patch( + partial: DFVal.Alias.ApplyIdx | DFVal.Alias.ApplyRange | DFVal.Alias.SelectField + )(using MemberGetSet): (DFMember, Patch) = + val dsn = new MetaDesign( + partial, + Patch.Add.Config.ReplaceWithLast(Patch.Replace.Config.FullReplacement) + ): + // looping through the partial references to find the outermost related value and its index + var currentPartial = partial + var relVal = currentPartial.relValRef.get + var idxLow: IntParam[Int] = 0 + var explore: Boolean = true + while (explore) + currentPartial match + case elemSel: DFVal.Alias.ApplyIdx => + val elemIdxVal = elemSel.relIdx.get + val elemIdx = elemIdxVal.getConstData[Option[BigInt]].toOption match + case Some(Some(idx: BigInt)) if elemIdxVal.isAnonymous => + idx.toInt.asInstanceOf[IntParam[Int]] + case _ => elemIdxVal.asValAny.asInstanceOf[IntParam[Int]] + val elemWidth = elemSel.asValAny.widthIntParam + val relValWidth = relVal.asValAny.widthIntParam + idxLow = (relValWidth - elemWidth * (elemIdx + 1)) + + idxLow + .asInstanceOf[IntParam[Int]] + case rangeSel: DFVal.Alias.ApplyRange => + val elemWidth = + replacementMap(relVal).dfType.asInstanceOf[DFVector] + .cellType.asFE[DFTypeAny].widthIntParam + val relValWidth = relVal.asValAny.widthIntParam + idxLow = (relValWidth - elemWidth * (rangeSel.idxHighRef.get + 1)) + + idxLow + .asInstanceOf[IntParam[Int]] + case fieldSel: DFVal.Alias.SelectField => + var relBitLow: IntParam[Int] = idxLow + val dfType = replacementMap(relVal).dfType.asInstanceOf[DFStruct] + dfType.fieldMap.toList.reverse.exists((fieldName, fieldType) => + val relWidth = fieldType.asFE[DFTypeAny].widthIntParam + val relBitHigh = ((relWidth + relBitLow) - 1).asInstanceOf[IntParam[Int]] + if (fieldName == fieldSel.fieldName) + idxLow = relBitLow + true + else + relBitLow = relBitLow + relWidth + false ) - if (requireCast) - dfhdl.core.DFVal.Alias.AsIs.forced(partial.dfType, bitsVal.asIR)(using - dfc.setMeta(partial.meta) - ) - dsn.patch + end match + relVal match + case nextPartial @ PartialSel() if nextPartial.isAnonymous => + handledPartials += nextPartial + currentPartial = nextPartial + relVal = currentPartial.relValRef.get + explore = true + case _ => + explore = false + end while + val requireCast = partial.dfType match + case _: DFBits => false + case _: DFVector => false + case _: DFStruct => false + case _ => true + val bitsMeta = if (requireCast) partial.meta.anonymize else partial.meta + val idxHigh: IntParam[ + Int + ] = (partial.asValAny.widthIntParam + idxLow - 1).asInstanceOf[IntParam[Int]] + val bitsVal = + dfhdl.core.DFVal.Alias.ApplyRange( + relVal.asValOf[Bits[Int]], + idxHigh.cloneAnonValueAndDepsHere, + idxLow.cloneAnonValueAndDepsHere + )(using + dfc.setMeta(bitsMeta) + ) + if (requireCast) + dfhdl.core.DFVal.Alias.AsIs.forced(partial.dfType, bitsVal.asIR)(using + dfc.setMeta(partial.meta) + ) + dsn.patch + end stage2Patch + + val stage2Subs: ListMap[DFOwner.Ref, DB] = ListMap.from( + stage1Root.subDBs.iterator.map { (key, sub) => + // we need to reverse the list because we want to handle the innermost partial first + val patchList2 = sub.atGetSet { + sub.members.view.reverse.collect { + case partial @ PartialSel() if !handledPartials.contains(partial) => + stage2Patch(partial) + } + // TODO: we need to reverse the list to avoid the issue of the partial references being replaced before the related value + // maybe patch can be fixed to handle this + .toList.reverse + } + key -> sub.patch(patchList2) } - // TODO: we need to reverse the list to avoid the issue of the partial references being replaced before the related value - // maybe patch can be fixed to handle this - .toList.reverse - stage1.patch(patchList2) - } - end transform + ) + stage1Root.update(subDBs = stage2Subs) + end transformGlobal end DropStructsVecs extension [T: HasDB](t: T) diff --git a/compiler/stages/src/test/scala/StagesSpec/DropStructsVecsSpec.scala b/compiler/stages/src/test/scala/StagesSpec/DropStructsVecsSpec.scala index 159825120..84d53b01e 100644 --- a/compiler/stages/src/test/scala/StagesSpec/DropStructsVecsSpec.scala +++ b/compiler/stages/src/test/scala/StagesSpec/DropStructsVecsSpec.scala @@ -249,6 +249,38 @@ class DropStructsVecsSpec extends StageSpec(stageCreatesUnrefAnons = true): ) } + test("Cross-design global vector") { + given options.CompilerOptions.Backend = _.verilog.v95 + // `arg` is a global constant vector referenced from two different designs. + // It must be flattened to a single shared `Bits` global (not duplicated per + // design), exercising the cross-design global handling of the stage. + val arg: Bits[8] X 4 <> CONST = Vector(h"01", h"02", h"03", h"04") + class Sub extends DFDesign: + val o = Bits(8) <> OUT + o := arg(0) + class Top extends DFDesign: + val o = Bits(8) <> OUT + val sub = new Sub + o := arg(1) ^ sub.o + val top = (new Top).dropStructsVecs + assertCodeString( + top, + """|val arg: Bits[32] <> CONST = (h"01", h"02", h"03", h"04").toBits + | + |class Sub extends DFDesign: + | val o = Bits(8) <> OUT + | o := arg(31, 24) + |end Sub + | + |class Top extends DFDesign: + | val o = Bits(8) <> OUT + | val sub = Sub() + | o := arg(23, 16) ^ sub.o + |end Top + |""".stripMargin + ) + } + test("Inline anomaly") { given options.CompilerOptions.Backend = _.verilog.v95 val Rcon: Bits[8] X 4 X 2 <> CONST = DFVector(Bits(8) X 4 X 2)( From de9623a9563de5cc83847eecb6d0501a3c0fdd89 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sun, 14 Jun 2026 13:59:09 +0300 Subject: [PATCH 33/72] migrate LocalToDesignParams to HierarchyStage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert LocalToDesignParams from a flat-DB `Stage` to a `HierarchyStage`. The stage (VHDL-only) turns a design's IO-used local consts into design parameters. The flat version iterated designs as PARENTS and patched their children's IO local params. Reframed from the child's perspective, each design converts its OWN IO local params (the patched members always live in the design being processed), so the work decomposes cleanly per sub-DB. The only cross-design dependency is a READ: a non-top design's converted param sources its applied value from the parent design's matching local const (`_`), reached via `subDB.parentSubDBOpt` — never a cross-design patch. Global params are excluded by `getIOLocalParams`, so there is no shared-global value to coordinate (unlike DropStructsVecs), making HierarchyStage the right base. `design.getName` is getSet-stable here (a design block's `dclName == meta.name`, so the immutable-DB `meta.name` path and the sub-DB `isTop -> dclName` path agree). `isTopTop` uses `design eq rootDB.top` (block `isTop` is unreliable in the hierarchical form). Uses the trait-supplied `refGen`. StagesSpec 436 (incl. LocalToDesignParamsSpec 8, with the cross-design "constant parameter regression" case) and lib 152 (AES CipherSpec exercises VHDL backends) all pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../compiler/stages/LocalToDesignParams.scala | 82 +++++++++++-------- 1 file changed, 48 insertions(+), 34 deletions(-) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/LocalToDesignParams.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/LocalToDesignParams.scala index 9fae1cc50..2b5ac3e03 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/LocalToDesignParams.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/LocalToDesignParams.scala @@ -4,13 +4,12 @@ import dfhdl.compiler.analysis.* import dfhdl.compiler.ir.* import dfhdl.compiler.patching.* import dfhdl.options.CompilerOptions -import scala.collection.mutable /** This stage converts local parameters that are used in IOs to be design parameters with default * values, since VHDL does not support local parameters for IO access. These kind of design * parameters remain at their default (relative) values and are never directly applied. */ -case object LocalToDesignParams extends Stage: +case object LocalToDesignParams extends HierarchyStage: override def runCondition(using co: CompilerOptions): Boolean = co.backend match case be: dfhdl.backends.vhdl => true @@ -20,38 +19,53 @@ case object LocalToDesignParams extends Stage: SimpleOrderMembers ) override def nullifies: Set[Stage] = Set() - def transform(designDB: DB)(using getSet: MemberGetSet, co: CompilerOptions): DB = - given RefGen = RefGen.fromGetSet - // adding design parameters with initial values set as the anonymized local parameters - val patchList: List[(DFMember, Patch)] = - designDB.designMemberList.flatMap { (design, members) => - val localConsts = members.view.collect { - case const @ DclConst() => const.getName -> const - }.toMap - val designInstances = members.collect { case di: DFDesignInst => di.getDesignBlock } - val exploredDesigns = if (design.isTopTop) design :: designInstances else designInstances - exploredDesigns.flatMap { designInstance => - val ioLocalParams = designInstance.getIOLocalParams - ioLocalParams.view.collect { lp => - val dsn = new MetaDesign( - lp, - Patch.Add.Config.ReplaceWithLast(Patch.Replace.Config.FullReplacement) - ): - val lpAnon = plantMember(lp.anonymize).asValAny - val paramValue = - if (designInstance.isTopTop) lpAnon - else - val paramName = s"${designInstance.getName}_${lp.getName}" - localConsts.get(paramName).getOrElse(lpAnon.asIR).asValAny - val dp = dfhdl.core.DFVal.DesignParam(paramValue, Some(lpAnon))(using - dfc.setMeta(lp.meta) - ) - dsn.patch - }.toList - } - } - designDB.patch(patchList) - end transform + // Per-design: each design converts its OWN IO-used local params to design + // parameters. The applied value of a non-top design's new param is read from + // its PARENT design's matching local const (`_`, + // produced upstream) — a cross-design READ via `parentSubDBOpt`, never a + // cross-design patch. Global params are excluded by `getIOLocalParams`, so + // there is no shared-global value to coordinate across sub-DBs. + def transformSubDB(rootDB: DB)(using getSet: MemberGetSet, co: CompilerOptions, rg: RefGen): DB = + val design = subDB.top + val isTopTop = design eq rootDB.top + val ioLocalParams = design.getIOLocalParams + if (ioLocalParams.isEmpty) subDB + else + // local consts of the parent design, used to source the applied value of a + // non-top design's converted param (see the class comment). + val parentLocalConsts: Map[String, DFVal.CanBeExpr] = + if (isTopTop) Map.empty + else + subDB.parentSubDBOpt match + case Some(parentSub) => + parentSub.atGetSet { + parentSub.members.view.collect { case const @ DclConst() => + const.getName -> const + }.toMap + } + case None => Map.empty + val instName = design.getName + // adding design parameters with initial values set as the anonymized local parameters + val patchList: List[(DFMember, Patch)] = + ioLocalParams.view.collect { lp => + val dsn = new MetaDesign( + lp, + Patch.Add.Config.ReplaceWithLast(Patch.Replace.Config.FullReplacement) + ): + val lpAnon = plantMember(lp.anonymize).asValAny + val paramValue = + if (isTopTop) lpAnon + else + val paramName = s"${instName}_${lp.getName}" + parentLocalConsts.get(paramName).getOrElse(lpAnon.asIR).asValAny + val dp = dfhdl.core.DFVal.DesignParam(paramValue, Some(lpAnon))(using + dfc.setMeta(lp.meta) + ) + dsn.patch + }.toList + subDB.patch(patchList) + end if + end transformSubDB end LocalToDesignParams extension [T: HasDB](t: T) From cbb3af0831da944137fad20086362af6e05c048c Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sun, 14 Jun 2026 14:15:38 +0300 Subject: [PATCH 34/72] fix iverilog crash in version scan --- .../scala/dfhdl/tools/toolsCore/Tool.scala | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala b/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala index 6c0137dbc..67c024ed1 100644 --- a/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala +++ b/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala @@ -40,7 +40,11 @@ trait Tool: val getVersionFullCmd = Process( s"$runExecFullPath $versionCmd", - new java.io.File(System.getProperty("java.io.tmpdir")) + new java.io.File(System.getProperty("java.io.tmpdir")), + // apply the same Windows DLL-search guard as `exec` (see `winDllPathEnv`); otherwise + // the version probe inherits the polluted PATH and the tool's own sub-process (e.g. + // `ivl -V`) loads the wrong runtime DLL and prints a spurious "Unable to get version" + winDllSearchPath(runExecFullPath).map("PATH" -> _).toSeq* ) getVersionFullCmd.lazyLines_!.mkString("\n") else runExecFullPath @@ -98,18 +102,22 @@ trait Tool: // prepend the executable's own directory plus its sibling `lib` and `lib\ivl` so the tool // always finds its own bundled runtime DLLs first. No-op off Windows. protected final def winDllPathEnv: Map[String, String] = - if (!osIsWindows || runExecFullPath.isEmpty) Map.empty + winDllSearchPath(runExecFullPath).map("PATH" -> _).toMap + + // Builds the PATH value used by `winDllPathEnv` (and the version probe), with the tool's own + // install dirs (its exe dir plus the sibling `lib` and `lib\ivl`) prepended ahead of the + // inherited PATH. Returns None off Windows or when the executable path is unknown. + private def winDllSearchPath(exeFullPath: String): Option[String] = + if (!osIsWindows || exeFullPath.isEmpty) None else - Option(Paths.get(runExecFullPath).getParent) match - case None => Map.empty - case Some(exeDir) => - val root = Option(exeDir.getParent) - val dllDirs = - (exeDir :: - root.toList.flatMap(r => List(r.resolve("lib"), r.resolve("lib").resolve("ivl")))) - .map(_.toString) - val pathSep = java.io.File.pathSeparator - Map("PATH" -> (dllDirs :+ sys.env.getOrElse("PATH", "")).mkString(pathSep)) + Option(Paths.get(exeFullPath).getParent).map { exeDir => + val root = Option(exeDir.getParent) + val dllDirs = + (exeDir :: + root.toList.flatMap(r => List(r.resolve("lib"), r.resolve("lib").resolve("ivl")))) + .map(_.toString) + (dllDirs :+ sys.env.getOrElse("PATH", "")).mkString(java.io.File.pathSeparator) + } protected def designFiles(using getSet: MemberGetSet): List[String] = getSet.designDB.srcFiles.collect { From 2169a3a6d4365e792a8e980369cac48540d0d991 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sun, 14 Jun 2026 14:36:34 +0300 Subject: [PATCH 35/72] migrate UniqueNames to GlobalStage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert UniqueNames (abstract; DFHDLUniqueNames / VerilogUniqueNames / VHDLUniqueNames) from a flat-DB `Stage` to a `GlobalStage`. Name uniqueness is inherently cross-design: global named types and members are renamed against the union of all design class names + reserved keywords, and per-block local renames use the resulting global reserved-name set. Global named types come from `hierGlobalNamedDFTypes` (root union); global named members are aggregated and identity-deduplicated across the sub-DB closures that share them. Critically, each member's rename patch is built ONCE and keyed by the member, so a global shared across sub-DBs renames to the SAME object in every one and `newToOld` dedups it to a single global — building a fresh rename per sub-DB would emit duplicate/divergent globals (the DropStructsVecs lesson). Local renames run per-block under each sub-DB's getSet, excluding hier-global types from the local-type set (same fix as the Step-5 printer). Both phases (member names, then dfType rewrites for renamed named types) apply via member-keyed patch maps, so globals and locals are handled uniformly. Member `==` is effectively identity (every distinct member carries unique refs), which the patch maps and `distinct` rely on. Adds a "Unique names of a global shared across designs" test: two colliding globals referenced from two designs must collapse to one shared rename (gv_0, gv_1), verifying no duplication/corruption. StagesSpec 437 (incl. UniqueNamesSpec 4) and lib 152 (AES CipherSpec compiles real multi-design designs with global tables under VHDL/Verilog, exercising VHDLUniqueNames/VerilogUniqueNames) all pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../dfhdl/compiler/stages/UniqueNames.scala | 222 +++++++++++------- .../scala/StagesSpec/UniqueNamesSpec.scala | 32 +++ 2 files changed, 168 insertions(+), 86 deletions(-) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueNames.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueNames.scala index 4208caef6..4c9ceb04d 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueNames.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueNames.scala @@ -6,14 +6,14 @@ import dfhdl.compiler.patching.* import dfhdl.options.CompilerOptions import dfhdl.internals.* import scala.collection.mutable -import scala.reflect.classTag +import scala.collection.immutable.ListMap //see `uniqueNames` for additional information private abstract class UniqueNames(reservedNames: Set[String], caseSensitive: Boolean) - extends Stage: + extends GlobalStage: def dependencies: List[Stage] = List() def nullifies: Set[Stage] = Set() - def transform(designDB: DB)(using MemberGetSet, CompilerOptions): DB = + def transformGlobal(designDB: DB)(using co: CompilerOptions, refGen: RefGen): DB = // conditionally lower cases the name according to the case sensitivity as // set by `caseSensitive` def lowerCase(name: String): String = if (caseSensitive) name else name.toLowerCase @@ -35,98 +35,148 @@ private abstract class UniqueNames(reservedNames: Set[String], caseSensitive: Bo } case _ => Nil } - // the existing design names - val designNames = designDB.members.collect { case block: DFDesignBlock => block.dclName } - // the existing global member names - val globalNamedMembers = designDB.membersGlobals.filterNot(_.isAnonymous) - // reserved names in lower case (if case-insensitive) - val reservedNamesLC = lowerCases(reservedNames) - // global type map for unique renamed names - val globalTypeUpdateMap = renamer(designDB.getGlobalNamedDFTypes, reservedNamesLC)( - _.name, - (e, n) => e -> n - ).toMap - // the global reserved type names, after unique global type renaming - val globalReservedTypeNames: Set[String] = - (designDB.getGlobalNamedDFTypes.map(e => e.name) ++ - globalTypeUpdateMap.values ++ designNames ++ reservedNames) - val globalReservedTypeNamesLC = lowerCases(globalReservedTypeNames) + val reservedNamesLC = lowerCases(reservedNames) + // member renames keyed by the member, BUILT ONCE so the renamed-member object + // is shared: a global member living (by identity) in several sub-DB closures + // is renamed identically in each and `newToOld` dedups it to one (building a + // fresh rename per sub-DB would emit duplicate, divergent globals). + val memberRenamePatches = mutable.LinkedHashMap.empty[DFMember, (DFMember, Patch)] + // named-type renames (global + per-design local), consumed by the phase-2 + // dfType rewrite. + val typeUpdateMap = mutable.LinkedHashMap.empty[NamedDFType, String] val localReservedNamesLCMutable = mutable.Set.from[String](reservedNamesLC) - // global named member patching - val globalNamedMemberPatchList = renamer( - globalNamedMembers, - globalReservedTypeNamesLC - )( - _.getName, - (m, n) => - localReservedNamesLCMutable += lowerCase(n) - m -> Patch.Replace(m.setName(n), Patch.Replace.Config.FullReplacement) - ).toList + + // ---- global named types + members (cross-design, computed once) ---- + // names resolve from member meta only, so any sub-DB getSet works; use the top's. + val globalReservedTypeNamesLC: Set[String] = designDB.topDB.atGetSet { + // the existing design (class) names — one per sub-DB + val designNames = designDB.subDBs.values.map(_.top.dclName) + // the global named types across the whole hierarchy + val globalNamedTypes = designDB.hierGlobalNamedDFTypes + // the global named members, de-duplicated across the sub-DB closures that + // share them by identity (member equality is effectively identity — every + // distinct member carries unique refs) + val globalNamedMembers = designDB.subDBs.values.iterator + .flatMap(_.membersGlobals).filterNot(_.isAnonymous).toList.distinct + // global type map for unique renamed names + val globalTypeUpdateMap = + renamer(globalNamedTypes, reservedNamesLC)(_.name, (e, n) => e -> n).toMap + typeUpdateMap ++= globalTypeUpdateMap + // the global reserved type names, after unique global type renaming + val globalReservedTypeNames: Set[String] = + (globalNamedTypes.map(e => e.name) ++ globalTypeUpdateMap.values ++ designNames ++ + reservedNames).toSet + val resultLC = lowerCases(globalReservedTypeNames) + // global named member patching + renamer(globalNamedMembers, resultLC)( + _.getName, + (m, n) => + localReservedNamesLCMutable += lowerCase(n) + m -> Patch.Replace(m.setName(n), Patch.Replace.Config.FullReplacement) + ).foreach(entry => memberRenamePatches(entry._1) = entry) + resultLC + } // the reserved names for local (design) values will be the given reservedNames // and the now additional global member names after renaming val localReservedNamesLC = localReservedNamesLCMutable.toSet - // going through all blocks with their own scope for unique names - val blockPatchesAndTypeUpdates = designDB.blockMemberList.map { case (block, members) => - val localTypeUpdateList = block match - case design: DFDesignBlock => - renamer(designDB.getLocalNamedDFTypes(design), globalReservedTypeNamesLC)( - _.name, - (e, n) => e -> n - ) - case _ => Nil - val patchList = renamer( - members.view.flatMap { - // ignore iterator declarations that can repeat the same name wihtout collision - // TODO: an iterator declaration may still collide with other members. Need to revisit this. - case IteratorDcl() => None - // no need to rename binds, since there is no collision - // and will be handled after the binds are converted to explicit selectors - case Bind(_) => None - // design block names are their declaration names (design/class name), so they are handled differently - case _: DFDesignBlock => None - case m: DFMember.Named if !m.isAnonymous => Some(m) - case _ => None - }, - localReservedNamesLC - )( - _.getName, - (m, n) => m -> Patch.Replace(m.setName(n), Patch.Replace.Config.FullReplacement) - ) - (patchList, localTypeUpdateList) - }.unzip - val memberNamesPatchList = globalNamedMemberPatchList ++ blockPatchesAndTypeUpdates._1.flatten - // first patching the member names - val firstStep = designDB.patch(memberNamesPatchList) - // then patching the member with updated named types - locally { - given MemberGetSet = firstStep.getSet - val typeUpdateMap = globalTypeUpdateMap ++ blockPatchesAndTypeUpdates._2.flatten - object ComposedNamedDFTypeReplacement - extends ComposedDFTypeReplacement( - preCheck = { - case dt: NamedDFType => typeUpdateMap.get(dt) - case _ => None + // ---- per-design local members + local named types ---- + // going through all blocks (across all sub-DBs) with their own scope for unique names + designDB.subDBs.values.foreach { sub => + sub.atGetSet { + sub.blockMemberList.foreach { (block, members) => + block match + case design: DFDesignBlock => + // exclude types promoted to global across the hierarchy (handled above); + // a single sub-DB may otherwise mis-classify a cross-design type as local + renamer( + sub.getLocalNamedDFTypes(design) + .filterNot(designDB.hierGlobalNamedDFTypes.contains), + globalReservedTypeNamesLC + )(_.name, (e, n) => e -> n) + .foreach(entry => typeUpdateMap(entry._1) = entry._2) + case _ => + renamer( + members.view.flatMap { + // ignore iterator declarations that can repeat the same name wihtout collision + // TODO: an iterator declaration may still collide with other members. Need to revisit this. + case IteratorDcl() => None + // no need to rename binds, since there is no collision + // and will be handled after the binds are converted to explicit selectors + case Bind(_) => None + // design block names are their declaration names (design/class name), so they are handled differently + case _: DFDesignBlock => None + case m: DFMember.Named if !m.isAnonymous => Some(m) + case _ => None }, - updateFunc = { case (dt: NamedDFType, name) => dt.updateName(name) } - ) - val typeNamesPatchList = firstStep.members.flatMap { - case dfVal: DFVal => - dfVal.dfType match - case ComposedNamedDFTypeReplacement(updatedDFType) => - Some( - dfVal -> Patch.Replace( - dfVal.updateDFType(updatedDFType), - Patch.Replace.Config.FullReplacement - ) - ) - case _ => None - case _ => None + localReservedNamesLC + )( + _.getName, + (m, n) => m -> Patch.Replace(m.setName(n), Patch.Replace.Config.FullReplacement) + ).foreach(entry => memberRenamePatches(entry._1) = entry) + } } - firstStep.patch(typeNamesPatchList) } - end transform + + // ---- phase 1: patch the member names, per sub-DB ---- + val firstStepSubs: ListMap[DFOwner.Ref, DB] = ListMap.from( + designDB.subDBs.iterator.map { (key, sub) => + val patches = sub.members.collect { + case m if memberRenamePatches.contains(m) => memberRenamePatches(m) + } + key -> sub.patch(patches) + } + ) + val firstStep = designDB.update(subDBs = firstStepSubs) + + // ---- phase 2: patch the members with updated named types ---- + if (typeUpdateMap.isEmpty) firstStep + else + val typeUpdates = typeUpdateMap.toMap + // built once (type rewriting reads only type structure + renames by name), + // keyed by member so a shared global member's update is reused across every + // sub-DB that holds it. + val typeUpdatePatches: mutable.LinkedHashMap[DFMember, (DFMember, Patch)] = + firstStep.topDB.atGetSet { + object ComposedNamedDFTypeReplacement + extends ComposedDFTypeReplacement( + preCheck = { + case dt: NamedDFType => typeUpdates.get(dt) + case _ => None + }, + updateFunc = { case (dt: NamedDFType, name) => dt.updateName(name) } + ) + val patches = mutable.LinkedHashMap.empty[DFMember, (DFMember, Patch)] + firstStep.subDBs.values.foreach { sub => + sub.members.foreach { + case dfVal: DFVal => + dfVal.dfType match + case ComposedNamedDFTypeReplacement(updatedDFType) => + patches.getOrElseUpdate( + dfVal, + dfVal -> Patch.Replace( + dfVal.updateDFType(updatedDFType), + Patch.Replace.Config.FullReplacement + ) + ) + case _ => + case _ => + } + } + patches + } + val secondStepSubs: ListMap[DFOwner.Ref, DB] = ListMap.from( + firstStep.subDBs.iterator.map { (key, sub) => + val patches = sub.members.collect { + case m if typeUpdatePatches.contains(m) => typeUpdatePatches(m) + } + key -> sub.patch(patches) + } + ) + firstStep.update(subDBs = secondStepSubs) + end if + end transformGlobal end UniqueNames case object DFHDLUniqueNames extends UniqueNames(Set(), caseSensitive = true) diff --git a/compiler/stages/src/test/scala/StagesSpec/UniqueNamesSpec.scala b/compiler/stages/src/test/scala/StagesSpec/UniqueNamesSpec.scala index 8b424eccd..9901b46a4 100644 --- a/compiler/stages/src/test/scala/StagesSpec/UniqueNamesSpec.scala +++ b/compiler/stages/src/test/scala/StagesSpec/UniqueNamesSpec.scala @@ -106,4 +106,36 @@ class UniqueNamesSpec extends StageSpec: ) } + test("Unique names of a global shared across designs") { + // `gv` is a pair of colliding global constants both referenced from two + // different designs. They must be renamed ONCE into shared globals (gv_0, + // gv_1), not duplicated/diverged per design. + val gv = Vector("01", "02").map(x => h"$x") + class Sub extends DFDesign: + val o = Bits(8) <> OUT + o := gv(0) ^ gv(1) + class Top extends DFDesign: + val o = Bits(8) <> OUT + val sub = new Sub + o := gv(0) ^ sub.o + val top = (new Top).uniqueNames(Set(), true) + assertCodeString( + top, + """|val gv_0: Bits[8] <> CONST = h"01" + |val gv_1: Bits[8] <> CONST = h"02" + | + |class Sub extends DFDesign: + | val o = Bits(8) <> OUT + | o := gv_0 ^ gv_1 + |end Sub + | + |class Top extends DFDesign: + | val o = Bits(8) <> OUT + | val sub = Sub() + | o := gv_0 ^ sub.o + |end Top + |""".stripMargin + ) + } + end UniqueNamesSpec From 7f5005dd985b71b7ec538eee7bd04fb8591500a5 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sun, 14 Jun 2026 16:55:36 +0300 Subject: [PATCH 36/72] migrate UniqueDesigns to GlobalStage + actually share duplicates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert UniqueDesigns from a flat-DB `Stage` to a `GlobalStage`, and change its duplicate handling from the legacy `DuplicateTag` marking to real sub-DB sharing (a pre-hierarchy concept; no compiler stage/printer reads the tag — only MutableDB's elaboration dedup and NVC's name list, both left untouched). Structural comparison (`=~`) must resolve refs from two designs at once, so it runs on the flattened DB (`designDB.newToOld`), whose design blocks are the same objects as the sub-DB tops — so the grouping maps straight back onto the hierarchy. For each group of structurally-identical designs the first is the canonical; the rest are SHARED into it by dropping their sub-DBs and retargeting every ref (notably the parent inst's `designRef`) onto the canonical, so `newToOld` emits one shared design reached by all instances. Case-insensitive name collisions across distinct designs still rename to `_`. The UniqueDesignsSpec "Unique designs" test was replaced: the old tests are no-ops for the stage (MutableDB's elaboration-time dedup already renames), so the new test uses ReduplicateDesign to create two identical post-elaboration `Leaf` clones and asserts they collapse to a single shared `Leaf`. Full suite green (StagesSpec 437, lib 152 incl. both AES CipherSpecs, platforms 1). Note: FullCompileSpec accumulates stale sandbox files — clearSandbox before re-running after output-affecting changes. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../dfhdl/compiler/stages/UniqueDesigns.scala | 96 +++++++++++------ .../scala/StagesSpec/UniqueDesignsSpec.scala | 100 ++++++++++-------- 2 files changed, 121 insertions(+), 75 deletions(-) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueDesigns.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueDesigns.scala index 9bbe73c21..d535a9bbb 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueDesigns.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueDesigns.scala @@ -2,56 +2,90 @@ package dfhdl.compiler.stages import dfhdl.compiler.analysis.* import dfhdl.compiler.ir.* -import dfhdl.compiler.patching.* import dfhdl.options.CompilerOptions import dfhdl.internals.* +import scala.collection.immutable.ListMap -case object UniqueDesigns extends Stage: +case object UniqueDesigns extends GlobalStage: def dependencies: List[Stage] = List() def nullifies: Set[Stage] = Set() - def transform(designDB: DB)(using MemberGetSet, CompilerOptions): DB = + + private def groupDesigns(db: DB)(using MemberGetSet): List[List[DFDesignBlock]] = val eqDesign: ((DFDesignBlock, List[DFMember]), (DFDesignBlock, List[DFMember])) => Boolean = case ((thisBlock, theseMembers), (thatBlock, thoseMembers)) if thisBlock.dclMeta == thatBlock.dclMeta => (theseMembers lazyZip thoseMembers).forall { case (l, r) => l =~ r } case _ => false - end eqDesign // we're grouping always according to case-insensitive design names because these affect // the eventual file names and we want these to be different across all operating systems. // the actual name case is preserved for design/entity/module generation. - val sameBlockLists: List[List[DFDesignBlock]] = - designDB.designMemberList.view - .groupByCompare(eqDesign, _._1.dclName.toLowerCase().hashCode()).map(_.unzip._1).toList + db.designMemberList.view + .groupByCompare(eqDesign, _._1.dclName.toLowerCase().hashCode()).map(_.unzip._1).toList + + def transformGlobal(designDB: DB)(using co: CompilerOptions, refGen: RefGen): DB = + // Cross-design structural comparison resolves refs from BOTH designs, so it + // needs a single getSet covering every design. We use the flattened DB for + // this — its design blocks are the SAME objects as the sub-DB tops, so the + // grouping/decisions map straight back onto the hierarchy. + val flatDB = designDB.newToOld + val sameBlockLists: List[List[DFDesignBlock]] = flatDB.atGetSet(groupDesigns(flatDB)) val uniqueTypeMap: Map[String, List[List[DFDesignBlock]]] = sameBlockLists.groupBy(g => g.head.dclName.toLowerCase()) - val patchList = uniqueTypeMap.flatMap { case (designType, list) => - list.zipWithIndex.flatMap { + + val topTop = designDB.top + // canonical design -> its unique (possibly renamed) declaration name + val canonicalRenames = collection.mutable.LinkedHashMap.empty[DFDesignBlock, String] + // redundant duplicate design -> the canonical design it is shared into + val dupToCanonical = collection.mutable.LinkedHashMap.empty[DFDesignBlock, DFDesignBlock] + uniqueTypeMap.foreach { case (_, list) => + list.zipWithIndex.foreach { case (group, i) if group.length > 1 || list.length > 1 => - val groupHead = group.head - // using the actual name and not the `designType` grouping, to preserve the original - // naming. we only lower-cased it for case-insensitive grouping. + val canonical = group.head + // using the actual name and not the lower-cased grouping key, to preserve + // the original naming. we only lower-cased it for case-insensitive grouping. val updatedDclName = if (list.length > 1) - if (groupHead.isTopTop) groupHead.dclName // top name should not be mangled - else s"${groupHead.dclName}_${i.toPaddedString(list.length)}" - else groupHead.dclName - var first = true - group.map(block => - val tags = - if (first) - first = false - block.tags - else block.tags.tag(DuplicateTag) - block -> Patch.Replace( - block.copy(meta = block.meta.copy(nameOpt = Some(updatedDclName)), tags = tags), - Patch.Replace.Config.FullReplacement - ) - ) - case _ => None + if (canonical eq topTop) canonical.dclName // top name should not be mangled + else s"${canonical.dclName}_${i.toPaddedString(list.length)}" + else canonical.dclName + if (updatedDclName != canonical.dclName) canonicalRenames(canonical) = updatedDclName + // every other structurally-identical design is shared into the canonical + group.drop(1).foreach(dup => dupToCanonical(dup) = canonical) + case _ => } - }.toList - designDB.patch(patchList) - end transform + } + + if (canonicalRenames.isEmpty && dupToCanonical.isEmpty) designDB + else + // Apply the sharing on the hierarchical sub-DBs. + val dupKeys = dupToCanonical.keysIterator.map(_.ownerRef).toSet + val newSubDBs: ListMap[DFOwner.Ref, DB] = ListMap.from( + designDB.subDBs.iterator.flatMap { (key, sub) => + // drop the redundant duplicate sub-DBs entirely + if (dupKeys.contains(key)) None + else + // rename the design block if this sub-DB's top is a renamed canonical. + // `newToOld`'s canonicalize (by ownerRef) propagates the rename to every + // ref that still targets the pre-rename block. + val newMembers = canonicalRenames.get(sub.top) match + case Some(updatedName) => + val renamedTop = + sub.top.copy(meta = sub.top.meta.copy(nameOpt = Some(updatedName))) + sub.members.map(m => if (m eq sub.top) renamedTop else m) + case None => sub.members + // retarget any ref to a duplicate design onto its canonical (covers the + // parent inst's `designRef`, so the shared design is reached by both). + val newRefTable = + sub.refTable.view.mapValues { + case d: DFDesignBlock if dupToCanonical.contains(d) => dupToCanonical(d) + case t => t + }.toMap + Some(key -> sub.update(members = newMembers, refTable = newRefTable)) + } + ) + designDB.update(subDBs = newSubDBs) + end if + end transformGlobal end UniqueDesigns extension [T: HasDB](t: T) diff --git a/compiler/stages/src/test/scala/StagesSpec/UniqueDesignsSpec.scala b/compiler/stages/src/test/scala/StagesSpec/UniqueDesignsSpec.scala index c5a03b516..6bb31dda5 100644 --- a/compiler/stages/src/test/scala/StagesSpec/UniqueDesignsSpec.scala +++ b/compiler/stages/src/test/scala/StagesSpec/UniqueDesignsSpec.scala @@ -5,56 +5,68 @@ import dfhdl.compiler.stages.uniqueDesigns // scalafmt: { align.tokens = [{code = "<>"}, {code = "="}, {code = "=>"}, {code = ":="}]} class UniqueDesignsSpec extends StageSpec: - test("Unique designs") { - object Container: - class ID extends DFDesign: - val x = SInt(15) <> IN - val y = SInt(15) <> OUT - y := x - - class ID extends DFDesign: - val x = SInt(16) <> IN - val y = SInt(16) <> OUT + test("Reduplicated identical designs are re-shared into one") { + // `ReduplicateDesign` clones the whole `dup_*` sub-tree for ref uniqueness, + // producing two structurally-identical `Leaf` designs. `UniqueDesigns` must + // re-share them into a SINGLE `Leaf` design referenced by both instances. + class Leaf extends DFDesign: + val x = UInt(8) <> IN + val y = UInt(8) <> OUT y := x - - class IDTop extends DFDesign: - val x1 = SInt(16) <> IN - val y1 = SInt(16) <> OUT - val x2 = SInt(15) <> IN - val y2 = SInt(15) <> OUT - val id1 = new ID - val id2 = new Container.ID - id1.x <> x1 - id1.y <> y1 - id2.x <> x2 - id2.y <> y2 - val id = (new IDTop).uniqueDesigns + class Inner extends DFDesign: + val x = UInt(8) <> IN + val y = UInt(8) <> OUT + val l = Leaf() + l.x <> x + l.y <> y + class Top extends DFDesign: + val in_a = UInt(8) <> IN + val out_a = UInt(8) <> OUT + val in_b = UInt(8) <> IN + val out_b = UInt(8) <> OUT + val dup_a = Inner() + val dup_b = Inner() + dup_a.x <> in_a + dup_a.y <> out_a + dup_b.x <> in_b + dup_b.y <> out_b + val id = (new Top).reduplicateNamedDup.uniqueDesigns assertCodeString( id, - """|class ID_0 extends DFDesign: - | val x = SInt(16) <> IN - | val y = SInt(16) <> OUT + """|class Leaf extends DFDesign: + | val x = UInt(8) <> IN + | val y = UInt(8) <> OUT | y := x - |end ID_0 + |end Leaf | - |class ID_1 extends DFDesign: - | val x = SInt(15) <> IN - | val y = SInt(15) <> OUT - | y := x - |end ID_1 + |class Inner_dup_a extends DFDesign: + | val x = UInt(8) <> IN + | val y = UInt(8) <> OUT + | val l = Leaf() + | l.x <> x + | y <> l.y + |end Inner_dup_a | - |class IDTop extends DFDesign: - | val x1 = SInt(16) <> IN - | val y1 = SInt(16) <> OUT - | val x2 = SInt(15) <> IN - | val y2 = SInt(15) <> OUT - | val id1 = ID_0() - | val id2 = ID_1() - | id1.x <> x1 - | y1 <> id1.y - | id2.x <> x2 - | y2 <> id2.y - |end IDTop + |class Inner_dup_b extends DFDesign: + | val x = UInt(8) <> IN + | val y = UInt(8) <> OUT + | val l = Leaf() + | l.x <> x + | y <> l.y + |end Inner_dup_b + | + |class Top extends DFDesign: + | val in_a = UInt(8) <> IN + | val out_a = UInt(8) <> OUT + | val in_b = UInt(8) <> IN + | val out_b = UInt(8) <> OUT + | val dup_a = Inner_dup_a() + | val dup_b = Inner_dup_b() + | dup_a.x <> in_a + | out_a <> dup_a.y + | dup_b.x <> in_b + | out_b <> dup_b.y + |end Top |""".stripMargin ) } From 31c317ad9a7cba964550dd50a49eed12b5177aa5 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sun, 14 Jun 2026 17:01:16 +0300 Subject: [PATCH 37/72] remove DuplicateTag reference in NVC producedFiles NVC's `producedFiles` filtered design names with `.filterNot(_.isDuplicate)`. Now that UniqueDesigns actually shares duplicate designs (and MutableDB's elaboration dedup removes them), no DuplicateTag-marked designs reach the backend, so the filter is a no-op. Drop it. This was the only `isDuplicate` consumer outside elaboration. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/src/main/scala/dfhdl/tools/toolsCore/NVC.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/scala/dfhdl/tools/toolsCore/NVC.scala b/lib/src/main/scala/dfhdl/tools/toolsCore/NVC.scala index 1d6b78441..aee243e84 100644 --- a/lib/src/main/scala/dfhdl/tools/toolsCore/NVC.scala +++ b/lib/src/main/scala/dfhdl/tools/toolsCore/NVC.scala @@ -35,7 +35,7 @@ object NVC extends VHDLLinter, VHDLSimulator: so: SimulatorOptions ): List[String] = val designWorkFiles = getSet.designDB.designMemberList.view.map(_._1) - .filterNot(_.isDuplicate).map(_.dclName) + .map(_.dclName) .flatMap(name => val nameUC = name.toUpperCase() List(s"WORK.${nameUC}", s"WORK.${nameUC}-${nameUC}_ARCH") From dfffac64fba7298548364744387586e6aca2ce5e Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sun, 14 Jun 2026 17:19:36 +0300 Subject: [PATCH 38/72] disable caching in FullCompileSpec --- lib/src/test/scala/util/FullCompileSpec.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/test/scala/util/FullCompileSpec.scala b/lib/src/test/scala/util/FullCompileSpec.scala index 2aaa6d5e6..e95c22d4f 100644 --- a/lib/src/test/scala/util/FullCompileSpec.scala +++ b/lib/src/test/scala/util/FullCompileSpec.scala @@ -14,6 +14,7 @@ import munit.Location abstract class FullCompileSpec extends FunSuite: def dut: core.Design given options.CompilerOptions.NewFolderForTop = false + given options.AppOptions.CacheEnable = false def projectFolderName = s"${this.getClass.getPackageName()}.${this.getClass.getSimpleName()}" def projectSandboxFolder = s"sandbox${S}FullCompileSpec$S$projectFolderName" def projectResourceFolder = s"lib${S}src${S}test${S}resources${S}ref$S$projectFolderName" From cbd4e7d82b1ba9888cd6e444e04d8a74a37c7e17 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sun, 14 Jun 2026 20:52:41 +0300 Subject: [PATCH 39/72] Unify DFDesignInst.designRef with child block ownerRef (subDBs key) Make inst.designRef equal the instantiated child design block's ownerRef -- the hierarchy/subDBs key -- so a parent reaches its child sub-DB directly via subDBs.get(inst.designRef), with no parent-side refTable entry for the design. The child block keeps its own ownerRef -> Empty in its own sub-DB, so isTop is unchanged. - MutableDB.immutable: unify each inst's designRef at the immutable build (unifyInst), replacing the now-redundant dupRefs remap. - oldToNew/refsFor: stop adding designRef to the parent sub-DB refTable; resolve inst -> block via getDesignBlock/subDBs. - getDesignBlock: 3-path resolution (mutable refTable / hierarchical subDBs / flat designBlockByKey map). prot_=~ compares via getDesignBlock; copyWithNewRefs no longer freshens designRef. - SanityCheck/Patch: drop the designRef orphan-whitelist special-cases. - ReduplicateDesign/UniqueDesigns/DropDesignDefs: resolve via getDesignBlock and retarget cloned/shared insts to the canonical key. - DFOwnerPrinter.csDFDesignDefInst: fall back to resolving def-instance PBNS ports via the current getSet when the cached designInstPBNS misses (elaboration printing iterates live mutable insts that still carry designRef -> block). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 31 +++++--- .../scala/dfhdl/compiler/ir/DFMember.scala | 27 ++++++- .../main/scala/dfhdl/compiler/ir/DFRef.scala | 15 +++- .../compiler/printing/DFOwnerPrinter.scala | 13 +++- .../compiler/stages/DropDesignDefs.scala | 6 +- .../compiler/stages/ReduplicateDesign.scala | 74 ++++++++++--------- .../dfhdl/compiler/stages/SanityCheck.scala | 6 -- .../dfhdl/compiler/stages/UniqueDesigns.scala | 39 ++++++---- .../scala/dfhdl/compiler/patching/Patch.scala | 5 -- .../src/main/scala/dfhdl/core/MutableDB.scala | 27 ++++--- 10 files changed, 153 insertions(+), 90 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index 7f9593158..36f4f9648 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -118,6 +118,14 @@ final case class DB private ( lazy val memberTable: Map[DFMember, Set[DFRefAny]] = refTable.invert + // Flat-DB analog of `subDBs` lookup: maps each design block's `ownerRef` + // (which, once unified with the instantiating `DFDesignInst.designRef`, is the + // design's hierarchy key) to the block itself. Used by `getDesignBlock` to + // resolve a parent's inst to its child block in the flat (round-trip) DB + // without a parent-side refTable entry for `designRef`. + lazy val designBlockByKey: Map[DFRefAny, DFDesignBlock] = + members.view.collect { case d: DFDesignBlock => (d.ownerRef: DFRefAny) -> d }.toMap + lazy val originRefTable: Map[DFRef.TwoWayAny, DFMember] = members.view.flatMap(origMember => origMember.getRefs.map(_ -> origMember)).toMap @@ -1568,7 +1576,7 @@ final case class DB private ( val designBlockParent = mutable.LinkedHashMap.empty[DFDesignBlock, DFDesignBlock] members.foreach { case inst: DFDesignInst => - designBlockParent.getOrElseUpdate(inst.designRef.get, inst.getOwnerDesign) + designBlockParent.getOrElseUpdate(inst.getDesignBlock, inst.getOwnerDesign) case _ => } members.foreach { @@ -1619,11 +1627,9 @@ final case class DB private ( dbMembers.foreach { m => refTable.get(m.ownerRef).foreach(t => result(m.ownerRef) = t) m.getRefs.foreach(r => refTable.get(r).foreach(t => result(r) = t)) - // DFDesignInst.designRef is OneWay and not reported by getRefs — pick it up explicitly. - m match - case inst: DFDesignInst => - refTable.get(inst.designRef).foreach(t => result(inst.designRef) = t) - case _ => + // NOTE: `DFDesignInst.designRef` is deliberately NOT added here. It is + // unified with the child block's `ownerRef` (the `subDBs` key) and is + // resolved structurally via `subDBs`, not through the parent's refTable. } result.toMap // Build sub-DBs in top-down elaboration order. Sub-DBs themselves have @@ -1728,11 +1734,12 @@ final case class DB private ( allDBs.foreach { db => db.members.foreach { case inst: DFDesignInst => - db.refTable.get(inst.designRef).foreach { - case d: DFDesignBlock => - instToDesign(inst) = canonicalize(d).asInstanceOf[DFDesignBlock] - case _ => - } + // Resolve inst -> child block structurally: `designRef` is unified with + // the child block's `ownerRef` (the `subDBs` key) and is not present in + // the parent sub-DB's refTable. `getDesignBlock` handles both the + // unified form and the pre-unification distinct form. + instToDesign(inst) = + canonicalize(inst.getDesignBlock(using db.getSet)).asInstanceOf[DFDesignBlock] case _ => } } @@ -1843,7 +1850,7 @@ final case class DB private ( out += d designLocals.getOrElse(d, mutable.ListBuffer.empty).foreach { case inst: DFDesignInst => - val target = inst.designRef.get + val target = inst.getDesignBlock if (!emittedDesigns.contains(target)) emittedDesigns += target emitDesign(target) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala index d57a6adf4..a96db12f3 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala @@ -520,6 +520,7 @@ object DFVal: case dv: DFVal => dv.getConstData(using getSet, updatedPolicy) case _ => ConstData.NotConst } + end match else ConstData.UnknownConst(this) protected def `prot_=~`(that: DFMember)(using MemberGetSet): Boolean = that match case that: DesignParam => @@ -1674,10 +1675,28 @@ final case class DFDesignInst( meta: Meta, tags: DFTags ) extends DFMember.Named derives ReadWriter: - def getDesignBlock(using MemberGetSet): DFDesignBlock = designRef.get + // Resolve the instantiated child design block. `designRef` is unified with the + // child block's `ownerRef` (the hierarchy key) and is deliberately NOT present + // in the parent's refTable, so resolution goes structurally: + // - mutable (elaboration): the ref still resolves via the live refTable. + // - hierarchical root: the ref is the `subDBs` key → the child sub-DB's top. + // - flat (round-trip) DB: the ref is the `designBlockByKey` map key. + // Each non-mutable path falls back to `designRef.get` for the pre-unification + // form (where `designRef` is still a distinct parent-side refTable entry). + def getDesignBlock(using getSet: MemberGetSet): DFDesignBlock = + if (getSet.isMutable) designRef.get + else + val root = getSet.designDB.rootDB + if (root.isRoot) + root.subDBs.get(designRef).map(_.top).getOrElse(designRef.get) + else root.designBlockByKey.getOrElse(designRef, designRef.get) protected def `prot_=~`(that: DFMember)(using MemberGetSet): Boolean = that match case that: DFDesignInst => - this.designRef =~ that.designRef && + // `designRef` is unified with the child block's `ownerRef` (the sub-DB key) + // and is not in the parent refTable, so compare the resolved design blocks + // structurally rather than via the ref's own `=~` (which resolves through + // the refTable). + this.getDesignBlock =~ that.getDesignBlock && this.paramMap =~ that.paramMap && this.meta =~ that.meta && this.tags =~ that.tags case _ => false @@ -1686,9 +1705,11 @@ final case class DFDesignInst( lazy val getRefs: List[DFRef.TwoWayAny] = paramMap.values.toList ++ meta.getRefs override def getAllRefs: List[DFRefAny] = ownerRef :: designRef :: getRefs + // NOTE: `designRef` is intentionally NOT freshened here. It is unified with the + // target block's `ownerRef` (the sub-DB key); when cloning a design sub-tree the + // caller rebinds it to the cloned block's ownerRef (see ReduplicateDesign). def copyWithNewRefs(using RefGen): this.type = copy( meta = meta.copyWithNewRefs, - designRef = designRef.copyAsNewRef, paramMap = paramMap.map((k, v) => k -> v.copyAsNewRef), ownerRef = ownerRef.copyAsNewRef ).asInstanceOf[this.type] diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFRef.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFRef.scala index 27b1ecc10..11851b271 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFRef.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFRef.scala @@ -263,10 +263,21 @@ end RefGen object RefGen: def initial: RefGen = RefGen(0, (0, 0), 0) def fromGetSet(using getSet: MemberGetSet): RefGen = - val rt = getSet.designDB.refTable + val db = getSet.designDB + // The hierarchical root holds no members/refTable of its own (all content + // lives in the sub-DBs), so aggregate across every sub-DB. A non-root DB + // (flat or a single sub-DB) is handled exactly as before. + val rt = + if (db.isRoot) db.subDBs.values.view.flatMap(_.refTable).toMap + else db.refTable + val members = + if (db.isRoot) db.subDBs.values.view.flatMap(_.members).toList + else db.members val grpId = rt.last._1.grpId val lastId = rt.keys.map(_.id).max - val magnetID = getSet.designDB.members.view.collect { + val magnetID = members.view.collect { case DFOpaque.Val(dfType) if dfType.isMagnet => dfType.id }.maxOption.getOrElse(0) RefGen(magnetID, grpId, lastId) + end fromGetSet +end RefGen diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/printing/DFOwnerPrinter.scala b/compiler/ir/src/main/scala/dfhdl/compiler/printing/DFOwnerPrinter.scala index 07df12d8a..d7864add2 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/printing/DFOwnerPrinter.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/printing/DFOwnerPrinter.scala @@ -228,7 +228,18 @@ protected trait DFOwnerPrinter extends AbstractOwnerPrinter: }.toList def csDFDesignDefInst(inst: DFDesignInst): String = val design = inst.getDesignBlock - val ports = getSet.designDB.designInstPBNS(inst).view.collect { + // `designInstPBNS` is keyed by the unified immutable insts. Elaboration-time + // printing (e.g. test helpers that print live mutable members) holds insts + // still carrying their pre-unification `designRef`, so a direct lookup can + // miss; fall back to resolving the PBNSes via the current getSet, which + // matches the member being printed. + val instPBNS = getSet.designDB.designInstPBNS.getOrElse( + inst, + getSet.designDB.members.collect { + case pbns: DFVal.PortByNameSelect if pbns.getDesignInst == inst => pbns + } + ) + val ports = instPBNS.view.collect { case pbns if pbns.isIn => // the positional def-instance form expects a single producer per input port; // a piecewise-connected input port (multiple partial nets) cannot be rendered diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropDesignDefs.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropDesignDefs.scala index f87f2140d..66eaeeefe 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropDesignDefs.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropDesignDefs.scala @@ -33,11 +33,11 @@ case object DropDesignDefs extends GlobalStage: parentSubDB.members.foreach { // only going after design definition instances case designInst: DFDesignInst => - parentSubDB.refTable.get(designInst.designRef) match - case Some(design @ DFDesignBlock( + designInst.getDesignBlock(using parentSubDB.getSet) match + case design @ DFDesignBlock( domainType = DomainType.DF, instMode = InstMode.Def - )) => + ) => val defKey = design.ownerRef // On the first instance of a given def design, stage its conversion // patches (in the def design's own sub-DB) and cache the output-port- diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ReduplicateDesign.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ReduplicateDesign.scala index 5d5c155f0..f2aafcc36 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ReduplicateDesign.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ReduplicateDesign.scala @@ -37,11 +37,9 @@ abstract class ReduplicateDesign extends GlobalStage: designDB.subDBs.foreach { case (_, parentSubDB) => parentSubDB.members.foreach { case inst: DFDesignInst => - parentSubDB.refTable.get(inst.designRef) match - case Some(d: DFDesignBlock) => - instsByTarget - .getOrElseUpdate(d.ownerRef, mutable.ListBuffer.empty) += ((inst, parentSubDB)) - case _ => + val d = inst.getDesignBlock(using parentSubDB.getSet) + instsByTarget + .getOrElseUpdate(d.ownerRef, mutable.ListBuffer.empty) += ((inst, parentSubDB)) case _ => } } @@ -68,7 +66,7 @@ abstract class ReduplicateDesign extends GlobalStage: if (k == n) val (firstInst, firstParent) = matching.head val firstName = firstParent.atGetSet(firstInst.getName) - val d = firstParent.refTable(firstInst.designRef).asInstanceOf[DFDesignBlock] + val d = firstInst.getDesignBlock(using firstParent.getSet) val renamed = d.copy(meta = d.meta.setName(s"${d.dclName}_$firstName")) originalRenames(d) = renamed cloneRequests ++= matching.drop(1) @@ -86,7 +84,7 @@ abstract class ReduplicateDesign extends GlobalStage: mutable.LinkedHashMap.empty[DFOwner.Ref, mutable.LinkedHashMap[DFDesignInst, DFDesignBlock]] cloneRequests.foreach { case (inst, parentSubDB) => - val origTargetBlock = parentSubDB.refTable(inst.designRef).asInstanceOf[DFDesignBlock] + val origTargetBlock = inst.getDesignBlock(using parentSubDB.getSet) val instName = parentSubDB.atGetSet(inst.getName) val acc = CloneAcc( memberMap = mutable.Map.empty, @@ -139,11 +137,11 @@ abstract class ReduplicateDesign extends GlobalStage: mergedByKey.get(key).foreach { sub => emitted(key) = sub sub.members.foreach { - case inst: DFDesignInst => - sub.refTable.get(inst.designRef) match - case Some(target: DFDesignBlock) => emit(target.ownerRef) - case _ => - case _ => + // `designRef` IS the target design's sub-DB key under unification, + // so it can be used directly (these merged sub-DBs are not yet wired + // into a root, so structural `getDesignBlock` is not available here). + case inst: DFDesignInst => emit(inst.designRef) + case _ => } } val topKey = designDB.subDBs.head._1 @@ -153,6 +151,7 @@ abstract class ReduplicateDesign extends GlobalStage: mergedByKey.foreach { case (key, sub) => if (!emitted.contains(key)) emitted(key) = sub } designDB.update(subDBs = ListMap.from(emitted)) + end if end transformGlobal // Mutable accumulators threaded through `cloneSubTree`. @@ -188,10 +187,22 @@ abstract class ReduplicateDesign extends GlobalStage: // is reached by multiple peer insts inside this sub-DB). origSubDB.members.foreach { case inst: DFDesignInst => - origSubDB.refTable.get(inst.designRef) match - case Some(childBlock: DFDesignBlock) if !acc.memberMap.contains(childBlock) => - cloneSubTree(childBlock, None, acc, designDB) - case _ => + val childBlock = inst.getDesignBlock(using origSubDB.getSet) + if (!acc.memberMap.contains(childBlock)) + cloneSubTree(childBlock, None, acc, designDB) + case _ => + } + + // Under unification a cloned inst's `designRef` must equal its cloned target + // block's `ownerRef` (the `subDBs` key). `copyWithNewRefs` does not freshen + // `designRef`, so set it explicitly to the cloned child block's fresh ownerRef. + origSubDB.members.foreach { + case inst: DFDesignInst => + val clonedInst = acc.memberMap(inst).asInstanceOf[DFDesignInst] + val clonedChild = + acc.memberMap(inst.getDesignBlock(using origSubDB.getSet)).asInstanceOf[DFDesignBlock] + acc.memberMap(inst) = + clonedInst.copy(designRef = clonedChild.ownerRef.asInstanceOf[DFDesignInst.DesignRef]) case _ => } @@ -209,7 +220,7 @@ abstract class ReduplicateDesign extends GlobalStage: val newRefTable: Map[DFRefAny, DFMember] = origSubDB.refTable.map { case (oR, target) => val nR = acc.refMap(oR) val newTarget = target match - case _: DFMember.Empty => target // sentinel — passthrough + case _: DFMember.Empty => target // sentinel — passthrough case t => acc.memberMap(t) nR -> newTarget }.toMap @@ -228,27 +239,20 @@ abstract class ReduplicateDesign extends GlobalStage: private def rewireParentSubDB( parentSubDB: DB, rewires: Map[DFDesignInst, DFDesignBlock] - )(using refGen: RefGen): DB = - // For each rewired inst, mint a fresh OneWay designRef pointing at the clone. - val instReplacements: Map[DFDesignInst, (DFDesignInst, DFRef.OneWay[DFDesignBlock])] = - rewires.map { case (origInst, _) => - val newDesignRef = refGen.genOneWay[DFDesignBlock] - val newInst = origInst.copy(designRef = newDesignRef) - origInst -> ((newInst, newDesignRef)) - } + ): DB = + // Point each rewired inst's `designRef` at its cloned target block's `ownerRef` + // (the cloned sub-DB's key). Under unification `designRef` IS that key and is + // resolved structurally via `subDBs`, so it is not added to the parent refTable. val instMap: Map[DFMember, DFMember] = - instReplacements.map { case (o, (n, _)) => o -> n } + rewires.map { case (origInst, clonedBlock) => + origInst -> origInst.copy(designRef = + clonedBlock.ownerRef.asInstanceOf[DFRef.OneWay[DFDesignBlock]] + ) + } val newMembers = parentSubDB.members.map(m => instMap.getOrElse(m, m)) - // Retarget any ref whose value was an old inst to the new inst, then strip - // the obsolete designRef entries and add the fresh designRef -> clone pairs. - val retargeted: Map[DFRefAny, DFMember] = - parentSubDB.refTable.view.mapValues(t => instMap.getOrElse(t, t)).toMap - val withoutOldDesignRefs: Map[DFRefAny, DFMember] = - retargeted -- instReplacements.keys.map(_.designRef) + // Retarget any ref whose value was an old inst to the new inst. val newRefTable: Map[DFRefAny, DFMember] = - withoutOldDesignRefs ++ instReplacements.map { case (origInst, (_, newDesignRef)) => - newDesignRef -> rewires(origInst) - } + parentSubDB.refTable.view.mapValues(t => instMap.getOrElse(t, t)).toMap parentSubDB.update(members = newMembers, refTable = newRefTable) end rewireParentSubDB end ReduplicateDesign diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala index 18f9ae73f..22f23b9e5 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala @@ -145,12 +145,6 @@ case class SanityCheck(skipAnonRefCheck: Boolean) extends Stage: val memberOwnerRefs = mutable.Set.empty[DFRefAny] memberSet.foreach { m => memberOwnerRefs += m.ownerRef - m match - // DFDesignInst emits a OneWay ref to its DFDesignBlock via designRef, - // outside of getRefs (which only carries TwoWay refs). Register it so - // the orphan OneWay.Gen detection below does not flag it. - case inst: DFDesignInst => memberOwnerRefs += inst.designRef - case _ => } // checks for all references refTable.foreach { (r, m) => diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueDesigns.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueDesigns.scala index d535a9bbb..89d4c46b3 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueDesigns.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueDesigns.scala @@ -59,26 +59,39 @@ case object UniqueDesigns extends GlobalStage: else // Apply the sharing on the hierarchical sub-DBs. val dupKeys = dupToCanonical.keysIterator.map(_.ownerRef).toSet + // duplicate design's sub-DB key -> the canonical design's sub-DB key. A parent + // inst that targeted a duplicate must be retargeted to the canonical key (its + // `designRef` IS that key under unification). + val dupKeyToCanonicalKey: Map[DFOwner.Ref, DFOwner.Ref] = + dupToCanonical.iterator.map((dup, canon) => dup.ownerRef -> canon.ownerRef).toMap val newSubDBs: ListMap[DFOwner.Ref, DB] = ListMap.from( designDB.subDBs.iterator.flatMap { (key, sub) => // drop the redundant duplicate sub-DBs entirely if (dupKeys.contains(key)) None else - // rename the design block if this sub-DB's top is a renamed canonical. - // `newToOld`'s canonicalize (by ownerRef) propagates the rename to every - // ref that still targets the pre-rename block. - val newMembers = canonicalRenames.get(sub.top) match - case Some(updatedName) => - val renamedTop = - sub.top.copy(meta = sub.top.meta.copy(nameOpt = Some(updatedName))) - sub.members.map(m => if (m eq sub.top) renamedTop else m) - case None => sub.members - // retarget any ref to a duplicate design onto its canonical (covers the - // parent inst's `designRef`, so the shared design is reached by both). + val instReplace = collection.mutable.Map.empty[DFDesignInst, DFDesignInst] + val newMembers = sub.members.map { + // rename the sub-DB's top if it is a renamed canonical design + case d: DFDesignBlock if canonicalRenames.contains(d) => + d.copy(meta = d.meta.copy(nameOpt = Some(canonicalRenames(d)))) + // retarget a parent inst that targeted a duplicate onto the canonical + // key (its `designRef` IS that key under unification) + case inst: DFDesignInst if dupKeyToCanonicalKey.contains(inst.designRef) => + val newInst = inst.copy(designRef = + dupKeyToCanonicalKey(inst.designRef).asInstanceOf[DFDesignInst.DesignRef] + ) + instReplace(inst) = newInst + newInst + case m => m + } + // keep refTable values consistent: point any ref that targeted a + // retargeted inst at its replacement, and any remaining duplicate design + // block at its canonical (designRef itself no longer lives in refTable). val newRefTable = sub.refTable.view.mapValues { - case d: DFDesignBlock if dupToCanonical.contains(d) => dupToCanonical(d) - case t => t + case inst: DFDesignInst if instReplace.contains(inst) => instReplace(inst) + case d: DFDesignBlock if dupToCanonical.contains(d) => dupToCanonical(d) + case t => t }.toMap Some(key -> sub.update(members = newMembers, refTable = newRefTable)) } diff --git a/core/src/main/scala/dfhdl/compiler/patching/Patch.scala b/core/src/main/scala/dfhdl/compiler/patching/Patch.scala index c723b3929..414abcbe5 100644 --- a/core/src/main/scala/dfhdl/compiler/patching/Patch.scala +++ b/core/src/main/scala/dfhdl/compiler/patching/Patch.scala @@ -560,11 +560,6 @@ extension (db: DB) val memberOwnerRefs = scala.collection.mutable.Set.empty[DFRefAny] patchedMembers.foreach { m => memberOwnerRefs += m.ownerRef - m match - // DFDesignInst's designRef is a OneWay.Gen ref outside of getRefs - // (which only carries TwoWay refs); keep it from being swept away. - case inst: DFDesignInst => memberOwnerRefs += inst.designRef - case _ => } patchedRefTable.filter { (r, _) => r match diff --git a/core/src/main/scala/dfhdl/core/MutableDB.scala b/core/src/main/scala/dfhdl/core/MutableDB.scala index 6f4b15640..e37a2ebfc 100644 --- a/core/src/main/scala/dfhdl/core/MutableDB.scala +++ b/core/src/main/scala/dfhdl/core/MutableDB.scala @@ -639,7 +639,17 @@ final class MutableDB(): // so user code could reference them, but in the immutable DB they are no // longer needed. val redundantRefs = mutable.Set.empty[DFRefAny] - val dupRefs = mutable.Map.empty[DFRefAny, DFMember] + // Unify a DFDesignInst's `designRef` with its (canonical) target block's + // `ownerRef` — the design's hierarchy / `subDBs` key. This replaces the old + // `dupRefs` remap: a duplicate inst now points directly at the canonical + // design's key. The previous distinct `designRef -> block` entry is no longer + // emitted by any member and is swept by `cleanedRefTable` below. Recomputed + // per occurrence (not memoized by object identity) so that the member-list + // and refTable occurrences of the same inst — which may be distinct objects — + // both map to EQUAL unified copies. + def unifyInst(inst: DFDesignInst): DFDesignInst = + val target = dupToOrigDesignMap.getOrElse(inst.designRef.get, inst.designRef.get) + inst.copy(designRef = target.ownerRef.asInstanceOf[DFDesignInst.DesignRef]) val finalMembers = members.flatMap { case m: DFVal if m.isGlobal => Some(finalFixFunc(m)) case m: (DomainBlock | DFVal) if dupToOrigDesignMap.contains(m.getOwnerDesign) => @@ -655,13 +665,8 @@ final class MutableDB(): redundantRefs += d.ownerRef redundantRefs ++= d.getRefs None - case designInst: DFDesignInst => - dupToOrigDesignMap.get(designInst.designRef.get) match - case Some(origDesign) => - dupRefs += designInst.designRef -> finalFixFunc(origDesign) - case _ => - Some(designInst) - case m => Some(finalFixFunc(m)) + case designInst: DFDesignInst => Some(unifyInst(designInst)) + case m => Some(finalFixFunc(m)) } // Every non-top sub-design should behave as a Top in the immutable // DB. We don't change the block instance itself; instead we remap @@ -674,9 +679,11 @@ final class MutableDB(): }.toSet val finalRefTable = fixedRefTable.view.flatMap { case (ref, member) => if (redundantRefs.contains(ref)) None - else if (dupRefs.contains(ref)) Some(ref -> dupRefs(ref)) else if (designBlockOwnerRefs.contains(ref)) Some(ref -> DFMember.Empty) - else Some(ref -> finalFixFunc(member)) + else + member match + case inst: DFDesignInst => Some(ref -> unifyInst(inst)) + case _ => Some(ref -> finalFixFunc(member)) }.toMap (finalMembers, finalRefTable) val membersNoGlobalCtx = members.map { From acb685032c64a9b5470a917264f829725d8d3a58 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sun, 14 Jun 2026 21:06:03 +0300 Subject: [PATCH 40/72] Remove legacy DuplicateTag DuplicateTag was created during MutableDB elaboration but had no remaining readers since the UniqueDesigns sub-DB-sharing migration (its isDuplicate extension's last consumer, NVC, was already removed). Drop the tag, its type alias, and the now-dead isDuplicate extension. MutableDB keeps the dup->canonical mapping (dupToOrigDesignMap) that drives the real sharing/removal; only the (unread) tagging is gone. Update the ir-reference / new-stage skill docs that referenced it. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/commands/ir-reference.md | 6 ------ .claude/commands/new-stage.md | 4 +--- .../main/scala/dfhdl/compiler/ir/DFMember.scala | 1 - .../src/main/scala/dfhdl/compiler/ir/DFTags.scala | 2 -- core/src/main/scala/dfhdl/core/MutableDB.scala | 15 +++------------ 5 files changed, 4 insertions(+), 24 deletions(-) diff --git a/.claude/commands/ir-reference.md b/.claude/commands/ir-reference.md index a19412abc..d04c0d5d5 100644 --- a/.claude/commands/ir-reference.md +++ b/.claude/commands/ir-reference.md @@ -470,7 +470,6 @@ enum InstMode.BlackBox: NA, Files(path), Library(libName, nameSpace), VendorIP(v **Extension methods:** ```scala -design.isDuplicate // tagged DuplicateTag — has NO members in DB (ports/domains removed) design.isBlackBox // instMode is BlackBox design.isVendorIPBlackbox design.inSimulation // instMode is Simulation @@ -808,7 +807,6 @@ DFTags.empty **Built-in tags:** ```scala -case object DuplicateTag // duplicate design instance — NO members in DB case object IteratorTag // Dcl is a for-loop iterator variable case object IdentTag // Alias.AsIs is a pure identity (named alias of itself) case object BindTag // Alias is a pattern-match bind variable @@ -888,10 +886,6 @@ db.inSimulation // top has no ports (simulation context) db.inBuild // top has a device constraint tag ``` -**Design duplication properties:** - -Duplicate designs (tagged `DuplicateTag`) have **no members** in the DB — their ports, domain blocks, and other members are removed during immutable DB creation. - **Patching:** ```scala db.patch(patches: List[(DFMember, Patch)]): DB diff --git a/.claude/commands/new-stage.md b/.claude/commands/new-stage.md index 7e88fde16..a959d3437 100644 --- a/.claude/commands/new-stage.md +++ b/.claude/commands/new-stage.md @@ -1072,9 +1072,7 @@ abstract class StageSpec(stageCreatesUnrefAnons: Boolean = false) fails with "wrong number of argument patterns" because `DFForBlock` has multiple fields. Use a type pattern instead: `case x: DFLoop.DFForBlock`. The same applies to other multi-field IR case classes that have no dedicated single-argument unapply. -16. **Assuming duplicate designs have members** — designs tagged `DuplicateTag` have **no members** - in the DB (ports, domain blocks, and values are removed during immutable DB creation). -17. **Rewriting nested same-kind constructs in one pass** — if your stage rewrites a construct that +16. **Rewriting nested same-kind constructs in one pass** — if your stage rewrites a construct that can nest inside another of the same kind (nested `for` loops, steps-in-conditionals nested in steps, etc.) via a `Move` / `ReplaceWithLast(ChangeRefAndRemove)` plus a body-anchored satellite patch, doing the outer and inner in one patch list conflicts: the inner appears both in the diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala index a96db12f3..14f901e40 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala @@ -1630,7 +1630,6 @@ object DFDesignBlock: case VendorIP(vendor: Vendor, typeName: String) extension (dsn: DFDesignBlock) - def isDuplicate: Boolean = dsn.hasTagOf[DuplicateTag] def isBlackBox: Boolean = dsn.instMode.isInstanceOf[InstMode.BlackBox] def isVendorIPBlackbox: Boolean = dsn.instMode match case InstMode.BlackBox(_: InstMode.BlackBox.Source.VendorIP) => true diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFTags.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFTags.scala index 190866b85..0df1f1471 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFTags.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFTags.scala @@ -4,8 +4,6 @@ import upickle.default.* //TODO: check why enum is not working properly sealed trait DFTag derives ReadWriter -case object DuplicateTag extends DFTag -type DuplicateTag = DuplicateTag.type case object IteratorTag extends DFTag type IteratorTag = IteratorTag.type case object IdentTag extends DFTag diff --git a/core/src/main/scala/dfhdl/core/MutableDB.scala b/core/src/main/scala/dfhdl/core/MutableDB.scala index e37a2ebfc..0c7a5fb0b 100644 --- a/core/src/main/scala/dfhdl/core/MutableDB.scala +++ b/core/src/main/scala/dfhdl/core/MutableDB.scala @@ -3,7 +3,6 @@ import dfhdl.internals.* import dfhdl.hw import dfhdl.compiler.ir.{ DB, - DuplicateTag, DFDesignInst, DFDesignInstOld, DFDesignBlock, @@ -600,17 +599,9 @@ final class MutableDB(): var first = true val orig = group.head group.view.map(design => - val tags = - if (first) - first = false - design.tags - else - dupToOrigDesignMap += design -> orig - design.tags.tag(DuplicateTag) - design -> design.copy( - meta = design.meta.copy(nameOpt = Some(updatedDclName)), - tags = tags - ) + if (first) first = false + else dupToOrigDesignMap += design -> orig + design -> design.copy(meta = design.meta.copy(nameOpt = Some(updatedDclName))) ) case _ => Nil } From 71b739050673f52ad4056a155b3367688549f05f Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sun, 14 Jun 2026 23:31:04 +0300 Subject: [PATCH 41/72] Run the stage pipeline natively on the hierarchical DB Make the hierarchical (root + sub-DBs) form canonical from the source and thread it through the whole stage pipeline with no per-stage round-trips: - getDB applies oldToNew (root); add getDBOld (raw flat immutable) for the meta-design/patch path which needs a flat member container (Patch.Add, ViaConnection) -- a root DB has empty members. - StageRunner stays clean; GlobalStage/HierarchyStage drop their internal oldToNew/newToOld and operate on the root threaded in. - DB ReadWriter recursively (de)serializes subDBs so a root DB round-trips losslessly through the disk cache. - OrderMembers no longer inlines child design blocks into the parent sub-DB (a flat-list-ordering artifact); UniqueDesigns keeps a renamed-canonical block and its refTable consistent (members resolve their owner to the renamed design). Two temporary measures, both flagged TODO, cover paths not yet hardened for a root DB: flatten only at the terminal printer (PrintCodeString + backends), and a single newToOld->oldToNew re-normalization in GlobalizePortVectorParams that restores the global vector TypeRefs the vec-type FullReplacements purge. StagesSpec 437/437, lib 152/152, core 74/74. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 26 +++++++----- .../src/main/scala/dfhdl/backends.scala | 12 ++++-- .../stages/GlobalizePortVectorParams.scala | 10 +++++ .../dfhdl/compiler/stages/OrderMembers.scala | 16 ++++---- .../compiler/stages/PrintCodeString.scala | 10 ++++- .../scala/dfhdl/compiler/stages/Stage.scala | 40 +++++++------------ .../dfhdl/compiler/stages/UniqueDesigns.scala | 26 +++++++++--- .../dfhdl/compiler/stages/ViaConnection.scala | 5 ++- .../StagesSpec/HierarchicalPrintSpec.scala | 6 +-- .../scala/dfhdl/compiler/patching/Patch.scala | 7 +++- core/src/main/scala/dfhdl/core/Design.scala | 11 ++++- 11 files changed, 108 insertions(+), 61 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index 36f4f9648..3145c8d9c 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -1888,18 +1888,26 @@ object DB: db.rootDB db - // Custom ReadWriter for DB: excludes `subDBs` from serialization. - // Phase 1 never persists a populated `subDBs` (it's only used - // transiently inside `sanityCheck`), so the round-trip over JSON stays - // lossless. When Phase 5 introduces disk-cached sub-DBs this will be - // replaced with a full recursive ReadWriter. + // Custom ReadWriter for DB: recursively serializes `subDBs` so a hierarchical + // (root) DB round-trips losslessly through JSON. The disk cache now persists + // root DBs because the pipeline runs natively on the hierarchical form. Sub-DBs + // and old-style flat DBs carry an empty `subDBs`, so the recursion terminates + // immediately (one level: root -> its sub-DBs). `subDBs` is serialized as an + // ordered list of (key, sub-DB) pairs to preserve the elaboration-order ListMap. private type DBSerialized = - (List[DFMember], Map[DFRefAny, DFMember], DFTags, List[SourceFile]) + ( + List[DFMember], + Map[DFRefAny, DFMember], + DFTags, + List[SourceFile], + List[(DFOwner.Ref, DB)] + ) given ReadWriter[DB] = readwriter[DBSerialized].bimap[DB]( - db => (db.members, db.refTable, db.globalTags, db.srcFiles), - { case (members, refTable, globalTags, srcFiles) => - DB(members, refTable, globalTags, srcFiles) + db => (db.members, db.refTable, db.globalTags, db.srcFiles, db.subDBs.toList), + { case (members, refTable, globalTags, srcFiles, subDBs) => + if (subDBs.isEmpty) DB(members, refTable, globalTags, srcFiles) + else DB(members, refTable, globalTags, srcFiles, ListMap.from(subDBs)) } ) extension (db: DB) diff --git a/compiler/stages/src/main/scala/dfhdl/backends.scala b/compiler/stages/src/main/scala/dfhdl/backends.scala index 4ac7c7564..4d4dc5f66 100644 --- a/compiler/stages/src/main/scala/dfhdl/backends.scala +++ b/compiler/stages/src/main/scala/dfhdl/backends.scala @@ -7,18 +7,22 @@ import dfhdl.compiler.stages.{BackendCompiler, CompiledDesign, StagedDesign, Sta import dfhdl.compiler.stages.verilog.{VerilogBackend, VerilogPrinter, VerilogDialect} import dfhdl.compiler.stages.vhdl.{VHDLBackend, VHDLPrinter, VHDLDialect} import dfhdl.compiler.ir.DB +// NOTE: the stage pipeline runs natively on the hierarchical (root) DB, but the +// printers are not yet hardened for it. `.newToOld` flattens the pipeline output +// right before constructing the printer (a no-op on an already-flat DB). +// TODO: temporary — drop the `.newToOld` once the printers render from root. object backends: sealed class verilog(val dialect: VerilogDialect) extends BackendCompiler: def printer( cd: CompiledDesign )(using CompilerOptions, PrinterOptions): Printer = val compiledDB = cd.stagedDB - new VerilogPrinter(dialect)(using compiledDB.getSet) + new VerilogPrinter(dialect)(using compiledDB.newToOld.getSet) def printer( designDB: DB )(using CompilerOptions, PrinterOptions): Printer = val compiledDB = StageRunner.run(VerilogBackend)(designDB) - new VerilogPrinter(dialect)(using compiledDB.getSet) + new VerilogPrinter(dialect)(using compiledDB.newToOld.getSet) override def toString(): String = s"verilog.$dialect" object verilog extends verilog(VerilogDialect.sv2009): val v95: verilog = new verilog(VerilogDialect.v95) @@ -33,12 +37,12 @@ object backends: cd: CompiledDesign )(using CompilerOptions, PrinterOptions): Printer = val compiledDB = cd.stagedDB - new VHDLPrinter(dialect)(using compiledDB.getSet) + new VHDLPrinter(dialect)(using compiledDB.newToOld.getSet) def printer( designDB: DB )(using CompilerOptions, PrinterOptions): Printer = val compiledDB = StageRunner.run(VHDLBackend)(designDB) - new VHDLPrinter(dialect)(using compiledDB.getSet) + new VHDLPrinter(dialect)(using compiledDB.newToOld.getSet) override def toString(): String = s"vhdl.$dialect" object vhdl extends vhdl(VHDLDialect.v2008): val v93: vhdl = new vhdl(VHDLDialect.v93) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/GlobalizePortVectorParams.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/GlobalizePortVectorParams.scala index 603c4d79c..9e88a26de 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/GlobalizePortVectorParams.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/GlobalizePortVectorParams.scala @@ -88,6 +88,16 @@ case object GlobalizePortVectorParams extends HierarchyStage: case _ => false case _ => false + // The vec-type `FullReplacement`s purge this stage's globalized port-vector + // `TypeRef`s from the sub-DB refTables (and the purge recurs in the downstream + // dfType-rewrite of DFHDLUniqueNames). Re-normalize the output via + // `newToOld -> oldToNew` — the closure/refTable repair the per-stage round-trip + // used to perform — so the globally-shared `TypeRef` bindings are restored. + // TODO: temporary — replace once the patch system maintains global-targeting + // `TypeRef`s across `FullReplacement` without re-normalization. + override def transform(designDB: DB)(using MemberGetSet, CompilerOptions): DB = + super.transform(designDB).newToOld.oldToNew + // Per-rootDB shared state, populated by the TOP sub-DB's `transformSubDB` // (the first DFS iteration). Subsequent sub-DBs read it. private case class GlobState( diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/OrderMembers.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/OrderMembers.scala index 2eba5e1ca..f62d380f1 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/OrderMembers.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/OrderMembers.scala @@ -12,21 +12,19 @@ private abstract class OrderMembers(order: OrderMembers.Order) extends Hierarchy def nullifies: Set[Stage] = Set() @tailrec private def orderMembers( remaining: List[DFMember], - retList: List[DFMember], - expandedInsts: Set[DFDesignInst] = Set.empty + retList: List[DFMember] )(using MemberGetSet): List[DFMember] = remaining match - // The referenced DFDesignBlock no longer appears in the parent owner's - // table, so inline it (and its children) right before the inst to keep - // the flat-list ordering [designBlock, ...children..., inst]. - case (inst: DFDesignInst) :: mList if !expandedInsts(inst) => - orderMembers(inst.getDesignBlock :: inst :: mList, retList, expandedInsts + inst) + // A DFDesignInst is a leaf in its parent sub-DB: the instantiated child + // DFDesignBlock lives exclusively in its own sub-DB and must NOT be inlined + // into the parent's member list. (`newToOld` reconstructs the flat-list + // ordering [designBlock, ...children..., inst] at the pipeline boundary.) case (block: DFOwnerNamed) :: mList => val members = getSet.designDB.namedOwnerMemberTable.getOrElse(block, Nil) val sortedMembers = block match case _: DFDesignBlock => members.sortBy(order()) case _ => members - orderMembers(sortedMembers ++ mList, block :: retList, expandedInsts) - case m :: mList => orderMembers(mList, m :: retList, expandedInsts) + orderMembers(sortedMembers ++ mList, block :: retList) + case m :: mList => orderMembers(mList, m :: retList) case Nil => retList.reverse def transformSubDB(rootDB: DB)(using MemberGetSet, CompilerOptions, RefGen): DB = diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/PrintCodeString.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/PrintCodeString.scala index d83f323bc..c88b4deee 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/PrintCodeString.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/PrintCodeString.scala @@ -15,14 +15,20 @@ extension [T: HasDB](t: T) given PrinterOptions.Align = align val designDB = StageRunner.run(PrintCodeString)(t.db) - val printer = new DFPrinter(using designDB.getSet) + // flatten the hierarchical pipeline output for the (flat-only) printer. + // TODO: temporary — remove once the backend/DFHDL printers are hardened for + // the hierarchical root DB; until then the printer always renders from flat. + val printer = new DFPrinter(using designDB.newToOld.getSet) printer.csDB def getCodeString(using CompilerOptions): String = getCodeString(align = false) def getCompiledCodeString(using po: PrinterOptions, co: CompilerOptions): String = co.backend.printer(t.db).csDB def printCodeString(using po: PrinterOptions, co: CompilerOptions): T = val designDB = StageRunner.run(PrintCodeString)(t.db) - val printer = new DFPrinter(using designDB.getSet) + // flatten the hierarchical pipeline output for the (flat-only) printer. + // TODO: temporary — remove once the backend/DFHDL printers are hardened for + // the hierarchical root DB; until then the printer always renders from flat. + val printer = new DFPrinter(using designDB.newToOld.getSet) println(printer.csDB) t end extension diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/Stage.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/Stage.scala index 5e85c7b0c..fbd74a1eb 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/Stage.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/Stage.scala @@ -13,15 +13,10 @@ trait Stage extends Product, Serializable, HasTypeName derives CanEqual: def transform(designDB: DB)(using MemberGetSet, CompilerOptions): DB /** Phase-2 bridge for stages that need cross-design information or whose work cannot be decomposed - * cleanly per-sub-DB. The stage implements `transformGlobal(newDB)` and operates on the NEW-STYLE - * hierarchical DB. - * - * The trait handles: - * - `oldToNew` at entry — converts a legacy flat DB into the hierarchical representation (root + - * per-design sub-DBs) so the body can walk the hierarchy via `subDBs` and patch each sub-DB - * independently. - * - `newToOld` at exit — flattens the result back into an old-style DB for the rest of the - * pipeline. + * cleanly per-sub-DB. The stage implements `transformGlobal(designDB)` and operates on the + * hierarchical DB (root + per-design sub-DBs), which is the native representation threaded through + * the whole pipeline by `StageRunner` (it does the single `oldToNew`/`newToOld` at the pipeline + * boundary, so individual stages neither convert at entry nor flatten at exit). * * Use `GlobalStage` when the body needs: * - cross-design tracking state (e.g. shared opaque type maps) @@ -40,27 +35,23 @@ trait GlobalStage extends Stage: outerGetSet: MemberGetSet, co: CompilerOptions ): DB = - // Seed RefGen from the flat old-style DB, whose refTable still has every - // ref. Under B-pure, the new-style root has empty refTable so - // `RefGen.fromGetSet(newDB.getSet)` would crash. The body must dispatch - // any ref resolution through sub-DB getSets explicitly — root's getSet - // is non-functional. + // `designDB` is the hierarchical root. Seed RefGen from it (root-aware: + // `RefGen.fromGetSet` aggregates across sub-DBs) — the root's own getSet is + // non-functional, so the body must dispatch any ref resolution through + // sub-DB getSets explicitly. val refGen = RefGen.fromGetSet(using outerGetSet) - val newDB = designDB.oldToNew - val transformed = transformGlobal(newDB)(using co, refGen) - transformed.newToOld + transformGlobal(designDB)(using co, refGen) end GlobalStage /** Phase-2 bridge for stages whose work decomposes cleanly per-sub-DB. * * The stage implements `transformSubDB(subDB)` which returns the TRANSFORMED sub-DB (typically via - * `subDB.patch(patches)`). The trait handles: - * - flat old-style → B-pure new-style conversion at entry (`oldToNew`) + * `subDB.patch(patches)`). `designDB` is the hierarchical root threaded through the pipeline by + * `StageRunner` (which does the single `oldToNew`/`newToOld` at the boundary). The trait handles: * - per-DB dispatch of `transformSubDB` on every sub-DB in the hierarchy. The root DB is a pure * hierarchy container (empty members, empty refTable) and is NOT passed to `transformSubDB`; * all design content lives in `subDBs`. - * - reassembly via `.copy(subDBs = ...).newToOld` to flatten back into an old-style DB for the - * rest of the pipeline + * - reassembly via `.update(subDBs = ...)` of the patched sub-DBs back into the root. * * If every `transformSubDB` returned its input by reference (no change), the original `designDB` * is returned by reference too. This lets iterative stages (e.g. `BreakOps`, @@ -77,17 +68,16 @@ trait HierarchyStage extends Stage: def transform(designDB: DB)(using getSet: MemberGetSet, co: CompilerOptions): DB = import scala.collection.immutable.ListMap given refGen: RefGen = RefGen.fromGetSet - val newDB = designDB.oldToNew var changed = false def run(subDB: DB): DB = // `transformSubDB` always sees the root DB and the current sub-DB's getSet. - val result = transformSubDB(newDB)(using subDB.getSet, co, refGen) + val result = transformSubDB(designDB)(using subDB.getSet, co, refGen) if (!(result eq subDB)) changed = true result val transformedSubs: ListMap[DFOwner.Ref, DB] = - newDB.subDBs.map { case (k, subDB) => k -> run(subDB) } + designDB.subDBs.map { case (k, subDB) => k -> run(subDB) } if (!changed) designDB - else newDB.update(subDBs = transformedSubs).newToOld + else designDB.update(subDBs = transformedSubs) end transform end HierarchyStage diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueDesigns.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueDesigns.scala index 89d4c46b3..d59920342 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueDesigns.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueDesigns.scala @@ -64,6 +64,16 @@ case object UniqueDesigns extends GlobalStage: // `designRef` IS that key under unification). val dupKeyToCanonicalKey: Map[DFOwner.Ref, DFOwner.Ref] = dupToCanonical.iterator.map((dup, canon) => dup.ownerRef -> canon.ownerRef).toMap + // canonical design block -> its renamed copy. Built ONCE and reused so the + // member-list head and EVERY refTable entry resolving to it stay the SAME + // object. Otherwise a member's `ownerRef` would resolve (via refTable) to the + // pre-rename block while the member list shows the renamed one — an + // inconsistency `newToOld.canonicalize` used to mask but which now breaks + // hierarchy navigation (e.g. OMLGen) since the root is threaded directly. + val canonicalReplace: Map[DFDesignBlock, DFDesignBlock] = + canonicalRenames.iterator.map { (canon, newName) => + canon -> canon.copy(meta = canon.meta.copy(nameOpt = Some(newName))) + }.toMap val newSubDBs: ListMap[DFOwner.Ref, DB] = ListMap.from( designDB.subDBs.iterator.flatMap { (key, sub) => // drop the redundant duplicate sub-DBs entirely @@ -72,8 +82,7 @@ case object UniqueDesigns extends GlobalStage: val instReplace = collection.mutable.Map.empty[DFDesignInst, DFDesignInst] val newMembers = sub.members.map { // rename the sub-DB's top if it is a renamed canonical design - case d: DFDesignBlock if canonicalRenames.contains(d) => - d.copy(meta = d.meta.copy(nameOpt = Some(canonicalRenames(d)))) + case d: DFDesignBlock if canonicalReplace.contains(d) => canonicalReplace(d) // retarget a parent inst that targeted a duplicate onto the canonical // key (its `designRef` IS that key under unification) case inst: DFDesignInst if dupKeyToCanonicalKey.contains(inst.designRef) => @@ -85,13 +94,18 @@ case object UniqueDesigns extends GlobalStage: case m => m } // keep refTable values consistent: point any ref that targeted a - // retargeted inst at its replacement, and any remaining duplicate design - // block at its canonical (designRef itself no longer lives in refTable). + // retargeted inst at its replacement, a renamed canonical at its renamed + // block (so members resolve their owner to the renamed design), and any + // remaining duplicate design block at its canonical (renamed if so). + // `designRef` itself no longer lives in refTable. val newRefTable = sub.refTable.view.mapValues { case inst: DFDesignInst if instReplace.contains(inst) => instReplace(inst) - case d: DFDesignBlock if dupToCanonical.contains(d) => dupToCanonical(d) - case t => t + case d: DFDesignBlock if canonicalReplace.contains(d) => canonicalReplace(d) + case d: DFDesignBlock if dupToCanonical.contains(d) => + val canon = dupToCanonical(d) + canonicalReplace.getOrElse(canon, canon) + case t => t }.toMap Some(key -> sub.update(members = newMembers, refTable = newRefTable)) } diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ViaConnection.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ViaConnection.scala index 4aeebfd57..8aa1901b6 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ViaConnection.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ViaConnection.scala @@ -60,6 +60,7 @@ case object ViaConnection extends HierarchyStage: // bridged through a synthesized variable case _ => // do nothing end match + end if } // Meta design to construct the variables to be connected to the ports @@ -77,7 +78,9 @@ case object ViaConnection extends HierarchyStage: // any synthesized PBNS that targets the inst sits AFTER it in the // flat member list — preserves the order check invariant. val connectDsn = new MetaDesign(ib, Patch.Add.Config.After): - dfc.mutableDB.injectMetaGetSet(addVarsDsn.getDB.getSet) + // the other meta-design's flat member container (its getSet resolves + // the added vars); `getDB` is the hierarchical staged form, unusable here. + dfc.mutableDB.injectMetaGetSet(addVarsDsn.getDBOld.getSet) dfc.enterLate() val refPatches: List[(DFMember, Patch)] = addVarsDsn.pbnssToVars.flatMap { case (pbnss @ (pbns :: _), v) => diff --git a/compiler/stages/src/test/scala/StagesSpec/HierarchicalPrintSpec.scala b/compiler/stages/src/test/scala/StagesSpec/HierarchicalPrintSpec.scala index e7df3a189..3fed0afd9 100644 --- a/compiler/stages/src/test/scala/StagesSpec/HierarchicalPrintSpec.scala +++ b/compiler/stages/src/test/scala/StagesSpec/HierarchicalPrintSpec.scala @@ -11,9 +11,9 @@ import dfhdl.compiler.printing.DefaultPrinter // the printer a flat DB, so without this the root path would be unexercised. class HierarchicalPrintSpec extends StageSpec: private def assertSamePrintFlatVsHier(dsn: core.Design): Unit = - val db = dsn.getDB - val flat = DefaultPrinter(using db.getSet).csDB - val hier = DefaultPrinter(using db.oldToNew.getSet).csDB + val db = dsn.getDB // hierarchical (root): getDB applies oldToNew + val flat = DefaultPrinter(using db.newToOld.getSet).csDB + val hier = DefaultPrinter(using db.getSet).csDB assertNoDiff(hier, flat) test("nested hierarchy: flat and hierarchical printing match") { diff --git a/core/src/main/scala/dfhdl/compiler/patching/Patch.scala b/core/src/main/scala/dfhdl/compiler/patching/Patch.scala index 414abcbe5..53a14d332 100644 --- a/core/src/main/scala/dfhdl/compiler/patching/Patch.scala +++ b/core/src/main/scala/dfhdl/compiler/patching/Patch.scala @@ -61,7 +61,12 @@ object Patch: override def toString(): String = s"""\nAdd $config, Members: ${db.members.mkString("\n ", ",\n ", "")}""" object Add: - def apply(design: MetaDesignAny, config: Config): Add = Add(design.getDB, config) + // A meta-design's DB is a flat container of the freshly-created members to + // inject (it has no design hierarchy), so use `getDBOld` (the raw flat + // immutable) — NOT `getDB`, which is the hierarchical staged form whose root + // has empty `members`. + def apply(design: MetaDesignAny, config: Config): Add = + Add(design.getDBOld, config) sealed trait Config extends Product with Serializable derives CanEqual: def ==(moveConfig: Move.Config): Boolean = (this, moveConfig) match case (Config.Before, Move.Config.Before) => true diff --git a/core/src/main/scala/dfhdl/core/Design.scala b/core/src/main/scala/dfhdl/core/Design.scala index 1177b87b4..a1e31914c 100644 --- a/core/src/main/scala/dfhdl/core/Design.scala +++ b/core/src/main/scala/dfhdl/core/Design.scala @@ -221,7 +221,16 @@ object Design: end apply end Inst extension [D <: Design](dsn: D) - def getDB: ir.DB = dsn.dfc.mutableDB.immutable + // The compiled design DB is hierarchical (root + per-design sub-DBs): the + // stage pipeline runs natively on this form. `oldToNew` is applied ONCE here + // at the source so the staged DB is hierarchical end-to-end (no per-stage + // round-trips). + def getDB: ir.DB = dsn.dfc.mutableDB.immutable.oldToNew + // The raw FLAT immutable DB (the pre-`oldToNew` form). Needed where a design's + // members are consumed as a flat container without the hierarchy — e.g. a + // meta-design in the patch system, whose DB is just the freshly-created + // members to inject (root would have empty `members`). + def getDBOld: ir.DB = dsn.dfc.mutableDB.immutable infix def tag[CT <: ir.DFTag: ClassTag](customTag: CT)(using dfc: DFC): D = import dfc.getSet dsn.containedOwner.asIR From f30f9e592c2776f0221de0402b3fb10e516f1976 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Mon, 15 Jun 2026 00:32:38 +0300 Subject: [PATCH 42/72] Replace GPVP re-normalization with a lightweight global-closure repair GlobalizePortVectorParams mints the globalized port-vector params in the top's MetaDesign, so descendant sub-DBs never get those globals in their closure and the top's globalized TypeRef->global bindings are purged by the vec-type FullReplacements. The previous fix re-normalized via newToOld.oldToNew -- a full flatten + rebuild of the whole hierarchy. Add DB.repairGlobalClosures: in place, per sub-DB, recompute the global closure (oldToNew's globalsClosure) and rebuild the refTable from the members' refs (oldToNew's refsFor), preferring each sub-DB's own binding and falling back to a shared pool. This drops orphan entries and fills the missing global bindings without flattening or rebuilding unchanged sub-DBs (carried over by reference) -- far lighter than the round-trip. StagesSpec 437/437, lib 152/152, core 74/74. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 64 +++++++++++++++++++ .../stages/GlobalizePortVectorParams.scala | 15 ++--- 2 files changed, 71 insertions(+), 8 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index 3145c8d9c..592a8c853 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -1809,6 +1809,70 @@ final case class DB private ( ) end newToOld + // Lightweight repair of the per-sub-DB global closure: adds any global member + // (and its missing ref bindings) that a sub-DB's members reference but that is + // absent from that sub-DB — the `globalsClosure`/`refsFor` work `oldToNew` + // does, but applied IN PLACE to the existing hierarchy with no flatten and no + // full rebuild. A stage that mints globals shared across sub-DBs (e.g. + // GlobalizePortVectorParams creates the globalized port-vector params in the + // top's MetaDesign, and the vec-type `FullReplacement`s purge their `TypeRef` + // bindings) leaves exactly these gaps; this restores them far more cheaply + // than `newToOld.oldToNew`. Only meaningful on a root DB; returns `this` + // unchanged (by reference) when nothing is missing. Sub-DBs needing no repair + // are likewise carried over by reference. + def repairGlobalClosures: DB = + if (subDBs.isEmpty) return this + // Merged ref pool: a member's ref whose binding is missing from its own + // sub-DB is recovered from whichever sub-DB does carry it (globals and their + // bindings are shared across sub-DBs by identity). First-wins is correct for + // the global/Empty targets we redistribute (their target is consistent). + val pool = mutable.Map.empty[DFRefAny, DFMember] + subDBs.valuesIterator.foreach(_.refTable.foreach((r, t) => pool.getOrElseUpdate(r, t))) + // Global members + a deterministic (first-occurrence) order across sub-DBs; + // `topDB` carries them first in canonical order, so this is topological (a + // global never references a later global). + val globalOrder = mutable.LinkedHashSet.empty[DFMember] + subDBs.valuesIterator.foreach { sub => + sub.atGetSet { + sub.members.foreach { + case g: DFVal.CanBeGlobal if g.isGlobal => globalOrder += g + case _ => + } + } + } + val globalSet: Set[DFMember] = globalOrder.toSet + var anyChanged = false + val updatedSubDBs = subDBs.map { (k, sub) => + // Resolve a ref preferring the sub-DB's own binding (the correct local + // target), falling back to the shared pool for a binding the sub-DB is + // missing. + def resolve(r: DFRefAny): Option[DFMember] = sub.refTable.get(r).orElse(pool.get(r)) + // Global closure: globals transitively reachable from the sub-DB's + // non-global members' refs (mirrors `oldToNew`'s `globalsClosure`). + val reachable = mutable.HashSet.empty[DFMember] + def pull(t: DFMember): Unit = t match + case g: DFVal.CanBeGlobal if globalSet.contains(g) && reachable.add(g) => + g.getRefs.foreach(r => resolve(r).foreach(pull)) + case _ => + val nonGlobals = sub.members.filterNot(globalSet.contains) + nonGlobals.foreach(_.getRefs.foreach(r => resolve(r).foreach(pull))) + // globals first, in canonical order, then the rest — `closure ::: d :: locals`. + val newMembers = globalOrder.iterator.filter(reachable.contains).toList ::: nonGlobals + // Rebuild the refTable from the new members' refs (drops orphan entries, + // fills the missing global bindings) — mirrors `oldToNew`'s `refsFor`. + val newRefTable = mutable.Map.empty[DFRefAny, DFMember] + newMembers.foreach { m => + (m.ownerRef :: m.getRefs).foreach(r => resolve(r).foreach(t => newRefTable(r) = t)) + } + val rebuilt = newRefTable.toMap + if (newMembers == sub.members && rebuilt == sub.refTable) k -> sub + else + anyChanged = true + k -> sub.update(members = newMembers, refTable = rebuilt) + } + if (anyChanged) update(subDBs = updatedSubDBs) else this + end repairGlobalClosures + // Normalizes an old-style flat DB so: // 1. globals appear BEFORE `top` in `members` (their mutual order // preserved). diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/GlobalizePortVectorParams.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/GlobalizePortVectorParams.scala index 9e88a26de..a7cbceddc 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/GlobalizePortVectorParams.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/GlobalizePortVectorParams.scala @@ -88,15 +88,14 @@ case object GlobalizePortVectorParams extends HierarchyStage: case _ => false case _ => false - // The vec-type `FullReplacement`s purge this stage's globalized port-vector - // `TypeRef`s from the sub-DB refTables (and the purge recurs in the downstream - // dfType-rewrite of DFHDLUniqueNames). Re-normalize the output via - // `newToOld -> oldToNew` — the closure/refTable repair the per-stage round-trip - // used to perform — so the globally-shared `TypeRef` bindings are restored. - // TODO: temporary — replace once the patch system maintains global-targeting - // `TypeRef`s across `FullReplacement` without re-normalization. + // This stage mints the globalized port-vector params in the top's MetaDesign, + // so the descendant sub-DBs that reference them never receive the globals in + // their closure, and the vec-type `FullReplacement`s purge the top's + // globalized `TypeRef`->global bindings from its refTable. `repairGlobalClosures` + // restores both gaps in place (far cheaper than a full `newToOld.oldToNew` + // re-normalization). override def transform(designDB: DB)(using MemberGetSet, CompilerOptions): DB = - super.transform(designDB).newToOld.oldToNew + super.transform(designDB).repairGlobalClosures // Per-rootDB shared state, populated by the TOP sub-DB's `transformSubDB` // (the first DFS iteration). Subsequent sub-DBs read it. From 1f4b8907fb8d9d9b876edce5aa947e585a40fa79 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Mon, 15 Jun 2026 00:43:47 +0300 Subject: [PATCH 43/72] Drop the DFDesignInst.getAllRefs override; use getAllRefs in the closure repair `designRef` is unified with the target block's `ownerRef` (the sub-DB key) and is resolved structurally via `subDBs`, never through a refTable -- so it must not appear where refs are enumerated for refTable building/rewiring. Drop the `DFDesignInst.getAllRefs` override (which added `designRef`); the sole consumer, ReduplicateDesign, never used that entry (designRef isn't a refTable key, and the cloned inst's designRef is rebound explicitly). `repairGlobalClosures` now uses the standard `getAllRefs` instead of `ownerRef :: getRefs`. StagesSpec 437/437, lib 152/152, core 74/74. Co-Authored-By: Claude Opus 4.8 (1M context) --- compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala | 2 +- compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index 592a8c853..4bd09e2a0 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -1862,7 +1862,7 @@ final case class DB private ( // fills the missing global bindings) — mirrors `oldToNew`'s `refsFor`. val newRefTable = mutable.Map.empty[DFRefAny, DFMember] newMembers.foreach { m => - (m.ownerRef :: m.getRefs).foreach(r => resolve(r).foreach(t => newRefTable(r) = t)) + m.getAllRefs.foreach(r => resolve(r).foreach(t => newRefTable(r) = t)) } val rebuilt = newRefTable.toMap if (newMembers == sub.members && rebuilt == sub.refTable) k -> sub diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala index 14f901e40..3079942f1 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala @@ -1703,7 +1703,11 @@ final case class DFDesignInst( protected def setTags(tags: DFTags): this.type = copy(tags = tags).asInstanceOf[this.type] lazy val getRefs: List[DFRef.TwoWayAny] = paramMap.values.toList ++ meta.getRefs - override def getAllRefs: List[DFRefAny] = ownerRef :: designRef :: getRefs + // NOTE: `designRef` is deliberately NOT part of `getAllRefs` (the default + // `ownerRef :: getRefs` is used): it is unified with the target block's + // `ownerRef` (the sub-DB key) and is resolved structurally via `subDBs`, never + // through a refTable, so it must not appear where refs are enumerated for + // refTable building/rewiring. // NOTE: `designRef` is intentionally NOT freshened here. It is unified with the // target block's `ownerRef` (the sub-DB key); when cloning a design sub-tree the // caller rebinds it to the cloned block's ownerRef (see ReduplicateDesign). From 5273a8755fcb8c811340c401bff12a9e9eb09f52 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Mon, 15 Jun 2026 10:49:19 +0300 Subject: [PATCH 44/72] make flat<->hierarchy DB transformations lazy and turn off infinite loop warning for "recursive" json --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 467 +++++++++--------- 1 file changed, 235 insertions(+), 232 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index 4bd09e2a0..3d8d27800 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -1552,151 +1552,152 @@ final case class DB private ( // - Round-trip note: with all globals partitioned per sub-DB by closure, // `newToOld` no longer guarantees global ordering matches the input. // The round-trip check in SanityCheck compares globals as a set. - def oldToNew: DB = - if (subDBs.nonEmpty) return this - given MemberGetSet = self.getSet - val topDsn = this.top - // designOwn(d) = d's own (non-global, non-self, non-nested-block) members - // in original order. Nested DFDesignBlocks are NOT included here — they - // become the `designBlock` of their own sub-DB and are reachable from the - // parent only through their DFDesignInst entries. - val designOwn = mutable.LinkedHashMap.empty[DFDesignBlock, mutable.ListBuffer[DFMember]] - designOwn(topDsn) = mutable.ListBuffer.empty - members.foreach { - case d: DFDesignBlock => designOwn.getOrElseUpdate(d, mutable.ListBuffer.empty) - case _ => - } - // Non-top DFDesignBlocks no longer carry their parent in `ownerRef` — it - // resolves to DFMember.Empty under the new convention. Recover the parent - // design via the FIRST DFDesignInst (in elaboration order) whose - // `designRef` targets the block. That parent is the canonical owner of - // the child's sub-DB in the design tree; any other parents that also - // instantiate the same block reach it only through their own - // DFDesignInst, not via a `directChildren` claim. - val designBlockParent = mutable.LinkedHashMap.empty[DFDesignBlock, DFDesignBlock] - members.foreach { - case inst: DFDesignInst => - designBlockParent.getOrElseUpdate(inst.getDesignBlock, inst.getOwnerDesign) - case _ => - } - members.foreach { - case _: DFDesignBlock => // nested blocks live in their own sub-DB only - case dfVal: DFVal.CanBeGlobal if dfVal.isGlobal => // globals handled separately - case m => designOwn(m.getOwnerDesign) += m - } - // parent → ordered list of canonical child DFDesignBlocks. Iteration of - // `designBlockParent` (a LinkedHashMap) preserves first-inst-encounter - // order, which matches the elaboration order of the children. - val parentToChildren = - mutable.LinkedHashMap.empty[DFDesignBlock, mutable.ListBuffer[DFDesignBlock]] - designBlockParent.foreach { (child, parent) => - parentToChildren.getOrElseUpdate(parent, mutable.ListBuffer.empty) += child - } - // All globals in their original elaboration order — used to project each - // sub-DB's closure back into a deterministic, topological, source-faithful - // order (elaboration order is itself topological since a global cannot - // reference a later-defined global). - val allGlobalsOrdered: List[DFMember] = members.collect { - case g: DFVal.CanBeGlobal if g.isGlobal => g - } - // Compute the closure of globals transitively reachable from a DB's refs. - // Walks local members' refs; when a ref target is a global, we include it - // and recurse through its own refs to pick up globals-referenced-by-globals. - // Non-global intermediaries are NOT included (they belong to their own - // design's locals), but their refs ARE walked because we iterate all of - // the design's locals directly. Returns reachable globals in original - // elaboration order — required because `newToOld` emits a sub-DB's - // members directly into the flat output, and both `SanityCheck.orderCheck` - // and code generation depend on stable, topological ordering. - def globalsClosure(localMembers: Iterable[DFMember]): List[DFMember] = - val reachable = mutable.Set.empty[DFMember] - def pull(target: DFMember): Unit = target match - case g: DFVal.CanBeGlobal if g.isGlobal && !reachable.contains(g) => - reachable += g - g.getRefs.foreach(r => refTable.get(r).foreach(pull)) + lazy val oldToNew: DB = + if (subDBs.nonEmpty) this + else + given MemberGetSet = self.getSet + val topDsn = this.top + // designOwn(d) = d's own (non-global, non-self, non-nested-block) members + // in original order. Nested DFDesignBlocks are NOT included here — they + // become the `designBlock` of their own sub-DB and are reachable from the + // parent only through their DFDesignInst entries. + val designOwn = mutable.LinkedHashMap.empty[DFDesignBlock, mutable.ListBuffer[DFMember]] + designOwn(topDsn) = mutable.ListBuffer.empty + members.foreach { + case d: DFDesignBlock => designOwn.getOrElseUpdate(d, mutable.ListBuffer.empty) + case _ => + } + // Non-top DFDesignBlocks no longer carry their parent in `ownerRef` — it + // resolves to DFMember.Empty under the new convention. Recover the parent + // design via the FIRST DFDesignInst (in elaboration order) whose + // `designRef` targets the block. That parent is the canonical owner of + // the child's sub-DB in the design tree; any other parents that also + // instantiate the same block reach it only through their own + // DFDesignInst, not via a `directChildren` claim. + val designBlockParent = mutable.LinkedHashMap.empty[DFDesignBlock, DFDesignBlock] + members.foreach { + case inst: DFDesignInst => + designBlockParent.getOrElseUpdate(inst.getDesignBlock, inst.getOwnerDesign) case _ => - localMembers.foreach { m => - m.getRefs.foreach(r => refTable.get(r).foreach(pull)) } - allGlobalsOrdered.filter(reachable.contains) - // Build the refTable partition for a DB: every ref emitted (via ownerRef - // or getRefs) by any of the DB's members, resolved against the original - // flat refTable. - def refsFor(dbMembers: Iterable[DFMember]): Map[DFRefAny, DFMember] = - val result = mutable.Map.empty[DFRefAny, DFMember] - dbMembers.foreach { m => - refTable.get(m.ownerRef).foreach(t => result(m.ownerRef) = t) - m.getRefs.foreach(r => refTable.get(r).foreach(t => result(r) = t)) - // NOTE: `DFDesignInst.designRef` is deliberately NOT added here. It is - // unified with the child block's `ownerRef` (the `subDBs` key) and is - // resolved structurally via `subDBs`, not through the parent's refTable. + members.foreach { + case _: DFDesignBlock => // nested blocks live in their own sub-DB only + case dfVal: DFVal.CanBeGlobal if dfVal.isGlobal => // globals handled separately + case m => designOwn(m.getOwnerDesign) += m } - result.toMap - // Build sub-DBs in top-down elaboration order. Sub-DBs themselves have - // empty `subDBs` — only the root collects the flat hierarchy. The - // LinkedHashMap preserves insertion order so the resulting list runs - // top → top's first child → grandchildren … in elaboration order. - val builtSubDBs = mutable.LinkedHashMap.empty[DFDesignBlock, DB] - def buildSubDB(d: DFDesignBlock): DB = - builtSubDBs.get(d) match - case Some(db) => db - case None => - val locals = designOwn(d).toList - // Walk d's own refs in addition to its locals: with nested blocks - // no longer present in the parent's `localMembers`, globals reached - // only through a sub-DB's `designBlock` refs would otherwise be - // missed by every closure that needs them. - val closure = globalsClosure(d :: locals) - val dbMembers = closure ::: d :: locals - val dbRefTable = refsFor(dbMembers) - val builtForD = DB( - members = dbMembers, - refTable = dbRefTable, - // Sub-DBs inherit globalTags from the root so per-design stage - // helpers (e.g. resolvedClkRstMap) find project-wide tags - // like DefaultRTDomainCfgTag when dispatched against a sub-DB. - globalTags = this.globalTags, - srcFiles = Nil - ) - // Insert d BEFORE recursing into children so the LinkedHashMap - // ordering is top-down (parent before children). - builtSubDBs(d) = builtForD - val directChildren = parentToChildren.getOrElse(d, Nil).toList - directChildren.foreach(buildSubDB) - builtForD - buildSubDB(topDsn) - // Orphan globals: any global in the original flat DB that is not reached - // by any sub-DB's `globalsClosure` (e.g. a global that nothing references). - // Without explicit handling these would vanish across `oldToNew + newToOld`, - // because root.members is empty and only sub-DB closures carry globals. - // Anchor them at the top design's sub-DB so they survive the round-trip. - val coveredGlobals = mutable.Set.empty[DFMember] - builtSubDBs.valuesIterator.foreach { sub => - sub.members.foreach { - case g: DFVal.CanBeGlobal if g.isGlobal => coveredGlobals += g - case _ => + // parent → ordered list of canonical child DFDesignBlocks. Iteration of + // `designBlockParent` (a LinkedHashMap) preserves first-inst-encounter + // order, which matches the elaboration order of the children. + val parentToChildren = + mutable.LinkedHashMap.empty[DFDesignBlock, mutable.ListBuffer[DFDesignBlock]] + designBlockParent.foreach { (child, parent) => + parentToChildren.getOrElseUpdate(parent, mutable.ListBuffer.empty) += child } - } - val orphanGlobals: List[DFMember] = members.collect { - case g: DFVal.CanBeGlobal if g.isGlobal && !coveredGlobals.contains(g) => g - } - if (orphanGlobals.nonEmpty) - val topSub = builtSubDBs(topDsn) - val newMembers = orphanGlobals ::: topSub.members - val orphanRefs = refsFor(orphanGlobals) - val newRefTable = topSub.refTable ++ orphanRefs - builtSubDBs(topDsn) = topSub.update(members = newMembers, refTable = newRefTable) - // Root is a pure hierarchy container: empty members, empty refTable, - // designBlock = None. `subDBs` lists every design — top first (so - // `topDB` resolves correctly), then the top's descendants in elaboration - // order. - DB( - members = Nil, - refTable = Map.empty, - globalTags = this.globalTags, - srcFiles = this.srcFiles, - subDBs = ListMap.from(builtSubDBs.iterator.map((d, sub) => d.ownerRef -> sub)) - ) + // All globals in their original elaboration order — used to project each + // sub-DB's closure back into a deterministic, topological, source-faithful + // order (elaboration order is itself topological since a global cannot + // reference a later-defined global). + val allGlobalsOrdered: List[DFMember] = members.collect { + case g: DFVal.CanBeGlobal if g.isGlobal => g + } + // Compute the closure of globals transitively reachable from a DB's refs. + // Walks local members' refs; when a ref target is a global, we include it + // and recurse through its own refs to pick up globals-referenced-by-globals. + // Non-global intermediaries are NOT included (they belong to their own + // design's locals), but their refs ARE walked because we iterate all of + // the design's locals directly. Returns reachable globals in original + // elaboration order — required because `newToOld` emits a sub-DB's + // members directly into the flat output, and both `SanityCheck.orderCheck` + // and code generation depend on stable, topological ordering. + def globalsClosure(localMembers: Iterable[DFMember]): List[DFMember] = + val reachable = mutable.Set.empty[DFMember] + def pull(target: DFMember): Unit = target match + case g: DFVal.CanBeGlobal if g.isGlobal && !reachable.contains(g) => + reachable += g + g.getRefs.foreach(r => refTable.get(r).foreach(pull)) + case _ => + localMembers.foreach { m => + m.getRefs.foreach(r => refTable.get(r).foreach(pull)) + } + allGlobalsOrdered.filter(reachable.contains) + // Build the refTable partition for a DB: every ref emitted (via ownerRef + // or getRefs) by any of the DB's members, resolved against the original + // flat refTable. + def refsFor(dbMembers: Iterable[DFMember]): Map[DFRefAny, DFMember] = + val result = mutable.Map.empty[DFRefAny, DFMember] + dbMembers.foreach { m => + refTable.get(m.ownerRef).foreach(t => result(m.ownerRef) = t) + m.getRefs.foreach(r => refTable.get(r).foreach(t => result(r) = t)) + // NOTE: `DFDesignInst.designRef` is deliberately NOT added here. It is + // unified with the child block's `ownerRef` (the `subDBs` key) and is + // resolved structurally via `subDBs`, not through the parent's refTable. + } + result.toMap + // Build sub-DBs in top-down elaboration order. Sub-DBs themselves have + // empty `subDBs` — only the root collects the flat hierarchy. The + // LinkedHashMap preserves insertion order so the resulting list runs + // top → top's first child → grandchildren … in elaboration order. + val builtSubDBs = mutable.LinkedHashMap.empty[DFDesignBlock, DB] + def buildSubDB(d: DFDesignBlock): DB = + builtSubDBs.get(d) match + case Some(db) => db + case None => + val locals = designOwn(d).toList + // Walk d's own refs in addition to its locals: with nested blocks + // no longer present in the parent's `localMembers`, globals reached + // only through a sub-DB's `designBlock` refs would otherwise be + // missed by every closure that needs them. + val closure = globalsClosure(d :: locals) + val dbMembers = closure ::: d :: locals + val dbRefTable = refsFor(dbMembers) + val builtForD = DB( + members = dbMembers, + refTable = dbRefTable, + // Sub-DBs inherit globalTags from the root so per-design stage + // helpers (e.g. resolvedClkRstMap) find project-wide tags + // like DefaultRTDomainCfgTag when dispatched against a sub-DB. + globalTags = this.globalTags, + srcFiles = Nil + ) + // Insert d BEFORE recursing into children so the LinkedHashMap + // ordering is top-down (parent before children). + builtSubDBs(d) = builtForD + val directChildren = parentToChildren.getOrElse(d, Nil).toList + directChildren.foreach(buildSubDB) + builtForD + buildSubDB(topDsn) + // Orphan globals: any global in the original flat DB that is not reached + // by any sub-DB's `globalsClosure` (e.g. a global that nothing references). + // Without explicit handling these would vanish across `oldToNew + newToOld`, + // because root.members is empty and only sub-DB closures carry globals. + // Anchor them at the top design's sub-DB so they survive the round-trip. + val coveredGlobals = mutable.Set.empty[DFMember] + builtSubDBs.valuesIterator.foreach { sub => + sub.members.foreach { + case g: DFVal.CanBeGlobal if g.isGlobal => coveredGlobals += g + case _ => + } + } + val orphanGlobals: List[DFMember] = members.collect { + case g: DFVal.CanBeGlobal if g.isGlobal && !coveredGlobals.contains(g) => g + } + if (orphanGlobals.nonEmpty) + val topSub = builtSubDBs(topDsn) + val newMembers = orphanGlobals ::: topSub.members + val orphanRefs = refsFor(orphanGlobals) + val newRefTable = topSub.refTable ++ orphanRefs + builtSubDBs(topDsn) = topSub.update(members = newMembers, refTable = newRefTable) + // Root is a pure hierarchy container: empty members, empty refTable, + // designBlock = None. `subDBs` lists every design — top first (so + // `topDB` resolves correctly), then the top's descendants in elaboration + // order. + DB( + members = Nil, + refTable = Map.empty, + globalTags = this.globalTags, + srcFiles = this.srcFiles, + subDBs = ListMap.from(builtSubDBs.iterator.map((d, sub) => d.ownerRef -> sub)) + ) end oldToNew // Collapses a new-style (option-a) DB back into a flat old-style DB. @@ -1711,102 +1712,103 @@ final case class DB private ( // pointing at it (mirroring the original elaboration order, in which the // nested block is added during the first instance's elaboration and so // appears just before that inst in the flat DB). - def newToOld: DB = - if (subDBs.isEmpty) return this - // Under B-pure, root has empty `members` and empty `refTable`; all design - // content lives in sub-DBs. `allDBs` only needs the sub-DBs. - val allDBs: List[DB] = subDBs.values.toList - // canonical DFDesignBlock per ownerRef: the one in its own sub-DB's - // `members` (the sub-DB owns patches against its header). - val canonicalDesign = mutable.Map.empty[DFOwner.Ref, DFDesignBlock] - subDBs.foreach { (key, subDB) => - subDB.members.collectFirst { - case d: DFDesignBlock if d.ownerRef == key => d - }.foreach(canonicalDesign(key) = _) - } - def canonicalize(m: DFMember): DFMember = m match - case d: DFDesignBlock => canonicalDesign.getOrElse(d.ownerRef, d) - case _ => m - // inst → canonical target DFDesignBlock. Resolved through each sub-DB's - // own refTable because the root DB's refTable does not necessarily cover - // refs that originate in sub-DB members. - val instToDesign = mutable.Map.empty[DFDesignInst, DFDesignBlock] - allDBs.foreach { db => - db.members.foreach { - case inst: DFDesignInst => - // Resolve inst -> child block structurally: `designRef` is unified with - // the child block's `ownerRef` (the `subDBs` key) and is not present in - // the parent sub-DB's refTable. `getDesignBlock` handles both the - // unified form and the pre-unification distinct form. - instToDesign(inst) = - canonicalize(inst.getDesignBlock(using db.getSet)).asInstanceOf[DFDesignBlock] - case _ => + lazy val newToOld: DB = + if (subDBs.isEmpty) this + else + // Under B-pure, root has empty `members` and empty `refTable`; all design + // content lives in sub-DBs. `allDBs` only needs the sub-DBs. + val allDBs: List[DB] = subDBs.values.toList + // canonical DFDesignBlock per ownerRef: the one in its own sub-DB's + // `members` (the sub-DB owns patches against its header). + val canonicalDesign = mutable.Map.empty[DFOwner.Ref, DFDesignBlock] + subDBs.foreach { (key, subDB) => + subDB.members.collectFirst { + case d: DFDesignBlock if d.ownerRef == key => d + }.foreach(canonicalDesign(key) = _) } - } - val flat = mutable.ListBuffer.empty[DFMember] - val seen = mutable.Set.empty[DFMember] - def emit(ms: List[DFMember]): Unit = - ms.foreach { m => - val c = canonicalize(m) - c match + def canonicalize(m: DFMember): DFMember = m match + case d: DFDesignBlock => canonicalDesign.getOrElse(d.ownerRef, d) + case _ => m + // inst → canonical target DFDesignBlock. Resolved through each sub-DB's + // own refTable because the root DB's refTable does not necessarily cover + // refs that originate in sub-DB members. + val instToDesign = mutable.Map.empty[DFDesignInst, DFDesignBlock] + allDBs.foreach { db => + db.members.foreach { case inst: DFDesignInst => - // Emit the target design block (and its sub-DB body) before the - // inst, on the first inst that references it. Subsequent insts - // for the same block just emit themselves. - instToDesign.get(inst).foreach { targetBlock => - if (!seen.contains(targetBlock)) - seen += targetBlock - flat += targetBlock - subDBs.get(targetBlock.ownerRef).foreach(sub => emit(sub.members)) - } - if (!seen.contains(c)) - seen += c - flat += c + // Resolve inst -> child block structurally: `designRef` is unified with + // the child block's `ownerRef` (the `subDBs` key) and is not present in + // the parent sub-DB's refTable. `getDesignBlock` handles both the + // unified form and the pre-unification distinct form. + instToDesign(inst) = + canonicalize(inst.getDesignBlock(using db.getSet)).asInstanceOf[DFDesignBlock] case _ => - if (!seen.contains(c)) - seen += c - flat += c - c match - case d: DFDesignBlock if subDBs.contains(d.ownerRef) => - emit(subDBs(d.ownerRef).members) - case _ => - end match + } } - // B-pure: root has empty `members`. Start emission from the topDB's - // members. The DFDesignBlock-descent guard inside `emit` prevents the - // re-entry from looping when topDsn (a member of topDB.members) triggers - // a recursive `emit(topDB.members)` on its own sub-DB. - emit(topDB.members) - // Merge refTables from root + every sub-DB. A shared ref key can live in - // multiple sub-DB refTables (e.g. a nested DFDesignBlock's ownerRef appears - // in both parent's refTable — where it points to the parent's own members — - // and child's refTable). When a sub-DB patches its member in-place, only - // that sub-DB's refTable is rewired; the peer sub-DB's entry for the same - // key still points at the pre-patch (now removed) instance. Prefer the - // entry whose target is in `flat` so stale targets lose when a fresh one - // exists for the same key. DFDesignBlock targets still go through - // `canonicalize` so refs pointing at the parent's stale copy retarget to - // the sub-DB's patched version. - val flatSet = mutable.Set.empty[DFMember] - flat.foreach(flatSet += _) - val mergedRefTable = mutable.Map.empty[DFRefAny, DFMember] - allDBs.foreach { db => - db.refTable.foreach { (k, target) => - val canonicalTarget = canonicalize(target) - val isFresh = flatSet.contains(canonicalTarget) - mergedRefTable.get(k) match - case None => mergedRefTable(k) = canonicalTarget - case Some(existing) if !flatSet.contains(existing) && isFresh => - mergedRefTable(k) = canonicalTarget - case _ => // keep existing + val flat = mutable.ListBuffer.empty[DFMember] + val seen = mutable.Set.empty[DFMember] + def emit(ms: List[DFMember]): Unit = + ms.foreach { m => + val c = canonicalize(m) + c match + case inst: DFDesignInst => + // Emit the target design block (and its sub-DB body) before the + // inst, on the first inst that references it. Subsequent insts + // for the same block just emit themselves. + instToDesign.get(inst).foreach { targetBlock => + if (!seen.contains(targetBlock)) + seen += targetBlock + flat += targetBlock + subDBs.get(targetBlock.ownerRef).foreach(sub => emit(sub.members)) + } + if (!seen.contains(c)) + seen += c + flat += c + case _ => + if (!seen.contains(c)) + seen += c + flat += c + c match + case d: DFDesignBlock if subDBs.contains(d.ownerRef) => + emit(subDBs(d.ownerRef).members) + case _ => + end match + } + // B-pure: root has empty `members`. Start emission from the topDB's + // members. The DFDesignBlock-descent guard inside `emit` prevents the + // re-entry from looping when topDsn (a member of topDB.members) triggers + // a recursive `emit(topDB.members)` on its own sub-DB. + emit(topDB.members) + // Merge refTables from root + every sub-DB. A shared ref key can live in + // multiple sub-DB refTables (e.g. a nested DFDesignBlock's ownerRef appears + // in both parent's refTable — where it points to the parent's own members — + // and child's refTable). When a sub-DB patches its member in-place, only + // that sub-DB's refTable is rewired; the peer sub-DB's entry for the same + // key still points at the pre-patch (now removed) instance. Prefer the + // entry whose target is in `flat` so stale targets lose when a fresh one + // exists for the same key. DFDesignBlock targets still go through + // `canonicalize` so refs pointing at the parent's stale copy retarget to + // the sub-DB's patched version. + val flatSet = mutable.Set.empty[DFMember] + flat.foreach(flatSet += _) + val mergedRefTable = mutable.Map.empty[DFRefAny, DFMember] + allDBs.foreach { db => + db.refTable.foreach { (k, target) => + val canonicalTarget = canonicalize(target) + val isFresh = flatSet.contains(canonicalTarget) + mergedRefTable.get(k) match + case None => mergedRefTable(k) = canonicalTarget + case Some(existing) if !flatSet.contains(existing) && isFresh => + mergedRefTable(k) = canonicalTarget + case _ => // keep existing + } } - } - DB( - members = flat.toList, - refTable = mergedRefTable.toMap, - globalTags = this.globalTags, - srcFiles = this.srcFiles - ) + DB( + members = flat.toList, + refTable = mergedRefTable.toMap, + globalTags = this.globalTags, + srcFiles = this.srcFiles + ) end newToOld // Lightweight repair of the per-sub-DB global closure: adds any global member @@ -1966,6 +1968,7 @@ object DB: List[SourceFile], List[(DFOwner.Ref, DB)] ) + @scala.annotation.nowarn("msg=Infinite loop") given ReadWriter[DB] = readwriter[DBSerialized].bimap[DB]( db => (db.members, db.refTable, db.globalTags, db.srcFiles, db.subDBs.toList), From 5353d5d8ae11bf5e08a9d3ce55d8ba79757b2a2b Mon Sep 17 00:00:00 2001 From: Oron Port Date: Mon, 15 Jun 2026 21:15:00 +0300 Subject: [PATCH 45/72] Remove the unused DFInterfaceOwner skeleton The interfaces work will model interface declarations as DFDesignBlock(InstMode.Interface) and views as new DFTypes, so the placeholder DFInterfaceOwner owner is no longer needed. Drops it from the DFMember hierarchy and ReadWriter, narrows DFNet.Ref to DFVal-only, simplifies DFNet.Connection.unapply / FlatNet, and removes the per-stage interface cases (AddClkRst, ToRT, ToED) and the <-> printer path. Updates the IR reference docs to match. Co-Authored-By: Claude Opus 4.8 --- .claude/commands/ir-reference.md | 7 ++-- .claude/commands/new-stage.md | 1 - .../src/main/scala/dfhdl/compiler/ir/DB.scala | 15 -------- .../scala/dfhdl/compiler/ir/DFMember.scala | 35 ++----------------- .../dfhdl/compiler/printing/Printer.scala | 3 +- .../dfhdl/compiler/stages/AddClkRst.scala | 5 --- .../scala/dfhdl/compiler/stages/ToED.scala | 6 ---- .../scala/dfhdl/compiler/stages/ToRT.scala | 5 --- .../src/main/scala/dfhdl/core/MutableDB.scala | 6 ++-- 9 files changed, 9 insertions(+), 74 deletions(-) diff --git a/.claude/commands/ir-reference.md b/.claude/commands/ir-reference.md index d04c0d5d5..4e6a576be 100644 --- a/.claude/commands/ir-reference.md +++ b/.claude/commands/ir-reference.md @@ -51,7 +51,6 @@ DFMember (sealed) ├── DFConditional.Header — if / match header expression │ ├── DFIfHeader │ └── DFMatchHeader -├── DFInterfaceOwner — interface abstraction └── DFRange — for-loop range ``` @@ -374,7 +373,7 @@ type PortByNameSelect.Ref = DFRef.TwoWay[DFDesignInst, PortByNameSelect] ```scala final case class DFNet( - lhsRef: DFNet.Ref, // DFRef.TwoWay[DFVal | DFInterfaceOwner, DFNet] + lhsRef: DFNet.Ref, // DFRef.TwoWay[DFVal, DFNet] op: DFNet.Op, rhsRef: DFNet.Ref, ownerRef: DFOwner.Ref, @@ -398,8 +397,8 @@ DFNet.Assignment(toVal, fromVal) // Assignment or NBAssignment; toVal and from DFNet.BAssignment(toVal, fromVal) // blocking only (op == Assignment) DFNet.NBAssignment(toVal, fromVal) // non-blocking only (op == NBAssignment) DFNet.Connection(toVal, fromVal, swapped) - // toVal: DFVal.Dcl | DFVal.Special | DFInterfaceOwner - // fromVal: DFVal | DFInterfaceOwner + // toVal: DFVal.Dcl | DFVal.PortByNameSelect | DFVal.Special + // fromVal: DFVal // swapped: Boolean — true if lhs/rhs were physically reversed ``` diff --git a/.claude/commands/new-stage.md b/.claude/commands/new-stage.md index a959d3437..539775d90 100644 --- a/.claude/commands/new-stage.md +++ b/.claude/commands/new-stage.md @@ -129,7 +129,6 @@ DFMember (sealed) │ ├── StepBlock ─ RT step (FSM state) │ └── DFConditional.Block ─ if/match/while clause ├── DFConditional.Header ─ if/match/while header -├── DFInterfaceOwner └── DFRange ``` diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index 3d8d27800..ad93db7b0 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -475,20 +475,6 @@ final case class DB private ( (net.lhsRef.get, net.rhsRef.get) match case (lhsVal: DFVal, rhsVal: DFVal) => List(FlatNet(lhsVal, rhsVal, net)) - case (lhsIfc: DFInterfaceOwner, rhsIfc: DFInterfaceOwner) => - FlatNet(lhsIfc, rhsIfc, net) - case _ => ??? - def apply(lhsIfc: DFInterfaceOwner, rhsIfc: DFInterfaceOwner, net: DFNet): List[FlatNet] = - val lhsMembers = getMembersOf(lhsIfc, MemberView.Folded) - val rhsMembers = getMembersOf(rhsIfc, MemberView.Folded) - assert(lhsMembers.length == rhsMembers.length) - lhsMembers.lazyZip(rhsMembers).flatMap { - case (lhsVal: DFVal, rhsVal: DFVal) => - List(FlatNet(lhsVal, rhsVal, net)) - case (lhsIfc: DFInterfaceOwner, rhsIfc: DFInterfaceOwner) => - FlatNet(lhsIfc, rhsIfc, net) - case _ => ??? - } end FlatNet given printer: Printer = DefaultPrinter @tailrec private def getConnToMap( @@ -925,7 +911,6 @@ final case class DB private ( |""".stripMargin ) else Some(domain -> inSourceDomains.head) - case ifc: DFInterfaceOwner => ??? case _ => None } } diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala index 3079942f1..3d2f7fb24 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala @@ -119,7 +119,6 @@ object DFMember: summon[ReadWriter[DFMember.Empty]], summon[ReadWriter[DFVal]], summon[ReadWriter[Statement]], - summon[ReadWriter[DFInterfaceOwner]], summon[ReadWriter[DFBlock]], summon[ReadWriter[DFConditional.Header]], summon[ReadWriter[DFRange]], @@ -152,8 +151,6 @@ object DFMember: design.meta.annotations case designInst: DFDesignInst => designInst.meta.annotations ++ designInst.getDesignBlock.getConstraints - case interface: DFInterfaceOwner => - interface.dclMeta.annotations.view ++ interface.meta.annotations case _ => member.meta.annotations.view allAnnotations.collect { case c: constraints.Constraint => c @@ -1058,7 +1055,7 @@ final case class DFNet( end DFNet object DFNet: - type Ref = DFRef.TwoWay[DFVal | DFInterfaceOwner, DFNet] + type Ref = DFRef.TwoWay[DFVal, DFNet] enum Op derives CanEqual, ReadWriter: case Assignment, NBAssignment, Connection, ViaConnection, LazyConnection extension (net: DFNet) @@ -1100,8 +1097,8 @@ object DFNet: MemberGetSet ): Option[ ( - toVal: DFVal.Dcl | DFVal.PortByNameSelect | DFVal.Special | DFInterfaceOwner, - fromVal: DFVal | DFInterfaceOwner, + toVal: DFVal.Dcl | DFVal.PortByNameSelect | DFVal.Special, + fromVal: DFVal, swapped: Boolean ) ] = @@ -1110,9 +1107,6 @@ object DFNet: val toLeft = getSet.designDB.connectionTable.getNets(lhsVal).contains(net) if (toLeft) Some(lhsVal.dealias.get, rhsVal, false) else Some(rhsVal.dealias.get, lhsVal, true) - case (lhsIfc: DFInterfaceOwner, rhsIfc: DFInterfaceOwner) => - Some(lhsIfc, rhsIfc, false) - case _ => ??? // not possible else None end Connection end DFNet @@ -1190,29 +1184,6 @@ sealed trait DFDomainOwner extends DFOwnerNamed: object DFOwner: type Ref = DFRef.OneWay[DFOwner | DFMember.Empty] -final case class DFInterfaceOwner( - domainType: DomainType, - dclMeta: Meta, - ownerRef: DFOwner.Ref, - meta: Meta, - tags: DFTags -) extends DFDomainOwner derives ReadWriter: - protected def `prot_=~`(that: DFMember)(using MemberGetSet): Boolean = that match - case that: DFInterfaceOwner => - this.domainType =~ that.domainType && - this.dclMeta =~ that.dclMeta && this.meta =~ that.meta && this.tags =~ that.tags - case _ => false - protected def setMeta(meta: Meta): this.type = copy(meta = meta).asInstanceOf[this.type] - protected def setTags(tags: DFTags): this.type = copy(tags = tags).asInstanceOf[this.type] - lazy val getRefs: List[DFRef.TwoWayAny] = domainType.getRefs ++ meta.getRefs - def copyWithNewRefs(using RefGen): this.type = copy( - dclMeta = dclMeta.copyWithNewRefs, - meta = meta.copyWithNewRefs, - domainType = domainType.copyWithNewRefs, - ownerRef = ownerRef.copyAsNewRef - ).asInstanceOf[this.type] -end DFInterfaceOwner - sealed trait DFBlock extends DFOwner object DFBlock: given ReadWriter[DFBlock] = ReadWriter.merge( diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/printing/Printer.scala b/compiler/ir/src/main/scala/dfhdl/compiler/printing/Printer.scala index 2369dd957..b70b1b1a9 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/printing/Printer.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/printing/Printer.scala @@ -65,8 +65,7 @@ trait Printer else swapped && normalizeConnection && !lhsVal.isInstanceOf[DFVal.Special] val directionStr = lhsOrig match - case dfIfc: DFInterfaceOwner => "<->" - case dfVal: DFVal => + case dfVal: DFVal => if (dfVal.getConnectionsTo.contains(net) ^ swapLR) "<--" else "-->" val (lhsRef, rhsRef) = if (swapLR) (net.rhsRef, net.lhsRef) else (net.lhsRef, net.rhsRef) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddClkRst.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddClkRst.scala index 9570b6d5b..180c8c1f7 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddClkRst.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddClkRst.scala @@ -278,11 +278,6 @@ case object AddClkRst extends GlobalStage: val updatedOwner = owner match case design: DFDesignBlock => design.copy(meta = updateMeta(design.meta)) - case interface: DFInterfaceOwner => - interface.copy( - dclMeta = updateMeta(interface.dclMeta), - meta = updateMeta(interface.meta) - ) case _ => owner.setMeta(updateMeta) Some(owner -> Patch.Replace(updatedOwner, Patch.Replace.Config.FullReplacement)) else None diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ToED.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ToED.scala index fca236d45..2d88a19ad 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ToED.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ToED.scala @@ -398,12 +398,6 @@ case object ToED extends HierarchyStage: design.copy(domainType = DomainType.ED, meta = stripTimingAnnotations(design.meta)) case domain: DomainBlock => domain.copy(domainType = DomainType.ED, meta = stripTimingAnnotations(domain.meta)) - case ifc: DFInterfaceOwner => - ifc.copy( - domainType = DomainType.ED, - dclMeta = stripTimingAnnotations(ifc.dclMeta), - meta = stripTimingAnnotations(ifc.meta) - ) domainOwner -> Patch.Replace(updatedOwner, Patch.Replace.Config.FullReplacement) } firstPart.patch(patchList) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ToRT.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ToRT.scala index 2df1373c5..1345d1e57 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ToRT.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ToRT.scala @@ -21,11 +21,6 @@ case object ToRT extends HierarchyStage: d.copy(domainType = DomainType.RT), Patch.Replace.Config.FullReplacement ) - case i @ DFInterfaceOwner(domainType = DomainType.DF) => - i -> Patch.Replace( - i.copy(domainType = DomainType.RT), - Patch.Replace.Config.FullReplacement - ) } subDB.patch(patchList) end transformSubDB diff --git a/core/src/main/scala/dfhdl/core/MutableDB.scala b/core/src/main/scala/dfhdl/core/MutableDB.scala index 0c7a5fb0b..63dd8a11f 100644 --- a/core/src/main/scala/dfhdl/core/MutableDB.scala +++ b/core/src/main/scala/dfhdl/core/MutableDB.scala @@ -20,7 +20,6 @@ import dfhdl.compiler.ir.{ DFTags, annotation, DFDomainOwner, - DFInterfaceOwner, Meta } import dfhdl.compiler.analysis.filterPublicMembers @@ -413,9 +412,8 @@ final class MutableDB(): val updatedAnnotations = updatedSigConstraints ++ otherAnnotations val updatedMeta = domainOwner.meta.copy(annotations = updatedAnnotations) val updatedDomainOwner = domainOwner match - case design: DFDesignBlock => design.copy(meta = updatedMeta) - case domain: DomainBlock => domain.copy(meta = updatedMeta) - case interface: DFInterfaceOwner => interface.copy(meta = updatedMeta) + case design: DFDesignBlock => design.copy(meta = updatedMeta) + case domain: DomainBlock => domain.copy(meta = updatedMeta) updatedDomainOwner case None => domainOwner end getConstrainedDomainOwner From 4c9ea6da1859876f6e50b9a640bcecc18915a87b Mon Sep 17 00:00:00 2001 From: Oron Port Date: Mon, 15 Jun 2026 21:32:53 +0300 Subject: [PATCH 46/72] Remove dead match branches left by DFInterfaceOwner removal With DFInterfaceOwner gone, every DFOwner is a DFBlock and DFNet.Ref is DFVal-only, so several fallback/recursion branches became unreachable (E121 'unreachable case except for null' warnings). Removes the dead branches in getOwnerBlock, getVeryLastMember, the ToED domain-owner and GlobalizePortVectorParams ref matches, and simplifies the now-trivial DFVal matches in Printer.csDFNet and FoldControlSteps.lhsDcl. Co-Authored-By: Claude Opus 4.8 --- .../main/scala/dfhdl/compiler/analysis/DFOwnerAnalysis.scala | 1 - compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala | 1 - .../ir/src/main/scala/dfhdl/compiler/printing/Printer.scala | 5 ++--- .../main/scala/dfhdl/compiler/stages/FoldControlSteps.scala | 4 +--- .../dfhdl/compiler/stages/GlobalizePortVectorParams.scala | 1 - .../stages/src/main/scala/dfhdl/compiler/stages/ToED.scala | 2 -- 6 files changed, 3 insertions(+), 11 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/analysis/DFOwnerAnalysis.scala b/compiler/ir/src/main/scala/dfhdl/compiler/analysis/DFOwnerAnalysis.scala index c731ab081..08aca4452 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/analysis/DFOwnerAnalysis.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/analysis/DFOwnerAnalysis.scala @@ -11,7 +11,6 @@ extension (owner: DFOwner) val last = owner match case block: DFDesignBlock => designDB.designMemberTable(block).lastOption case block: DFBlock => designDB.blockMemberTable(block).lastOption - case _ => designDB.ownerMemberTable(owner).lastOption last match // if last member is an owner then we search further case Some(o: DFOwner) => diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala index 3d2f7fb24..729dba8c8 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala @@ -31,7 +31,6 @@ sealed trait DFMember extends Product, Serializable, HasRefCompare[DFMember] der case o => o.getOwnerNamed final def getOwnerBlock(using MemberGetSet): DFBlock = getOwner match case b: DFBlock => b - case o => o.getOwnerBlock final def getOwnerStepBlock(using MemberGetSet): StepBlock = getOwner match case b: StepBlock => b case o => o.getOwnerStepBlock diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/printing/Printer.scala b/compiler/ir/src/main/scala/dfhdl/compiler/printing/Printer.scala index b70b1b1a9..ac64bd95a 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/printing/Printer.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/printing/Printer.scala @@ -84,9 +84,8 @@ trait Printer val lhsDin = net.lhsRef.get match case dfVal: DFVal if dfVal.dealias.get.asInstanceOf[DFVal.Dcl].isReg => ".din" case _ => "" - val lhsShared = net.lhsRef.get match - case dfVal: DFVal => dfVal.dealias.get.asInstanceOf[DFVal.Dcl].modifier.isShared - case _ => false + val lhsShared = + net.lhsRef.get.dealias.get.asInstanceOf[DFVal.Dcl].modifier.isShared val lhsStr = net.lhsRef.refCodeString + lhsDin val rhsStr = net.rhsRef.refCodeString net.op.runtimeChecked match diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/FoldControlSteps.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/FoldControlSteps.scala index fe74947c1..ba9589b0d 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/FoldControlSteps.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/FoldControlSteps.scala @@ -184,9 +184,7 @@ case object FoldControlSteps extends HierarchyStage: case b: DFConditional.DFIfElseBlock if b.prevBlockOrHeaderRef.get == thenB => b } private def lhsDcl(n: DFNet)(using MemberGetSet): Option[DFVal.Dcl] = - n.lhsRef.get match - case v: DFVal => v.departialDcl.map(_._1) - case _ => None + n.lhsRef.get.departialDcl.map(_._1) // the index-update net plus its anonymous dependency subtree (the rhs expression) private def idxSubtree(lvl: Level)(using MemberGetSet): Set[DFMember] = (lvl.idxNet :: lvl.idxNet.collectRelMembers).toSet diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/GlobalizePortVectorParams.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/GlobalizePortVectorParams.scala index a7cbceddc..0ff33c8ea 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/GlobalizePortVectorParams.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/GlobalizePortVectorParams.scala @@ -427,7 +427,6 @@ case object GlobalizePortVectorParams extends HierarchyStage: case otherVec: DFVector if otherVec != dclType => out += otherVec -> dclType case _ => - case _ => } } case _ => diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ToED.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ToED.scala index 2d88a19ad..b2da9a92d 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ToED.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ToED.scala @@ -348,8 +348,6 @@ case object ToED extends HierarchyStage: // other domains case _ => None end match - // other owners - case _ => None } val firstPart = subDB.patch(patchList) locally { From 28263786fb3242320f5562ca6c4bb66cceaa8cf0 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Tue, 16 Jun 2026 14:49:06 +0300 Subject: [PATCH 47/72] Add DFInterface/DFView IR types and InstMode.Interface Adds two composite DFTypes for the interfaces/views feature: - DFInterface: a bundle of named ports/fields, like DFStruct but structural (no bit representation; never lowered to Bits). - DFView (sealed): a directed view over a DFInterface. DFView.AsIs carries a per-leaf-port direction overlay (dirMap); DFView.Flipped is the converse of another view (IR analog of VHDL 'converse). Both reuse the DFStruct/DFVector conventions for =~/isSimilarTo/getRefs/copyWithNewRefs; the bit-data methods throw, as these are not packable types. Also adds InstMode.Interface so an interface declaration can be represented as a DFDesignBlock(InstMode.Interface) (an 'empty design'). The new variants surface exhaustivity warnings in the type printers (DFTypePrinter, VHDLTypePrinter); emitting interfaces/views to HDL is the deferred backend phase. Co-Authored-By: Claude Opus 4.8 --- .../scala/dfhdl/compiler/ir/DFMember.scala | 2 +- .../main/scala/dfhdl/compiler/ir/DFType.scala | 121 ++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala index 729dba8c8..b9c0bf84b 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala @@ -1588,7 +1588,7 @@ end DFDesignBlock object DFDesignBlock: import InstMode.BlackBox.Source enum InstMode derives CanEqual, ReadWriter: - case Normal, Def, Simulation + case Normal, Def, Simulation, Interface case BlackBox(source: Source) object InstMode: import constraints.DeviceID.Vendor diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala index 020676f52..d7257da77 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala @@ -33,6 +33,8 @@ object DFType: summon[ReadWriter[DFEnum]], summon[ReadWriter[DFVector]], summon[ReadWriter[DFStruct]], + summon[ReadWriter[DFInterface]], + summon[ReadWriter[DFView]], summon[ReadWriter[DFOpaque]], summon[ReadWriter[DFDouble.type]], summon[ReadWriter[DFString.type]], @@ -522,6 +524,125 @@ object DFTuple: ) ///////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////// +// DFInterface +// ----------- +// A bundle of named ports/fields, structurally similar to DFStruct but NOT a +// packed data aggregate: it has no bit representation and is never lowered to +// Bits. A leaf field is a scalar DFType (a port); a nested-interface field's +// type is itself a DFInterface. A directed view over it is a DFView (below). +///////////////////////////////////////////////////////////////////////////// +final case class DFInterface( + name: String, + fieldMap: ListMap[String, DFType] +) extends NamedDFType, + ComposedDFType derives ReadWriter: + type Data = Unit + private def noTypeErr = throw new Exception(s"Unexpected access to $this data type") + def updateName(newName: String)(using MemberGetSet): this.type = + copy(name = newName).asInstanceOf[this.type] + def widthIntOpt(using MemberGetSet): Option[Int] = None + def createBubbleData(using MemberGetSet): Data = noTypeErr + def isDataBubble(data: Data): Boolean = noTypeErr + def dataToBitsData(data: Data)(using MemberGetSet): (BitVector, BitVector) = noTypeErr + def bitsDataToData(data: (BitVector, BitVector))(using MemberGetSet): Data = noTypeErr + def defaultData(using MemberGetSet): Data = noTypeErr + protected def `prot_=~`(that: DFType)(using MemberGetSet): Boolean = that match + case that: DFInterface => + this.name == that.name && + this.fieldMap.lazyZip(that.fieldMap).forall { case ((fnL, ftL), (fnR, ftR)) => + fnL == fnR && ftL =~ ftR + } + case _ => false + def isSimilarTo(that: DFType)(using MemberGetSet): Boolean = that match + case that: DFInterface => + this.name == that.name && + this.fieldMap.lazyZip(that.fieldMap).forall { case ((fnL, ftL), (fnR, ftR)) => + fnL == fnR && ftL.isSimilarTo(ftR) + } + case _ => false + lazy val getRefs: List[DFRef.TypeRef] = fieldMap.values.flatMap(_.getRefs).toList + def copyWithNewRefs(using RefGen): this.type = copy( + fieldMap = ListMap.from(fieldMap.view.mapValues(_.copyWithNewRefs)) + ).asInstanceOf[this.type] +end DFInterface + +object DFInterface extends DFType.Companion[DFInterface, Unit] +///////////////////////////////////////////////////////////////////////////// + +///////////////////////////////////////////////////////////////////////////// +// DFView +// ------ +// A directed view over a DFInterface (the IR of a SV modport / VHDL mode view). +// `AsIs` carries a per-leaf-port direction overlay (`dirMap`); a nested-view +// field's directions live in its own DFView type inside `interfaceType`. +// `Flipped` is the converse of another view (the IR analog of VHDL `'converse`), +// inverting directions recursively. The frontend `.flip` normalizes +// Flipped(Flipped(v)) -> v, so `Flipped` always wraps a base `AsIs` view. +///////////////////////////////////////////////////////////////////////////// +sealed trait DFView extends NamedDFType, ComposedDFType derives ReadWriter: + def interfaceType: DFInterface + type Data = Unit + private def noTypeErr = throw new Exception(s"Unexpected access to $this data type") + final def widthIntOpt(using MemberGetSet): Option[Int] = None + final def createBubbleData(using MemberGetSet): Data = noTypeErr + final def isDataBubble(data: Data): Boolean = noTypeErr + final def dataToBitsData(data: Data)(using MemberGetSet): (BitVector, BitVector) = noTypeErr + final def bitsDataToData(data: (BitVector, BitVector))(using MemberGetSet): Data = noTypeErr + final def defaultData(using MemberGetSet): Data = noTypeErr + +object DFView: + // A base (directly-defined) view: `dirMap` assigns a direction to each leaf + // port of `interfaceType`; nested-view fields carry their own directions. + final case class AsIs( + interfaceType: DFInterface, + instName: String, + name: String, + dirMap: Map[String, DFVal.Modifier.Dir] + ) extends DFView: + def updateName(newName: String)(using MemberGetSet): this.type = + copy(name = newName).asInstanceOf[this.type] + protected def `prot_=~`(that: DFType)(using MemberGetSet): Boolean = that match + case that: AsIs => + this.interfaceType =~ that.interfaceType && + this.instName == that.instName && + this.name == that.name && + this.dirMap.size == that.dirMap.size && + this.dirMap.forall { case (k, d) => that.dirMap.get(k).contains(d) } + case _ => false + def isSimilarTo(that: DFType)(using MemberGetSet): Boolean = that match + case that: AsIs => + this.interfaceType.isSimilarTo(that.interfaceType) && + this.name == that.name && + this.dirMap.size == that.dirMap.size && + this.dirMap.forall { case (k, d) => that.dirMap.get(k).contains(d) } + case _ => false + lazy val getRefs: List[DFRef.TypeRef] = interfaceType.getRefs + def copyWithNewRefs(using RefGen): this.type = + copy(interfaceType = interfaceType.copyWithNewRefs).asInstanceOf[this.type] + end AsIs + + // The converse of a base view (≙ VHDL `'converse`): same structure, all + // directions inverted recursively. + final case class Flipped(name: String, view: DFView.AsIs) extends DFView: + def interfaceType: DFInterface = view.interfaceType + def updateName(newName: String)(using MemberGetSet): this.type = + copy(name = newName).asInstanceOf[this.type] + protected def `prot_=~`(that: DFType)(using MemberGetSet): Boolean = that match + case that: Flipped => + this.name == that.name && this.view =~ that.view + case _ => false + def isSimilarTo(that: DFType)(using MemberGetSet): Boolean = that match + case that: Flipped => + this.name == that.name && this.view.isSimilarTo(that.view) + case _ => false + lazy val getRefs: List[DFRef.TypeRef] = view.getRefs + def copyWithNewRefs(using RefGen): this.type = + copy(view = view.copyWithNewRefs).asInstanceOf[this.type] + end Flipped +end DFView +///////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// // DFDouble ///////////////////////////////////////////////////////////////////////////// From fa1c99df3dc9a84d6d42fa26e8fed37953d8604f Mon Sep 17 00:00:00 2001 From: Oron Port Date: Tue, 16 Jun 2026 14:49:30 +0300 Subject: [PATCH 48/72] Add basic Interface frontend trait and HasConstParams Adds a basic Interface frontend (trait Interface + DFInterface/RTInterface/ EDInterface), mirroring Design's lifecycle but trimmed for a nested 'empty design': scope is DFC.Scope.Interface, the owner is a Design.Block built with InstMode.Interface, and param collection + instance creation reuse Design.Inst. Drops the top-level/device-top/resource and top-check machinery (never applies to a nested interface). Adds HasConstParams as a common ancestor of Design and Interface so the compiler plugin can key constant-parameter handling on it rather than on Design specifically. Co-Authored-By: Claude Opus 4.8 --- .../src/main/scala/dfhdl/core/Container.scala | 7 +++ core/src/main/scala/dfhdl/core/Design.scala | 2 +- .../src/main/scala/dfhdl/core/Interface.scala | 53 +++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 core/src/main/scala/dfhdl/core/Interface.scala diff --git a/core/src/main/scala/dfhdl/core/Container.scala b/core/src/main/scala/dfhdl/core/Container.scala index 894ccbc80..c83715c52 100644 --- a/core/src/main/scala/dfhdl/core/Container.scala +++ b/core/src/main/scala/dfhdl/core/Container.scala @@ -30,3 +30,10 @@ abstract class RTDomainContainer extends DomainContainer(DomainType.RT): final case class Clk() extends DFOpaque.Clk final case class Rst() extends DFOpaque.Rst end RTDomainContainer + +// Common ancestor of `Design` and `Interface`: marks a container whose +// `<> CONST` constructor parameters are turned into design-parameter members +// (`DFVal.DesignParam`) by the compiler plugin. The plugin keys body-parameter +// generation on this trait rather than on `Design` specifically, so interfaces +// (which are not `Design`s) get the same treatment. +trait HasConstParams diff --git a/core/src/main/scala/dfhdl/core/Design.scala b/core/src/main/scala/dfhdl/core/Design.scala index a1e31914c..74b0b5e94 100644 --- a/core/src/main/scala/dfhdl/core/Design.scala +++ b/core/src/main/scala/dfhdl/core/Design.scala @@ -10,7 +10,7 @@ import scala.collection.immutable.ListMap import scala.collection.mutable import scala.reflect.ClassTag -trait Design extends Container, HasClsMetaArgs: +trait Design extends Container, HasClsMetaArgs, HasConstParams: private[core] type TScope = DFC.Scope.Design private[core] type TOwner = Design.Block final protected given TScope = DFC.Scope.Design diff --git a/core/src/main/scala/dfhdl/core/Interface.scala b/core/src/main/scala/dfhdl/core/Interface.scala new file mode 100644 index 000000000..a25cfb26f --- /dev/null +++ b/core/src/main/scala/dfhdl/core/Interface.scala @@ -0,0 +1,53 @@ +package dfhdl.core +import dfhdl.internals.* +import dfhdl.compiler.ir +import ir.DFDesignBlock.InstMode + +import scala.annotation.Annotation + +// A basic interface declaration: structurally an "empty design" that carries +// ports and views but no behavioral statements (no processes/connections/ +// assignments). It elaborates to a `DFDesignBlock(InstMode.Interface)` and runs +// under `DFC.Scope.Interface`, which is used to reject illegal constructs inside +// an interface body. The lifecycle mirrors `Design` but drops the top-level / +// device-top / resource handling, since an interface is always nested. +trait Interface extends Container, HasClsMetaArgs, HasConstParams: + private[core] type TScope = DFC.Scope.Interface + private[core] type TOwner = Design.Block + final protected given TScope = DFC.Scope.Interface + private[core] def mkInstMode: InstMode = InstMode.Interface + private[dfhdl] def initOwner: TOwner = + Design.Block(__domainType, InstMode.Interface)(using dfc.anonymize) + final protected def setClsNamePos( + name: String, + position: Position, + docOpt: Option[String], + annotations: List[Annotation] + ): Unit = + import dfc.getSet + val designBlock = containedOwner.asIR + getSet.replace(designBlock)( + designBlock.copy( + meta = r__For_Plugin.metaGen(Some(name), position, docOpt, annotations), + instMode = mkInstMode + ) + ) + end setClsNamePos + private var hasStartedLate: Boolean = false + final override def onCreateStartLate: Unit = + hasStartedLate = true + val paramEntries = Design.Inst.collectParamEntries + val endedInterface = containedOwner.asIR + dfc.exitOwner() + Design.Inst(endedInterface, paramEntries) + dfc.enterLate() + final override def onCreateEnd(thisOwner: Option[This]): Unit = + if (hasStartedLate) dfc.exitLate() + else dfc.exitOwner() +end Interface + +abstract class DFInterface extends DomainContainer(DomainType.DF), Interface + +abstract class RTInterface extends RTDomainContainer, Interface + +abstract class EDInterface extends DomainContainer(DomainType.ED), Interface From 4a4d71c3878532c8e0c1111d228267003d1e894c Mon Sep 17 00:00:00 2001 From: Oron Port Date: Tue, 16 Jun 2026 14:49:41 +0300 Subject: [PATCH 49/72] Generalize plugin design handling to containers; keep interfaces non-top Teaches the compiler plugin about interfaces by generalizing the Design-keyed constant-parameter machinery to the shared HasConstParams ancestor: - MetaContextPlacerPhase now gates body-parameter generation on dfhdl.core.HasConstParams (was dfhdl.core.Design), so interfaces get the same '<> CONST' param-to-member generation. Renames genDesignBodyParams -> genContainerBodyParams. - Renames the param-generation helpers for container generality: r__For_Plugin.genDesignParam -> genContainerParam, and the plugin's genDesignParamSym/genDesignParamValDef -> genContainerParam* . PreTyperPhase recognizes interface parents (EDInterface/RTInterface/DFInterface) and excludes them from auto-@top: an interface with '<> CONST' params would otherwise match shouldAddTop and then be rejected by the Design-only TopAnnotPhase. TopAnnotPhase is intentionally left Design-only (no @top for interfaces). Co-Authored-By: Claude Opus 4.8 --- .../main/scala/dfhdl/core/r__For_Plugin.scala | 2 +- .../src/main/scala/plugin/CommonPhase.scala | 8 ++--- .../main/scala/plugin/DesignDefsPhase.scala | 2 +- .../scala/plugin/MetaContextPlacerPhase.scala | 30 +++++++++---------- .../src/main/scala/plugin/PreTyperPhase.scala | 10 ++++++- 5 files changed, 30 insertions(+), 22 deletions(-) diff --git a/core/src/main/scala/dfhdl/core/r__For_Plugin.scala b/core/src/main/scala/dfhdl/core/r__For_Plugin.scala index cd9fbbfec..4ec08f3a6 100644 --- a/core/src/main/scala/dfhdl/core/r__For_Plugin.scala +++ b/core/src/main/scala/dfhdl/core/r__For_Plugin.scala @@ -111,7 +111,7 @@ object r__For_Plugin: ): Pattern = Pattern.BindSI(op, parts, bindVals.map(_.asIR.refTW[DFConditional.DFCaseBlock])) @metaContextIgnore - def genDesignParam[V <: DFValAny]( + def genContainerParam[V <: DFValAny]( appliedVal: DFValAny, defaultVal: Option[DFValAny], paramMeta: ir.Meta diff --git a/plugin/src/main/scala/plugin/CommonPhase.scala b/plugin/src/main/scala/plugin/CommonPhase.scala index 63c4fe052..9035b43dc 100755 --- a/plugin/src/main/scala/plugin/CommonPhase.scala +++ b/plugin/src/main/scala/plugin/CommonPhase.scala @@ -146,7 +146,7 @@ abstract class CommonPhase extends PluginPhase: var inlineAnnotSym: Symbol = uninitialized var dfValSym: Symbol = uninitialized var constModTpe: Type = uninitialized - var genDesignParamSym: TermSymbol = uninitialized + var genContainerParamSym: TermSymbol = uninitialized private var bTpe: Type = uninitialized extension (tree: TypeDef) @@ -201,10 +201,10 @@ abstract class CommonPhase extends PluginPhase: end extension extension (v: ValDef)(using Context) - def genDesignParamValDef(default: Option[Tree], dfcTree: Tree): ValDef = + def genContainerParamValDef(default: Option[Tree], dfcTree: Tree): ValDef = val meta = v.genMeta val paramGen = - ref(genDesignParamSym) + ref(genContainerParamSym) .appliedToType(v.tpt.tpe) .appliedToArgs(List(ref(v.symbol), mkOption(default), meta)) .appliedTo(dfcTree) @@ -445,7 +445,7 @@ abstract class CommonPhase extends PluginPhase: inlineAnnotSym = requiredClass("scala.inline") constModTpe = requiredClassRef("dfhdl.core.ISCONST").appliedTo(ConstantType(Constant(true))) contextFunctionSym = defn.FunctionSymbol(1, isContextual = true) - genDesignParamSym = requiredMethod("dfhdl.core.r__For_Plugin.genDesignParam") + genContainerParamSym = requiredMethod("dfhdl.core.r__For_Plugin.genContainerParam") bTpe = requiredClassRef("dfhdl.hdl.B") if (debugFilter(tree.source.path.toString)) println( diff --git a/plugin/src/main/scala/plugin/DesignDefsPhase.scala b/plugin/src/main/scala/plugin/DesignDefsPhase.scala index 5c995c3bd..3e5ce6611 100644 --- a/plugin/src/main/scala/plugin/DesignDefsPhase.scala +++ b/plugin/src/main/scala/plugin/DesignDefsPhase.scala @@ -77,7 +77,7 @@ class DesignDefsPhase(setting: Setting) extends CommonPhase: // constant parameter generation val designParamGenValDefs: List[ValDef] = inContext(ctx.withOwner(anonDef.symbol)) { dfConstValArgs.map { v => - val valDef = v.genDesignParamValDef(None, dfc) + val valDef = v.genContainerParamValDef(None, dfc) inputMap += v.symbol -> ref(valDef.symbol) valDef } diff --git a/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala b/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala index 29638a664..1d56482ca 100644 --- a/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala +++ b/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala @@ -44,7 +44,7 @@ class MetaContextPlacerPhase(setting: Setting) extends CommonPhase: var dfSpecTpe: Type = uninitialized var hasClsMetaArgsTpe: TypeRef = uninitialized var clsMetaArgsTpe: TypeRef = uninitialized - var designTpe: TypeRef = uninitialized + var hasConstParamsTpe: TypeRef = uninitialized var topAnnotSym: ClassSymbol = uninitialized var appTpe: TypeRef = uninitialized var noTopAnnotIsRequired: TypeRef = uninitialized @@ -78,7 +78,7 @@ class MetaContextPlacerPhase(setting: Setting) extends CommonPhase: private def clsMetaArgsOverrideDef(owner: Symbol)(using Context): Tree = clsMetaArgsOverrideDef(owner, ref(requiredMethod("dfhdl.internals.ClsMetaArgs.empty"))) - private def genDesignBodyParams( + private def genContainerBodyParams( body: List[Tree], paramList: List[Tree], defaults: Map[Int, Tree], @@ -86,22 +86,22 @@ class MetaContextPlacerPhase(setting: Setting) extends CommonPhase: )(using Context ): (List[Tree], List[ValDef]) = - val designParamMap = mutable.Map.empty[Symbol, Tree] - val designParamGenValDefs: List[ValDef] = paramList.view.zipWithIndex.collect { + val paramMap = mutable.Map.empty[Symbol, Tree] + val paramGenValDefs: List[ValDef] = paramList.view.zipWithIndex.collect { case (v: ValDef, i) if v.dfValTpeOpt.nonEmpty => // check and report error if the user did not apply a constant modifier - // on a design parameter + // on a design/interface parameter if (!v.tpt.tpe.isDFConst) report.error( - "DFHDL design parameters must be constant values (use a `<> CONST` modifier).", + "DFHDL design/interface parameters must be constant values (use a `<> CONST` modifier).", v.tpt ) - val valDef = v.genDesignParamValDef(defaults.get(i), dfcTree) - designParamMap += v.symbol -> ref(valDef.symbol) + val valDef = v.genContainerParamValDef(defaults.get(i), dfcTree) + paramMap += v.symbol -> ref(valDef.symbol) valDef }.toList - (body.map(b => replaceArgs(b, designParamMap.toMap)), designParamGenValDefs) - end genDesignBodyParams + (body.map(b => replaceArgs(b, paramMap.toMap)), paramGenValDefs) + end genContainerBodyParams override def prepareForStats(trees: List[Tree])(using Context): Context = var explored: List[Tree] = trees @@ -156,10 +156,10 @@ class MetaContextPlacerPhase(setting: Setting) extends CommonPhase: case _ => false } val nonParamBody = template.body.drop(paramBody.length) - val (updatedBody, designParamGenValDefs) = dfcArgOpt match - case Some(dfcTree) if clsTpe <:< designTpe => + val (updatedBody, containerParamGenValDefs) = dfcArgOpt match + case Some(dfcTree) if clsTpe <:< hasConstParamsTpe => val defaults = defaultParamMap.getOrElse(clsSym, Map.empty) - genDesignBodyParams(nonParamBody, paramBody, defaults, dfcTree)(using + genContainerBodyParams(nonParamBody, paramBody, defaults, dfcTree)(using ctx.withOwner(clsSym.primaryConstructor) ) case _ => (nonParamBody, Nil) @@ -194,7 +194,7 @@ class MetaContextPlacerPhase(setting: Setting) extends CommonPhase: ) val newTemplate = cpy.Template(template)(body = - paramBody ++ List(setClsNamePosTree) ++ designParamGenValDefs ++ updatedBody + paramBody ++ List(setClsNamePosTree) ++ containerParamGenValDefs ++ updatedBody ) cpy.TypeDef(tree)(rhs = newTemplate) else tree @@ -347,7 +347,7 @@ class MetaContextPlacerPhase(setting: Setting) extends CommonPhase: dfSpecTpe = requiredClassRef("dfhdl.DFSpec") hasClsMetaArgsTpe = requiredClassRef("dfhdl.internals.HasClsMetaArgs") clsMetaArgsTpe = requiredClassRef("dfhdl.internals.ClsMetaArgs") - designTpe = requiredClassRef("dfhdl.core.Design") + hasConstParamsTpe = requiredClassRef("dfhdl.core.HasConstParams") topAnnotSym = requiredClass("dfhdl.top") appTpe = requiredClassRef("dfhdl.app.DFApp") noTopAnnotIsRequired = requiredClassRef("dfhdl.internals.NoTopAnnotIsRequired") diff --git a/plugin/src/main/scala/plugin/PreTyperPhase.scala b/plugin/src/main/scala/plugin/PreTyperPhase.scala index 4b617d276..2e762b342 100644 --- a/plugin/src/main/scala/plugin/PreTyperPhase.scala +++ b/plugin/src/main/scala/plugin/PreTyperPhase.scala @@ -44,7 +44,9 @@ end CustomReporter * - change process{} to process.forever{} * - auto-add `@top` annotation to concrete classes that look like DFHDL designs (extend * EDDesign/RTDesign/DFDesign, have `type <> CONST` parameters, or use `<>` in their body), - * provided `import dfhdl.*` is in lexical scope and no `@top` annotation is already present + * provided `import dfhdl.*` is in lexical scope and no `@top` annotation is already present. + * Interfaces (EDInterface/RTInterface/DFInterface) are excluded, since they are never + * entry points and must not receive `@top`. */ class PreTyperPhase(setting: Setting) extends CommonPhase: import untpd.* @@ -154,9 +156,12 @@ class PreTyperPhase(setting: Setting) extends CommonPhase: case _ => false private val designParentNames = Set("EDDesign", "RTDesign", "DFDesign") + private val interfaceParentNames = Set("EDInterface", "RTInterface", "DFInterface") private def hasDesignParent(parents: List[Tree]): Boolean = parents.exists(p => rightmostName(p).exists(designParentNames)) + private def hasInterfaceParent(parents: List[Tree]): Boolean = + parents.exists(p => rightmostName(p).exists(interfaceParentNames)) private def isConstParamTpt(tpt: Tree): Boolean = tpt match @@ -233,6 +238,9 @@ class PreTyperPhase(setting: Setting) extends CommonPhase: !m.is(Case) && !m.is(Enum) && !hasTopAnnot(m) && + // interfaces are never entry points, so never auto-`@top` them (even when they + // carry `<> CONST` params or use `<>` in their body for port/view declarations) + !hasInterfaceParent(tmpl.parents) && tmpl.constr.paramss.length <= 1 && allParamsTopCompatible(tmpl.constr.paramss) && (hasDesignParent(tmpl.parents) || From 2733f8b013908c7ea1b90b1f1d4e6e45b4e36371 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Tue, 16 Jun 2026 17:51:05 +0300 Subject: [PATCH 50/72] Collapse Interface to a single ED-based class An interface is purely structural (ports + views, no behavioral statements), so the DF/RT/ED distinction has nothing to act on. Replace the trait Interface + DFInterface/RTInterface/EDInterface variants with a single domain-neutral `Interface` based on the ED domain under the hood: ED is terminal in the lowering pipeline (DF -> RT -> ED), so an ED interface is never transformed by ToRT/ToED nor given clk/rst, and the same Interface is reusable inside a design of any domain. Updates the plugin's interface recognition to the single name (Set("Interface")). Co-Authored-By: Claude Opus 4.8 --- .../src/main/scala/dfhdl/core/Interface.scala | 20 +++++++++++-------- .../src/main/scala/plugin/PreTyperPhase.scala | 6 +++--- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/core/src/main/scala/dfhdl/core/Interface.scala b/core/src/main/scala/dfhdl/core/Interface.scala index a25cfb26f..6b21394d3 100644 --- a/core/src/main/scala/dfhdl/core/Interface.scala +++ b/core/src/main/scala/dfhdl/core/Interface.scala @@ -10,8 +10,18 @@ import scala.annotation.Annotation // assignments). It elaborates to a `DFDesignBlock(InstMode.Interface)` and runs // under `DFC.Scope.Interface`, which is used to reject illegal constructs inside // an interface body. The lifecycle mirrors `Design` but drops the top-level / -// device-top / resource handling, since an interface is always nested. -trait Interface extends Container, HasClsMetaArgs, HasConstParams: +// device-top / resource and top-check machinery (never applies to an interface). +// +// An interface is purely structural, so it has no behavioral (DF/RT/ED) domain +// semantics of its own. There is a single, domain-neutral `Interface` based on +// the ED domain under the hood: ED is terminal in the lowering pipeline +// (DF -> RT -> ED), so an ED interface is never transformed by ToRT/ToED and +// never has clk/rst injected, and the same `Interface` is reusable inside a +// design of any domain. +abstract class Interface + extends DomainContainer(DomainType.ED), + HasClsMetaArgs, + HasConstParams: private[core] type TScope = DFC.Scope.Interface private[core] type TOwner = Design.Block final protected given TScope = DFC.Scope.Interface @@ -45,9 +55,3 @@ trait Interface extends Container, HasClsMetaArgs, HasConstParams: if (hasStartedLate) dfc.exitLate() else dfc.exitOwner() end Interface - -abstract class DFInterface extends DomainContainer(DomainType.DF), Interface - -abstract class RTInterface extends RTDomainContainer, Interface - -abstract class EDInterface extends DomainContainer(DomainType.ED), Interface diff --git a/plugin/src/main/scala/plugin/PreTyperPhase.scala b/plugin/src/main/scala/plugin/PreTyperPhase.scala index 2e762b342..8d88cce25 100644 --- a/plugin/src/main/scala/plugin/PreTyperPhase.scala +++ b/plugin/src/main/scala/plugin/PreTyperPhase.scala @@ -45,8 +45,8 @@ end CustomReporter * - auto-add `@top` annotation to concrete classes that look like DFHDL designs (extend * EDDesign/RTDesign/DFDesign, have `type <> CONST` parameters, or use `<>` in their body), * provided `import dfhdl.*` is in lexical scope and no `@top` annotation is already present. - * Interfaces (EDInterface/RTInterface/DFInterface) are excluded, since they are never - * entry points and must not receive `@top`. + * Classes extending `Interface` are excluded, since they are never entry points and + * must not receive `@top`. */ class PreTyperPhase(setting: Setting) extends CommonPhase: import untpd.* @@ -156,7 +156,7 @@ class PreTyperPhase(setting: Setting) extends CommonPhase: case _ => false private val designParentNames = Set("EDDesign", "RTDesign", "DFDesign") - private val interfaceParentNames = Set("EDInterface", "RTInterface", "DFInterface") + private val interfaceParentNames = Set("Interface") private def hasDesignParent(parents: List[Tree]): Boolean = parents.exists(p => rightmostName(p).exists(designParentNames)) From 0f89f9ba7f3d43bd0630005f68f7d747e906296e Mon Sep 17 00:00:00 2001 From: Oron Port Date: Wed, 17 Jun 2026 03:19:35 +0300 Subject: [PATCH 51/72] Basic Interface frontend compiles --- core/src/main/scala/dfhdl/core/DFVal.scala | 11 +- core/src/main/scala/dfhdl/core/DFView.scala | 8 + .../src/main/scala/dfhdl/core/Interface.scala | 148 +++++++++++++++++- 3 files changed, 160 insertions(+), 7 deletions(-) create mode 100644 core/src/main/scala/dfhdl/core/DFView.scala diff --git a/core/src/main/scala/dfhdl/core/DFVal.scala b/core/src/main/scala/dfhdl/core/DFVal.scala index 76960f856..523a5f736 100644 --- a/core/src/main/scala/dfhdl/core/DFVal.scala +++ b/core/src/main/scala/dfhdl/core/DFVal.scala @@ -303,10 +303,15 @@ object DFVal extends DFValLP: protected type FieldsWithModifier[V <: NamedTuple.AnyNamedTuple, M <: ModifierAny] = NamedTuple.Map[V, [t] =>> FieldWithModifier[t, M]] protected[core] type Fields[T <: DFTypeAny, M <: ModifierAny] = T match - case DFType[t, Args1[a]] => + case DFType[t, args] => t match - case ir.DFStruct => FieldsWithModifier[NamedTuple.From[a], M] - case _ => Any + case ir.DFStruct => + args match + case Args1[a] => FieldsWithModifier[NamedTuple.From[a], M] + case ir.DFView => + args match + case Args2[i, f] => f + case _ => Any case _ => Any // constructing a front-end DFVal value class object. if it's a global value, then diff --git a/core/src/main/scala/dfhdl/core/DFView.scala b/core/src/main/scala/dfhdl/core/DFView.scala new file mode 100644 index 000000000..beb0b21a1 --- /dev/null +++ b/core/src/main/scala/dfhdl/core/DFView.scala @@ -0,0 +1,8 @@ +package dfhdl.core +import dfhdl.internals.* +import dfhdl.compiler.ir +import scala.annotation.unchecked.uncheckedVariance +import NamedTuple.AnyNamedTuple + +type DFView[+I <: Interface, +F <: AnyNamedTuple] = + DFType[ir.DFView, Args2[I @uncheckedVariance, F @uncheckedVariance]] diff --git a/core/src/main/scala/dfhdl/core/Interface.scala b/core/src/main/scala/dfhdl/core/Interface.scala index 6b21394d3..62bbf4d5d 100644 --- a/core/src/main/scala/dfhdl/core/Interface.scala +++ b/core/src/main/scala/dfhdl/core/Interface.scala @@ -2,8 +2,10 @@ package dfhdl.core import dfhdl.internals.* import dfhdl.compiler.ir import ir.DFDesignBlock.InstMode - import scala.annotation.Annotation +import Interface.ViewBuilder +import NamedTuple.AnyNamedTuple +import scala.quoted.* // A basic interface declaration: structurally an "empty design" that carries // ports and views but no behavioral statements (no processes/connections/ @@ -19,9 +21,8 @@ import scala.annotation.Annotation // never has clk/rst injected, and the same `Interface` is reusable inside a // design of any domain. abstract class Interface - extends DomainContainer(DomainType.ED), - HasClsMetaArgs, - HasConstParams: + extends DomainContainer(DomainType.ED), HasClsMetaArgs, HasConstParams: + self => private[core] type TScope = DFC.Scope.Interface private[core] type TOwner = Design.Block final protected given TScope = DFC.Scope.Interface @@ -48,10 +49,149 @@ abstract class Interface hasStartedLate = true val paramEntries = Design.Inst.collectParamEntries val endedInterface = containedOwner.asIR + // TODO: check interface has at least one port or view, otherwise error dfc.exitOwner() Design.Inst(endedInterface, paramEntries) dfc.enterLate() final override def onCreateEnd(thisOwner: Option[This]): Unit = if (hasStartedLate) dfc.exitLate() else dfc.exitOwner() + protected object view: + transparent inline def apply(inline args: DFDclAny*)(using + DFC + ): ViewBuilder[? <: Interface, ? <: AnyNamedTuple] = ??? + // Entry points are macros (not delegations to the `ViewBuilder` extensions) + // so the port names are read from the literal `p1, p2` references at the + // call site rather than from a forwarded `Seq`. The enclosing interface type + // (e.g. `MyIfc`) is discovered by the macro; we deliberately do NOT use + // `self.type` here (the `self =>` alias leaks into member types and crashes + // a later transform phase), and the interface class type is the right choice + // anyway so views of distinct instances stay type-compatible. + transparent inline def in(inline args: DFDclAny*)(using + DFC + ): ViewBuilder[? <: Interface, ? <: AnyNamedTuple] = + ${ ViewBuilder.viewEntryMacro('args, true) } + transparent inline def out(inline args: DFDclAny*)(using + DFC + ): ViewBuilder[? <: Interface, ? <: AnyNamedTuple] = + ${ ViewBuilder.viewEntryMacro('args, false) } + end view + transparent inline def VIEW(using DFC): DFValOf[DFView[self.type, AnyNamedTuple]] = ??? +end Interface + +object Interface: + sealed class ViewBuilder[I <: Interface, F <: AnyNamedTuple]: + type VIEW = DFValOf[DFView[I, F]] + def VIEW(using DFC): DFValOf[DFView[I, F]] = ??? + + object ViewBuilder: + extension [I <: Interface, F <: AnyNamedTuple](vb: ViewBuilder[I, F]) + transparent inline def apply(inline args: DFDclAny*)(using + DFC + ): ViewBuilder[I, ? <: AnyNamedTuple] = + ??? + transparent inline def in(inline args: DFDclAny*)(using + DFC + ): ViewBuilder[I, ? <: AnyNamedTuple] = + ${ addPortsMacro[I, F]('args, true) } + transparent inline def out(inline args: DFDclAny*)(using + DFC + ): ViewBuilder[I, ? <: AnyNamedTuple] = + ${ addPortsMacro[I, F]('args, false) } + end extension + + // Chaining: the interface type `I` and current field tuple `F` come from the + // builder's type parameters. + def addPortsMacro[I <: Interface: Type, F <: AnyNamedTuple: Type]( + args: Expr[Seq[DFDclAny]], + isIn: Boolean + )(using Quotes): Expr[ViewBuilder[I, ? <: AnyNamedTuple]] = + import quotes.reflect.* + buildViewBuilder(TypeRepr.of[I], TypeRepr.of[F], args, isIn) + .asExprOf[ViewBuilder[I, ? <: AnyNamedTuple]] + + // Entry: the field tuple starts empty and the interface type is the enclosing + // interface class at the call site. + def viewEntryMacro( + args: Expr[Seq[DFDclAny]], + isIn: Boolean + )(using Quotes): Expr[ViewBuilder[? <: Interface, ? <: AnyNamedTuple]] = + import quotes.reflect.* + buildViewBuilder(enclosingInterfaceTpe, TypeRepr.of[NamedTuple.Empty], args, isIn) + + private def enclosingInterfaceTpe(using Quotes): quotes.reflect.TypeRepr = + import quotes.reflect.* + def loop(s: Symbol): TypeRepr = + if (!s.exists) + report.errorAndAbort("`view` can only be used inside an Interface.") + else if (s.isClassDef && s.typeRef <:< TypeRepr.of[Interface]) s.typeRef + else loop(s.owner) + loop(Symbol.spliceOwner) + + // Appends the given ports to the field named-tuple, each tagged with the + // IN/OUT direction, and returns a (currently empty) ViewBuilder carrying the + // extended named-tuple type. The runtime value holds no fields yet — only the + // type is computed here. + private def buildViewBuilder(using + Quotes + )( + iTpe: quotes.reflect.TypeRepr, + fTpe: quotes.reflect.TypeRepr, + args: Expr[Seq[DFDclAny]], + isIn: Boolean + ): Expr[ViewBuilder[? <: Interface, ? <: AnyNamedTuple]] = + import quotes.reflect.* + val argExprs: Seq[Expr[DFDclAny]] = args match + case Varargs(es) => es + case _ => + report.errorAndAbort("`view` ports must be passed as explicit arguments.") + // extract the (port name, port type) of each argument from the reference + def portInfo(expr: Expr[DFDclAny]): (String, TypeRepr) = + def loop(t: Term): Term = t match + case Inlined(_, _, inner) => loop(inner) + case Typed(inner, _) => loop(inner) + case Block(_, inner) => loop(inner) + case TypeApply(inner, _) => loop(inner) + case _ => t + val core = loop(expr.asTerm) + if (!core.symbol.exists) + report.errorAndAbort( + "`view` arguments must be references to interface ports.", + core.pos + ) + (core.symbol.name, core.tpe.widen) + val infos = argExprs.map(portInfo).toList + val newNameTpes: List[TypeRepr] = + infos.map((n, _) => ConstantType(StringConstant(n))) + val newValTpes: List[TypeRepr] = infos.map { (n, portTpe) => + portTpe.asType match + case '[DFVal[t, m]] => + if (isIn) TypeRepr.of[DFVal[t, Modifier.IN.type]] + else TypeRepr.of[DFVal[t, Modifier.OUT.type]] + case _ => + report.errorAndAbort(s"`$n` is not a DFHDL port value.") + } + // the builder's existing field names/value types + val (existingNameTpes, existingValTpes) = fTpe.asType match + case '[type f <: AnyNamedTuple; f] => + ( + TypeRepr.of[NamedTuple.Names[f]].getTupleArgs, + TypeRepr.of[NamedTuple.DropNames[f]].getTupleArgs + ) + case _ => report.errorAndAbort("internal error: expected a named tuple") + // build a tuple type from a list of element type reprs + def mkTupleType(elems: List[TypeRepr]): TypeRepr = + elems.foldRight(TypeRepr.of[EmptyTuple]) { (h, acc) => + (h.asType, acc.asType) match + case ('[h], '[type t <: Tuple; t]) => TypeRepr.of[h *: t] + case _ => report.errorAndAbort("internal error: view field tuple build") + } + val namesTpe = mkTupleType(existingNameTpes ++ newNameTpes) + val valsTpe = mkTupleType(existingValTpes ++ newValTpes) + (iTpe.asType, namesTpe.asType, valsTpe.asType) match + case ('[type i <: Interface; i], '[type n <: Tuple; n], '[type v <: Tuple; v]) => + '{ new ViewBuilder[i, NamedTuple.NamedTuple[n, v]]() } + case _ => report.errorAndAbort("internal error: view type build") + end buildViewBuilder + end ViewBuilder end Interface From 1fd12402bc5b715c52a0dcc011d6c2bee23bdb07 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Wed, 17 Jun 2026 18:55:49 +0300 Subject: [PATCH 52/72] Require interface ports and params to be `protected` Interface DFVal members (ports and const parameters) are internal wiring; external code should reach them only through a view's `.VIEW`. Enforce this at compile time in MetaContextPlacerPhase.prepareForValDef: any non-protected (and non-private) DFVal declared in a class extending Interface is rejected. The check runs post-typer so `<:< Interface` resolves transitively, catching ports declared in indirect subclasses that a pre-typer syntactic check would miss. Native `protected` access control then blocks `ifc.port` selections and hides the members from IDE autocomplete. Co-Authored-By: Claude Opus 4.8 --- .../scala/plugin/MetaContextPlacerPhase.scala | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala b/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala index 1d56482ca..4f6b79c65 100644 --- a/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala +++ b/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala @@ -45,6 +45,7 @@ class MetaContextPlacerPhase(setting: Setting) extends CommonPhase: var hasClsMetaArgsTpe: TypeRef = uninitialized var clsMetaArgsTpe: TypeRef = uninitialized var hasConstParamsTpe: TypeRef = uninitialized + var interfaceTpe: TypeRef = uninitialized var topAnnotSym: ClassSymbol = uninitialized var appTpe: TypeRef = uninitialized var noTopAnnotIsRequired: TypeRef = uninitialized @@ -310,6 +311,22 @@ class MetaContextPlacerPhase(setting: Setting) extends CommonPhase: cpy.Block(tree)(stats = List(updatedTypeDef), expr = tree.expr) case _ => tree + // Any DFVal member of an interface (port or const parameter) must be + // access-restricted so it is only reachable through a view's `.VIEW`. + override def prepareForValDef(tree: ValDef)(using Context): Context = + val sym = tree.symbol + if ( + sym.exists && sym.owner.isClass && sym.owner.typeRef <:< interfaceTpe && + tree.dfValTpeOpt.nonEmpty && !sym.isOneOf(Protected | Private) + ) + report.error( + """|Interface ports and parameters must be declared `protected`. + |They are internal to the interface; expose ports through a view and + |access them via `..VIEW`.""".stripMargin, + tree.srcPos + ) + ctx + // transform basic val x = y to val x = dfhdl.core.r__For_Plugin.identVal(y) if y is a DFVal override def transformValDef(tree: ValDef)(using Context): ValDef = object DFValIdent: @@ -348,6 +365,7 @@ class MetaContextPlacerPhase(setting: Setting) extends CommonPhase: hasClsMetaArgsTpe = requiredClassRef("dfhdl.internals.HasClsMetaArgs") clsMetaArgsTpe = requiredClassRef("dfhdl.internals.ClsMetaArgs") hasConstParamsTpe = requiredClassRef("dfhdl.core.HasConstParams") + interfaceTpe = requiredClassRef("dfhdl.core.Interface") topAnnotSym = requiredClass("dfhdl.top") appTpe = requiredClassRef("dfhdl.app.DFApp") noTopAnnotIsRequired = requiredClassRef("dfhdl.internals.NoTopAnnotIsRequired") From 12053681074824e965f83d63f23a2c417738a9bb Mon Sep 17 00:00:00 2001 From: Oron Port Date: Wed, 17 Jun 2026 23:26:51 +0300 Subject: [PATCH 53/72] change DFInterface type to have reference instead of name of the interface --- .../main/scala/dfhdl/compiler/ir/DFType.scala | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala index d7257da77..0c56b26a2 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala @@ -533,14 +533,11 @@ object DFTuple: // type is itself a DFInterface. A directed view over it is a DFView (below). ///////////////////////////////////////////////////////////////////////////// final case class DFInterface( - name: String, + interfaceRef: DFInterface.Ref, fieldMap: ListMap[String, DFType] -) extends NamedDFType, - ComposedDFType derives ReadWriter: +) extends ComposedDFType derives ReadWriter: type Data = Unit private def noTypeErr = throw new Exception(s"Unexpected access to $this data type") - def updateName(newName: String)(using MemberGetSet): this.type = - copy(name = newName).asInstanceOf[this.type] def widthIntOpt(using MemberGetSet): Option[Int] = None def createBubbleData(using MemberGetSet): Data = noTypeErr def isDataBubble(data: Data): Boolean = noTypeErr @@ -549,25 +546,31 @@ final case class DFInterface( def defaultData(using MemberGetSet): Data = noTypeErr protected def `prot_=~`(that: DFType)(using MemberGetSet): Boolean = that match case that: DFInterface => - this.name == that.name && + // interfaceRef is intentionally compared by regular equality (not `=~`) because + // it is a OneWay ref unified with the interface block's ownerRef. + this.interfaceRef == that.interfaceRef && this.fieldMap.lazyZip(that.fieldMap).forall { case ((fnL, ftL), (fnR, ftR)) => fnL == fnR && ftL =~ ftR } case _ => false def isSimilarTo(that: DFType)(using MemberGetSet): Boolean = that match case that: DFInterface => - this.name == that.name && this.fieldMap.lazyZip(that.fieldMap).forall { case ((fnL, ftL), (fnR, ftR)) => fnL == fnR && ftL.isSimilarTo(ftR) } case _ => false lazy val getRefs: List[DFRef.TypeRef] = fieldMap.values.flatMap(_.getRefs).toList + // NOTE: `interfaceRef` is a OneWay ref unified with the interface block's + // `ownerRef` (the sub-DB key); like `DFDesignInst.designRef` it is intentionally + // NOT freshened here. When cloning a design sub-tree the caller rebinds it to the + // cloned block's ownerRef. def copyWithNewRefs(using RefGen): this.type = copy( fieldMap = ListMap.from(fieldMap.view.mapValues(_.copyWithNewRefs)) ).asInstanceOf[this.type] end DFInterface -object DFInterface extends DFType.Companion[DFInterface, Unit] +object DFInterface extends DFType.Companion[DFInterface, Unit]: + type Ref = DFRef.OneWay[DFDesignBlock] ///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// From 3c3d09c089e869321e7e04c3c0361e2bfe80d574 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Wed, 17 Jun 2026 23:42:26 +0300 Subject: [PATCH 54/72] Protect against anonymous Interface class instances --- .../main/scala/plugin/MetaContextPlacerPhase.scala | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala b/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala index 4f6b79c65..40b96d119 100644 --- a/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala +++ b/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala @@ -61,9 +61,21 @@ class MetaContextPlacerPhase(setting: Setting) extends CommonPhase: report.error("DFHDL classes cannot be final.", tree.srcPos) else if (sym.is(CaseClass)) report.error("DFHDL classes cannot be case classes.", tree.srcPos) + // Reject user-written anonymous interface instances (e.g. `new MyIfc() {}`). + // An interface must be a named class so it has a stable identity (its + // `interfaceRef` design block). The plugin's own instance anon-classes are + // created in the transform pass (`transformApply`), which this prepare hook + // never re-traverses, so every anon interface class seen here is user-written. + else if (sym.isAnonymousClass && sym.typeRef <:< interfaceTpe) + report.error( + s"Cannot create an anonymous Interface class instance.\nInstantiate the class without a body (e.g. just `${sym.typeRef.parents.head.typeSymbol.name}()`)", + tree.srcPos + ) dfcArgStack = ContextArg.at(tree).get :: dfcArgStack case _ => + end match ctx + end prepareForTypeDef private def clsMetaArgsOverrideDef(owner: Symbol, clsMetaArgsTree: Tree)(using Context From 118a0ff04750cd0ba6d686311782586c7e7b7b27 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Thu, 18 Jun 2026 01:21:40 +0300 Subject: [PATCH 55/72] Add Interfaces user-guide chapter Document how to define and use interfaces: declaration, protected ports/params, views (in/out/flip/generic), instantiation and view projection, nested interfaces (AXI4-Lite), vector views, HDL mapping, and the restrictions summary. Wire the chapter into the nav. Co-Authored-By: Claude Opus 4.8 --- docs/user-guide/interfaces/index.md | 379 +++++++++++++++++++++++++++- mkdocs.yml | 2 +- 2 files changed, 379 insertions(+), 2 deletions(-) diff --git a/docs/user-guide/interfaces/index.md b/docs/user-guide/interfaces/index.md index 8b7518581..fb5f4ed64 100644 --- a/docs/user-guide/interfaces/index.md +++ b/docs/user-guide/interfaces/index.md @@ -1 +1,378 @@ -# Interfaces [WIP] {#interfaces} \ No newline at end of file +--- +typora-copy-images-to: ./ +--- +[](){#interfaces} +# Interfaces + +An *interface* groups a set of related ports (and the parameters that shape them) into +a single, reusable bundle, and describes, through one or more *views*, how that bundle +is driven from each side of a connection. Interfaces let you declare a protocol such as a +streaming handshake or an AXI bus *once*, then wire it between designs with a single `<>` +connection instead of dozens of per-port connections. + +
+ +/// admonition | Terminology + type: quote +* _interface_ - A Scala class extending `Interface` that bundles related ports and + parameters. It is purely structural: it carries ports and views but no behavior (no + processes, connections, or assignments). +* _port_ - A value declared inside an interface (e.g. `Bits(8) <> VAR`). Ports are + internal to the interface and are exposed only through a view. +* _view_ - A named, directional projection of an interface's ports. A view assigns each + port a direction (`in`/`out`) *from the point of view of the design that uses it*. An + interface may declare several views over the same ports. +* _flip_ - The converse of a view: every `in` becomes `out` and every `out` becomes `in`. + Used to derive the opposite side of a protocol (e.g. `subordinate = manager.flip`). +* _interface instance_ - An interface class instantiated inside a design, exactly like a + child design instance. +/// + +```scala linenums="0" title="A streaming handshake interface, used by two designs" +class Stream(width: Int <> CONST) extends Interface: + protected val data = Bits(width) <> VAR + protected val valid = Bit <> VAR + protected val ready = Bit <> VAR + // the producer drives data+valid and reads ready + val source = view.out(data, valid).in(ready) + // the consumer side is simply the converse + val sink = source.flip +``` + +
+ +## What Interfaces Are For + +Hardware protocols rarely consist of a single wire. A handshake carries `data`, `valid`, +and `ready`; an AXI channel carries a dozen signals that always travel together and always +have a fixed relative direction. Declaring and connecting those signals individually is +verbose and error-prone; a single flipped direction or forgotten port is a connectivity +bug. + +Interfaces solve this by letting you: + +* **Bundle related ports** into one named, parameterized unit that can be instantiated + anywhere a child design can. +* **Declare directionality once** with a *view*, and obtain the opposite side for free with + `flip` (so a `manager`/`subordinate` or `source`/`sink` pair is always consistent). +* **Connect an entire protocol with one `<>`**, with the connection checked port-by-port + through the whole (possibly nested) hierarchy. +* **Map to native target constructs** (SystemVerilog `interface`/`modport` and VHDL-2019 + mode views) while remaining portable to every tool through an automatic flattening + fallback (see [Generated HDL](#generated-hdl)). + +Interfaces are the DFHDL analog of a SystemVerilog `interface` with `modport`s, or a +VHDL-2019 record with mode `view`s. + +## Declaring an Interface + +### Syntax {#interface-dcl-syntax} + +An interface declaration follows standard [Scala class](https://docs.scala-lang.org/tour/classes.html){target="_blank"} +syntax and extends the `Interface` base class: + +```scala linenums="0" title="Interface declaration syntax" +/** _documentation_ */ +[_modifiers_] class _name_(_params_) extends Interface: + protected val _port_ = _type_ <> VAR // one or more ports + val _view_ = view._dir_(_ports_)... // one or more views +end _name_ //optional `end` marker +``` + +* __`_name_`__ - The Scala class name for the interface. As with designs, this name is + preserved by the DFHDL compiler and used in generated artifacts (the SystemVerilog + `interface` name / VHDL record name) and in error messages. See [naming][naming]. + +* __`(_params_)`__ - An optional parameter block, identical to a + [design parameter block][design-params-syntax]. DFHDL parameters (`_type_ <> CONST`) are + preserved through compilation and may shape port widths; pure Scala parameters are + inlined during elaboration. Interface parameters must be `protected` (see below). + +* __`_port_`__ - An interface port, declared just like a design [variable/port][Dcl] but + always with the `<> VAR` modifier. Ports are undirected at declaration; a view assigns + each port its direction. Ports must be `protected`. + +* __`_view_`__ - A named view built with the `view` builder (see [Views](#views)). + +Unlike a design, an interface is **domain-neutral**: there is a single `Interface` base +class rather than `DF`/`RT`/`ED` variants, and the same interface can be instantiated +inside a design of any [domain][design-domains]. + +### Ports and Parameters Must Be `protected` {#protected-rule} + +Every port and DFHDL parameter of an interface must be declared `protected` (or +`private`). This is enforced by the DFHDL compiler plugin: + +```scala linenums="0" +class BadIfc extends Interface: + val data = Bits(8) <> VAR // error: interface ports must be `protected` +``` + +```scala linenums="0" +class GoodIfc extends Interface: + protected val data = Bits(8) <> VAR // OK + val mon = view.out(data) // views stay public; they are the public surface +``` + +/// admonition | Why ports are protected + type: note +A view, not a raw port, is the public surface of an interface. Ports are the internal +wiring, reachable only through a view's `.VIEW` projection. Marking them `protected` means +Scala's own access control (enforced by the typechecker) blocks any `ifc.data` selection +from outside the interface, and the ports are hidden from IDE autocomplete. This keeps every +connection going through a *directed* view, so the compiler can check directionality. The +rule applies transitively: ports declared in a subclass of an interface must be `protected` +too. +/// + +### Example: a parameterized handshake {#stream-example} +/// admonition | A reusable streaming handshake interface + type: example +The `Stream` interface below bundles a `data` bus with a `valid`/`ready` handshake. The +`width` of the data bus is a DFHDL parameter, so it is preserved in the generated backend +code. Two views describe the two sides of the protocol: `source` (the producer) drives +`data` and `valid` and reads `ready`; `sink` (the consumer) is simply its converse. + +```scala linenums="0" +class Stream(protected val width: Int <> CONST = 8) extends Interface: + protected val data = Bits(width) <> VAR + protected val valid = Bit <> VAR + protected val ready = Bit <> VAR + val source = view.out(data, valid).in(ready) + val sink = source.flip +``` +/// + +## Views + +A *view* is a named, directional projection of an interface's ports. Each port that +participates in a view is given a direction with `view.in(...)` or `view.out(...)`, and an +interface may declare any number of views over the same ports (e.g. a producer view, a +consumer view, and a monitor view). + +### Defining views {#defining-views} + +Views are created with the `view` builder inside the interface body. Calls can be chained +to mix directions: + +```scala linenums="0" title="View builder forms" +val v1 = view.out(a, b) // a, b are outputs of the using design +val v2 = view.in(a, b, c) // a, b, c are inputs of the using design +val v3 = view.in(a, b).out(c) // mixed: a, b inputs; c output +``` + +The ports passed to `view.in`/`view.out` must be ports declared in *this* interface; the +builder reads them by reference. A view is itself a public `val`; it is the member you +reach through from outside the interface. + +### Direction perspective {#direction-perspective} + +A view's directions are written **from the point of view of the design that uses the +view**: + +* `view.out(p)`: port `p` is an **output** of the using design (the design *drives* `p`). +* `view.in(p)`: port `p` is an **input** to the using design (the design *reads* `p`). + +This is the same convention as a SystemVerilog `modport` (directions are from the consuming +module's viewpoint). It follows that the two ends of a connection must use *opposite* +directions for each shared port, which is exactly what `flip` produces. + +### `flip`: the converse view {#flip} + +`flip` returns the converse of a view: every `in` becomes `out` and every `out` becomes +`in`. It is the idiomatic way to declare the opposite side of a protocol without repeating +(and risking inconsistency in) the direction list: + +```scala linenums="0" +val source = view.out(data, valid).in(ready) +val sink = source.flip // in(data, valid).out(ready) +``` + +`flip` is involutive (`source.flip.flip` is `source`) and maps directly onto the target +languages: a VHDL-2019 `'converse` alias, or an inverted SystemVerilog `modport`. + +### The generic view {#generic-view} + +Every interface instance also exposes a generic, undirected view through `.VIEW` directly +on the instance (without naming a declared view). The generic view carries all ports with +no enforced direction and is useful for internal wiring where directionality is not being +checked. Prefer a named, directed view for design boundaries. + +## Using an Interface in a Design + +### Instantiation {#instantiation} + +An interface is instantiated inside a design exactly like a child design, by calling its +constructor. Empty parentheses are required even when there are no parameters: + +```scala linenums="0" +class Producer extends RTDesign: + val io = Stream(width = 8) // an interface instance + // ... +``` + +### Projecting a view: `instance.view.VIEW` {#projecting-a-view} + +To obtain a connectable value, select a declared view on the instance and call `.VIEW`. +This *projects* the view onto that specific instance: + +```scala linenums="0" +val src = io.source.VIEW // the producer side of `io` +val snk = io.sink.VIEW // the consumer side of `io` +``` + +Because the interface ports are `protected`, `io.data` is rejected by the compiler; all +access goes through a view's `.VIEW`. + +### Connecting views with `<>` {#connecting-views} + +Two views are connected with the [`<>` connection operator][connectivity], just like +scalar ports. The connection is checked **port-by-port through the entire (possibly nested) +hierarchy**: each shared port must have complementary directions on the two sides. Pairing +a view with its `flip` is therefore always a legal connection: + +```scala linenums="0" +class Link extends RTDesign: + val io = Stream(8) + val prod = Producer() + val cons = Consumer() + prod.out <> io.source.VIEW // producer drives the source side + cons.in <> io.sink.VIEW // consumer reads the sink side +``` + +A single `<>` here replaces one connection per port (`data`, `valid`, `ready`), and the +direction of every wire is guaranteed consistent by construction. + +/// admonition | Interfaces are purely structural + type: note +An interface body may contain only ports and views. Processes, `<>` connections, and `:=` +/`:==` assignments are **not** allowed inside an interface; these belong in designs. +Additionally, an interface must be a *named* class: anonymous instances such as +`new Stream() {}` are rejected by the compiler, because each interface needs a stable +identity to be emitted as a named SystemVerilog `interface` / VHDL record. +/// + +## Nested Interfaces {#nested-interfaces} + +An interface may instantiate other interfaces and compose their views into a higher-level +view. This is how multi-channel protocols such as AXI are modeled: one interface per +channel, then a top interface that nests them and composes their per-channel views with the +`view(...)` apply form (which takes already-directed sub-views). + +/// admonition | AXI4-Lite: a nested, multi-channel interface + type: example +Each AXI4-Lite channel is its own interface with a `manager` view (directions written from +the manager/master point of view) and a `subordinate = manager.flip`. The top `Axi4Lite` +interface nests the five channels and composes their `manager` views into a single +`manager` view; `subordinate` flips recursively through every nested channel. + +```scala linenums="0" +// One interface per AXI4-Lite channel (write-address channel shown). +class Axi4LiteAW(protected val addrWidth: Int <> CONST) extends Interface: + protected val AWADDR = UInt(addrWidth) <> VAR + protected val AWPROT = Bits(3) <> VAR + protected val AWVALID = Bit <> VAR + protected val AWREADY = Bit <> VAR + val manager = view.out(AWADDR, AWPROT, AWVALID).in(AWREADY) + val subordinate = manager.flip +// ... Axi4LiteW, Axi4LiteB, Axi4LiteAR, Axi4LiteR similarly ... + +// Top interface nesting the five channels and composing their views. +class Axi4Lite( + protected val addrWidth: Int <> CONST, + protected val dataWidth: Int <> CONST +) extends Interface: + protected val aw = Axi4LiteAW(addrWidth) + protected val w = Axi4LiteW(dataWidth) + protected val b = Axi4LiteB() + protected val ar = Axi4LiteAR(addrWidth) + protected val r = Axi4LiteR(dataWidth) + val manager = view(aw.manager, w.manager, b.manager, ar.manager, r.manager) + val subordinate = manager.flip // flips recursively through every nested channel +``` + +A manager and a subordinate side then connect with a single `<>`: + +```scala linenums="0" +val bus = Axi4Lite(addrWidth = 32, dataWidth = 32) +val mgr = bus.manager.VIEW +val sub = bus.subordinate.VIEW +mgr <> sub // legal: matches port-per-port through all five nested channels +``` +/// + +## Vector Views {#vector-views} + +A view can be replicated into a vector with the `X` operator, mirroring DFHDL +[vector construction][type-system]. This is useful for interconnects, for example a +1-to-N crossbar that exposes N manager-side buses. Individual elements are selected by +index: + +```scala linenums="0" +val managers = Axi4Lite(32, 32).manager.VIEW X 4 // a vector of 4 manager-side views +val m0 = managers(0) // select element 0 +``` + +## How Interfaces Map to Generated HDL {#generated-hdl} + +DFHDL emits interfaces in one of two ways, depending on the target dialect. + +**Portable default (flattening).** By default, the compiler *flattens* interfaces and views +into plain scalar ports and per-port connections before backend emission. This produces +valid, synthesizable HDL on **every** supported tool, with no reliance on +`interface`/`modport` or mode-view support. Nested interfaces are expanded with prefixed +signal names (`aw_AWADDR`, …). The top-level design boundary is *always* flattened to scalar +ports, regardless of dialect. + +**Native emission (opt-in).** For dialects that support them, interfaces can be emitted +natively, preserving the structure: + +/// tab | SystemVerilog (SV2017/SV2019) +A DFHDL interface becomes a SystemVerilog `interface`, each view becomes a `modport`, and +`flip` becomes a `modport` with inverted directions: + +```systemverilog linenums="0" +interface Axi4LiteAW #(parameter ADDR_WIDTH = 32) (); + logic [ADDR_WIDTH-1:0] AWADDR; + logic [2:0] AWPROT; + logic AWVALID; + logic AWREADY; + modport manager (output AWADDR, AWPROT, AWVALID, input AWREADY); + modport subordinate (input AWADDR, AWPROT, AWVALID, output AWREADY); +endinterface +``` + +> Nested interfaces are not portable for synthesis (e.g. Vivado does not support them), so +> they are flattened unless the target is known to accept them. +/// + +/// tab | VHDL-2019 +A DFHDL interface becomes a `record` type, each view becomes a mode `view`, and `flip` +becomes a `'converse` alias: + +```vhdl linenums="0" +type axi4lite_aw_t is record + AWADDR : unsigned; + AWPROT : std_logic_vector(2 downto 0); + AWVALID : std_logic; + AWREADY : std_logic; +end record; + +view axi4lite_aw_manager of axi4lite_aw_t is + AWADDR, AWPROT, AWVALID : out; + AWREADY : in; +end view; +alias axi4lite_aw_subordinate is axi4lite_aw_manager'converse; +``` + +> VHDL-2019 mode views are currently supported mainly by NVC. The portable default +> therefore stays flattened for VHDL; native mode-view emission is opt-in. +/// + +## Restrictions Summary {#restrictions} + +* Interface ports and DFHDL parameters must be `protected` (or `private`). +* An interface body may contain only ports and views: no processes, connections, or + assignments. +* Interfaces must be named classes; anonymous instances (`new Ifc() {}`) are rejected. +* External access to ports is only through a view's `.VIEW` projection. diff --git a/mkdocs.yml b/mkdocs.yml index 6c6a25cc4..56f65fb39 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -80,7 +80,7 @@ nav: - Conditionals: user-guide/conditionals/index.md - Processes: user-guide/processes/index.md - Domain Abstractions: user-guide/design-domains/index.md - # - Interfaces [WIP]: user-guide/interfaces/index.md + - Interfaces: user-guide/interfaces/index.md # - Domains [WIP]: user-guide/domains/index.md # - Scopes [WIP]: user-guide/scopes/index.md - Naming: user-guide/naming/index.md From 62d397e0c85c2bc494d188bb5c9b7c2c9e4e4bca Mon Sep 17 00:00:00 2001 From: Oron Port Date: Thu, 18 Jun 2026 01:21:54 +0300 Subject: [PATCH 56/72] Reject init/initFile inside an Interface An interface is purely structural and carries no initial values. Add a NotInsideInterface[A] compile-time constraint (AssertGiven over the declaration scope encoded in the modifier's `A` type parameter) and apply it to both `init` overloads and `initFile`, producing a clear error message at the call site. Co-Authored-By: Claude Opus 4.8 --- core/src/main/scala/dfhdl/core/DFVal.scala | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/core/src/main/scala/dfhdl/core/DFVal.scala b/core/src/main/scala/dfhdl/core/DFVal.scala index 523a5f736..4056371e7 100644 --- a/core/src/main/scala/dfhdl/core/DFVal.scala +++ b/core/src/main/scala/dfhdl/core/DFVal.scala @@ -380,6 +380,13 @@ object DFVal extends DFValLP: "Can only initialize a DFHDL port or variable that are not already initialized." ] ): InitCheck[I] with {} + // An interface is purely structural and carries no initial values, so `init` + // (and `initFile`) are rejected on its ports/variables. The declaration scope + // is carried in the modifier's `A` type parameter (see `<>` in `Modifier`). + protected type NotInsideInterface[A] = AssertGiven[ + util.NotGiven[A <:< DFC.Scope.Interface], + "Cannot initialize a port or variable inside an interface.\nAn interface is purely structural and carries no initial values." + ] extension [T <: DFTypeAny, M <: ModifierAny](dfVal: DFVal[T, M]) @metaContextForward(0) @@ -566,7 +573,11 @@ object DFVal extends DFValLP: infix def init( initValues: InitValue[T]* - )(using DFC, InitCheck[I]): DFVal[T, Modifier[A, C, Modifier.Initialized, P]] = trydf { + )(using + DFC, + InitCheck[I], + NotInsideInterface[A] + ): DFVal[T, Modifier[A, C, Modifier.Initialized, P]] = trydf { val tvList = initValues.view.filter(_.enable).map(tv => tv(dfVal.dfType)(using dfc.anonymize)).toList dfVal.initForced(tvList) @@ -575,7 +586,11 @@ object DFVal extends DFValLP: extension [T <: NonEmptyTuple, A, C, I, P](dfVal: DFVal[DFTuple[T], Modifier[A, C, I, P]]) infix def init( initValues: InitTupleValues[T] - )(using DFC, InitCheck[I]): DFVal[DFTuple[T], Modifier[A, C, Modifier.Initialized, P]] = + )(using + DFC, + InitCheck[I], + NotInsideInterface[A] + ): DFVal[DFTuple[T], Modifier[A, C, Modifier.Initialized, P]] = trydf { if (initValues.enable) dfVal.initForced(initValues(dfVal.dfType)(using dfc.anonymize)) @@ -591,7 +606,8 @@ object DFVal extends DFValLP: undefinedValue: ir.InitFileUndefinedValue = ir.InitFileUndefinedValue.Zeros )(using dfc: DFC, - check: InitCheck[I] + check: InitCheck[I], + notInsideInterface: NotInsideInterface[A] ): DFVal[DFVector[T, Tuple1[D1]], Modifier[A, C, Modifier.Initialized, P]] = trydf: import dfc.getSet val vectorType = dfVal.dfType From 5d049d4b5bf4d2ef19b240cb10df2e859af881f2 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Thu, 18 Jun 2026 11:19:25 +0300 Subject: [PATCH 57/72] Document anchored-direction ports and projection terminals Fold the anchored-direction port and projection-terminal design into the user-guide interfaces chapter: new terminology entries, an Anchored-Direction Ports section (flippable vs anchored, the two view rules, and sibling vs parent-to-child connection behavior), a projection-terminals table (VIEW/FLIP/FLIPALL/MONITOR/DRIVER), and updated restrictions. Co-Authored-By: Claude Opus 4.8 --- docs/user-guide/interfaces/index.md | 103 +++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 8 deletions(-) diff --git a/docs/user-guide/interfaces/index.md b/docs/user-guide/interfaces/index.md index fb5f4ed64..4dc8ce9e8 100644 --- a/docs/user-guide/interfaces/index.md +++ b/docs/user-guide/interfaces/index.md @@ -19,11 +19,15 @@ connection instead of dozens of per-port connections. processes, connections, or assignments). * _port_ - A value declared inside an interface (e.g. `Bits(8) <> VAR`). Ports are internal to the interface and are exposed only through a view. +* _flippable port_ - A `<> VAR` port: undirected at declaration, given a *relative* + direction by each view (opposite on the two ends of a connection). This is the default. +* _anchored-direction port_ - A `<> IN` or `<> OUT` port: its direction is *absolute* (the + same for every side, never reversed by `flip`), like a clock or reset. * _view_ - A named, directional projection of an interface's ports. A view assigns each port a direction (`in`/`out`) *from the point of view of the design that uses it*. An interface may declare several views over the same ports. -* _flip_ - The converse of a view: every `in` becomes `out` and every `out` becomes `in`. - Used to derive the opposite side of a protocol (e.g. `subordinate = manager.flip`). +* _flip_ - The converse of a view: every flippable `in` becomes `out` and every `out` becomes + `in`. Used to derive the opposite side of a protocol (e.g. `subordinate = manager.flip`). * _interface instance_ - An interface class instantiated inside a design, exactly like a child design instance. /// @@ -88,9 +92,11 @@ end _name_ //optional `end` marker preserved through compilation and may shape port widths; pure Scala parameters are inlined during elaboration. Interface parameters must be `protected` (see below). -* __`_port_`__ - An interface port, declared just like a design [variable/port][Dcl] but - always with the `<> VAR` modifier. Ports are undirected at declaration; a view assigns - each port its direction. Ports must be `protected`. +* __`_port_`__ - An interface port, declared just like a design [variable/port][Dcl]. A + `<> VAR` port is *flippable*: undirected at declaration, with a view assigning its direction. + A `<> IN` or `<> OUT` port is *anchored*: its direction is fixed at declaration and never + reversed by a view or by `flip` (see [Anchored-Direction Ports](#anchored-ports)). Ports + must be `protected`. * __`_view_`__ - A named view built with the `view` builder (see [Views](#views)). @@ -196,7 +202,54 @@ languages: a VHDL-2019 `'converse` alias, or an inverted SystemVerilog `modport` Every interface instance also exposes a generic, undirected view through `.VIEW` directly on the instance (without naming a declared view). The generic view carries all ports with no enforced direction and is useful for internal wiring where directionality is not being -checked. Prefer a named, directed view for design boundaries. +checked. Prefer a named, directed view for design boundaries. A generic projection also +supports the `.MONITOR` and `.DRIVER` terminals (see [Projection terminals](#projection-terminals)); +it has no `.FLIP`/`.FLIPALL`, because its ports carry no direction to flip. + +## Anchored-Direction Ports {#anchored-ports} + +Most interface ports are *flippable* (`<> VAR`): they have no direction of their own, and each +view gives them a *relative* direction that `flip` reverses. Some signals, though, have an +*absolute* direction that is the same for every side and must never be reversed. The canonical +example is a clock or reset: it enters an interface one way and is observed identically by +everyone connected to it. + +For these, declare the port with an *anchored* direction, `<> IN` or `<> OUT`, instead of +`<> VAR`: + +```scala linenums="0" +class Bus(protected val width: Int <> CONST) extends Interface: + protected val clk = Bit <> IN // anchored: always an input + protected val data = Bits(width) <> VAR // flippable + protected val valid = Bit <> VAR // flippable + val manager = view.in(clk).out(data, valid) + val subordinate = manager.flip // data/valid flip; clk stays `in` +``` + +An anchored port is the DFHDL counterpart of a SystemVerilog interface *header port* (the +`(input clk, ...)` list): the direction belongs to the signal itself, not to the view. + +Anchored ports follow two rules: + +* **A view may use an anchored port only in its declared direction.** Adding a `<> OUT` port + with `view.in(...)` (or a `<> IN` port with `view.out(...)`) is a compile error. Flippable + ports, by contrast, take whichever direction the view assigns. +* **`flip` leaves anchored ports untouched.** It reverses only the flippable ports of a view + (recursing through nested views), so an anchored `clk : in` stays `in` on both the `manager` + and the `subordinate` side. + +### How anchored ports affect connections {#anchored-connections} + +Because an anchored port keeps its direction on both sides, what a `<>` connection wires up +depends on where the connection is made: + +* **Between sibling designs** (a view connected to its `flip`): only the *flippable* ports are + connected. Anchored ports are left for you to wire separately, since both ends see the same + direction (two `clk : in` ports cannot drive each other); a shared clock is typically routed + from a common source. +* **From a parent design down into a child** (a view connected to the same-orientation view, no + `flip`): *all* ports are connected, flippable and anchored alike, so the parent passes its + anchored inputs (such as `clk`/`rst`) down into the child. ## Using an Interface in a Design @@ -222,7 +275,32 @@ val snk = io.sink.VIEW // the consumer side of `io` ``` Because the interface ports are `protected`, `io.data` is rejected by the compiler; all -access goes through a view's `.VIEW`. +access goes through a view's projection terminal. + +#### Projection terminals {#projection-terminals} + +`.VIEW` is one of a small family of *projection terminals*. Each one projects the view onto the +instance and bakes in a directionality, so you choose the terminal instead of transforming the +view first. On a named view: + +| Terminal | Projects the view... | +|---|---| +| `.VIEW` | as declared | +| `.FLIP` | flipped, anchored ports respected (the same as projecting `view.flip`) | +| `.FLIPALL` | flipped, anchored ports inverted too | +| `.MONITOR` | with *every* port forced to an input (observe everything) | +| `.DRIVER` | with *every* port forced to an output (drive everything) | + +`.FLIP` is the safe, anchor-respecting flip. `.FLIPALL`, `.MONITOR`, and `.DRIVER` are +*overrides*: they ignore the anchored/flippable distinction, so reach for them deliberately +(for example, a `.MONITOR` tap that observes a whole bus, or a `.DRIVER` stimulus block). +Unlike `flip`, these overrides exist *only* as projection terminals: there is no reusable +"forced" view, so an override is always visible at the connection site and can never be hidden +inside a shared named view. + +On the [generic projection](#generic-view) only `.VIEW`, `.MONITOR`, and `.DRIVER` are +available: the flip terminals need declared relative directions to act on, and a generic +projection's ports carry none, so there is nothing for `.FLIP`/`.FLIPALL` to reverse. ### Connecting views with `<>` {#connecting-views} @@ -243,6 +321,10 @@ class Link extends RTDesign: A single `<>` here replaces one connection per port (`data`, `valid`, `ready`), and the direction of every wire is guaranteed consistent by construction. +If the interface has [anchored-direction ports](#anchored-connections), what a `<>` connects +depends on the connection site: a sibling-to-sibling connection wires only the flippable ports, +while a parent-to-child connection wires all ports, including the anchored ones. + /// admonition | Interfaces are purely structural type: note An interface body may contain only ports and views. Processes, `<>` connections, and `:=` @@ -374,5 +456,10 @@ alias axi4lite_aw_subordinate is axi4lite_aw_manager'converse; * Interface ports and DFHDL parameters must be `protected` (or `private`). * An interface body may contain only ports and views: no processes, connections, or assignments. +* Ports cannot be initialized (`init`/`initFile`) inside an interface; an interface is purely + structural and carries no initial values. +* An anchored-direction port (`<> IN`/`<> OUT`) may be added to a view only in its declared + direction, and `flip` never reverses it. * Interfaces must be named classes; anonymous instances (`new Ifc() {}`) are rejected. -* External access to ports is only through a view's `.VIEW` projection. +* External access to ports is only through a view's projection terminal (`.VIEW`, `.FLIP`, + `.MONITOR`, etc.). From 3bf2d7a10b247b9c93c07e0548f07a331eb1c9ed Mon Sep 17 00:00:00 2001 From: Oron Port Date: Thu, 18 Jun 2026 11:58:10 +0300 Subject: [PATCH 58/72] Rename projection terminal VIEW to ASIS in interfaces chapter Align the user-guide with the finalized projection-terminal family (ASIS/FLIP/FLIPALL/MONITOR/DRIVER), renaming the plain `.VIEW` terminal to `.ASIS` so every terminal is parallel and maps 1:1 to the IR Terminal enum. Co-Authored-By: Claude Opus 4.8 --- docs/user-guide/interfaces/index.md | 30 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/user-guide/interfaces/index.md b/docs/user-guide/interfaces/index.md index 4dc8ce9e8..8bf2939b2 100644 --- a/docs/user-guide/interfaces/index.md +++ b/docs/user-guide/interfaces/index.md @@ -123,7 +123,7 @@ class GoodIfc extends Interface: /// admonition | Why ports are protected type: note A view, not a raw port, is the public surface of an interface. Ports are the internal -wiring, reachable only through a view's `.VIEW` projection. Marking them `protected` means +wiring, reachable only through a view's `.ASIS` projection. Marking them `protected` means Scala's own access control (enforced by the typechecker) blocks any `ifc.data` selection from outside the interface, and the ports are hidden from IDE autocomplete. This keeps every connection going through a *directed* view, so the compiler can check directionality. The @@ -199,7 +199,7 @@ languages: a VHDL-2019 `'converse` alias, or an inverted SystemVerilog `modport` ### The generic view {#generic-view} -Every interface instance also exposes a generic, undirected view through `.VIEW` directly +Every interface instance also exposes a generic, undirected view through `.ASIS` directly on the instance (without naming a declared view). The generic view carries all ports with no enforced direction and is useful for internal wiring where directionality is not being checked. Prefer a named, directed view for design boundaries. A generic projection also @@ -264,14 +264,14 @@ class Producer extends RTDesign: // ... ``` -### Projecting a view: `instance.view.VIEW` {#projecting-a-view} +### Projecting a view: `instance.view.ASIS` {#projecting-a-view} -To obtain a connectable value, select a declared view on the instance and call `.VIEW`. +To obtain a connectable value, select a declared view on the instance and call `.ASIS`. This *projects* the view onto that specific instance: ```scala linenums="0" -val src = io.source.VIEW // the producer side of `io` -val snk = io.sink.VIEW // the consumer side of `io` +val src = io.source.ASIS // the producer side of `io` +val snk = io.sink.ASIS // the consumer side of `io` ``` Because the interface ports are `protected`, `io.data` is rejected by the compiler; all @@ -279,13 +279,13 @@ access goes through a view's projection terminal. #### Projection terminals {#projection-terminals} -`.VIEW` is one of a small family of *projection terminals*. Each one projects the view onto the +`.ASIS` is one of a small family of *projection terminals*. Each one projects the view onto the instance and bakes in a directionality, so you choose the terminal instead of transforming the view first. On a named view: | Terminal | Projects the view... | |---|---| -| `.VIEW` | as declared | +| `.ASIS` | as declared | | `.FLIP` | flipped, anchored ports respected (the same as projecting `view.flip`) | | `.FLIPALL` | flipped, anchored ports inverted too | | `.MONITOR` | with *every* port forced to an input (observe everything) | @@ -298,7 +298,7 @@ Unlike `flip`, these overrides exist *only* as projection terminals: there is no "forced" view, so an override is always visible at the connection site and can never be hidden inside a shared named view. -On the [generic projection](#generic-view) only `.VIEW`, `.MONITOR`, and `.DRIVER` are +On the [generic projection](#generic-view) only `.ASIS`, `.MONITOR`, and `.DRIVER` are available: the flip terminals need declared relative directions to act on, and a generic projection's ports carry none, so there is nothing for `.FLIP`/`.FLIPALL` to reverse. @@ -314,8 +314,8 @@ class Link extends RTDesign: val io = Stream(8) val prod = Producer() val cons = Consumer() - prod.out <> io.source.VIEW // producer drives the source side - cons.in <> io.sink.VIEW // consumer reads the sink side + prod.out <> io.source.ASIS // producer drives the source side + cons.in <> io.sink.ASIS // consumer reads the sink side ``` A single `<>` here replaces one connection per port (`data`, `valid`, `ready`), and the @@ -377,8 +377,8 @@ A manager and a subordinate side then connect with a single `<>`: ```scala linenums="0" val bus = Axi4Lite(addrWidth = 32, dataWidth = 32) -val mgr = bus.manager.VIEW -val sub = bus.subordinate.VIEW +val mgr = bus.manager.ASIS +val sub = bus.subordinate.ASIS mgr <> sub // legal: matches port-per-port through all five nested channels ``` /// @@ -391,7 +391,7 @@ A view can be replicated into a vector with the `X` operator, mirroring DFHDL index: ```scala linenums="0" -val managers = Axi4Lite(32, 32).manager.VIEW X 4 // a vector of 4 manager-side views +val managers = Axi4Lite(32, 32).manager.ASIS X 4 // a vector of 4 manager-side views val m0 = managers(0) // select element 0 ``` @@ -461,5 +461,5 @@ alias axi4lite_aw_subordinate is axi4lite_aw_manager'converse; * An anchored-direction port (`<> IN`/`<> OUT`) may be added to a view only in its declared direction, and `flip` never reverses it. * Interfaces must be named classes; anonymous instances (`new Ifc() {}`) are rejected. -* External access to ports is only through a view's projection terminal (`.VIEW`, `.FLIP`, +* External access to ports is only through a view's projection terminal (`.ASIS`, `.FLIP`, `.MONITOR`, etc.). From dee8bea7aae46a4f515772c24bed8a1b8fb7651a Mon Sep 17 00:00:00 2001 From: Oron Port Date: Thu, 18 Jun 2026 12:22:24 +0300 Subject: [PATCH 59/72] Model anchored ports and view projections in the IR Update the interface/view IR types to the finalized design: - DFInterface.fieldMap now carries a per-field direction via a new DFInterface.Field(dfType, dir) (VAR = flippable, IN/OUT = anchored). Field extends HasRefCompare so the field map compares with `=~`. - DFView collapses to a single concrete type (drop AsIs/Flipped). It stores only its overlay over the interface: a leaf `dirMap` plus a `nestedMap` of chosen sub-views, with a derived `projectedFieldMap` that merges in the field types on demand (no duplication). Adds the flip/flipAll/monitor/driver direction transforms; flip leaves anchored leaves untouched, flipAll is the true VHDL 'converse. - Add Modifier.Terminal{AsIs,Flip,FlipAll,Monitor,Driver}, reshape ViewSite to Template | Projection(terminal), and add the IR-only Dir.VIEW(site) marker. Co-Authored-By: Claude Opus 4.8 --- .../scala/dfhdl/compiler/ir/DFMember.scala | 13 ++ .../main/scala/dfhdl/compiler/ir/DFType.scala | 171 ++++++++++-------- 2 files changed, 113 insertions(+), 71 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala index b9c0bf84b..471862f8e 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala @@ -282,8 +282,21 @@ object DFVal: def isPort: Boolean = mod.dir match case Modifier.IN | Modifier.OUT | Modifier.INOUT => true case _ => false + // The projection terminal applied to a view at a use site (1:1 with the + // frontend terminals ASIS / FLIP / FLIPALL / MONITOR / DRIVER). + enum Terminal derives CanEqual, ReadWriter: + case AsIs, Flip, FlipAll, Monitor, Driver + // Where a view value lives: a `Template` declared inside the interface, or a + // `Projection` of a template onto an instance via a given terminal. + enum ViewSite derives CanEqual, ReadWriter: + case Template + case Projection(terminal: Terminal) enum Dir derives CanEqual, ReadWriter: case VAR, IN, OUT, INOUT + // IR-only: marks a `DFView`-typed declaration as a view template/projection. + // Never produced by a user port declaration. Parametrized, so it is auto- + // excluded by the wildcard fall-through in the `isPort`/`isVar`/... predicates. + case VIEW(site: ViewSite) export Dir.{VAR, IN, OUT, INOUT} enum Special derives CanEqual, ReadWriter: case Ordinary, REG, SHARED diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala index 0c56b26a2..8f6993402 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala @@ -534,7 +534,7 @@ object DFTuple: ///////////////////////////////////////////////////////////////////////////// final case class DFInterface( interfaceRef: DFInterface.Ref, - fieldMap: ListMap[String, DFType] + fieldMap: ListMap[String, DFInterface.Field] ) extends ComposedDFType derives ReadWriter: type Data = Unit private def noTypeErr = throw new Exception(s"Unexpected access to $this data type") @@ -548,15 +548,12 @@ final case class DFInterface( case that: DFInterface => // interfaceRef is intentionally compared by regular equality (not `=~`) because // it is a OneWay ref unified with the interface block's ownerRef. - this.interfaceRef == that.interfaceRef && - this.fieldMap.lazyZip(that.fieldMap).forall { case ((fnL, ftL), (fnR, ftR)) => - fnL == fnR && ftL =~ ftR - } + this.interfaceRef == that.interfaceRef && this.fieldMap =~ that.fieldMap case _ => false def isSimilarTo(that: DFType)(using MemberGetSet): Boolean = that match case that: DFInterface => - this.fieldMap.lazyZip(that.fieldMap).forall { case ((fnL, ftL), (fnR, ftR)) => - fnL == fnR && ftL.isSimilarTo(ftR) + this.fieldMap.lazyZip(that.fieldMap).forall { case ((fnL, flL), (fnR, frR)) => + fnL == fnR && flL.dfType.isSimilarTo(frR.dfType) && flL.dir == frR.dir } case _ => false lazy val getRefs: List[DFRef.TypeRef] = fieldMap.values.flatMap(_.getRefs).toList @@ -571,78 +568,110 @@ end DFInterface object DFInterface extends DFType.Companion[DFInterface, Unit]: type Ref = DFRef.OneWay[DFDesignBlock] + // A field of an interface (or a resolved view): its DFType plus its direction. + // For a leaf port `dir` is the declared/resolved direction (VAR = flippable, + // IN/OUT = anchored). For a nested field `dfType` is itself a DFInterface (in a + // declaration) or a DFView (in a resolved view), which is self-describing, so + // `dir` is unused. Extending `HasRefCompare` gives `=~`/`getRefs`/`copyWithNewRefs` + // and lets a `ListMap[String, Field]` be compared with `=~` directly. + final case class Field(dfType: DFType, dir: DFVal.Modifier.Dir) + extends HasRefCompare[Field] derives ReadWriter: + protected def `prot_=~`(that: Field)(using MemberGetSet): Boolean = + this.dfType =~ that.dfType && this.dir == that.dir + lazy val getRefs: List[DFRef.TypeRef] = dfType.getRefs + def copyWithNewRefs(using RefGen): this.type = + copy(dfType = dfType.copyWithNewRefs).asInstanceOf[this.type] ///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// // DFView // ------ // A directed view over a DFInterface (the IR of a SV modport / VHDL mode view). -// `AsIs` carries a per-leaf-port direction overlay (`dirMap`); a nested-view -// field's directions live in its own DFView type inside `interfaceType`. -// `Flipped` is the converse of another view (the IR analog of VHDL `'converse`), -// inverting directions recursively. The frontend `.flip` normalizes -// Flipped(Flipped(v)) -> v, so `Flipped` always wraps a base `AsIs` view. -///////////////////////////////////////////////////////////////////////////// -sealed trait DFView extends NamedDFType, ComposedDFType derives ReadWriter: - def interfaceType: DFInterface +// A single concrete type carrying the *resolved* per-field directions in +// `fieldMap` (parallel to the interface's shape): a leaf field is (scalar, dir); +// a nested field is (nested DFView, _) and is self-describing. The direction +// transforms recompute a new view, recursing into nested views: `flip` consults +// the *declared* dirs in `interfaceType` to leave anchored (IN/OUT-declared) +// leaves untouched, while `flipAll`/`monitor`/`driver` ignore that distinction. +///////////////////////////////////////////////////////////////////////////// +final case class DFView( + interfaceType: DFInterface, + name: String, + // direction overlay over `interfaceType`, for LEAF ports only. The field + // DFTypes are NOT repeated here — they live in `interfaceType`. + dirMap: Map[String, DFVal.Modifier.Dir], + // the chosen sub-view per nested field (whose declared type in `interfaceType` + // is an undirected nested DFInterface). This is the only structure a view adds + // beyond `interfaceType`, so nothing is reduplicated. + nestedMap: ListMap[String, DFView] +) extends NamedDFType, ComposedDFType derives ReadWriter: type Data = Unit private def noTypeErr = throw new Exception(s"Unexpected access to $this data type") - final def widthIntOpt(using MemberGetSet): Option[Int] = None - final def createBubbleData(using MemberGetSet): Data = noTypeErr - final def isDataBubble(data: Data): Boolean = noTypeErr - final def dataToBitsData(data: Data)(using MemberGetSet): (BitVector, BitVector) = noTypeErr - final def bitsDataToData(data: (BitVector, BitVector))(using MemberGetSet): Data = noTypeErr - final def defaultData(using MemberGetSet): Data = noTypeErr - -object DFView: - // A base (directly-defined) view: `dirMap` assigns a direction to each leaf - // port of `interfaceType`; nested-view fields carry their own directions. - final case class AsIs( - interfaceType: DFInterface, - instName: String, - name: String, - dirMap: Map[String, DFVal.Modifier.Dir] - ) extends DFView: - def updateName(newName: String)(using MemberGetSet): this.type = - copy(name = newName).asInstanceOf[this.type] - protected def `prot_=~`(that: DFType)(using MemberGetSet): Boolean = that match - case that: AsIs => - this.interfaceType =~ that.interfaceType && - this.instName == that.instName && - this.name == that.name && - this.dirMap.size == that.dirMap.size && - this.dirMap.forall { case (k, d) => that.dirMap.get(k).contains(d) } - case _ => false - def isSimilarTo(that: DFType)(using MemberGetSet): Boolean = that match - case that: AsIs => - this.interfaceType.isSimilarTo(that.interfaceType) && - this.name == that.name && - this.dirMap.size == that.dirMap.size && - this.dirMap.forall { case (k, d) => that.dirMap.get(k).contains(d) } - case _ => false - lazy val getRefs: List[DFRef.TypeRef] = interfaceType.getRefs - def copyWithNewRefs(using RefGen): this.type = - copy(interfaceType = interfaceType.copyWithNewRefs).asInstanceOf[this.type] - end AsIs - - // The converse of a base view (≙ VHDL `'converse`): same structure, all - // directions inverted recursively. - final case class Flipped(name: String, view: DFView.AsIs) extends DFView: - def interfaceType: DFInterface = view.interfaceType - def updateName(newName: String)(using MemberGetSet): this.type = - copy(name = newName).asInstanceOf[this.type] - protected def `prot_=~`(that: DFType)(using MemberGetSet): Boolean = that match - case that: Flipped => - this.name == that.name && this.view =~ that.view - case _ => false - def isSimilarTo(that: DFType)(using MemberGetSet): Boolean = that match - case that: Flipped => - this.name == that.name && this.view.isSimilarTo(that.view) - case _ => false - lazy val getRefs: List[DFRef.TypeRef] = view.getRefs - def copyWithNewRefs(using RefGen): this.type = - copy(view = view.copyWithNewRefs).asInstanceOf[this.type] - end Flipped + def widthIntOpt(using MemberGetSet): Option[Int] = None + def createBubbleData(using MemberGetSet): Data = noTypeErr + def isDataBubble(data: Data): Boolean = noTypeErr + def dataToBitsData(data: Data)(using MemberGetSet): (BitVector, BitVector) = noTypeErr + def bitsDataToData(data: (BitVector, BitVector))(using MemberGetSet): Data = noTypeErr + def defaultData(using MemberGetSet): Data = noTypeErr + def updateName(newName: String)(using MemberGetSet): this.type = + copy(name = newName).asInstanceOf[this.type] + // The full, directed field map of this view: `interfaceType`'s structure with the + // resolved directions merged in (leaf dirs from `dirMap`; nested fields replaced by + // their chosen sub-view). Derived on demand, so nothing is stored redundantly. + lazy val projectedFieldMap: ListMap[String, DFInterface.Field] = + ListMap.from(interfaceType.fieldMap.view.map { case (fname, field) => + nestedMap.get(fname) match + case Some(nestedView) => fname -> field.copy(dfType = nestedView) + case None => fname -> field.copy(dir = dirMap.getOrElse(fname, field.dir)) + }) + protected def `prot_=~`(that: DFType)(using MemberGetSet): Boolean = that match + case that: DFView => + this.name == that.name && this.interfaceType =~ that.interfaceType && + this.dirMap.size == that.dirMap.size && + this.dirMap.forall { case (k, d) => that.dirMap.get(k).contains(d) } && + this.nestedMap.size == that.nestedMap.size && + this.nestedMap.forall { case (k, v) => that.nestedMap.get(k).exists(_ =~ v) } + case _ => false + def isSimilarTo(that: DFType)(using MemberGetSet): Boolean = that match + case that: DFView => + this.name == that.name && this.interfaceType.isSimilarTo(that.interfaceType) && + this.dirMap.size == that.dirMap.size && + this.dirMap.forall { case (k, d) => that.dirMap.get(k).contains(d) } && + this.nestedMap.size == that.nestedMap.size && + this.nestedMap.forall { case (k, v) => that.nestedMap.get(k).exists(_.isSimilarTo(v)) } + case _ => false + lazy val getRefs: List[DFRef.TypeRef] = + interfaceType.getRefs ++ nestedMap.values.flatMap(_.getRefs) + def copyWithNewRefs(using RefGen): this.type = copy( + interfaceType = interfaceType.copyWithNewRefs, + nestedMap = ListMap.from(nestedMap.view.mapValues(_.copyWithNewRefs)) + ).asInstanceOf[this.type] + + // ---- direction transforms (recursive into nested views) ---- + private def invert(dir: DFVal.Modifier.Dir): DFVal.Modifier.Dir = dir match + case DFVal.Modifier.IN => DFVal.Modifier.OUT + case DFVal.Modifier.OUT => DFVal.Modifier.IN + case other => other + private def mapDirs( + leaf: (DFVal.Modifier.Dir, DFVal.Modifier.Dir) => DFVal.Modifier.Dir, + recurse: DFView => DFView + ): DFView = copy( + dirMap = dirMap.map { case (k, cur) => + // consult the declared dir to decide whether the leaf is anchored + val declared = interfaceType.fieldMap.get(k).fold(cur)(_.dir) + k -> leaf(declared, cur) + }, + nestedMap = ListMap.from(nestedMap.view.mapValues(recurse)) + ) + // flip: invert flippable (VAR-declared) leaves only; anchored leaves pass through. + def flip: DFView = + mapDirs((declared, cur) => if (declared == DFVal.Modifier.VAR) invert(cur) else cur, _.flip) + // flipAll: invert every leaf (≙ VHDL `'converse`), anchored included. + def flipAll: DFView = mapDirs((_, cur) => invert(cur), _.flipAll) + // monitor: force every leaf to an input (observe-all). + def monitor: DFView = mapDirs((_, _) => DFVal.Modifier.IN, _.monitor) + // driver: force every leaf to an output (drive-all). + def driver: DFView = mapDirs((_, _) => DFVal.Modifier.OUT, _.driver) end DFView ///////////////////////////////////////////////////////////////////////////// From 40bb26ec6a09413468a17ae2fbbec1103ac5a997 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Thu, 18 Jun 2026 12:35:26 +0300 Subject: [PATCH 60/72] Support INOUT in views and simplify interface/view comparison - Treat INOUT as a first-class anchored direction; it is its own converse, so flip/flipAll leave it unchanged (matching SV inout modports and VHDL 'converse inout->inout). Documented on `invert` and DFInterface.Field. - Add DFInterface.Field.isSimilarTo and leverage it from both DFInterface.isSimilarTo and DFView.isSimilarTo (with size checks), removing the duplicated per-field logic. - DFView.prot_=~ now compares `projectedFieldMap =~` (a ListMap of HasRefCompare Fields), folding the leaf dirMap and nested sub-views into a single comparison. Co-Authored-By: Claude Opus 4.8 --- .../main/scala/dfhdl/compiler/ir/DFType.scala | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala index 8f6993402..0e97b27af 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala @@ -552,8 +552,9 @@ final case class DFInterface( case _ => false def isSimilarTo(that: DFType)(using MemberGetSet): Boolean = that match case that: DFInterface => + this.fieldMap.size == that.fieldMap.size && this.fieldMap.lazyZip(that.fieldMap).forall { case ((fnL, flL), (fnR, frR)) => - fnL == fnR && flL.dfType.isSimilarTo(frR.dfType) && flL.dir == frR.dir + fnL == fnR && flL.isSimilarTo(frR) } case _ => false lazy val getRefs: List[DFRef.TypeRef] = fieldMap.values.flatMap(_.getRefs).toList @@ -569,15 +570,17 @@ end DFInterface object DFInterface extends DFType.Companion[DFInterface, Unit]: type Ref = DFRef.OneWay[DFDesignBlock] // A field of an interface (or a resolved view): its DFType plus its direction. - // For a leaf port `dir` is the declared/resolved direction (VAR = flippable, - // IN/OUT = anchored). For a nested field `dfType` is itself a DFInterface (in a - // declaration) or a DFView (in a resolved view), which is self-describing, so + // For a leaf port `dir` is the declared/resolved direction (VAR = flippable; + // IN / OUT / INOUT = anchored). For a nested field `dfType` is itself a DFInterface + // (in a declaration) or a DFView (in a resolved view), which is self-describing, so // `dir` is unused. Extending `HasRefCompare` gives `=~`/`getRefs`/`copyWithNewRefs` // and lets a `ListMap[String, Field]` be compared with `=~` directly. final case class Field(dfType: DFType, dir: DFVal.Modifier.Dir) extends HasRefCompare[Field] derives ReadWriter: protected def `prot_=~`(that: Field)(using MemberGetSet): Boolean = this.dfType =~ that.dfType && this.dir == that.dir + def isSimilarTo(that: Field)(using MemberGetSet): Boolean = + this.dfType.isSimilarTo(that.dfType) && this.dir == that.dir lazy val getRefs: List[DFRef.TypeRef] = dfType.getRefs def copyWithNewRefs(using RefGen): this.type = copy(dfType = dfType.copyWithNewRefs).asInstanceOf[this.type] @@ -626,19 +629,18 @@ final case class DFView( }) protected def `prot_=~`(that: DFType)(using MemberGetSet): Boolean = that match case that: DFView => + // `projectedFieldMap` (a ListMap of HasRefCompare `Field`s) folds in both the leaf + // `dirMap` and the `nestedMap` sub-views, so a single `=~` covers the whole overlay. this.name == that.name && this.interfaceType =~ that.interfaceType && - this.dirMap.size == that.dirMap.size && - this.dirMap.forall { case (k, d) => that.dirMap.get(k).contains(d) } && - this.nestedMap.size == that.nestedMap.size && - this.nestedMap.forall { case (k, v) => that.nestedMap.get(k).exists(_ =~ v) } + this.projectedFieldMap =~ that.projectedFieldMap case _ => false def isSimilarTo(that: DFType)(using MemberGetSet): Boolean = that match case that: DFView => this.name == that.name && this.interfaceType.isSimilarTo(that.interfaceType) && - this.dirMap.size == that.dirMap.size && - this.dirMap.forall { case (k, d) => that.dirMap.get(k).contains(d) } && - this.nestedMap.size == that.nestedMap.size && - this.nestedMap.forall { case (k, v) => that.nestedMap.get(k).exists(_.isSimilarTo(v)) } + this.projectedFieldMap.size == that.projectedFieldMap.size && + this.projectedFieldMap.lazyZip(that.projectedFieldMap).forall { + case ((fnL, flL), (fnR, frR)) => fnL == fnR && flL.isSimilarTo(frR) + } case _ => false lazy val getRefs: List[DFRef.TypeRef] = interfaceType.getRefs ++ nestedMap.values.flatMap(_.getRefs) @@ -648,6 +650,8 @@ final case class DFView( ).asInstanceOf[this.type] // ---- direction transforms (recursive into nested views) ---- + // INOUT is its own converse (and VAR/VIEW are not flippable directions), so only + // IN/OUT swap; everything else passes through unchanged. private def invert(dir: DFVal.Modifier.Dir): DFVal.Modifier.Dir = dir match case DFVal.Modifier.IN => DFVal.Modifier.OUT case DFVal.Modifier.OUT => DFVal.Modifier.IN From 062a64a5476c2ade12229fd7439d0771d2b968d1 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Thu, 18 Jun 2026 12:35:31 +0300 Subject: [PATCH 61/72] Document INOUT anchored ports and view.inout in interfaces chapter Note that an anchored port may be <> INOUT (a bidirectional, self- converse direction that flip/flipAll leave unchanged, mapping to SV inout modports and VHDL inout mode-view elements), and show the view.inout(...) builder form. Co-Authored-By: Claude Opus 4.8 --- docs/user-guide/interfaces/index.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/user-guide/interfaces/index.md b/docs/user-guide/interfaces/index.md index 8bf2939b2..ce15dcf8c 100644 --- a/docs/user-guide/interfaces/index.md +++ b/docs/user-guide/interfaces/index.md @@ -165,11 +165,13 @@ to mix directions: val v1 = view.out(a, b) // a, b are outputs of the using design val v2 = view.in(a, b, c) // a, b, c are inputs of the using design val v3 = view.in(a, b).out(c) // mixed: a, b inputs; c output +val v4 = view.in(a).inout(b) // b is bidirectional ``` -The ports passed to `view.in`/`view.out` must be ports declared in *this* interface; the -builder reads them by reference. A view is itself a public `val`; it is the member you -reach through from outside the interface. +The ports passed to `view.in`/`view.out`/`view.inout` must be ports declared in *this* +interface; the builder reads them by reference. A view is itself a public `val`; it is the +member you reach through from outside the interface. A port given `inout` keeps that direction +under `flip` (`inout` is its own converse). ### Direction perspective {#direction-perspective} @@ -214,8 +216,8 @@ view gives them a *relative* direction that `flip` reverses. Some signals, thoug example is a clock or reset: it enters an interface one way and is observed identically by everyone connected to it. -For these, declare the port with an *anchored* direction, `<> IN` or `<> OUT`, instead of -`<> VAR`: +For these, declare the port with an *anchored* direction, `<> IN`, `<> OUT`, or `<> INOUT`, +instead of `<> VAR`: ```scala linenums="0" class Bus(protected val width: Int <> CONST) extends Interface: @@ -238,6 +240,10 @@ Anchored ports follow two rules: (recursing through nested views), so an anchored `clk : in` stays `in` on both the `manager` and the `subordinate` side. +A bidirectional `<> INOUT` port is a special case: `inout` is its own converse, so `flip` (and +`flipAll`) leave it `inout` on both sides. It maps directly to a SystemVerilog `inout` modport +entry and a VHDL mode-view `inout` element (whose `'converse` is also `inout`). + ### How anchored ports affect connections {#anchored-connections} Because an anchored port keeps its direction on both sides, what a `<>` connection wires up From ca81326cf54a4c25a6b4e9a9f49169bd8da74d53 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Thu, 18 Jun 2026 14:07:54 +0300 Subject: [PATCH 62/72] Detect interface scope for init via ambient given, not modifier A The init/initFile interface restriction constrained the modifier's `A` type parameter (`NotInsideInterface[A]`). That extra constraint destabilized inference of the register/assignable markers carried in `A` at call sites like `(Bit <> VAR.REG).init(1)`, so the resulting register lost its assignable-REG marker and a later `.din` failed with "This is not an assignable register" (e.g. in FoldControlSteps and DropRTProcess). Detect the interface scope via the ambient `DFC.Scope.Interface` given (present inside an interface body) instead, so the constraint no longer touches `A` and register inference is unaffected. The rejection behavior is unchanged. Co-Authored-By: Claude Opus 4.8 --- core/src/main/scala/dfhdl/core/DFVal.scala | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/core/src/main/scala/dfhdl/core/DFVal.scala b/core/src/main/scala/dfhdl/core/DFVal.scala index 4056371e7..e549eca67 100644 --- a/core/src/main/scala/dfhdl/core/DFVal.scala +++ b/core/src/main/scala/dfhdl/core/DFVal.scala @@ -381,10 +381,12 @@ object DFVal extends DFValLP: ] ): InitCheck[I] with {} // An interface is purely structural and carries no initial values, so `init` - // (and `initFile`) are rejected on its ports/variables. The declaration scope - // is carried in the modifier's `A` type parameter (see `<>` in `Modifier`). - protected type NotInsideInterface[A] = AssertGiven[ - util.NotGiven[A <:< DFC.Scope.Interface], + // (and `initFile`) are rejected on its ports/variables. Detection is via the + // ambient `DFC.Scope.Interface` given (present inside an interface body), NOT the + // modifier's `A` type parameter: constraining `A` here destabilizes inference of + // register/assignable markers at call sites like `(Bit <> VAR.REG).init(1)`. + protected type NotInsideInterface = AssertGiven[ + util.NotGiven[DFC.Scope.Interface], "Cannot initialize a port or variable inside an interface.\nAn interface is purely structural and carries no initial values." ] @@ -576,7 +578,7 @@ object DFVal extends DFValLP: )(using DFC, InitCheck[I], - NotInsideInterface[A] + NotInsideInterface ): DFVal[T, Modifier[A, C, Modifier.Initialized, P]] = trydf { val tvList = initValues.view.filter(_.enable).map(tv => tv(dfVal.dfType)(using dfc.anonymize)).toList @@ -589,7 +591,7 @@ object DFVal extends DFValLP: )(using DFC, InitCheck[I], - NotInsideInterface[A] + NotInsideInterface ): DFVal[DFTuple[T], Modifier[A, C, Modifier.Initialized, P]] = trydf { if (initValues.enable) @@ -607,7 +609,7 @@ object DFVal extends DFValLP: )(using dfc: DFC, check: InitCheck[I], - notInsideInterface: NotInsideInterface[A] + notInsideInterface: NotInsideInterface ): DFVal[DFVector[T, Tuple1[D1]], Modifier[A, C, Modifier.Initialized, P]] = trydf: import dfc.getSet val vectorType = dfVal.dfType From a2482f01243e588ed04121eec19063d60f7e8de6 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Thu, 18 Jun 2026 14:08:08 +0300 Subject: [PATCH 63/72] Introduce opaque StaticRef for design-block structural keys Add `opaque type StaticRef = DFRef.OneWay[DFDesignBlock]` (fully opaque, no subtype bound) so the refs used as stable structural keys are not treated as ordinary references by accident. It provides an explicit `.asRef` unwrap, an `apply`/`Conversion` from `DFOwner.Ref` (covering both `OneWay[DFDesignBlock]` and the unified `ownerRef` sub-DB keys), and its own `CanEqual`/`ReadWriter`. Retype the three structural-key sites to `StaticRef`: `DB.subDBs` keys, `DFDesignInst.designRef`, and `DFInterface.interfaceRef`. Every site that genuinely needs the underlying ref now unwraps via `.asRef` (resolution, refTable/designBlockByKey lookups), and sub-DB keys built from `ownerRef` wrap via `StaticRef(...)`, across the IR, core, and the hierarchy stages. Co-Authored-By: Claude Opus 4.8 --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 14 +++++------ .../scala/dfhdl/compiler/ir/DFMember.scala | 9 ++++---- .../main/scala/dfhdl/compiler/ir/DFRef.scala | 23 +++++++++++++++++++ .../main/scala/dfhdl/compiler/ir/DFType.scala | 3 ++- .../dfhdl/compiler/stages/AddClkRst.scala | 2 +- .../compiler/stages/DropDesignDefs.scala | 8 +++---- .../compiler/stages/DropStructsVecs.scala | 4 ++-- .../stages/GlobalizePortVectorParams.scala | 4 ++-- .../compiler/stages/ReduplicateDesign.scala | 18 +++++++-------- .../dfhdl/compiler/stages/SanityCheck.scala | 4 ++-- .../scala/dfhdl/compiler/stages/Stage.scala | 2 +- .../dfhdl/compiler/stages/UniqueDesigns.scala | 8 +++---- .../dfhdl/compiler/stages/UniqueNames.scala | 4 ++-- core/src/main/scala/dfhdl/core/Design.scala | 2 +- .../src/main/scala/dfhdl/core/MutableDB.scala | 8 ++++--- 15 files changed, 69 insertions(+), 44 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index ad93db7b0..18ab84104 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -22,7 +22,7 @@ final case class DB private ( // new-style root DB has a populated `subDBs`: a flat ListMap of // every design in elaboration order (top first, then descendants). Sub-DBs // and old-style flat DBs both have an empty `subDBs`. - subDBs: ListMap[DFOwner.Ref, DB] = ListMap.empty + subDBs: ListMap[StaticRef, DB] = ListMap.empty )(_rootDB: => DB) derives CanEqual: private val self = this lazy val rootDB: DB = _rootDB @@ -31,7 +31,7 @@ final case class DB private ( refTable: Map[DFRefAny, DFMember] = refTable, globalTags: DFTags = globalTags, srcFiles: List[SourceFile] = srcFiles, - subDBs: ListMap[DFOwner.Ref, DB] = subDBs + subDBs: ListMap[StaticRef, DB] = subDBs ): DB = DB(members, refTable, globalTags, srcFiles, subDBs) given getSet: MemberGetSet with @@ -1681,7 +1681,7 @@ final case class DB private ( refTable = Map.empty, globalTags = this.globalTags, srcFiles = this.srcFiles, - subDBs = ListMap.from(builtSubDBs.iterator.map((d, sub) => d.ownerRef -> sub)) + subDBs = ListMap.from(builtSubDBs.iterator.map((d, sub) => StaticRef(d.ownerRef) -> sub)) ) end oldToNew @@ -1708,8 +1708,8 @@ final case class DB private ( val canonicalDesign = mutable.Map.empty[DFOwner.Ref, DFDesignBlock] subDBs.foreach { (key, subDB) => subDB.members.collectFirst { - case d: DFDesignBlock if d.ownerRef == key => d - }.foreach(canonicalDesign(key) = _) + case d: DFDesignBlock if StaticRef(d.ownerRef) == key => d + }.foreach(canonicalDesign(key.asRef) = _) } def canonicalize(m: DFMember): DFMember = m match case d: DFDesignBlock => canonicalDesign.getOrElse(d.ownerRef, d) @@ -1931,7 +1931,7 @@ object DB: refTable: Map[DFRefAny, DFMember], globalTags: DFTags, srcFiles: List[SourceFile], - subDBs: ListMap[DFOwner.Ref, DB] + subDBs: ListMap[StaticRef, DB] ): DB = lazy val updatedSubDBs = subDBs.map((r, subDB) => r -> subDB.copy()(db)) lazy val db: DB = new DB(members, refTable, globalTags, srcFiles, updatedSubDBs)(db) @@ -1951,7 +1951,7 @@ object DB: Map[DFRefAny, DFMember], DFTags, List[SourceFile], - List[(DFOwner.Ref, DB)] + List[(StaticRef, DB)] ) @scala.annotation.nowarn("msg=Infinite loop") given ReadWriter[DB] = diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala index 471862f8e..47dde83ae 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFMember.scala @@ -301,6 +301,7 @@ object DFVal: enum Special derives CanEqual, ReadWriter: case Ordinary, REG, SHARED export Special.{Ordinary, REG, SHARED} + end Modifier extension (dfVal: DFVal) def isPort: Boolean = dfVal match @@ -1666,12 +1667,12 @@ final case class DFDesignInst( // Each non-mutable path falls back to `designRef.get` for the pre-unification // form (where `designRef` is still a distinct parent-side refTable entry). def getDesignBlock(using getSet: MemberGetSet): DFDesignBlock = - if (getSet.isMutable) designRef.get + if (getSet.isMutable) designRef.asRef.get else val root = getSet.designDB.rootDB if (root.isRoot) - root.subDBs.get(designRef).map(_.top).getOrElse(designRef.get) - else root.designBlockByKey.getOrElse(designRef, designRef.get) + root.subDBs.get(designRef).map(_.top).getOrElse(designRef.asRef.get) + else root.designBlockByKey.getOrElse(designRef.asRef, designRef.asRef.get) protected def `prot_=~`(that: DFMember)(using MemberGetSet): Boolean = that match case that: DFDesignInst => // `designRef` is unified with the child block's `ownerRef` (the sub-DB key) @@ -1702,7 +1703,7 @@ final case class DFDesignInst( end DFDesignInst object DFDesignInst: - type DesignRef = DFRef.OneWay[DFDesignBlock] + type DesignRef = StaticRef type ParamRef = DFRef.TwoWay[DFVal, DFDesignInst] type ParamMap = ListMap[String, ParamRef] diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFRef.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFRef.scala index 11851b271..84c8a16d4 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFRef.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFRef.scala @@ -94,6 +94,29 @@ object DFRef: ) end DFRef +// A reference to a design block used as a stable structural key — the `subDBs` +// key, `DFDesignInst.designRef`, and `DFInterface.interfaceRef`. Fully opaque (no +// `<: OneWay[...]` bound) so it is NOT a regular reference: it cannot be resolved, +// freshened, or placed in a refTable by accident. Use `.asRef` for the deliberate, +// explicit unwrap when the underlying design-block reference is genuinely needed. +opaque type StaticRef = DFRef.OneWay[DFDesignBlock] +object StaticRef: + // The source is `DFOwner.Ref` (the broad owner-ref type) rather than just + // `OneWay[DFDesignBlock]`: a design block's `ownerRef` is unified with its + // instantiating `designRef` (a design-block key) but is typed as `DFOwner.Ref`, + // so this also re-tags those `subDBs` keys/lookups. `designRef`/`interfaceRef` + // (`OneWay[DFDesignBlock] <: DFOwner.Ref`) convert through the same path. + def apply(ref: DFOwner.Ref): StaticRef = ref.asInstanceOf[StaticRef] + given Conversion[DFOwner.Ref, StaticRef] = apply(_) + given CanEqual[StaticRef, StaticRef] = CanEqual.derived + // Opaque, so it does not inherit the `[T <: DFRefAny]` ReadWriter; provide its own + // (the underlying ref serializes with the standard DFRef string format). + given ReadWriter[StaticRef] = + summon[ReadWriter[DFRefAny]].asInstanceOf[ReadWriter[StaticRef]] + // The deliberate, explicit unwrap to the underlying design-block reference. + extension (ref: StaticRef) def asRef: DFRef.OneWay[DFDesignBlock] = ref +end StaticRef + opaque type IntParamRef = DFRef.TypeRef | Int object IntParamRef: def apply(int: Int): IntParamRef = int diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala index 0e97b27af..d477fb8f3 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala @@ -568,7 +568,7 @@ final case class DFInterface( end DFInterface object DFInterface extends DFType.Companion[DFInterface, Unit]: - type Ref = DFRef.OneWay[DFDesignBlock] + type Ref = StaticRef // A field of an interface (or a resolved view): its DFType plus its direction. // For a leaf port `dir` is the declared/resolved direction (VAR = flippable; // IN / OUT / INOUT = anchored). For a nested field `dfType` is itself a DFInterface @@ -584,6 +584,7 @@ object DFInterface extends DFType.Companion[DFInterface, Unit]: lazy val getRefs: List[DFRef.TypeRef] = dfType.getRefs def copyWithNewRefs(using RefGen): this.type = copy(dfType = dfType.copyWithNewRefs).asInstanceOf[this.type] +end DFInterface ///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddClkRst.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddClkRst.scala index 180c8c1f7..efcc1c3af 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddClkRst.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddClkRst.scala @@ -326,7 +326,7 @@ case object AddClkRst extends GlobalStage: // Iterate sub-DBs in elaboration order (top first) so the shared cross-design // state accumulates deterministically; build and apply each sub-DB's patches // under its own getSet. - val newSubDBs = mutable.ListBuffer.empty[(DFOwner.Ref, DB)] + val newSubDBs = mutable.ListBuffer.empty[(StaticRef, DB)] designDB.subDBs.foreach { case (subKey, subDB) => val patchList = subDB.atGetSet(subDBPatches(subDB)) newSubDBs += subKey -> (if (patchList.isEmpty) subDB else subDB.patch(patchList)) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropDesignDefs.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropDesignDefs.scala index 66eaeeefe..5e87af45e 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropDesignDefs.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropDesignDefs.scala @@ -22,12 +22,12 @@ case object DropDesignDefs extends GlobalStage: // lands in the def design's own sub-DB. `newToOld` canonicalizes the parents' // cross-sub-DB `designRef`s onto the now-Normal block. val patchesByKey = - mutable.LinkedHashMap.empty[DFOwner.Ref, mutable.ListBuffer[(DFMember, Patch)]] - def addPatch(key: DFOwner.Ref, patch: (DFMember, Patch)): Unit = + mutable.LinkedHashMap.empty[StaticRef, mutable.ListBuffer[(DFMember, Patch)]] + def addPatch(key: StaticRef, patch: (DFMember, Patch)): Unit = patchesByKey.getOrElseUpdate(key, mutable.ListBuffer.empty) += patch // For each def design (computed once), the name prefix derived from its output // port's `suggestName`, reused to name every anonymous instance of it. - val suggestPrefixByDef = mutable.Map.empty[DFOwner.Ref, String] + val suggestPrefixByDef = mutable.Map.empty[StaticRef, String] designDB.subDBs.foreach { (parentKey, parentSubDB) => parentSubDB.members.foreach { @@ -38,7 +38,7 @@ case object DropDesignDefs extends GlobalStage: domainType = DomainType.DF, instMode = InstMode.Def ) => - val defKey = design.ownerRef + val defKey = StaticRef(design.ownerRef) // On the first instance of a given def design, stage its conversion // patches (in the def design's own sub-DB) and cache the output-port- // derived name prefix reused by every anonymous instance. diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropStructsVecs.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropStructsVecs.scala index 0725c0258..6d89c33b6 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropStructsVecs.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropStructsVecs.scala @@ -147,7 +147,7 @@ case object DropStructsVecs extends GlobalStage: dsn.patch end stage1Patch - val stage1Subs: ListMap[DFOwner.Ref, DB] = ListMap.from( + val stage1Subs: ListMap[StaticRef, DB] = ListMap.from( designDB.subDBs.iterator.map { (key, sub) => val patchList = sub.atGetSet { sub.members.collect { @@ -246,7 +246,7 @@ case object DropStructsVecs extends GlobalStage: dsn.patch end stage2Patch - val stage2Subs: ListMap[DFOwner.Ref, DB] = ListMap.from( + val stage2Subs: ListMap[StaticRef, DB] = ListMap.from( stage1Root.subDBs.iterator.map { (key, sub) => // we need to reverse the list because we want to handle the innermost partial first val patchList2 = sub.atGetSet { diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/GlobalizePortVectorParams.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/GlobalizePortVectorParams.scala index 0ff33c8ea..67e5622f6 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/GlobalizePortVectorParams.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/GlobalizePortVectorParams.scala @@ -227,7 +227,7 @@ case object GlobalizePortVectorParams extends HierarchyStage: val parentSubOpt = instOpt.flatMap { inst => rootDB.subDBs.values.find(_.members.exists(_ eq inst)) } - val designSub = rootDB.subDBs.get(design.ownerRef).getOrElse(topSub) + val designSub = rootDB.subDBs.get(StaticRef(design.ownerRef)).getOrElse(topSub) val parentDesignOpt = parentSubOpt.flatMap( _.members.collectFirst { case d: DFDesignBlock => d } ) @@ -389,7 +389,7 @@ case object GlobalizePortVectorParams extends HierarchyStage: val out = mutable.Map.empty[DFVector, DFVector] def pbnsPortType(pbns: DFVal.PortByNameSelect): Option[DFVector] = val target = pbns.designInstRef.get.getDesignBlock - rootDB.subDBs.get(target.ownerRef).flatMap { tsub => + rootDB.subDBs.get(StaticRef(target.ownerRef)).flatMap { tsub => tsub.atGetSet { tsub.members.collectFirst { case dcl: DFVal.Dcl diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ReduplicateDesign.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ReduplicateDesign.scala index f2aafcc36..e54ca6fba 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ReduplicateDesign.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ReduplicateDesign.scala @@ -103,7 +103,7 @@ abstract class ReduplicateDesign extends GlobalStage: // ----- Step 4: rewire each touched parent sub-DB ----- val updatedParents = parentRewires.map { case (parentKey, rewires) => - val parentSubDB = designDB.subDBs(parentKey) + val parentSubDB = designDB.subDBs(StaticRef(parentKey)) parentKey -> rewireParentSubDB(parentSubDB, rewires.toMap) }.toMap @@ -125,14 +125,14 @@ abstract class ReduplicateDesign extends GlobalStage: }.toMap sub.update(members = newMembers, refTable = newRefTable) - val mergedByKey = mutable.LinkedHashMap.empty[DFOwner.Ref, DB] + val mergedByKey = mutable.LinkedHashMap.empty[StaticRef, DB] designDB.subDBs.foreach { case (key, sub) => - mergedByKey(key) = applyOriginalRenames(updatedParents.getOrElse(key, sub)) + mergedByKey(key) = applyOriginalRenames(updatedParents.getOrElse(key.asRef, sub)) } - newClonedSubDBs.foreach { case (key, sub) => mergedByKey(key) = sub } + newClonedSubDBs.foreach { case (key, sub) => mergedByKey(StaticRef(key)) = sub } - val emitted = mutable.LinkedHashMap.empty[DFOwner.Ref, DB] - def emit(key: DFOwner.Ref): Unit = + val emitted = mutable.LinkedHashMap.empty[StaticRef, DB] + def emit(key: StaticRef): Unit = if (!emitted.contains(key)) mergedByKey.get(key).foreach { sub => emitted(key) = sub @@ -171,7 +171,7 @@ abstract class ReduplicateDesign extends GlobalStage: acc: CloneAcc, designDB: DB )(using RefGen): DFDesignBlock = - val origSubDB = designDB.subDBs(d.ownerRef) + val origSubDB = designDB.subDBs(StaticRef(d.ownerRef)) // Clone every member; record (oldMember -> newMember) and pairwise // (oldRef -> newRef) via the symmetric `getAllRefs` enumeration on @@ -245,9 +245,7 @@ abstract class ReduplicateDesign extends GlobalStage: // resolved structurally via `subDBs`, so it is not added to the parent refTable. val instMap: Map[DFMember, DFMember] = rewires.map { case (origInst, clonedBlock) => - origInst -> origInst.copy(designRef = - clonedBlock.ownerRef.asInstanceOf[DFRef.OneWay[DFDesignBlock]] - ) + origInst -> origInst.copy(designRef = StaticRef(clonedBlock.ownerRef)) } val newMembers = parentSubDB.members.map(m => instMap.getOrElse(m, m)) // Retarget any ref whose value was an old inst to the new inst. diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala index 22f23b9e5..7c915ddcd 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/SanityCheck.scala @@ -33,7 +33,7 @@ case class SanityCheck(skipAnonRefCheck: Boolean) extends Stage: // TODO: once DFDesignInst.designRef is unified with the child // DFDesignBlock.ownerRef (the `subDBs` key), this simplifies to // `rootDB.subDBs.get(inst.designRef)` with no block resolution first. - rootDB.subDBs.get(childBlock.ownerRef) match + rootDB.subDBs.get(StaticRef(childBlock.ownerRef)) match case Some(childSub) => childSub.atGetSet { childBlock.members(MemberView.Folded).view.collect { @@ -155,7 +155,7 @@ case class SanityCheck(skipAnonRefCheck: Boolean) extends Stage: // DFDesignBlock.ownerRef (the `subDBs` key), the whitelist key `d.ownerRef` // is exactly that unified ref. val isLiveChildDesign = m match - case d: DFDesignBlock => rootDB.subDBs.contains(d.ownerRef) + case d: DFDesignBlock => rootDB.subDBs.contains(StaticRef(d.ownerRef)) case _ => false if (!m.isInstanceOf[DFMember.Empty] && !isLiveChildDesign && !memberSet.contains(m)) reportViolation(s"Ref $r exists for a removed member: $m") diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/Stage.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/Stage.scala index fbd74a1eb..6b51cc7d4 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/Stage.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/Stage.scala @@ -74,7 +74,7 @@ trait HierarchyStage extends Stage: val result = transformSubDB(designDB)(using subDB.getSet, co, refGen) if (!(result eq subDB)) changed = true result - val transformedSubs: ListMap[DFOwner.Ref, DB] = + val transformedSubs: ListMap[StaticRef, DB] = designDB.subDBs.map { case (k, subDB) => k -> run(subDB) } if (!changed) designDB else designDB.update(subDBs = transformedSubs) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueDesigns.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueDesigns.scala index d59920342..8412c1c37 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueDesigns.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueDesigns.scala @@ -74,10 +74,10 @@ case object UniqueDesigns extends GlobalStage: canonicalRenames.iterator.map { (canon, newName) => canon -> canon.copy(meta = canon.meta.copy(nameOpt = Some(newName))) }.toMap - val newSubDBs: ListMap[DFOwner.Ref, DB] = ListMap.from( + val newSubDBs: ListMap[StaticRef, DB] = ListMap.from( designDB.subDBs.iterator.flatMap { (key, sub) => // drop the redundant duplicate sub-DBs entirely - if (dupKeys.contains(key)) None + if (dupKeys.contains(key.asRef)) None else val instReplace = collection.mutable.Map.empty[DFDesignInst, DFDesignInst] val newMembers = sub.members.map { @@ -85,9 +85,9 @@ case object UniqueDesigns extends GlobalStage: case d: DFDesignBlock if canonicalReplace.contains(d) => canonicalReplace(d) // retarget a parent inst that targeted a duplicate onto the canonical // key (its `designRef` IS that key under unification) - case inst: DFDesignInst if dupKeyToCanonicalKey.contains(inst.designRef) => + case inst: DFDesignInst if dupKeyToCanonicalKey.contains(inst.designRef.asRef) => val newInst = inst.copy(designRef = - dupKeyToCanonicalKey(inst.designRef).asInstanceOf[DFDesignInst.DesignRef] + StaticRef(dupKeyToCanonicalKey(inst.designRef.asRef)) ) instReplace(inst) = newInst newInst diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueNames.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueNames.scala index 4c9ceb04d..f5f6c3327 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueNames.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueNames.scala @@ -120,7 +120,7 @@ private abstract class UniqueNames(reservedNames: Set[String], caseSensitive: Bo } // ---- phase 1: patch the member names, per sub-DB ---- - val firstStepSubs: ListMap[DFOwner.Ref, DB] = ListMap.from( + val firstStepSubs: ListMap[StaticRef, DB] = ListMap.from( designDB.subDBs.iterator.map { (key, sub) => val patches = sub.members.collect { case m if memberRenamePatches.contains(m) => memberRenamePatches(m) @@ -166,7 +166,7 @@ private abstract class UniqueNames(reservedNames: Set[String], caseSensitive: Bo } patches } - val secondStepSubs: ListMap[DFOwner.Ref, DB] = ListMap.from( + val secondStepSubs: ListMap[StaticRef, DB] = ListMap.from( firstStep.subDBs.iterator.map { (key, sub) => val patches = sub.members.collect { case m if typeUpdatePatches.contains(m) => typeUpdatePatches(m) diff --git a/core/src/main/scala/dfhdl/core/Design.scala b/core/src/main/scala/dfhdl/core/Design.scala index 74b0b5e94..fb75fead1 100644 --- a/core/src/main/scala/dfhdl/core/Design.scala +++ b/core/src/main/scala/dfhdl/core/Design.scala @@ -215,7 +215,7 @@ object Design: // `containedOwner.asIR`, which may be stale after `setClsNamePos` // replaced it. Resolve the ref to reach the current DB version so // the cache lives on the block that `getDesignInst` looks up later. - inst.designRef.get.setDesignInstCache(inst) + inst.designRef.asRef.get.setDesignInstCache(inst) dfc.mutableDB.addMember(inst) end if end apply diff --git a/core/src/main/scala/dfhdl/core/MutableDB.scala b/core/src/main/scala/dfhdl/core/MutableDB.scala index 63dd8a11f..0ac3a1d14 100644 --- a/core/src/main/scala/dfhdl/core/MutableDB.scala +++ b/core/src/main/scala/dfhdl/core/MutableDB.scala @@ -10,6 +10,7 @@ import dfhdl.compiler.ir.{ DFOwner, DFRef, DFRefAny, + StaticRef, DFTag, DFVal, DFType, @@ -637,8 +638,9 @@ final class MutableDB(): // and refTable occurrences of the same inst — which may be distinct objects — // both map to EQUAL unified copies. def unifyInst(inst: DFDesignInst): DFDesignInst = - val target = dupToOrigDesignMap.getOrElse(inst.designRef.get, inst.designRef.get) - inst.copy(designRef = target.ownerRef.asInstanceOf[DFDesignInst.DesignRef]) + val target = + dupToOrigDesignMap.getOrElse(inst.designRef.asRef.get, inst.designRef.asRef.get) + inst.copy(designRef = StaticRef(target.ownerRef)) val finalMembers = members.flatMap { case m: DFVal if m.isGlobal => Some(finalFixFunc(m)) case m: (DomainBlock | DFVal) if dupToOrigDesignMap.contains(m.getOwnerDesign) => @@ -694,7 +696,7 @@ final class MutableDB(): m match // DFDesignInst's designRef is a OneWay.Gen ref outside of getRefs // (which only carries TwoWay refs); keep it from being swept away. - case inst: DFDesignInst => memberOwnerRefs += inst.designRef + case inst: DFDesignInst => memberOwnerRefs += inst.designRef.asRef case _ => } refTable.filter { (r, _) => From d9dd642bffda22f393d46ee4585ed1a7d182fbc4 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Thu, 18 Jun 2026 16:57:03 +0300 Subject: [PATCH 64/72] actuall fix DB serialization infinite loop --- .../src/main/scala/dfhdl/compiler/ir/DB.scala | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala index 18ab84104..97f07f3eb 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala @@ -1945,21 +1945,39 @@ object DB: // and old-style flat DBs carry an empty `subDBs`, so the recursion terminates // immediately (one level: root -> its sub-DBs). `subDBs` is serialized as an // ordered list of (key, sub-DB) pairs to preserve the elaboration-order ListMap. + // Each sub-DB is serialized as its own JSON string rather than a nested `DB`. + // This keeps `DBSerialized` free of any reference to `DB`, so deriving + // `ReadWriter[DBSerialized]` does not recursively summon `ReadWriter[DB]` (which + // would trigger a spurious "Infinite loop in function body" warning). The + // recursion now happens only at runtime inside the mapping functions below. private type DBSerialized = ( List[DFMember], Map[DFRefAny, DFMember], DFTags, List[SourceFile], - List[(StaticRef, DB)] + List[(StaticRef, String)] ) - @scala.annotation.nowarn("msg=Infinite loop") given ReadWriter[DB] = readwriter[DBSerialized].bimap[DB]( - db => (db.members, db.refTable, db.globalTags, db.srcFiles, db.subDBs.toList), + db => + ( + db.members, + db.refTable, + db.globalTags, + db.srcFiles, + db.subDBs.toList.map((ref, subDB) => ref -> write(subDB)) + ), { case (members, refTable, globalTags, srcFiles, subDBs) => if (subDBs.isEmpty) DB(members, refTable, globalTags, srcFiles) - else DB(members, refTable, globalTags, srcFiles, ListMap.from(subDBs)) + else + DB( + members, + refTable, + globalTags, + srcFiles, + ListMap.from(subDBs.map((ref, json) => ref -> read[DB](json))) + ) } ) extension (db: DB) From 9fc0af50706952839a4d344f73cadec3cc18441c Mon Sep 17 00:00:00 2001 From: Oron Port Date: Thu, 18 Jun 2026 16:57:32 +0300 Subject: [PATCH 65/72] add missing `into` on `StaticRef` --- compiler/ir/src/main/scala/dfhdl/compiler/ir/DFRef.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFRef.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFRef.scala index 84c8a16d4..b4d835656 100644 --- a/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFRef.scala +++ b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFRef.scala @@ -99,7 +99,7 @@ end DFRef // `<: OneWay[...]` bound) so it is NOT a regular reference: it cannot be resolved, // freshened, or placed in a refTable by accident. Use `.asRef` for the deliberate, // explicit unwrap when the underlying design-block reference is genuinely needed. -opaque type StaticRef = DFRef.OneWay[DFDesignBlock] +into opaque type StaticRef = DFRef.OneWay[DFDesignBlock] object StaticRef: // The source is `DFOwner.Ref` (the broad owner-ref type) rather than just // `OneWay[DFDesignBlock]`: a design block's `ownerRef` is unified with its From f7a3eedf182ad31d16bc85b9f69286744b5e7338 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Thu, 18 Jun 2026 16:58:35 +0300 Subject: [PATCH 66/72] prepare for Scala 3.10.x: change export of compiletime.ops.string internals --- internals/src/main/scala/dfhdl/internals/Inlined.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internals/src/main/scala/dfhdl/internals/Inlined.scala b/internals/src/main/scala/dfhdl/internals/Inlined.scala index fad0b2c4a..e7ecec899 100644 --- a/internals/src/main/scala/dfhdl/internals/Inlined.scala +++ b/internals/src/main/scala/dfhdl/internals/Inlined.scala @@ -3,7 +3,7 @@ import scala.quoted.* import compiletime.ops.{int, string, boolean, any} import compiletime.{constValue, constValueOpt} export int.{ToString => _, + => _, ^ => _, *} -export string.{+ => _, *} +export string.Length export boolean.{^ => _, *} export any.* type +[L, R] = (L, R) match From b10fc17ce1bfd163114c123daa814e8d9a72e457 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Thu, 18 Jun 2026 17:04:11 +0300 Subject: [PATCH 67/72] add missing unique keyword to verilog printer --- .../scala/dfhdl/compiler/stages/verilog/VerilogPrinter.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogPrinter.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogPrinter.scala index abc90e06b..354b95850 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogPrinter.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogPrinter.scala @@ -247,7 +247,8 @@ class VerilogPrinter(val dialect: VerilogDialect)(using "begin", "end", "case", "default", "endcase", "default_nettype", "include", "initial", "inside", "timescale", "if", "else", "typedef", "enum", "posedge", "negedge", "assign", "parameter", "struct", "packed", "ifndef", "endif", "define", "function", "endfunction", "for", "while", - "assert", "write", "display", "info", "warning", "error", "fatal", "finish", "localparam" + "assert", "write", "display", "info", "warning", "error", "fatal", "finish", "localparam", + "unique" ) val verilogOps: Set[String] = Set("=", "<=") val verilogTypes: Set[String] = Set( From 1cc9357e00ece94e6bb89018872baab296f2ae53 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Thu, 18 Jun 2026 17:28:50 +0300 Subject: [PATCH 68/72] fix Verilator tool execution regression --- lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala b/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala index 67c024ed1..7bce98ee9 100644 --- a/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala +++ b/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala @@ -116,7 +116,8 @@ trait Tool: (exeDir :: root.toList.flatMap(r => List(r.resolve("lib"), r.resolve("lib").resolve("ivl")))) .map(_.toString) - (dllDirs :+ sys.env.getOrElse("PATH", "")).mkString(java.io.File.pathSeparator) + val inheritedPath = Option(System.getenv("PATH")).getOrElse("") + (dllDirs :+ inheritedPath).mkString(java.io.File.pathSeparator) } protected def designFiles(using getSet: MemberGetSet): List[String] = From 661bcccc4c7f10b3265c45b0bb5e452e12469bf9 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Thu, 18 Jun 2026 20:01:41 +0300 Subject: [PATCH 69/72] make external tool Ctrl+C cancellation robust and immediate Previously, cancelling a running tool (simulator/builder) with Ctrl+C only worked under foreground sbt, and could leave the tool (and its children) running, stall on heavy tool output, continue past the cancelled step, or print the notice twice. - handle both delivery mechanisms: SIGINT (sbt/standalone) and the Thread.interrupt() the sbtn background-job thread receives. - force-kill the whole process tree via the underlying java.lang.Process (descendants + destroyForcibly), so Windows tools that fork workers (gw_sh, vsim -> vsimk, ...) don't survive. - wait on the underlying process rather than os-lib's waitFor(), which joins the output-pumper thread; under an output flood that join blocked on the back-pressured writer and made the abort feel slow. - propagate a stack-trace-free ToolInterruptedException, caught at the DFApp boundary, so the run stops cleanly without a noisy trace and without sys.exit (which would tear down the reusable sbtn/sbt-shell server JVM). Co-Authored-By: Claude Opus 4.8 --- .../main/scala/dfhdl/internals/helpers.scala | 7 ++ lib/src/main/scala/dfhdl/app/DFApp.scala | 29 +++++---- .../scala/dfhdl/tools/toolsCore/Tool.scala | 65 +++++++++++++------ 3 files changed, 71 insertions(+), 30 deletions(-) diff --git a/internals/src/main/scala/dfhdl/internals/helpers.scala b/internals/src/main/scala/dfhdl/internals/helpers.scala index 76870b91b..86f421a75 100644 --- a/internals/src/main/scala/dfhdl/internals/helpers.scala +++ b/internals/src/main/scala/dfhdl/internals/helpers.scala @@ -404,6 +404,13 @@ lazy val sbtIsRunning: Boolean = lazy val scala_cliIsRunning: Boolean = getShellCommand.exists(_.contains(".scala-build")) +// Thrown to unwind a tool execution that the user cancelled with Ctrl+C. It carries no stack +// trace (so propagation stays quiet) and is caught at the application boundary (`DFApp`), which +// lets the run stop cleanly without a noisy trace and without `sys.exit` — the latter matters +// under `sbtn`/sbt-shell, where exiting would tear down the reusable server JVM. +final class ToolInterruptedException(message: String) + extends RuntimeException(message, null, false, false) + // detecting if running in Scastie by checking the PWD lazy val scastieIsRunning: Boolean = System.getProperty("user.dir").startsWith("/tmp/scastie") diff --git a/lib/src/main/scala/dfhdl/app/DFApp.scala b/lib/src/main/scala/dfhdl/app/DFApp.scala index 041c5dbd5..184563891 100644 --- a/lib/src/main/scala/dfhdl/app/DFApp.scala +++ b/lib/src/main/scala/dfhdl/app/DFApp.scala @@ -6,7 +6,7 @@ import wvlet.log.{Logger, LogFormatter} import scala.collection.mutable import dfhdl.options.* import org.rogach.scallop.* -import dfhdl.internals.{sbtShellIsRunning, sbtnIsRunning} +import dfhdl.internals.{sbtShellIsRunning, sbtnIsRunning, ToolInterruptedException} import scala.util.chaining.scalaUtilChainingOps import java.time.Instant import dfhdl.compiler.stages.{StagedDesign, CompiledDesign} @@ -356,16 +356,23 @@ trait DFApp: end listSimulateTools private def execute(mode: AppMode): Unit = - mode match - case AppMode.help => ??? - case AppMode.elaborate => elaborate() - case AppMode.compile => compile() - case AppMode.commit => commit() - case AppMode.lint => lint(uncached = true) - case AppMode.simulate => simRun(uncached = true) - case AppMode.build => build() - case AppMode.program => program(uncached = true) - end match + try + mode match + case AppMode.help => ??? + case AppMode.elaborate => elaborate() + case AppMode.compile => compile() + case AppMode.commit => commit() + case AppMode.lint => lint(uncached = true) + case AppMode.simulate => simRun(uncached = true) + case AppMode.build => build() + case AppMode.program => program(uncached = true) + end match + catch + case _: ToolInterruptedException => + // user cancelled a tool run with Ctrl+C. `Tool.exec` already printed the interruption + // notice and killed the tool's process tree, so just stop cleanly here without + // re-reporting it (logging the message again is what caused the duplicate print). + () end execute def runManual(appMode: AppMode)(using diff --git a/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala b/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala index 7bce98ee9..c0895b037 100644 --- a/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala +++ b/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala @@ -204,32 +204,59 @@ trait Tool: stdout = processOutput, mergeErrIntoOut = true ) - // setup an interrupt handler to destroy the process. - // this covers the `sbt` case, where Ctrl+C is delivered to the JVM as a POSIX/Windows - // SIGINT signal. we keep the previously installed handler so we can restore it afterwards, - // which matters under `sbtn` where the same long-lived server JVM is reused across runs. + // Destroys the spawned tool together with any child processes it forked. We force-kill the + // descendants and the process itself directly via the underlying java.lang.Process rather + // than os-lib's `destroy`: `destroy(async = false)` joins the output-pumper thread, which can + // be blocked on a back-pressured write when the tool floods stdout (e.g. a runaway sim print + // loop), stalling the whole cancellation. `destroyForcibly` returns immediately; the orphaned + // pumper hits EOF once the process dies and exits on its own. On Windows in particular, + // killing only the direct child (`gw_sh`, `vsim` -> `vsimk`, ...) leaves the real workers + // running, hence the descendants walk. Best-effort and idempotent: safe to call repeatedly. + def destroyToolTree(): Unit = + try process.wrapped.toHandle.descendants().forEach(p => { p.destroyForcibly(); () }) + catch case _: Throwable => () + process.wrapped.destroyForcibly() + + // Ctrl+C handling. The cancellation reaches us through two different mechanisms depending on + // the launcher: + // - under `sbt`/standalone, Ctrl+C is delivered to the JVM as a POSIX/Windows SIGINT and the + // signal handler below fires on the signal-dispatch thread. + // - under `sbtn`, the run executes on an sbt background-job thread that is cancelled via + // Thread.interrupt(); no signal is raised, so `waitFor()` throws InterruptedException. + // We keep and restore the previous signal handler since under `sbtn` the same long-lived + // server JVM is reused across runs. + @volatile var interruptedBySignal = false val interruptHandler = new sun.misc.SignalHandler: def handle(sig: sun.misc.Signal): Unit = - process.destroy(shutdownGracePeriod = 100) - println(s"\n${toolName} interrupted by user") + interruptedBySignal = true + destroyToolTree() val prevHandler = sun.misc.Signal.handle(new sun.misc.Signal("INT"), interruptHandler) - // wait for the process to finish. - // under `sbtn`, the app runs on an sbt background-job thread that is cancelled via - // Thread.interrupt() rather than a SIGINT, so the signal handler above never fires and - // waitFor() throws InterruptedException. destroy the process here as well so the spawned - // tool never outlives the run in that case. - val interrupted = + // Block on the underlying java.lang.Process rather than os-lib's `process.waitFor()`. Both + // wait on the same handle and are interrupted promptly by a thread cancel (`sbtn`), but + // os-lib's no-arg `waitFor()` additionally joins the output-pumper thread once the process + // exits — and under an output flood that join blocks on the back-pressured writer, which is + // what made the abort feel slow on the signal path. Waiting on `wrapped` skips that join, so + // cancellation is immediate via either mechanism. (Polling with the timed `waitFor` was worse: + // it doesn't surface the interrupt as promptly.) We drain the pumper below, only when the tool + // finishes on its own. + val interruptedByThread = try - process.waitFor() + process.wrapped.waitFor() false - catch - case _: InterruptedException => - process.destroy(shutdownGracePeriod = 100) - println(s"\n${toolName} interrupted by user") - true + catch case _: InterruptedException => true finally sun.misc.Signal.handle(new sun.misc.Signal("INT"), prevHandler) - if (interrupted) {} + if (interruptedByThread || interruptedBySignal) + // the tool (and its children) are now being torn down; unwind the whole run so the app + // actually stops instead of silently continuing past the cancelled step. + // ToolInterruptedException carries no stack trace and is caught by DFApp, so this neither + // prints a noisy trace nor (under `sbtn`/sbt-shell) kills the reusable server JVM. + destroyToolTree() + println(s"\n${toolName} interrupted by user") + throw new ToolInterruptedException(s"${toolName} interrupted by user") else + // the tool finished on its own; join the output pumper (no-arg waitFor) so any buffered + // lines are flushed before we read and report the exit code + process.waitFor() // get the error code, which may be overridden by the logger val errCode = loggerOpt.map { logger => if (logger.lineIsErrorOpt.nonEmpty) From 711d4d36f869c1d9483b00077367057cbf6c4551 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Thu, 18 Jun 2026 20:18:01 +0300 Subject: [PATCH 70/72] fix potential crash when reporting errors --- core/src/main/scala/dfhdl/core/DFError.scala | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/dfhdl/core/DFError.scala b/core/src/main/scala/dfhdl/core/DFError.scala index c10ee1ad4..5bce35b2d 100644 --- a/core/src/main/scala/dfhdl/core/DFError.scala +++ b/core/src/main/scala/dfhdl/core/DFError.scala @@ -65,10 +65,16 @@ class DFWarning( )(using dfc: DFC) extends LogEvent derives CanEqual: import dfc.getSet - val designName = dfc.ownerOption match + // TODO: revisit this in the future, since maybe laziness will get a wrong position + // --- + // Names are computed lazily: a warning may be constructed while its design is + // still mid-elaboration (its `designInstCache` not yet set). Deferring the + // lookup to `toString` time -- which happens at the end of top-level + // elaboration -- ensures all design inst caches are populated. + lazy val designName = dfc.ownerOption match case Some(owner) => owner.asIR.getThisOrOwnerDesign.getFullName case None => "" - val fullName = + lazy val fullName = if (dfc.isAnonymous) designName else if (designName.nonEmpty) s"$designName.${dfc.name}" else dfc.name From 5c8168f38dae9c99b2cdbeb5317a6ba2c3f5ea46 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Thu, 18 Jun 2026 20:25:48 +0300 Subject: [PATCH 71/72] docs: temporarily hide interfaces docs --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 56f65fb39..96df3829f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -80,7 +80,7 @@ nav: - Conditionals: user-guide/conditionals/index.md - Processes: user-guide/processes/index.md - Domain Abstractions: user-guide/design-domains/index.md - - Interfaces: user-guide/interfaces/index.md + # - Interfaces: user-guide/interfaces/index.md # - Domains [WIP]: user-guide/domains/index.md # - Scopes [WIP]: user-guide/scopes/index.md - Naming: user-guide/naming/index.md From c64d8b8ad5de341197abfe7fdec6108780ce41d0 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Thu, 18 Jun 2026 20:39:58 +0300 Subject: [PATCH 72/72] docs: DFHDL v0.20.0 under Scala 3.8.4 --- CLAUDE.md | 2 +- docs/getting-started/hello-world/index.md | 6 +++--- .../hello-world/scala-project/project.scala | 6 +++--- .../hello-world/scala-single-file/Counter8.scala | 6 +++--- docs/getting-started/initial-setup/index.md | 8 ++++---- docs/javascripts/scastie.js | 4 ++-- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0f6a87bd0..3d400f39b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ Outputs: Verilog, SystemVerilog, VHDL. ## Build System -**Tool**: SBT 1.12.9 — **Scala**: 3.8.3 (nightly resolver enabled) +**Tool**: SBT 1.12.12 — **Scala**: 3.8.4 (nightly resolver enabled) ```bash sbtn compile # compile all subprojects diff --git a/docs/getting-started/hello-world/index.md b/docs/getting-started/hello-world/index.md index 97bc5508c..a1366d43b 100644 --- a/docs/getting-started/hello-world/index.md +++ b/docs/getting-started/hello-world/index.md @@ -78,9 +78,9 @@ For more information, please run `scala run --help` or consult the [online docum In a scala-cli project with multiple `.scala` files, shared `given` declarations (such as compiler options) must appear in exactly one file. Place them in your `project.scala` file to avoid duplicate definition errors. ```scala title="project.scala" -//> using scala 3.8.3 -//> using dep io.github.dfianthdl::dfhdl::0.19.0 -//> using plugin io.github.dfianthdl:::dfhdl-plugin:0.19.0 +//> using scala 3.8.4 +//> using dep io.github.dfianthdl::dfhdl::0.20.0 +//> using plugin io.github.dfianthdl:::dfhdl-plugin:0.20.0 import dfhdl.* given options.CompilerOptions.Backend = _.verilog diff --git a/docs/getting-started/hello-world/scala-project/project.scala b/docs/getting-started/hello-world/scala-project/project.scala index 503ed3476..fe91cf626 100644 --- a/docs/getting-started/hello-world/scala-project/project.scala +++ b/docs/getting-started/hello-world/scala-project/project.scala @@ -1,3 +1,3 @@ -//> using scala 3.8.3 -//> using dep io.github.dfianthdl::dfhdl::0.19.0 -//> using plugin io.github.dfianthdl:::dfhdl-plugin:0.19.0 +//> using scala 3.8.4 +//> using dep io.github.dfianthdl::dfhdl::0.20.0 +//> using plugin io.github.dfianthdl:::dfhdl-plugin:0.20.0 diff --git a/docs/getting-started/hello-world/scala-single-file/Counter8.scala b/docs/getting-started/hello-world/scala-single-file/Counter8.scala index afa359877..3792250ef 100644 --- a/docs/getting-started/hello-world/scala-single-file/Counter8.scala +++ b/docs/getting-started/hello-world/scala-single-file/Counter8.scala @@ -1,6 +1,6 @@ -//> using scala 3.8.3 -//> using dep io.github.dfianthdl::dfhdl::0.19.0 -//> using plugin io.github.dfianthdl:::dfhdl-plugin:0.19.0 +//> using scala 3.8.4 +//> using dep io.github.dfianthdl::dfhdl::0.20.0 +//> using plugin io.github.dfianthdl:::dfhdl-plugin:0.20.0 import dfhdl.* //import all the DFHDL goodness diff --git a/docs/getting-started/initial-setup/index.md b/docs/getting-started/initial-setup/index.md index 4cb7b7ff1..af3c6504f 100755 --- a/docs/getting-started/initial-setup/index.md +++ b/docs/getting-started/initial-setup/index.md @@ -1,12 +1,12 @@ # Initial Setup {#getting-started} -DFHDL is a domain specific language (DSL) library written in the [Scala programming language](https://www.scala-lang.org){target="_blank"} (Scala 3.8.3), and as such it lets you utilize the entire Scala ecosystem, including IDEs, various tools, and other libraries. +DFHDL is a domain specific language (DSL) library written in the [Scala programming language](https://www.scala-lang.org){target="_blank"} (Scala 3.8.4), and as such it lets you utilize the entire Scala ecosystem, including IDEs, various tools, and other libraries. Is your system already fit for Scala development? [Jump to the DFHDL hello-world section][hello-world] ## Installing Scala and Other Dependencies -We recommend directly installing Scala 3.8.3 (no need to install either [Coursier](https://get-coursier.io/){target="_blank"}, [Scala CLI](https://scala-cli.virtuslab.org/){target="_blank"}, or [sbt](https://www.scala-sbt.org/){target="_blank"}): +We recommend directly installing Scala 3.8.4 (no need to install either [Coursier](https://get-coursier.io/){target="_blank"}, [Scala CLI](https://scala-cli.virtuslab.org/){target="_blank"}, or [sbt](https://www.scala-sbt.org/){target="_blank"}):
@@ -18,7 +18,7 @@ We recommend directly installing Scala 3.8.3 (no need to install either [Coursie Run the following in Windows command or powershell: ```{.cmd .copy linenums="0"} - choco install scala --version=3.8.3 + choco install scala --version=3.8.4 ``` /// @@ -30,7 +30,7 @@ We recommend directly installing Scala 3.8.3 (no need to install either [Coursie Run the following in your shell: ```{.sh-session .copy linenums="0"} - sdk install scala 3.8.3 + sdk install scala 3.8.4 ``` /// diff --git a/docs/javascripts/scastie.js b/docs/javascripts/scastie.js index ffa80e76d..6ccea97eb 100755 --- a/docs/javascripts/scastie.js +++ b/docs/javascripts/scastie.js @@ -1,5 +1,5 @@ -let dfhdlVersion = "0.19.0"; -let scalaVersion = "3.8.3"; +let dfhdlVersion = "0.20.0"; +let scalaVersion = "3.8.4"; function buildSbtConfig(mainClass) { let config = `