diff --git a/.claude/commands/ir-reference.md b/.claude/commands/ir-reference.md index 1812ae51d..4e6a576be 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 @@ -49,7 +51,6 @@ DFMember (sealed) ├── DFConditional.Header — if / match header expression │ ├── DFIfHeader │ └── DFMatchHeader -├── DFInterfaceOwner — interface abstraction └── DFRange — for-loop range ``` @@ -372,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, @@ -396,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 ``` @@ -468,7 +469,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 @@ -511,6 +511,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( @@ -783,7 +806,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 @@ -863,10 +885,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..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 ``` @@ -1072,9 +1071,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/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/build.sbt b/build.sbt index 55e21f1e1..e64f75709 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( 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..08aca4452 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] = @@ -12,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) => @@ -22,15 +20,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/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..97f07f3eb 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 @@ -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 @@ -188,6 +196,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 )( @@ -453,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( @@ -599,59 +607,94 @@ final case class DB private ( end match end getConnToMap - // To From - lazy val magnetConnectionMap: Map[ConnectPoint, ConnectPoint] = MagnetMap.get - + // 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). 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.get(this) + lazy val magnetConnectionMap: Map[ConnectPoint, ConnectPoint] = magnetData._1 + lazy val magnetPointInfo: Map[ConnectPoint, (DFDesignBlock, String)] = magnetData._2 + + // 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 = - 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 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 } - 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( + }.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 ++ 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 - ) - case _ => None + }.toList } } - + val danglingPorts = (danglingInputs ++ danglingOutputs).toList if (danglingPorts.nonEmpty) - throw new IllegalArgumentException( - danglingPorts.mkString("\n") - ) + throw new IllegalArgumentException(danglingPorts.mkString("\n")) end checkDanglingPorts extension (domainOwner: DFDomainOwner) @@ -667,221 +710,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): ClkRstTiming = - 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 @@ -890,93 +722,406 @@ 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 + // =========================================================================== + // 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 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 relatedAnnotMap: Map[DFDomainOwner, DFDomainOwner] = + if (!isRoot) rootDB.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 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 pbnsToPort - @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 + // 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 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) + } + + // `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 + ): Option[(DFDomainOwner, DB)] = + val ownerAndSub: Option[(DFDomainOwner, DB)] = member match + case design: DFDesignBlock => designEnclosingDomain(design) + case pbns: DFVal.PortByNameSelect => + 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 getRTOwnerWithSub + + // 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 + // 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 + case d: DFDesignBlock => ownerSub.atGetSet(d.getFullName) + case _ => + val designName = ownerSub.atGetSet(design.getFullName) + s"$designName.${ownerSub.atGetSet(owner.getRelativeName(design))}" + end fullNameViaInst + 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 => + 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 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, _) => + getRTOwnerWithSub(from, netSub) + case _ => None + } + } + }.toSet + val inSourceDomains = inSources.map { case (o, _) => o } + if (inSourceDomains.isEmpty) + 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: + |${fullNameViaInst(domain, subDB)} + |Sources: + |${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. + |""".stripMargin + ) + else Some(domain -> inSourceDomains.head) + case _ => None + } + } + }.toMap + + // Hierarchical equivalent of the `isDependentOn` analysis: does `domainOwner` + // transitively depend on `thatDomainOwner` per `dependentRTDomainOwners`? + // Invoked on the root DB. + @tailrec final def isDependentOn( + domainOwner: DFDomainOwner, + thatDomainOwner: DFDomainOwner + ): Boolean = + dependentRTDomainOwners.get(domainOwner) match + case Some(dependency) => + if (dependency == thatDomainOwner) true + else 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 + + // 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] = - 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 - - /** 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. - */ + if (isOldStyleFlatDB) oldToNew.resolvedClkRstMap + else if (!isRoot) rootDB.resolvedClkRstMap + else + val reversedDependents: Map[DFDomainOwner, Set[DFDomainOwner]] = + 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 => + 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 + + // 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 = + def atOwner[T](owner: DFDomainOwner)(f: MemberGetSet ?=> T): T = + domainOwnerToSubDB(owner).atGetSet(f) val errors = collection.mutable.ArrayBuffer[String]() - domainOwnerMemberList.view.map(_._1).foreach { - case domainOwner if domainOwner.getThisOrOwnerDesign.isDeviceTop && domainOwner.usesClk => + domainOwnerMemberList.view.map(_._1).foreach { domainOwner => + val resolvedClkOpt = resolvedClkRstMap.get(domainOwner).flatMap(_._1) + if ( + resolvedClkOpt.isDefined && + atOwner(domainOwner)(domainOwner.getThisOrOwnerDesign.isDeviceTop) + ) def waitError(msg: String): Unit = - val pos = + 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: ${domainOwner.getFullName} + |Hierarchy: ${atOwner(domainOwner)(domainOwner.getFullName)} |Message: $msg""".stripMargin - val explicitRateOpt = resolvedClkRstMap.get(domainOwner) - .flatMap(_._1) - .flatMap(_.rate.toOption) - val timingConstraintRateOpt = domainOwner.getTimingConstraintClkRateOpt - + 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) @@ -1002,61 +1147,64 @@ final case class DB private ( ) case _ => end match - case _ => + end if } if (errors.nonEmpty) throw new IllegalArgumentException(errors.mkString("\n")) end domainClkRateCheck + // 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 + // instance path (plain getFullName on a sub-DB would be relative). 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." - ) + 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: ${fullNameViaInst(wait.getOwnerDesign, sub)} + |Message: $msg""".stripMargin + val ownerDomain = wait.getOwnerDomain + trigger.getConstData[TimeNumber].toOption match + case Some(waitTime) => + 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 statement is missing an explicit clock configuration in its domain." - ) + waitError(s"Wait duration is not constant.") end match - case _ => - waitError(s"Wait duration is not constant.") - end match - end for - + end for + } + } if (errors.nonEmpty) throw new IllegalArgumentException(errors.mkString("\n")) end waitCheck + // 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 = - // Helper function to perform DFS and detect cycles @tailrec def dfs( node: DFDomainOwner, visited: Set[DFDomainOwner], @@ -1065,17 +1213,14 @@ final case class DB private ( if (stack.contains(node)) throw new IllegalArgumentException( s"""|Circular derived RT configuration detected. Involved in the cycle: - |${stack.map(fullNameViaInst).mkString("\n")} + |${stack.map(o => fullNameViaInst(o, domainOwnerToSubDB(o))).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 + case Some(dependentNode) => dfs(dependentNode, visited + node, stack + node) + case None => end dfs - // Iterate over all nodes in the map and perform DFS for (node <- dependentRTDomainOwners.keys) dfs(node, Set.empty, Set.empty) end circularDerivedDomainsCheck @@ -1177,74 +1322,77 @@ final case class DB private ( ) end directRefCheck + // 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 = 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 _ => + domainOwnerToSubDB(design).atGetSet { + val locationMap = mutable.Map.empty[String, String] // loc -> portName(idx) + // 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 + 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: @@ -1254,7 +1402,6 @@ final case class DB private ( |by using the `@io` constraint. |""".stripMargin ) - if (locationCollisions.nonEmpty) throw new IllegalArgumentException( s"""|The following location constraints have collisions: @@ -1265,28 +1412,30 @@ final case class DB private ( ) end portLocationCheck + // 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] - 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 _ => + 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: @@ -1297,18 +1446,36 @@ 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. + // Callers always invoke this on the root (via `oldToNew.check`). + lazy val check: Unit = + if (isRoot) + subDBs.view.values.foreach(_.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, run once on the root: the cross-design connectivity / + // RT-domain / device-top checks, via the `*` clones that navigate the + // sub-DB tree. + 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) @@ -1370,153 +1537,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.designRef.get, 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)) - // 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) + 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)) 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 _ => + } } - 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 } - } - 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)) - ) + 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) => StaticRef(d.ownerRef) -> sub)) + ) end oldToNew // Collapses a new-style (option-a) DB back into a flat old-style DB. @@ -1531,103 +1697,169 @@ 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 => - db.refTable.get(inst.designRef).foreach { - case d: DFDesignBlock => - instToDesign(inst) = canonicalize(d).asInstanceOf[DFDesignBlock] - case _ => - } - 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 StaticRef(d.ownerRef) == key => d + }.foreach(canonicalDesign(key.asRef) = _) } - } - 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 + // (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.getAllRefs.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). @@ -1669,7 +1901,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) @@ -1699,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) @@ -1707,18 +1939,45 @@ 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. + // 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[DFMember], + Map[DFRefAny, DFMember], + DFTags, + List[SourceFile], + List[(StaticRef, String)] + ) 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.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.map((ref, json) => ref -> read[DB](json))) + ) } ) extension (db: 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 eb33e8dbd..47dde83ae 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 @@ -119,7 +118,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 +150,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 @@ -286,12 +282,26 @@ 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 export Special.{Ordinary, REG, SHARED} + end Modifier extension (dfVal: DFVal) def isPort: Boolean = dfVal match @@ -520,6 +530,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 => @@ -1057,7 +1068,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) @@ -1099,8 +1110,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 ) ] = @@ -1109,9 +1120,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 @@ -1189,33 +1197,12 @@ 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( summon[ReadWriter[ProcessBlock]], + summon[ReadWriter[ForkBlock]], + summon[ReadWriter[LocalBlock]], summon[ReadWriter[DFConditional.Block]], summon[ReadWriter[DFLoop.Block]], summon[ReadWriter[StepBlock]], @@ -1264,6 +1251,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 @@ -1572,7 +1602,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 @@ -1584,7 +1614,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 @@ -1629,10 +1658,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.asRef.get + else + val root = getSet.designDB.rootDB + if (root.isRoot) + 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 => - 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 @@ -1640,17 +1687,23 @@ 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). 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] 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 27b1ecc10..b4d835656 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. +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 + // 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 @@ -263,10 +286,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/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/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala b/compiler/ir/src/main/scala/dfhdl/compiler/ir/DFType.scala index 020676f52..d477fb8f3 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,162 @@ 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( + interfaceRef: DFInterface.Ref, + 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") + 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 => + // 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 =~ that.fieldMap + 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.isSimilarTo(frR) + } + 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]: + 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 + // (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] +end DFInterface +///////////////////////////////////////////////////////////////////////////// + +///////////////////////////////////////////////////////////////////////////// +// DFView +// ------ +// A directed view over a DFInterface (the IR of a SV modport / VHDL mode view). +// 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") + 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 => + // `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.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.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) + 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) ---- + // 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 + 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 +///////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// // DFDouble ///////////////////////////////////////////////////////////////////////////// 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..94ca78b16 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( @@ -72,168 +73,223 @@ end ConnectPoint type MagnetMap = Map[ConnectPoint, ConnectPoint] object MagnetMap: - def get(using MemberGetSet): MagnetMap = + // 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( + cp: ConnectPoint, + ownerDesign: DFDesignBlock, // cp.getOwnerDesign + containerDesign: DFDesignBlock, // design directly containing the point's member + ownerIsBlackBox: Boolean, + name: String, + 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[ConnectPoint] = + def newError(errMsg: String): Option[RMP] = 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 - } + + // 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, + cp.getName, + s"$instFullName.${dcl.getRelativeName(childDesign)}", + instPos + ) + } + } + } } - val magnetPointGrps: List[List[ConnectPoint]] = magnetPointView.groupBy { - case cp => cp.dfType - }.view.values.map(_.toList).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 + // 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.getName, + 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] = - 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 + 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 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 + 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 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) => + if rmp.isPortIn || rmp.isPortOut && rmp.ownerIsBlackBox || + alreadyConnectedOrAssignedDcls.contains(dcl) => + false + case via: ConnectPoint.Via if rmp.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) => + }.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 { 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 + }.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: ${targetMP.position} - |Target Path: ${targetMP.getFullName} - |Source1 Position: ${srcIn.position} - |Source1 Path: ${srcIn.getFullName} - |Source2 Position: ${srcOut.position} - |Source2 Path: ${srcOut.getFullName}""".stripMargin + |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 ) - 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 -> _) - } + 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 + throw new IllegalArgumentException(errors.view.reverse.mkString("\n\n")) + val pointInfo: Map[ConnectPoint, (DFDesignBlock, String)] = + allRMPs.iterator.map(rmp => rmp.cp -> (rmp.ownerDesign, rmp.name)).toMap + (ret, pointInfo) end get end MagnetMap 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..d7864add2 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 @@ -223,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 @@ -375,6 +391,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/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 05b43d421..ac64bd95a 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, @@ -49,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) @@ -69,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 @@ -101,6 +115,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) @@ -119,7 +135,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 @@ -173,7 +194,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 @@ -181,7 +202,7 @@ trait Printer SourceOrigin.Compiled, sourceType, hdlFolderName + separatorChar + designFileName(block.dclName), - formatCode(csFile(block), withColor = false) + formatCode(p.csFile(block), withColor = false) ) } ).flatten @@ -195,13 +216,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 @@ -270,6 +316,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 = "" @@ -303,13 +351,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/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/AddClkRst.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddClkRst.scala index 302c38f10..efcc1c3af 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,257 @@ 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 _ => 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[(StaticRef, 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/AddMagnets.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/AddMagnets.scala index ba68a36a3..da4e68deb 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.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.magnetConnectionMap.foreach { (toMP, fromMP) => - val toDsn = toMP.getOwnerDesign - val fromDsn = fromMP.getOwnerDesign + 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/BackendPrepStage.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/BackendPrepStage.scala index 47cf626a9..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,6 +11,8 @@ case object BackendPrepStage NamedVerilogSelection, NamedVHDLSelection, ToED, + DropForkJoinsED, + DropLocalBlocksED, ApplyInvertConstraint, DropStructsVecs, MatchToIf, 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..0940fb23a 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.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.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.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/DropDesignDefs.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropDesignDefs.scala index 50ebc9319..5e87af45e 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[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[StaticRef, 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 => + designInst.getDesignBlock(using parentSubDB.getSet) match + case design @ DFDesignBlock( + domainType = DomainType.DF, + instMode = InstMode.Def + ) => + 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. + 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 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..8b2410fd9 --- /dev/null +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropForkJoins.scala @@ -0,0 +1,260 @@ +package dfhdl.compiler.stages + +import dfhdl.compiler.analysis.* +import dfhdl.compiler.ir.* +import dfhdl.compiler.patching.* +import dfhdl.options.CompilerOptions +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 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. + * + * 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` + 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 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 (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 [[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. + * - Nested forks are lowered innermost-first across repeated passes so an outer and inner fork + * are never patched together. + */ +//format: on +private abstract class DropForkJoins extends HierarchyStage: + def nullifies: Set[Stage] = Set() + + // 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 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 + } + + // 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 handlesFork(fork) && isInnermost(fork) && needsLowering(fork) => fork + }.flatMap(lowerFork).toList + if (patches.isEmpty) db + else lowerRepeatedly(db.patch(patches)) + + 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)) + else + val parentProc = fork.getOwnerProcessBlock + val forkName = fork.getName + val joinMode = fork.join + + // (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 = 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)) + } + } + + // (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 = metaDesignDomain + ): + // 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 + startSignals.foreach(s => driveBit(s, bitConst(true))) + joinMode match + case ForkBlock.Join.All => + waitUntil(doneSignals.reduce(_ && _)) + startSignals.foreach(s => driveBit(s, bitConst(false))) + case ForkBlock.Join.Any => + waitUntil(doneSignals.reduce(_ || _)) + startSignals.foreach(s => driveBit(s, bitConst(false))) + case ForkBlock.Join.None => + // no wait, no re-arm (single-shot; see limitations) + + List((sigMember, sigPatch), branchesDsn.patch) ++ + branchRemovalPatches(branches) ++ List(parentDsn.patch) + end if + end lowerFork + + def transformSubDB(rootDB: DB)(using MemberGetSet, CompilerOptions, RefGen): DB = + 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 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 new file mode 100644 index 000000000..a0f48f2e6 --- /dev/null +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropLocalBlocks.scala @@ -0,0 +1,104 @@ +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: each local block (produced by `locally:`) is + * dissolved, promoting its members to the parent owner and removing the block itself. + * + * 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== + * {{{ + * // 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 +private abstract class DropLocalBlocks extends HierarchyStage: + def nullifies: Set[Stage] = Set() + + // 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 = + !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 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) + }.toList + if (patches.isEmpty) db + else dissolveRepeatedly(db.patch(patches)) + + def transformSubDB(rootDB: DB)(using MemberGetSet, CompilerOptions, RefGen): DB = + 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 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/DropStructsVecs.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropStructsVecs.scala index f737b7ca1..6d89c33b6 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[StaticRef, 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[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 { + 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/main/scala/dfhdl/compiler/stages/DropTimedRTWaits.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropTimedRTWaits.scala index d00154edc..13b4aa01c 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 { @@ -26,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.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 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..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,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 (`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 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.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.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/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 603c4d79c..67e5622f6 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,15 @@ case object GlobalizePortVectorParams extends HierarchyStage: case _ => false case _ => false + // 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).repairGlobalClosures + // 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( @@ -218,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 } ) @@ -380,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 @@ -418,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/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) 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/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 02db1564d..c88b4deee 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/PrintCodeString.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/PrintCodeString.scala @@ -4,26 +4,31 @@ 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 = 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/ReduplicateDesign.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ReduplicateDesign.scala index 5d5c155f0..e54ca6fba 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, @@ -105,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 @@ -127,23 +125,23 @@ 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 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`. @@ -172,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 @@ -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,18 @@ 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 = 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, 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 d5ea09c81..7c915ddcd 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(StaticRef(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 @@ -131,16 +145,19 @@ 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) => - 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(StaticRef(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) => @@ -274,6 +291,7 @@ case class SanityCheck(skipAnonRefCheck: Boolean) extends Stage: ) require(false, "Failed ownership check!") ownerStack.pop() + end match end while end ownershipCheck @@ -307,56 +325,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): Unit = - val lhs = designDB.oldToNew.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." - ) - def transform(designDB: DB)(using MemberGetSet, CompilerOptions): DB = - refCheck() - memberExistenceCheck() - ownershipCheck(designDB.top, designDB.membersNoGlobals.drop(1)) - orderCheck() - designDB.check() - hierarchicalDBRoundTripCheck(designDB) + // 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 designDB + end transform end SanityCheck extension [T: HasDB](t: T) 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/Stage.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/Stage.scala index 18e47c73e..6b51cc7d4 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,58 +35,49 @@ 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`, * `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.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 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 = - 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(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) } + val transformedSubs: ListMap[StaticRef, DB] = + 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/ToED.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ToED.scala index ffdb85132..b2da9a92d 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,22 @@ 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 `subDB` helper). + 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 +51,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 +65,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 @@ -343,10 +348,8 @@ case object ToED extends Stage: // other domains case _ => None end match - // other owners - case _ => None } - val firstPart = designDB.patch(patchList) + val firstPart = subDB.patch(patchList) locally { import firstPart.getSet val patchList = firstPart.members.collect { @@ -393,17 +396,11 @@ case object ToED extends Stage: 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) } - end transform + end transformSubDB end ToED extension [T: HasDB](t: T) 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/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueDesigns.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/UniqueDesigns.scala index 9bbe73c21..8412c1c37 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,117 @@ 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 + // 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 + // 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[StaticRef, DB] = ListMap.from( + designDB.subDBs.iterator.flatMap { (key, sub) => + // drop the redundant duplicate sub-DBs entirely + if (dupKeys.contains(key.asRef)) None + else + 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 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.asRef) => + val newInst = inst.copy(designRef = + StaticRef(dupKeyToCanonicalKey(inst.designRef.asRef)) + ) + instReplace(inst) = newInst + newInst + case m => m + } + // keep refTable values consistent: point any ref that targeted a + // 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 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)) + } + ) + designDB.update(subDBs = newSubDBs) + end if + end transformGlobal end UniqueDesigns extension [T: HasDB](t: T) 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..f5f6c3327 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[StaticRef, 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[StaticRef, 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/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/main/scala/dfhdl/compiler/stages/verilog/VerilogOwnerPrinter.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogOwnerPrinter.scala index 0c5ba2f08..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 @@ -257,6 +257,37 @@ 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 = + // `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) + .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/verilog/VerilogPrinter.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogPrinter.scala index 65a39c1c3..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 @@ -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." ) @@ -245,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( 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..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 @@ -244,6 +244,10 @@ protected trait VHDLOwnerPrinter extends AbstractOwnerPrinter: |${body.hindent} |end process;""" end csProcessBlock + // 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 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..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 @@ -45,15 +47,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..9af9651d9 --- /dev/null +++ b/compiler/stages/src/test/scala/StagesSpec/DropForkJoinsSpec.scala @@ -0,0 +1,242 @@ +package StagesSpec + +import dfhdl.* +import dfhdl.compiler.stages.{dropForkJoinsED, dropForkJoinsRT} +// 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).dropForkJoinsED + 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("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).dropForkJoinsED + 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 + ) + + // 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 + process: + val fkAll = forkJoin: + locally: + a :== 1 + locally: + b :== 1 + val fkAny = forkJoinAny: + locally: + a :== 0 + locally: + b :== 0 + val fkNone = forkJoinNone: + locally: + a :== 1 + locally: + b :== 0 + end FJ + val fj = (new FJ).dropForkJoinsED + assertCodeString( + fj, + """|class FJ extends EDDesign: + | val a = Bit <> OUT + | val b = Bit <> OUT + | 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 :== 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(fk_start_0) + | locally: + | a.din := 1 + | fk_done_0.din := 1 + | waitWhile(fk_start_0) + | fk_done_0.din := 0 + | process: + | waitUntil(fk_start_1) + | locally: + | 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 new file mode 100644 index 000000000..db23256b0 --- /dev/null +++ b/compiler/stages/src/test/scala/StagesSpec/DropLocalBlocksSpec.scala @@ -0,0 +1,137 @@ +package StagesSpec + +import dfhdl.* +import dfhdl.compiler.stages.{dropLocalBlocksED, dropLocalBlocksRT} +// 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).dropLocalBlocksED + 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).dropLocalBlocksED + 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).dropLocalBlocksED + 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 + ) + + 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/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)( 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..3fed0afd9 --- /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 // 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") { + 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 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..60dc28d71 100644 --- a/compiler/stages/src/test/scala/StagesSpec/PrintVHDLCodeSpec.scala +++ b/compiler/stages/src/test/scala/StagesSpec/PrintVHDLCodeSpec.scala @@ -1674,4 +1674,168 @@ class PrintVHDLCodeSpec extends StageSpec: |end Foo_arch;""".stripMargin ) } + // 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 FJ extends EDDesign: + val a = Bit <> OUT + val b = Bit <> OUT + process: + val jAll = forkJoin: + locally: + a :== 0 + locally: + b :== 0 + end FJ + val fj = (new FJ).getCompiledCodeString + assertNoDiff( + fj, + """|library ieee; + |use ieee.std_logic_1164.all; + |use ieee.numeric_std.all; + |use work.dfhdl_pkg.all; + | + |entity FJ is + |port ( + | a : out std_logic; + | b : out std_logic + |); + |end FJ; + | + |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; + |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'; + | 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; + |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 + | 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 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 544eccfe7..2034241d9 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,235 @@ 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 unsupported under old Verilog (v2001)") { + given options.CompilerOptions.Backend = _.verilog.v2001 + // 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 + process: + val j = forkJoinAny: + locally: + a :== 0 + locally: + b :== 0 + end FJvAny + 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( + top, + """|`default_nettype none + |`timescale 1ns/1ps + | + |module ForkJoinFSM( + | input wire logic clk, + | input wire logic rst, + | output logic a, + | output logic b + |); + | `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 + | if (rst == 1'b1) begin + | a <= 1'b0; + | 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 + | end + |endmodule""".stripMargin + ) + } end PrintVerilogCodeSpec 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 ) } 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 diff --git a/core/src/main/scala/dfhdl/compiler/patching/Patch.scala b/core/src/main/scala/dfhdl/compiler/patching/Patch.scala index c723b3929..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 @@ -560,11 +565,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/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/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/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 diff --git a/core/src/main/scala/dfhdl/core/DFVal.scala b/core/src/main/scala/dfhdl/core/DFVal.scala index 76f8bc51c..e549eca67 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: @@ -301,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 @@ -373,6 +380,15 @@ 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. 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." + ] extension [T <: DFTypeAny, M <: ModifierAny](dfVal: DFVal[T, M]) @metaContextForward(0) @@ -431,20 +447,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 +479,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 @@ -552,7 +575,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 + ): 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) @@ -561,7 +588,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 + ): DFVal[DFTuple[T], Modifier[A, C, Modifier.Initialized, P]] = trydf { if (initValues.enable) dfVal.initForced(initValues(dfVal.dfType)(using dfc.anonymize)) @@ -577,7 +608,8 @@ object DFVal extends DFValLP: undefinedValue: ir.InitFileUndefinedValue = ir.InitFileUndefinedValue.Zeros )(using dfc: DFC, - check: InitCheck[I] + check: InitCheck[I], + notInsideInterface: NotInsideInterface ): DFVal[DFVector[T, Tuple1[D1]], Modifier[A, C, Modifier.Initialized, P]] = trydf: import dfc.getSet val vectorType = dfVal.dfType 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/Design.scala b/core/src/main/scala/dfhdl/core/Design.scala index f20d73559..fb75fead1 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 @@ -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 @@ -215,13 +215,22 @@ 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 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 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..e981097e2 --- /dev/null +++ b/core/src/main/scala/dfhdl/core/Fork.scala @@ -0,0 +1,55 @@ +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: + // 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, + "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, + "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() + 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/main/scala/dfhdl/core/Interface.scala b/core/src/main/scala/dfhdl/core/Interface.scala new file mode 100644 index 000000000..62bbf4d5d --- /dev/null +++ b/core/src/main/scala/dfhdl/core/Interface.scala @@ -0,0 +1,197 @@ +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/ +// 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 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: + self => + 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 + // 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 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/MutableDB.scala b/core/src/main/scala/dfhdl/core/MutableDB.scala index 6f4b15640..0ac3a1d14 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, @@ -11,6 +10,7 @@ import dfhdl.compiler.ir.{ DFOwner, DFRef, DFRefAny, + StaticRef, DFTag, DFVal, DFType, @@ -21,7 +21,6 @@ import dfhdl.compiler.ir.{ DFTags, annotation, DFDomainOwner, - DFInterfaceOwner, Meta } import dfhdl.compiler.analysis.filterPublicMembers @@ -414,9 +413,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 @@ -600,17 +598,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 } @@ -639,7 +629,18 @@ 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.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) => @@ -655,13 +656,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 +670,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 { @@ -698,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, _) => 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/core/r__For_Plugin.scala b/core/src/main/scala/dfhdl/core/r__For_Plugin.scala index 8c89e8c2e..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 @@ -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/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]] diff --git a/core/src/test/scala/CoreSpec/DFDecimalSpec.scala b/core/src/test/scala/CoreSpec/DFDecimalSpec.scala index 5f6a31af8..aeca28321 100644 --- a/core/src/test/scala/CoreSpec/DFDecimalSpec.scala +++ b/core/src/test/scala/CoreSpec/DFDecimalSpec.scala @@ -1076,12 +1076,42 @@ 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) + } + 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/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 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/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 = ` diff --git a/docs/user-guide/interfaces/index.md b/docs/user-guide/interfaces/index.md index 8b7518581..ce15dcf8c 100644 --- a/docs/user-guide/interfaces/index.md +++ b/docs/user-guide/interfaces/index.md @@ -1 +1,471 @@ -# 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. +* _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 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. +/// + +```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]. 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)). + +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 `.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 +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 +val v4 = view.in(a).inout(b) // b is bidirectional +``` + +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} + +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 `.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 +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`, `<> OUT`, or `<> INOUT`, +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. + +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 +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 + +### 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.ASIS` {#projecting-a-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.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 +access goes through a view's projection terminal. + +#### Projection terminals {#projection-terminals} + +`.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... | +|---|---| +| `.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) | +| `.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 `.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. + +### 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.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 +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 `:=` +/`:==` 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.ASIS +val sub = bus.subordinate.ASIS +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.ASIS 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. +* 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 projection terminal (`.ASIS`, `.FLIP`, + `.MONITOR`, etc.). 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. 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/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 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/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/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") 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 e5a94084a..c0895b037 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 @@ -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 @@ -83,8 +87,38 @@ 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] = + 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(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) + val inheritedPath = Option(System.getenv("PATH")).getOrElse("") + (dllDirs :+ inheritedPath).mkString(java.io.File.pathSeparator) + } protected def designFiles(using getSet: MemberGetSet): List[String] = getSet.designDB.srcFiles.collect { @@ -170,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) @@ -214,6 +275,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..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" @@ -23,7 +24,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) diff --git a/mkdocs.yml b/mkdocs.yml index 6c6a25cc4..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 [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 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/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 => diff --git a/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala b/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala index 29638a664..40b96d119 100644 --- a/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala +++ b/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala @@ -44,7 +44,8 @@ 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 interfaceTpe: TypeRef = uninitialized var topAnnotSym: ClassSymbol = uninitialized var appTpe: TypeRef = uninitialized var noTopAnnotIsRequired: TypeRef = uninitialized @@ -60,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 @@ -78,7 +91,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 +99,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 +169,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 +207,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 @@ -310,6 +323,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: @@ -347,7 +376,8 @@ 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") + interfaceTpe = requiredClassRef("dfhdl.core.Interface") 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 1a32b020e..8d88cce25 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 @@ -43,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. + * Classes extending `Interface` are excluded, since they are never entry points and + * must not receive `@top`. */ class PreTyperPhase(setting: Setting) extends CommonPhase: import untpd.* @@ -153,9 +156,12 @@ class PreTyperPhase(setting: Setting) extends CommonPhase: case _ => false private val designParentNames = Set("EDDesign", "RTDesign", "DFDesign") + private val interfaceParentNames = Set("Interface") 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 @@ -193,14 +199,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 { @@ -231,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) ||