From 07a18c268f2475deb731a8662264f175dd4a6710 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Tue, 2 Jun 2026 13:47:29 +0300 Subject: [PATCH 01/24] potentially allowing `:=` to be implemented with `exactOp2`. currently is missing support for match expressions (lack of MatchWrapper in Exact.scala) and has a problem with if expressions. --- core/src/main/scala/dfhdl/core/DFVal.scala | 66 +++++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/dfhdl/core/DFVal.scala b/core/src/main/scala/dfhdl/core/DFVal.scala index 101db690a..4ce81c35a 100644 --- a/core/src/main/scala/dfhdl/core/DFVal.scala +++ b/core/src/main/scala/dfhdl/core/DFVal.scala @@ -1045,11 +1045,11 @@ object DFVal extends DFValLP: type OutP = CONST def conv(dfType: T, value: V)(using DFC): Out = Bubble.constValOf(dfType, named = true) // Accept NOTHING for any DFType, unless not in DF domain, and then we limit it to Bits or Bit type - given fromNOTHING[T <: DFTypeAny](using + given fromNOTHING[IRT <: ir.DFType, A <: Args, T <: DFType[IRT, A]](using dt: DomainType )(using AssertGiven[ - dt.type <:< DomainType.DF | T <:< DFBit | T <:< DFType[ir.DFBits, Args], + dt.type <:< DomainType.DF | T =:= DFBit | IRT =:= ir.DFBits, "`NOTHING` can only be assigned to either `Bits` or `Bit` DFHDL values outside of a dataflow (DF) domain." ] ): TC[T, NOTHING] with @@ -1273,6 +1273,7 @@ object DFVal extends DFValLP: export DFOpaque.Val.Ops.{evOpAsDFOpaqueIterable, evOpClkAsClkComp, evOpRstAsRstComp} export TDFString.Val.Ops.given export ConnectOps.given + export DFVarOps.given given evOpCompare[LT <: DFTypeAny, LP, L <: DFValTP[LT, LP], R, Op <: FuncOp, RP](using tc: Compare.Aux[LT, R, Op, false, RP], @@ -1590,6 +1591,21 @@ final class REG_DIN[T <: DFTypeAny](val irValue: DFError.REG_DIN[T]) extends Any val dfVar = irValue.dfVar dfVar.assign(rhs(dfVar.dfType)) } + // transparent inline def :=[R](inline rhs: R)(using DFC): Unit = + // exactOp2[":=", DFC, Unit](this, rhs) +object REG_DIN: + given evREG_DIN_AssignDcl[ + T <: DFTypeAny, + L <: REG_DIN[T], + R + ](using + tc: DFVal.TC[T, R] + ): ExactOp2Aux[":=", DFC, Unit, L, R, Unit] = new ExactOp2[":=", DFC, Unit, L, R]: + type Out = Unit + def apply(lhs: L, rhs: R)(using DFC): Out = trydf { + val dfVar = lhs.irValue.dfVar + dfVar.assign(tc(dfVar.dfType, rhs)) + }(using dfc, CTName(":=")) object DFVarOps: protected type NotREG[A] = AssertGiven[ @@ -1628,6 +1644,12 @@ object DFVarOps: A <:< DomainType.RT, "`.din` selection is only allowed under register-transfer (RT) domains." ] + // extension [L](inline lhs: L) + // transparent inline def :=[R](inline rhs: R)(using DFC): Unit = + // exactOp2[":=", DFC, Unit](lhs, rhs) + // extension [L](inline lhs: L) + // transparent inline def :==[R](inline rhs: R)(using DFC): Unit = + // exactOp2[":==", DFC, Unit](lhs, rhs) extension [T <: DFTypeAny, A](dfVar: DFVal[T, Modifier[A, Any, Any, Any]]) def :=(rhs: DFVal.TC.Exact[T])(using DFC @@ -1744,6 +1766,46 @@ object DFVarOps: } assignRecur(dfVarsIR, argsBitsIR, 0, Nil) end extension + + given evAssignDcl[ + T <: DFTypeAny, + A, + M <: Modifier[A, Any, Any, Any], + L <: DFVal[T, M], + R + ](using + dt: DomainType, + idA: Id[A] // hack to prevent widening A to Any + )(using + notREG: NotREG[A], + varOnly: VarOnly[A], + insideProcess: `InsideProcess:=`[dt.type, A], + tc: DFVal.TC[T, R] + ): ExactOp2Aux[":=", DFC, Unit, L, R, Unit] = new ExactOp2[":=", DFC, Unit, L, R]: + type Out = Unit + def apply(lhs: L, rhs: R)(using DFC): Out = trydf { + lhs.assign(tc(lhs.dfType, rhs)) + }(using dfc, CTName(":=")) + + given evNBAssignDcl[ + T <: DFTypeAny, + A, + M <: Modifier[A, Any, Any, Any], + L <: DFVal[T, M], + R + ](using + dt: DomainType, + idA: Id[A] // hack to prevent widening A to Any + )(using + varOnly: VarOnly[A], + edDomainOnly: EDDomainOnly[dt.type], + insideProcess: `InsideProcess:=`[dt.type, A], + tc: DFVal.TC[T, R] + ): ExactOp2Aux[":==", DFC, Unit, L, R, Unit] = new ExactOp2[":==", DFC, Unit, L, R]: + type Out = Unit + def apply(lhs: L, rhs: R)(using DFC): Out = trydf { + lhs.nbassign(tc(lhs.dfType, rhs)) + }(using dfc, CTName(":==")) end DFVarOps object ConnectOps: From 78325f572919f81454ae8a4912a1ed8b8e658164 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Wed, 3 Jun 2026 11:16:16 +0300 Subject: [PATCH 02/24] remove redundant compiler_stages compilation dependency from corePlayground --- project/DFHDLCommands.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/project/DFHDLCommands.scala b/project/DFHDLCommands.scala index 7bdc48a47..d516aaf95 100644 --- a/project/DFHDLCommands.scala +++ b/project/DFHDLCommands.scala @@ -12,6 +12,7 @@ object DFHDLCommands { val newState = extracted.appendWithSession(Seq( (LocalProject("internals") / Test / sources) := Nil, (LocalProject("core") / Test / sources) := ((LocalProject("core") / Test / sources).value.filter(_.toString.contains("Playground.scala"))), + (LocalProject("compiler_stages") / Compile / sources) := Nil, (LocalProject("compiler_stages") / Test / sources) := Nil, (LocalProject("platforms") / Compile / sources) := Nil, (LocalProject("platforms") / Test / sources) := Nil, From 54ff20f0df622ad9ecb41dc00a541c65f776a9eb Mon Sep 17 00:00:00 2001 From: Oron Port Date: Wed, 3 Jun 2026 22:52:32 +0300 Subject: [PATCH 03/24] docs: better reduction not op for bit --- docs/transitioning/from-verilog/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/transitioning/from-verilog/index.md b/docs/transitioning/from-verilog/index.md index 3b191d7c5..38483fc24 100644 --- a/docs/transitioning/from-verilog/index.md +++ b/docs/transitioning/from-verilog/index.md @@ -566,8 +566,8 @@ val v = Bits(8) <> VAR val all_set = v.& // Bit: AND reduce val any_set = v.| // Bit: OR reduce val parity = v.^ // Bit: XOR reduce -val not_all = !v.& // Bit: NAND reduce -val none_set = !v.| // Bit: NOR reduce +val not_all = ~v.& // Bit: NAND reduce +val none_set = ~v.| // Bit: NOR reduce ``` From 0cb2e8573ea4de39d843d1dff74ed9c7b035e406 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Thu, 4 Jun 2026 17:03:05 +0300 Subject: [PATCH 04/24] change to `transparent inline implicit` modifier order for consistency --- core/src/main/scala/dfhdl/core/DFVal.scala | 22 +++++++++---------- .../main/scala/dfhdl/internals/Exact.scala | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/core/src/main/scala/dfhdl/core/DFVal.scala b/core/src/main/scala/dfhdl/core/DFVal.scala index 4ce81c35a..afdd30e01 100644 --- a/core/src/main/scala/dfhdl/core/DFVal.scala +++ b/core/src/main/scala/dfhdl/core/DFVal.scala @@ -178,7 +178,7 @@ sealed protected trait DFValLP: */ type CommonR = DFValAny | Bubble | DFVal.NOTHING | BoolSelWrapper[?, ?, ?] - implicit transparent inline def DFBitsValConversion[ + transparent inline implicit def DFBitsValConversion[ W <: IntP, P <: Boolean, R <: CommonR | SameElementsVector[?] | NonEmptyTuple @@ -188,7 +188,7 @@ sealed protected trait DFValLP: DFValConversionMacro[DFBits[W], ISCONST[P], R]('from)('dfc) } // TODO: candidate should be fixed to cause UInt[?]->SInt[Int] conversion - implicit transparent inline def DFXIntValConversion[ + transparent inline implicit def DFXIntValConversion[ S <: Boolean, W <: IntP, N <: NativeType, @@ -199,7 +199,7 @@ sealed protected trait DFValLP: )(using dfc: DFCG): DFValTP[DFXInt[S, W, N], ISCONST[P]] = ${ DFValConversionMacro[DFXInt[S, W, N], ISCONST[P], R]('from)('dfc) } - implicit transparent inline def DFOpaqueValConversion[ + transparent inline implicit def DFOpaqueValConversion[ TFE <: DFOpaque.Abstract, P <: Boolean, R <: CommonR @@ -208,7 +208,7 @@ sealed protected trait DFValLP: )(using dfc: DFCG): DFValTP[DFOpaque[TFE], ISCONST[P]] = ${ DFValConversionMacro[DFOpaque[TFE], ISCONST[P], R]('from)('dfc) } - implicit transparent inline def DFStructValConversion[ + transparent inline implicit def DFStructValConversion[ F <: DFStruct.Fields, P <: Boolean, R <: CommonR | DFStruct.Fields @@ -217,7 +217,7 @@ sealed protected trait DFValLP: )(using dfc: DFCG): DFValTP[DFStruct[F], ISCONST[P]] = ${ DFValConversionMacro[DFStruct[F], ISCONST[P], R]('from)('dfc) } - implicit transparent inline def DFTupleValConversion[ + transparent inline implicit def DFTupleValConversion[ T <: NonEmptyTuple, P <: Boolean, R <: CommonR | NonEmptyTuple @@ -226,7 +226,7 @@ sealed protected trait DFValLP: )(using dfc: DFCG): DFValTP[DFTuple[T], ISCONST[P]] = ${ DFValConversionMacro[DFTuple[T], ISCONST[P], R]('from)('dfc) } - implicit transparent inline def DFVectorValConversion[ + transparent inline implicit def DFVectorValConversion[ T <: DFTypeAny, D <: IntP, P <: Boolean, @@ -236,7 +236,7 @@ sealed protected trait DFValLP: )(using dfc: DFCG): DFValTP[DFVector[T, Tuple1[D]], ISCONST[P]] = ${ DFValConversionMacro[DFVector[T, Tuple1[D]], ISCONST[P], R]('from)('dfc) } - implicit transparent inline def DFBitValConversion[ + transparent inline implicit def DFBitValConversion[ P <: Boolean, R <: CommonR | Int | Boolean ]( @@ -244,7 +244,7 @@ sealed protected trait DFValLP: )(using dfc: DFCG): DFValTP[DFBit, ISCONST[P]] = ${ DFValConversionMacro[DFBit, ISCONST[P], R]('from)('dfc) } - implicit transparent inline def DFBoolValConversion[ + transparent inline implicit def DFBoolValConversion[ P <: Boolean, R <: CommonR | Int | Boolean ]( @@ -252,7 +252,7 @@ sealed protected trait DFValLP: )(using dfc: DFCG): DFValTP[DFBool, ISCONST[P]] = ${ DFValConversionMacro[DFBool, ISCONST[P], R]('from)('dfc) } - implicit transparent inline def DFEnumValConversion[ + transparent inline implicit def DFEnumValConversion[ E <: DFEncoding, P <: Boolean, R <: CommonR | E @@ -261,7 +261,7 @@ sealed protected trait DFValLP: )(using dfc: DFCG): DFValTP[DFEnum[E], ISCONST[P]] = ${ DFValConversionMacro[DFEnum[E], ISCONST[P], R]('from)('dfc) } - implicit transparent inline def DFDoubleValConversion[ + transparent inline implicit def DFDoubleValConversion[ P <: Boolean, R <: CommonR | Double ]( @@ -269,7 +269,7 @@ sealed protected trait DFValLP: )(using dfc: DFCG): DFValTP[DFDouble, ISCONST[P]] = ${ DFValConversionMacro[DFDouble, ISCONST[P], R]('from)('dfc) } - implicit transparent inline def DFStringValConversion[ + transparent inline implicit def DFStringValConversion[ P <: Boolean, R <: CommonR | String ]( diff --git a/internals/src/main/scala/dfhdl/internals/Exact.scala b/internals/src/main/scala/dfhdl/internals/Exact.scala index a55222d34..16577dac8 100644 --- a/internals/src/main/scala/dfhdl/internals/Exact.scala +++ b/internals/src/main/scala/dfhdl/internals/Exact.scala @@ -89,7 +89,7 @@ object Exact: value match case exact: Exact[?] => strip(exact.value) case _ => value - implicit transparent inline def fromValue[T]( + transparent inline implicit def fromValue[T]( inline value: T ): Exact[?] = ${ fromValueMacro[T]('value) } def fromValueMacro[T]( From 1da58ce55cf17b87e72463f3dc2af16fe2e62d2a Mon Sep 17 00:00:00 2001 From: Oron Port Date: Thu, 4 Jun 2026 17:39:50 +0300 Subject: [PATCH 05/24] fix position errors in transparent inline implicit conversion --- .../test/scala/CoreSpec/DFDecimalSpec.scala | 8 +++ core/src/test/scala/NoDFCSpec.scala | 16 ++++++ .../internals/ControlledMacroError.scala | 50 +++++++++++++--- .../dfhdl/internals/DualSummonTrapError.scala | 2 +- .../main/scala/dfhdl/internals/Exact.scala | 57 ++++++++++++------- 5 files changed, 106 insertions(+), 27 deletions(-) diff --git a/core/src/test/scala/CoreSpec/DFDecimalSpec.scala b/core/src/test/scala/CoreSpec/DFDecimalSpec.scala index f48e8cefb..5f6a31af8 100644 --- a/core/src/test/scala/CoreSpec/DFDecimalSpec.scala +++ b/core/src/test/scala/CoreSpec/DFDecimalSpec.scala @@ -1076,4 +1076,12 @@ 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""") + // } end DFDecimalSpec diff --git a/core/src/test/scala/NoDFCSpec.scala b/core/src/test/scala/NoDFCSpec.scala index f17ab5657..3ca8cc0b2 100644 --- a/core/src/test/scala/NoDFCSpec.scala +++ b/core/src/test/scala/NoDFCSpec.scala @@ -19,6 +19,22 @@ abstract class NoDFCSpec extends FunSuite, NoTopAnnotIsRequired: ) end assertCompileError + // TODO: there is a problem in DFDecimalSpec position error check + transparent inline def assertCompileErrorPos(expectedErr: String, column: Int)( + inline code: String + ): Unit = + val err = compiletime.testing.typeCheckErrors(code) match + case x @ (_ :+ last) => + scala.Predef.println(x.map(_.column)) + scala.Predef.println(x.map(_.lineContent)) + last.message + case _ => noErrMsg + assertNoDiff( + err, + expectedErr + ) + end assertCompileErrorPos + inline def assertRuntimeError(expectedErr: String)(runTimeCode: => Unit): Unit = val err = try diff --git a/internals/src/main/scala/dfhdl/internals/ControlledMacroError.scala b/internals/src/main/scala/dfhdl/internals/ControlledMacroError.scala index 12092047e..9a21417d5 100644 --- a/internals/src/main/scala/dfhdl/internals/ControlledMacroError.scala +++ b/internals/src/main/scala/dfhdl/internals/ControlledMacroError.scala @@ -5,23 +5,59 @@ object ControlledMacroError: // if contains a key, it means to activate error control. // if the value is empty (by default), it means the implicit given is found. // if the value is not empty, it means the implicit given with the error message. - private val positionError = collection.concurrent.TrieMap.empty[String, String] + private val macroAbortPositionError = collection.concurrent.TrieMap.empty[String, String] + private val compiletimeErrorPositionError = collection.concurrent.TrieMap.empty[String, String] private def getKey(using Quotes): String = import quotes.reflect.* Position.ofMacroExpansion.toString - def activate()(using Quotes): Unit = positionError += getKey -> "" - def deactivate()(using Quotes): Unit = positionError.remove(getKey) - def getLastError(using Quotes): String = positionError.getOrElse(getKey, "") - def getLastErrorAndDeactivate(using Quotes): String = positionError.remove(getKey).getOrElse("") + def activate()(using Quotes): Unit = macroAbortPositionError += getKey -> "" + def deactivate()(using Quotes): Unit = + compiletimeErrorPositionError.remove(getKey) + macroAbortPositionError.remove(getKey) + def getLastMacroAbortError(using Quotes): String = macroAbortPositionError.getOrElse(getKey, "") + def getLastCompiletimeError(using Quotes): String = + compiletimeErrorPositionError.getOrElse(getKey, "") def report(msg: String)(using Quotes): Expr[Nothing] = import quotes.reflect.report as macroReport val key = getKey - positionError.get(key) match + macroAbortPositionError.get(key) match case Some("") => - positionError += key -> msg + macroAbortPositionError += key -> msg macroReport.errorAndAbort(msg) case Some(existingMsg) => macroReport.errorAndAbort(existingMsg) case _ => + compiletimeErrorPositionError += key -> msg '{ compiletime.error(${ Expr(msg) }) } + def getLastErrorInExpr[T](expr: Expr[T])(using Quotes, Type[T]): Option[String] = + import quotes.reflect.* + def searchList(trees: List[Tree]): Option[String] = + trees.view.flatMap(search).headOption + def search(tree: Tree): Option[String] = + tree match + case Inlined(_, _, inner) => search(inner) + case Typed(inner, _) => search(inner) + case Select(inner, _) => search(inner) + case TypeApply(fun, _) => search(fun) + case Block(stats, expr) => searchList(expr :: stats) + case DefDef(_, _, _, Some(rhs)) => search(rhs) + case ValDef(_, _, Some(rhs)) => search(rhs) + case ClassDef(_, _, _, _, body) => searchList(body) + case Apply( + Select(Select(Ident("compiletime"), "package$package"), "error"), + List(Inlined(_, _, Literal(StringConstant(msg)))) + ) => + Some(msg) + case Apply(fun, args) => searchList(fun :: args) + case _ => None + if (getLastCompiletimeError.nonEmpty) search(expr.asTerm) + else None + end getLastErrorInExpr end ControlledMacroError + +extension [T](expr: Expr[T])(using Quotes, Type[T]) + def mapExprOrError[R](f: Expr[T] => Expr[R]): Expr[R] = + ControlledMacroError.getLastErrorInExpr(expr) match + case Some(msg) => '{ compiletime.error(${ Expr(msg) }) } + case None => f(expr) + end mapExprOrError diff --git a/internals/src/main/scala/dfhdl/internals/DualSummonTrapError.scala b/internals/src/main/scala/dfhdl/internals/DualSummonTrapError.scala index f31bc9d20..de627c6c7 100644 --- a/internals/src/main/scala/dfhdl/internals/DualSummonTrapError.scala +++ b/internals/src/main/scala/dfhdl/internals/DualSummonTrapError.scala @@ -24,7 +24,7 @@ object DualSummonTrapError: case iss: ImplicitSearchSuccess => Success(iss.tree.asExprOf[T]) case isf: ImplicitSearchFailure => - val lastError = ControlledMacroError.getLastError + val lastError = ControlledMacroError.getLastMacroAbortError if (lastError.nonEmpty) PriorityError[T](lastError) else NotFoundError[T](isf.explanation) end match diff --git a/internals/src/main/scala/dfhdl/internals/Exact.scala b/internals/src/main/scala/dfhdl/internals/Exact.scala index 16577dac8..f5b763c78 100644 --- a/internals/src/main/scala/dfhdl/internals/Exact.scala +++ b/internals/src/main/scala/dfhdl/internals/Exact.scala @@ -96,8 +96,10 @@ object Exact: value: Expr[T] )(using Quotes, Type[T]): Expr[Exact[?]] = import quotes.reflect.* - val exactInfo = value.exactInfo - '{ Exact[exactInfo.Underlying](${ exactInfo.exactExpr }) } + value.mapExprOrError { value => + val exactInfo = value.exactInfo + '{ Exact[exactInfo.Underlying](${ exactInfo.exactExpr }) } + } end fromValueMacro implicit inline def toValue[T](inline exact: Exact[T]): T = exact.value @@ -177,12 +179,14 @@ object Exact0: TC[From] <: Exact0.TC[From, Ctx]: Type ](from: Expr[From])(using Quotes): Expr[Exact0[Ctx, TC]] = import quotes.reflect.* - val fromExactInfo = from.exactInfo - '{ - Exact0[fromExactInfo.Underlying, Ctx, TC]( - ${ fromExactInfo.exactExpr }, - compiletime.summonInline[TC[fromExactInfo.Underlying]] - ) + from.mapExprOrError { from => + val fromExactInfo = from.exactInfo + '{ + Exact0[fromExactInfo.Underlying, Ctx, TC]( + ${ fromExactInfo.exactExpr }, + compiletime.summonInline[TC[fromExactInfo.Underlying]] + ) + } } end convMacro end Exact0 @@ -252,12 +256,14 @@ object Exact1: TC[Arg1 <: Arg1UB, From] <: Exact1.TC[Arg1UB, Arg1, FArg1, From, Ctx]: Type ](from: Expr[From])(using Quotes): Expr[Exact1[Arg1UB, Arg1, FArg1, Ctx, TC]] = import quotes.reflect.* - val fromExactInfo = from.exactInfo - '{ - Exact1[fromExactInfo.Underlying, Arg1UB, Arg1, FArg1, Ctx, TC]( - ${ fromExactInfo.exactExpr }, - compiletime.summonInline[TC[Arg1, fromExactInfo.Underlying]] - ) + from.mapExprOrError { from => + val fromExactInfo = from.exactInfo + '{ + Exact1[fromExactInfo.Underlying, Arg1UB, Arg1, FArg1, Ctx, TC]( + ${ fromExactInfo.exactExpr }, + compiletime.summonInline[TC[Arg1, fromExactInfo.Underlying]] + ) + } } end convMacro end Exact1 @@ -288,7 +294,9 @@ private def ascribeWidenedType(using Quotes)(term: quotes.reflect.Term): quotes. import quotes.reflect.* Typed(term, TypeTree.of(using term.tpe.dealias.asType)) -private def flattenInlined(using Quotes)(term: quotes.reflect.Term): (List[quotes.reflect.Definition], quotes.reflect.Term) = +private def flattenInlined(using + Quotes +)(term: quotes.reflect.Term): (List[quotes.reflect.Definition], quotes.reflect.Term) = import quotes.reflect.* term match case Inlined(_, bindings, inner) => @@ -315,7 +323,7 @@ private def exactOp1Macro[Op, Ctx, OutUB](lhs: Expr[Any])(ctx: Expr[Ctx])(using else val innerTerm = appTerm match case Inlined(_, Nil, inner) => inner - case t => t + case t => t Block(lhsBindings, innerTerm).asExprOf[OutUB] case None => ControlledMacroError.report("Unsupported argument type for this operation.") @@ -380,13 +388,24 @@ private def exactOp2Macro[Op, Ctx, 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) + 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 + case t => t Block(allBindings, innerTerm).asExprOf[OutUB] + end buildFlattened exactOp2ExprOrError match case Right(expr) => buildFlattened(lhsExactInfo.exactExpr.asTerm, rhsExactInfo.exactExpr.asTerm, expr) @@ -460,7 +479,7 @@ private def exactOp3Macro[Op, Ctx, OutUB]( else val innerTerm = appTerm match case Inlined(_, Nil, inner) => inner - case t => t + case t => t Block(allBindings, innerTerm).asExprOf[OutUB] case None => ControlledMacroError.report("Unsupported argument types for this operation.") From 3d4afafa3492713cc1da47f547d35858b4697632 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Fri, 5 Jun 2026 10:13:37 +0300 Subject: [PATCH 06/24] fix order members so it does not move local vars --- .../src/main/scala/dfhdl/compiler/stages/OrderMembers.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 83299a07f..2eba5e1ca 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/OrderMembers.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/OrderMembers.scala @@ -54,7 +54,8 @@ object OrderMembers: // fifth are ports case DclPort() => 5 // sixth are variables, but not iterators - case dcl @ DclVar() if !dcl.hasTagOf[IteratorTag] => 6 + case dcl @ DclVar() + if !dcl.hasTagOf[IteratorTag] && dcl.getOwner.isInstanceOf[DFDesignBlock] => 6 // seventh are design blocks that are direct children of named instances // (e.g., design blocks inside conditional blocks are not included) case dsn: (DFDesignBlock | DFDesignInst) if dsn.getOwner == dsn.getOwnerNamed => 7 From f5b2e0803368028b259ab37248b0a8445301f1e9 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Fri, 5 Jun 2026 10:53:36 +0300 Subject: [PATCH 07/24] DropLocalDcls: hoist local declarations out of step blocks Local VAR/CONST declarations inside a StepBlock (RT FSM state) could not survive into the generated FSM but were left in place. Generalize the owner-scope handling into a tailrec climbToScope helper that escapes nested conditional and step blocks: for non-VHDL the declaration moves to design level (before the process), for VHDL it moves to process-body level (before the outermost step). Behavior for the existing conditional/process cases is unchanged. Add step-block tests for both backends. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../dfhdl/compiler/stages/DropLocalDcls.scala | 83 +++++++++++++++---- .../scala/StagesSpec/DropLocalDclsSpec.scala | 59 +++++++++++++ 2 files changed, 125 insertions(+), 17 deletions(-) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropLocalDcls.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropLocalDcls.scala index c25745bb2..de750101a 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropLocalDcls.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/DropLocalDcls.scala @@ -4,6 +4,7 @@ import dfhdl.compiler.analysis.* import dfhdl.compiler.ir.* import dfhdl.compiler.patching.* import dfhdl.options.CompilerOptions +import scala.annotation.tailrec //format: off /** This stage moves local variable and constant declarations out of their lexical scope to a @@ -76,6 +77,36 @@ import dfhdl.options.CompilerOptions * stmt1 * stmt2 * }}} + * + * ===Rule 3: Declarations inside step blocks=== + * A local variable or constant declared inside a `StepBlock` (an RT FSM state) — either directly + * or nested inside a conditional within the step — is lifted out of the step, because the FSM + * states generated from steps cannot carry declarations: + * - For non-VHDL backends: moved to before the enclosing process block (design level). + * - For VHDL: moved to just before the outermost (top-level) step block, keeping the declaration + * at the process-body level where VHDL allows process variable declarations. + * {{{ + * // Before — zz declared inside a step + * class ID extends RTDesign: + * process: + * def S_0: Step = + * val zz = SInt(16) <> VAR + * ... + * + * // After (Verilog) — moved to design level, before the process + * class ID extends RTDesign: + * val zz = SInt(16) <> VAR + * process: + * def S_0: Step = + * ... + * + * // After (VHDL) — moved to just before the top-level step, inside the process + * class ID extends RTDesign: + * process: + * val zz = SInt(16) <> VAR + * def S_0: Step = + * ... + * }}} */ //format: on case object DropLocalDcls extends HierarchyStage: @@ -84,7 +115,7 @@ case object DropLocalDcls extends HierarchyStage: def transformSubDB(rootDB: DB)(using getSet: MemberGetSet, co: CompilerOptions, rg: RefGen): DB = val keepProcessDcls = co.backend.isVHDL val patches = subDB.members.view - // only var or constant declarations , + // only var or constant declarations, // and we also require their anonymous dependencies .flatMap { // skip iterator declarations @@ -93,25 +124,43 @@ case object DropLocalDcls extends HierarchyStage: case m @ DclConst() if !m.isGlobal => m.collectRelMembers(includeOrigVal = true) case _ => None } - .map(m => (m, m.getOwnerBlock)) - .flatMap { - // declarations inside conditional blocks - case (dcl, cb: DFConditional.Block) => - val topCondHeader = cb.getTopConditionalHeader - // if we don't keep process vars, we check if the owner is a process block, - // and if so, we need to move the declarations before it. - val moveBeforeMember = topCondHeader.getOwnerBlock match - case pb: ProcessBlock if !keepProcessDcls => pb - case _ => topCondHeader - Some(moveBeforeMember -> Patch.Move(dcl, Patch.Move.Config.Before)) - // declarations inside process blocks if we should not keep them - case (dcl, pb: ProcessBlock) if !keepProcessDcls => - Some(pb -> Patch.Move(dcl, Patch.Move.Config.Before)) - case _ => None - } + .flatMap(dclMovePatch(_, keepProcessDcls)) .toList subDB.patch(patches) end transformSubDB + + // Computes the move patch (if any) relocating a local declaration `dcl` out of its lexical scope + // to a position supported by the target language. Returns `None` when the declaration is already + // at a valid position (directly at design level, or — under VHDL — directly inside a process). + private def dclMovePatch(dcl: DFMember, keepProcessDcls: Boolean)(using + MemberGetSet + ): Option[(DFMember, Patch)] = + val (anchor, scopeBlock) = climbToScope(dcl) + scopeBlock match + // non-VHDL: declarations are not allowed inside process blocks, so move to the design level + // before the process block. + case pb: ProcessBlock if !keepProcessDcls => + Some(pb -> Patch.Move(dcl, Patch.Move.Config.Before)) + // VHDL process scope, or design scope: move before the outermost in-scope anchor, but only + // when the declaration actually needs to escape an enclosing conditional or step block. + case _ => + if anchor ne dcl then Some(anchor -> Patch.Move(dcl, Patch.Move.Config.Before)) + else None + + // Climbs from a member up through enclosing conditional and step blocks, returning the outermost + // in-scope anchor to move before, paired with the nearest enclosing non-conditional, non-step + // scope block (a ProcessBlock or DFDesignBlock, or a loop block). When the member is inside a + // conditional, the anchor is the top-level conditional header for that scope; otherwise it is the + // member itself. Step blocks are escaped one level at a time until a non-step scope is reached. + @tailrec private def climbToScope(m: DFMember)(using MemberGetSet): (DFMember, DFBlock) = + val (anchor, scopeBlock) = m.getOwnerBlock match + case cb: DFConditional.Block => + val topCondHeader = cb.getTopConditionalHeader + (topCondHeader, topCondHeader.getOwnerBlock) + case b => (m, b) + scopeBlock match + case sb: StepBlock => climbToScope(sb) + case _ => (anchor, scopeBlock) end DropLocalDcls extension [T: HasDB](t: T) diff --git a/compiler/stages/src/test/scala/StagesSpec/DropLocalDclsSpec.scala b/compiler/stages/src/test/scala/StagesSpec/DropLocalDclsSpec.scala index a388c47a2..af1e21edc 100644 --- a/compiler/stages/src/test/scala/StagesSpec/DropLocalDclsSpec.scala +++ b/compiler/stages/src/test/scala/StagesSpec/DropLocalDclsSpec.scala @@ -130,4 +130,63 @@ class DropLocalDclsSpec extends StageSpec: |end ID |""".stripMargin ) + + test("Step block drops local dcls"): + class ID extends RTDesign: + val x = SInt(16) <> IN + val y = SInt(16) <> OUT.REG + process: + def S_0: Step = + val zz = SInt(16) <> VAR + zz := x + y.din := zz + NextStep + end S_0 + end ID + val id = (new ID).dropLocalDcls + assertCodeString( + id, + """|class ID extends RTDesign: + | val x = SInt(16) <> IN + | val y = SInt(16) <> OUT.REG + | val zz = SInt(16) <> VAR + | process: + | def S_0: Step = + | zz := x + | y.din := zz + | NextStep + | end S_0 + |end ID + |""".stripMargin + ) + + test("Step block keeps local dcls under VHDL"): + given options.CompilerOptions.Backend = _.vhdl + class ID extends RTDesign: + val x = SInt(16) <> IN + val y = SInt(16) <> OUT.REG + process: + def S_0: Step = + val zz = SInt(16) <> VAR + zz := x + y.din := zz + NextStep + end S_0 + end ID + val id = (new ID).dropLocalDcls + assertCodeString( + id, + """|class ID extends RTDesign: + | val x = SInt(16) <> IN + | val y = SInt(16) <> OUT.REG + | process: + | val zz = SInt(16) <> VAR + | def S_0: Step = + | zz := x + | y.din := zz + | NextStep + | end S_0 + |end ID + |""".stripMargin + ) end DropLocalDclsSpec From 857c5b3f64cbd3727dd6219d51c3935066fa7f98 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Fri, 5 Jun 2026 18:33:19 +0300 Subject: [PATCH 08/24] fix SimplifyRTOps to handle nested for loops --- .../dfhdl/compiler/stages/SimplifyRTOps.scala | 37 ++++++++++++++++--- .../scala/StagesSpec/SimplifyRTOpsSpec.scala | 29 +++++++++++++++ 2 files changed, 61 insertions(+), 5 deletions(-) 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 f5ae7ad59..2053fb688 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/SimplifyRTOps.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/SimplifyRTOps.scala @@ -7,6 +7,7 @@ import dfhdl.options.CompilerOptions import dfhdl.internals.* import DFVal.Func.Op as FuncOp import scala.collection.mutable +import scala.annotation.tailrec //format: off /** This stage simplifies RT-domain operations into lower-level constructs that downstream stages * can process. It handles four kinds of transformations: rising/falling edge predicates, boolean @@ -107,12 +108,32 @@ case object SimplifyRTOps extends HierarchyStage: def nullifies: Set[Stage] = Set(DropUnreferencedAnons, DFHDLUniqueNames, DropLocalDcls) def transformSubDB(rootDB: DB)(using MemberGetSet, CompilerOptions, RefGen): DB = + transformRepeatedly(subDB) + + // Nested for loops must not be rewritten in the same pass: the inner loop is a body member of the + // outer loop, so transforming both at once (each via `ReplaceWithLast(ChangeRefAndRemove)` plus an + // `After` increment patch) interleaves their patches and mis-orders the inner while-block's members + // relative to the outer one, breaking the flat-list ownership invariant. Instead, rewrite one + // nesting level per pass (innermost transformable for loops first) until none remain. The edge and + // wait rules only match their original IR shapes, so they fire once on the first pass and produce + // no further patches on later passes. + @tailrec private def transformRepeatedly(db: DB)(using CompilerOptions, RefGen): DB = + given MemberGetSet = db.getSet + val patches = collectPatches + if patches.isEmpty then db + else transformRepeatedly(db.patch(patches)) + + // A for loop this stage rewrites into a while loop (Rule 4). + private def isTransformableForLoop(fb: DFLoop.DFForBlock)(using MemberGetSet): Boolean = + fb.isInRTDomain && !fb.isCombinational && fb.isInProcess + + private def collectPatches(using MemberGetSet, CompilerOptions, RefGen): List[(DFMember, Patch)] = extension (dfVal: DFVal) def isAnonReferencedByWait: Boolean = dfVal.isAnonymous && dfVal.originMembers.view.exists { case _: Wait => true case _ => false } - val patches = subDB.members.view.flatMap { + subDB.members.view.flatMap { case trigger @ DFVal.Func( _, op @ (FuncOp.rising | FuncOp.falling), @@ -199,9 +220,16 @@ case object SimplifyRTOps extends HierarchyStage: else None end if - // replace RT for loops with while loops + iterator VAR.REG + increment at end of body + // replace RT for loops with while loops + iterator VAR.REG + increment at end of body. + // Only rewrite the innermost transformable for loop in each pass (one whose body contains no + // further transformable for loop); outer loops are handled in subsequent passes (see + // `transformRepeatedly`). case forBlock: DFLoop.DFForBlock - if forBlock.isInRTDomain && !forBlock.isCombinational && forBlock.isInProcess => + if isTransformableForLoop(forBlock) && + !forBlock.members(MemberView.Flattened).exists { + case inner: DFLoop.DFForBlock => isTransformableForLoop(inner) + case _ => false + } => val iteratorDcl = forBlock.iteratorRef.get val range = forBlock.rangeRef.get val startBigInt: BigInt = range.startRef.get match @@ -257,8 +285,7 @@ case object SimplifyRTOps extends HierarchyStage: case _ => None }.toList - subDB.patch(patches) - end transformSubDB + end collectPatches end SimplifyRTOps extension [T: HasDB](t: T) diff --git a/compiler/stages/src/test/scala/StagesSpec/SimplifyRTOpsSpec.scala b/compiler/stages/src/test/scala/StagesSpec/SimplifyRTOpsSpec.scala index 323a601e9..6fb2d7a9f 100644 --- a/compiler/stages/src/test/scala/StagesSpec/SimplifyRTOpsSpec.scala +++ b/compiler/stages/src/test/scala/StagesSpec/SimplifyRTOpsSpec.scala @@ -287,4 +287,33 @@ class SimplifyRTOpsSpec extends StageSpec(stageCreatesUnrefAnons = true): ) } + test("nested RT for loops are converted to nested while loops") { + class Foo extends RTDesign: + val o = Int <> OUT.REG + process: + for (i <- 0 until 10) + for (j <- 0 until 10) + o.din := i + j + end Foo + val top = (new Foo).simplifyRTOps + assertCodeString( + top, + """|class Foo extends RTDesign: + | val o = Int <> OUT.REG + | process: + | val i = Int <> VAR.REG + | i.din := 0 + | while (i < 10) + | val j = Int <> VAR.REG + | j.din := 0 + | while (j < 10) + | o.din := i + j + | j.din := j + 1 + | end while + | i.din := i + 1 + | end while + |end Foo""".stripMargin + ) + } + end SimplifyRTOpsSpec From 8d19fd862f1536e770be84e15f7d1841d56aaf16 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Fri, 5 Jun 2026 18:34:00 +0300 Subject: [PATCH 09/24] fix oss-cad-suite verilator execution under windows --- lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala | 5 +++++ .../main/scala/dfhdl/tools/toolsCore/Verilator.scala | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala b/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala index 304699481..8d72a9ba2 100644 --- a/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala +++ b/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala @@ -82,6 +82,10 @@ trait Tool: protected def convertWindowsToLinuxPaths: String = 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 + protected def designFiles(using getSet: MemberGetSet): List[String] = getSet.designDB.srcFiles.collect { case SourceFile( @@ -151,6 +155,7 @@ trait Tool: // spawn the process val process = os.proc(os.Shellable(fullExec.split(" ").toSeq)).spawn( cwd = os.Path(execPath, os.pwd), + env = execEnv, stdin = os.Inherit, stdout = processOutput, mergeErrIntoOut = true diff --git a/lib/src/main/scala/dfhdl/tools/toolsCore/Verilator.scala b/lib/src/main/scala/dfhdl/tools/toolsCore/Verilator.scala index 1e6790265..fbd6038cf 100644 --- a/lib/src/main/scala/dfhdl/tools/toolsCore/Verilator.scala +++ b/lib/src/main/scala/dfhdl/tools/toolsCore/Verilator.scala @@ -24,6 +24,16 @@ object Verilator extends VerilogLinter, VerilogSimulator: override val convertWindowsToLinuxPaths: Boolean = true protected def includeFolderFlag: String = "-I" + // On Windows the oss-cad-suite sets VERILATOR_ROOT with backslashes (e.g. + // `c:\oss-cad-suite\share\verilator`). Verilator's generated Makefile invokes its Python + // includer as `$(PYTHON3) $(VERILATOR_ROOT)/bin/verilator_includer ...`, and the MSYS shell + // eats those backslashes as escapes, mangling the script path. The includer then fails and + // writes an empty `V__ALL.cpp` (no `main()`), so the final link fails with + // `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 + protected def lintCmdLanguageFlag(dialect: VerilogDialect): String = val language = dialect match case VerilogDialect.v95 => "1364-1995" From a4bcdc36165fd7ab8f72e4872e2dd38be17c0372 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Fri, 5 Jun 2026 18:40:34 +0300 Subject: [PATCH 10/24] update the skill --- .claude/commands/new-stage.md | 102 ++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/.claude/commands/new-stage.md b/.claude/commands/new-stage.md index 75eea3f3e..80a1a00e3 100644 --- a/.claude/commands/new-stage.md +++ b/.claude/commands/new-stage.md @@ -744,6 +744,39 @@ Key invariants: - `given MemberGetSet = db.getSet` must be re-established at the top of each recursive call so navigation helpers see the updated member structure. +**This pattern is not just for flattening — it is the general cure for "nested same-kind +rewrites".** Whenever a stage rewrites a construct via `Patch.Move` or +`ReplaceWithLast(ChangeRefAndRemove)` *plus* a satellite patch anchored on its body (an `After` +increment, a `Before` goto, a consumed-member `Remove`), and that construct can **nest inside +another of the same kind**, rewriting both in one pass conflicts: the inner one appears both inside +the outer one's moved descendants (`Flattened`) AND in its own patches → ownership-order breakage +or `Received two different patches for the same member`. Two real instances from this codebase: +- `SimplifyRTOps` rewriting nested `for` loops (`DFForBlock` → `while` + iterator + `After` + increment). +- `FlattenStepBlocks` extracting `StepBlock`s nested in conditional branches (`Move` + inserted + goto + consumed-goto `Remove`). + +The fix in both was identical: drive the rewrite with `@tailrec ...Repeatedly(db)` and **gate each +pass to the non-nested instances** so an outer and inner are never patched together — e.g. process +only the *innermost* (no transformable descendant) or only the *outermost* (no transformable +ancestor), and let later passes pick up the rest: +```scala +// innermost-first gate (SimplifyRTOps): skip a for loop that contains another transformable one +case fb: DFLoop.DFForBlock + if isTransformable(fb) && !fb.members(MemberView.Flattened).exists { + case inner: DFLoop.DFForBlock => isTransformable(inner); case _ => false + } => + +// outermost-first gate (FlattenStepBlocks): skip a step that has a transformable step ancestor +val targets = pb.members(MemberView.Flattened).collect { + case sb: StepBlock if isCondBranchStep(sb) && !hasCondBranchStepAncestor(sb) => sb +} +``` +Each pass strictly reduces the count of nested instances, so the `@tailrec` loop converges. This +keeps the multi-phase structure intact — a stage with several phases can wrap just the affected +phase(s) in their own `...Repeatedly` driver (as `FlattenStepBlocks` does for conditional +extraction and structural flattening independently). + ### Pattern 10 — Replace a DFOwner while preserving its children Use when you want to swap one owner block for another (e.g. `DFForBlock` → `DFWhileBlock`) but @@ -867,6 +900,67 @@ extension [T: HasDB](t: T) --- +## Debugging a Stage Failure with TRACE + +When a full compilation blows up inside the stage pipeline (a `SanityCheck` failure, a patch +conflict, etc.), the fastest way to localize and reproduce it is the **TRACE log**, which prints +the full design code string after every stage that changes the DB. + +### Enabling and running + +Put a design in `lib/src/test/scala/Playground.scala` (or `core/.../Playground.scala`) and add at +the top of the file: +```scala +given options.CompilerOptions.LogLevel = _.TRACE +``` +Then run the whole pipeline for a top-level design named `Foo` via its generated `top_Foo` main: +```bash +sbtn.bat ";libPlayground;lib/Test/runMain top_Foo" # core equivalent: corePlayground + core/Test/runMain +``` +Pass tool/backend arguments after `--`: +```bash +sbtn.bat ";libPlayground;lib/Test/runMain top_Foo -- simulate -t questa" # or -t nvc/ghdl with -b vhdl +``` +A design with **no ports + a `finish()`** is treated as a self-contained simulation top, so the +default (no-arg) action becomes *simulate* instead of *compile*. + +### Reading the trace to localize the failing stage + +The log emits `Running stage X....` / `Finished stage X` around each stage, runs a `SanityCheck` +after every non-`NoCheckStage`, and prints the code string after any stage that changed the DB. So: +- The **`SanityCheck` that throws** (`Failed ownership check!`, `Received two different patches for + the same member`, …) fires *immediately after* the offending stage — that stage name is your + culprit, even if the symptom (a dangling owner, a duplicate Goto) looks structural. +- **Don't assume the failing stage is the one you changed.** A stage can pass its own sanity check + and emit a valid DB, yet a *later* stage chokes on a shape your stage newly produced. Read the + `Running stage` sequence to find the first failure, not the first suspect. + +### Harvesting a self-contained reproducer from the trace + +The code printout emitted **immediately before** the failing stage is that stage's *input* — and +crucially it is a **valid** design (it just passed the previous stage's sanity check). Because +stages run before later lowering, the printed constructs are exactly the failing stage's input +form, so you can drop that printout almost verbatim into a self-contained `Spec` test (see +*Test Authoring Rules* below) as the reproducer. This turns an opaque end-to-end crash into a fast, +isolated unit test in one step — write the test, watch it fail with the same error, then fix. + +### Caveats + +- Re-running an unchanged design may short-circuit on the on-disk cache + (`Loading committed design from cache...`) and skip the stages (and the trace) entirely. Pass + **`--nocache`** to disable caching — it goes *after* `--` and *before* the DFHDL App command + (`compile` / `simulate` / …): + ```bash + sbtn.bat ";libPlayground;lib/Test/runMain top_Foo -- --nocache compile" + sbtn.bat ";libPlayground;lib/Test/runMain top_Foo -- --nocache simulate -t questa" + ``` + (Alternatively clear `sandbox/` via `sbtn clearSandbox`, or edit the design — but `--nocache` + is the lightweight option for repeated trace runs.) +- `libPlayground` / `corePlayground` zero out other subprojects' test sources for the session. To + run `StagesSpec` tests again afterwards, reset with a leading `;reload`. + +--- + ## Test Authoring Rules **Tests must be self-contained.** Each test should only exercise the stage under test. Do not write input designs that rely on a prior stage to produce the IR shape that the current stage expects — write that IR shape directly using the DFHDL DSL. @@ -978,6 +1072,14 @@ abstract class StageSpec(stageCreatesUnrefAnons: Boolean = false) 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 + 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 + outer's `Flattened` moved descendants and in its own patches → ownership breakage or + `Received two different patches for the same member`. Drive it `@tailrec` and gate each pass to + the innermost-only or outermost-only instances (see Pattern 9). This is easy to miss because a + single-level test passes — **always add a nested test** for any such rewrite. --- ## API Notes From 4d79d13fd1f24b3a66b00d876dbf893120392e6d Mon Sep 17 00:00:00 2001 From: Oron Port Date: Fri, 5 Jun 2026 19:35:40 +0300 Subject: [PATCH 11/24] fix FlattenStepBlocks to better handle nesting --- .../compiler/stages/FlattenStepBlocks.scala | 48 ++++++++++++----- .../StagesSpec/FlattenStepBlocksSpec.scala | 53 +++++++++++++++++++ 2 files changed, 89 insertions(+), 12 deletions(-) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/FlattenStepBlocks.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/FlattenStepBlocks.scala index 636852301..554103568 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/FlattenStepBlocks.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/FlattenStepBlocks.scala @@ -182,22 +182,29 @@ case object FlattenStepBlocks extends HierarchyStage: case _ => Nil }.toList ) - // Phase 1: conditional branch extraction (uses db0 for updated member structure) - val db1 = locally { - given MemberGetSet = db0.getSet - db0.patch( - db0.members.view.flatMap { - case pb: ProcessBlock if pb.isInRTDomain => collectConditionalExtractionPatches(pb) - case _ => Nil - }.toList - ) - } + // Phase 1: conditional branch extraction, one level at a time (uses db0 for updated structure) + val db1 = extractCondBranchStepsRepeatedly(db0) // Phase 2: structural flattening, one level at a time (uses db1, applied repeatedly) val db2 = flattenRepeatedly(db1) // Phase 3: Goto ChangeRef db2.patch(gotoPatchList) end transformSubDB + // Repeatedly extract one nesting level of conditional-branch StepBlocks until none remain nested + // inside another conditional-branch step. Extracting an outer and an inner conditional-branch step + // in the same pass conflicts: the inner step (and its Gotos) appears both in the outer step's + // moved descendants (`Flattened`) and in its own extraction patches. Processing the outermost + // conditional-branch steps first un-nests them to ProcessBlock level, so the formerly-inner steps + // become outermost on the next pass. + @tailrec private def extractCondBranchStepsRepeatedly(db: DB)(using RefGen): DB = + given MemberGetSet = db.getSet + val patches = db.members.view.flatMap { + case pb: ProcessBlock if pb.isInRTDomain => collectConditionalExtractionPatches(pb) + case _ => Nil + }.toList + if patches.isEmpty then db + else extractCondBranchStepsRepeatedly(db.patch(patches)) + // Repeatedly flatten one nesting level of StepBlocks until all are direct pb children. @tailrec private def flattenRepeatedly(db: DB)(using RefGen): DB = given MemberGetSet = db.getSet @@ -210,6 +217,19 @@ case object FlattenStepBlocks extends HierarchyStage: // --- Shared helpers --- + // A regular StepBlock that sits directly inside a conditional branch. + private def isCondBranchStep(s: StepBlock)(using MemberGetSet): Boolean = + s.isRegular && s.getOwner.isInstanceOf[DFConditional.Block] + + // True if any enclosing StepBlock ancestor (up to the ProcessBlock) is itself a conditional-branch + // step — i.e. `s` is nested inside another conditional-branch step and must wait for a later pass. + @tailrec private def hasCondBranchStepAncestor(m: DFMember)(using MemberGetSet): Boolean = + m.getOwner match + case parentStep: StepBlock => + isCondBranchStep(parentStep) || hasCondBranchStepAncestor(parentStep) + case _: ProcessBlock => false + case owner => hasCondBranchStepAncestor(owner) + private def collectDirectFlatSteps(owner: DFOwner)(using MemberGetSet): List[StepBlock] = owner.members(MemberView.Folded).flatMap { case sb: StepBlock if sb.isRegular => sb :: collectDirectFlatSteps(sb) @@ -350,15 +370,19 @@ case object FlattenStepBlocks extends HierarchyStage: step5InterStep ++ step6 end collectInterStepPatches - // --- Phase 1: Conditional branch extraction (uses db0's member structure) --- + // --- Phase 1: Conditional branch extraction (one level per pass; see + // `extractCondBranchStepsRepeatedly`) --- private def collectConditionalExtractionPatches( pb: ProcessBlock )(using MemberGetSet, RefGen): List[(DFMember, Patch)] = val flatSteps = collectDirectFlatSteps(pb) if flatSteps.isEmpty then return Nil + // Only the outermost conditional-branch steps this pass: a step nested inside another + // conditional-branch step is moved as part of that ancestor's descendants and is extracted on a + // later pass, avoiding overlapping Move/Remove patches on the shared nested members. val conditionalBranchSteps = pb.members(MemberView.Flattened).collect { - case sb: StepBlock if sb.isRegular && sb.getOwner.isInstanceOf[DFConditional.Block] => sb + case sb: StepBlock if isCondBranchStep(sb) && !hasCondBranchStepAncestor(sb) => sb } conditionalBranchSteps.flatMap { s => val (cb, consumedGoto) = findConsumedGoto(s) diff --git a/compiler/stages/src/test/scala/StagesSpec/FlattenStepBlocksSpec.scala b/compiler/stages/src/test/scala/StagesSpec/FlattenStepBlocksSpec.scala index 0e342996c..1090dba26 100644 --- a/compiler/stages/src/test/scala/StagesSpec/FlattenStepBlocksSpec.scala +++ b/compiler/stages/src/test/scala/StagesSpec/FlattenStepBlocksSpec.scala @@ -368,6 +368,59 @@ class FlattenStepBlocksSpec extends StageSpec(): ) } + test("nested steps inside nested conditional branches") { + class Foo extends RTDesign: + val i = Bit <> IN + val x = Bit <> OUT.REG + process: + def S_0: Step = + if (i) + def S_0_0: Step = + if (i) + def S_0_0_0: Step = + NextStep + end S_0_0_0 + x.din := 1 + ThisStep + else + NextStep + end if + end S_0_0 + ThisStep + else + NextStep + end if + end S_0 + def S_1: Step = + NextStep + end S_1 + end Foo + val top = (new Foo).flattenStepBlocks + assertCodeString( + top, + """|class Foo extends RTDesign: + | val i = Bit <> IN + | val x = Bit <> OUT.REG + | process: + | def S_0: Step = + | if (i) S_0_0 + | else S_1 + | end S_0 + | def S_0_0: Step = + | if (i) S_0_0_0 + | else S_0 + | end S_0_0 + | def S_0_0_0: Step = + | x.din := 1 + | S_0_0 + | end S_0_0_0 + | def S_1: Step = + | S_0 + | end S_1 + |end Foo""".stripMargin + ) + } + test("onEntry and onExit blocks move with their parent step during flattening") { class Foo extends RTDesign: val x = Bit <> OUT.REG From ed3809538690ca3ae804221da7f7f494c2ab3c8e Mon Sep 17 00:00:00 2001 From: Oron Port Date: Fri, 5 Jun 2026 23:30:10 +0300 Subject: [PATCH 12/24] fix Ctrl+C tool abort under sbtn --- .../scala/dfhdl/tools/toolsCore/Tool.scala | 64 ++++++++++++------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala b/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala index 8d72a9ba2..a1cef10d6 100644 --- a/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala +++ b/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala @@ -160,32 +160,50 @@ trait Tool: stdout = processOutput, mergeErrIntoOut = true ) - // setup an interrupt handler to destroy the process - val handler = new sun.misc.SignalHandler: + // 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. + val interruptHandler = new sun.misc.SignalHandler: def handle(sig: sun.misc.Signal): Unit = process.destroy(shutdownGracePeriod = 100) println(s"\n${toolName} interrupted by user") - sun.misc.Signal.handle(new sun.misc.Signal("INT"), handler) - // wait for the process to finish - process.waitFor() - // get the error code, which may be overridden by the logger - val errCode = loggerOpt.map { logger => - if (logger.lineIsErrorOpt.nonEmpty) - if (logger.hasErrors) 1 else 0 - else process.exitCode() - }.getOrElse(process.exitCode()) - // check if there are warnings - val hasWarnings = loggerOpt.map(logger => logger.hasWarnings).getOrElse(false) - // if there are errors or warnings and Werror is turned on, raise an application error - if (errCode != 0 || hasWarnings && summon[ToolOptions].Werror.toBoolean) - val msg = - if (errCode != 0) s"${toolName} exited with the error code ${errCode}." - else s"${toolName} exited with warnings while `Werror-tool` is turned on." - error( - s"""|$msg - |Path: ${Paths.get(execPath).toAbsolutePath()} - |Command: $fullExec""".stripMargin - ) + 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 = + try + process.waitFor() + false + catch + case _: InterruptedException => + process.destroy(shutdownGracePeriod = 100) + println(s"\n${toolName} interrupted by user") + true + finally sun.misc.Signal.handle(new sun.misc.Signal("INT"), prevHandler) + if (interrupted) {} + else + // get the error code, which may be overridden by the logger + val errCode = loggerOpt.map { logger => + if (logger.lineIsErrorOpt.nonEmpty) + if (logger.hasErrors) 1 else 0 + else process.exitCode() + }.getOrElse(process.exitCode()) + // check if there are warnings + val hasWarnings = loggerOpt.map(logger => logger.hasWarnings).getOrElse(false) + // if there are errors or warnings and Werror is turned on, raise an application error + if (errCode != 0 || hasWarnings && summon[ToolOptions].Werror.toBoolean) + val msg = + if (errCode != 0) s"${toolName} exited with the error code ${errCode}." + else s"${toolName} exited with warnings while `Werror-tool` is turned on." + error( + s"""|$msg + |Path: ${Paths.get(execPath).toAbsolutePath()} + |Command: $fullExec""".stripMargin + ) end exec override def toString(): String = binExec end Tool From fd1a870e5897961a504c95a39160c37490927e7a Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 6 Jun 2026 00:14:57 +0300 Subject: [PATCH 13/24] update skill with right application arguments order --- .claude/commands/new-stage.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.claude/commands/new-stage.md b/.claude/commands/new-stage.md index 80a1a00e3..7e88fde16 100644 --- a/.claude/commands/new-stage.md +++ b/.claude/commands/new-stage.md @@ -948,11 +948,13 @@ isolated unit test in one step — write the test, watch it fail with the same e - Re-running an unchanged design may short-circuit on the on-disk cache (`Loading committed design from cache...`) and skip the stages (and the trace) entirely. Pass - **`--nocache`** to disable caching — it goes *after* `--` and *before* the DFHDL App command - (`compile` / `simulate` / …): + **`--nocache`** to disable caching — it is a DFHDL App option that goes *before* the `--` + separator (it is NOT a positional arg after `--`; placing it after `--` fails with + `[scallop] Error: Excess arguments provided: '--nocache'`). The DFHDL App command + (`compile` / `simulate` / …) goes *after* `--`: ```bash - sbtn.bat ";libPlayground;lib/Test/runMain top_Foo -- --nocache compile" - sbtn.bat ";libPlayground;lib/Test/runMain top_Foo -- --nocache simulate -t questa" + sbtn.bat ";libPlayground;lib/Test/runMain top_Foo --nocache -- compile" + sbtn.bat ";libPlayground;lib/Test/runMain top_Foo --nocache -- simulate -t questa" ``` (Alternatively clear `sandbox/` via `sbtn clearSandbox`, or edit the design — but `--nocache` is the lightweight option for repeated trace runs.) From 673547082aabc8bb6d3c66a0afabc3fe909afdf5 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 6 Jun 2026 00:46:26 +0300 Subject: [PATCH 14/24] fix nested step and if blocks combination that led to an cast exception --- .../StagesSpec/PrintCodeStringSpec.scala | 38 +++++++++++++ core/src/main/scala/dfhdl/core/DFIf.scala | 57 +++++++++++-------- 2 files changed, 72 insertions(+), 23 deletions(-) diff --git a/compiler/stages/src/test/scala/StagesSpec/PrintCodeStringSpec.scala b/compiler/stages/src/test/scala/StagesSpec/PrintCodeStringSpec.scala index 8269aba09..d36455f08 100644 --- a/compiler/stages/src/test/scala/StagesSpec/PrintCodeStringSpec.scala +++ b/compiler/stages/src/test/scala/StagesSpec/PrintCodeStringSpec.scala @@ -1036,6 +1036,44 @@ class PrintCodeStringSpec extends StageSpec(stageCreatesUnrefAnons = true): |end Foo""".stripMargin ) } + test("RTDesign step with statement before a nested conditional goto") { + // Regression: a step-control branch that is a block `{ statement; nested-if(goto) }` used to + // throw `java.lang.ClassCastException: DFVal cannot be cast to Step`, because the nested `if` + // returned the if-header DFVal where a `Step` token was expected. + class Foo extends RTDesign: + val x = Bit <> IN + val c = UInt(4) <> VAR.REG init 0 + process: + def S: Step = + if (x) ThisStep + else + c.din := 0 + if (c < 9) ThisStep + else NextStep + def T: Step = + ThisStep + end Foo + val top = (new Foo) + assertCodeString( + top, + """|class Foo extends RTDesign: + | val x = Bit <> IN + | val c = UInt(4) <> VAR.REG init d"4'0" + | process: + | def S: Step = + | if (x) ThisStep + | else + | c.din := d"4'0" + | if (c < d"4'9") ThisStep + | else NextStep + | end if + | end S + | def T: Step = + | ThisStep + | end T + |end Foo""".stripMargin + ) + } test("wait statements") { class Foo extends EDDesign: val x = Bit <> OUT diff --git a/core/src/main/scala/dfhdl/core/DFIf.scala b/core/src/main/scala/dfhdl/core/DFIf.scala index b70024767..50623b624 100644 --- a/core/src/main/scala/dfhdl/core/DFIf.scala +++ b/core/src/main/scala/dfhdl/core/DFIf.scala @@ -132,29 +132,40 @@ object DFIf: val (dfType, _) = singleBranch(None, midIfsBlock, e)(using dfcAnon) branchTypes = dfType.asIR :: branchTypes } - val hasNoType = branchTypes.contains(ir.DFUnit) - // if one branch has DFUnit, the return type is DFUnit. - // otherwise, all types must be the same. - if (hasNoType || branchTypes.forall(_.isSimilarTo(branchTypes.head))) - val retDFType = if (hasNoType) ir.DFUnit else branchTypes.head - val DFVal(headerIR: DFIfHeader) = header.runtimeChecked - val headerUpdate = headerIR.copy(dfType = retDFType.dropUnreachableRefs) - // updating the type of the if header - headerIR.replaceMemberWith(headerUpdate).asValAny.asInstanceOf[R] - else // violation - given printer: Printer = DefaultPrinter(using dfc.getSet) - val err = DFError.Basic( - "if", - new IllegalArgumentException( - s"""|This DFHDL `if` expression has different return types for branches. - |These are its branch types in order: - |${branchTypes.view.reverse.map(t => printer.csDFType(t)).mkString("\n")} - |""".stripMargin - ) - ) - dfc.logEvent(err) - err.asVal[DFTypeAny, ModifierAny].asInstanceOf[R] - end if + firstIfRet match + // Control-flow `if`: its branches are gotos (`ThisStep`/`NextStep`/`FirstStep` return the + // `Step` token). The value of such an `if` is that `Step` token, NOT the if-header value. + // Returning the header is harmless when the value is discarded (a plain step body), but + // when the `if` is itself the value of a step-control branch — e.g. an `else { stmt; if + // (...) goto else goto }` block, whose lambda is typed `() => Step` — the returned value is + // cast to `Step`, and a `DFVal` header throws a `ClassCastException`. Return the `Step` + // token so the cast succeeds. The conditional IR structure (header/blocks/gotos) has + // already been built above as a side effect of running the branches. + case Some(step: Step) => step.asInstanceOf[R] + case _ => + val hasNoType = branchTypes.contains(ir.DFUnit) + // if one branch has DFUnit, the return type is DFUnit. + // otherwise, all types must be the same. + if (hasNoType || branchTypes.forall(_.isSimilarTo(branchTypes.head))) + val retDFType = if (hasNoType) ir.DFUnit else branchTypes.head + val DFVal(headerIR: DFIfHeader) = header.runtimeChecked + val headerUpdate = headerIR.copy(dfType = retDFType.dropUnreachableRefs) + // updating the type of the if header + headerIR.replaceMemberWith(headerUpdate).asValAny.asInstanceOf[R] + else // violation + given printer: Printer = DefaultPrinter(using dfc.getSet) + val err = DFError.Basic( + "if", + new IllegalArgumentException( + s"""|This DFHDL `if` expression has different return types for branches. + |These are its branch types in order: + |${branchTypes.view.reverse.map(t => printer.csDFType(t)).mkString("\n")} + |""".stripMargin + ) + ) + dfc.logEvent(err) + err.asVal[DFTypeAny, ModifierAny].asInstanceOf[R] + end if catch case e: DFError => DFVal(DFError.Derived(e)).asInstanceOf[R] end fromBranches From 7a00ad1a0d4a8d281eca114e76abbafa463bcd5e Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 6 Jun 2026 11:33:40 +0300 Subject: [PATCH 15/24] add FoldControlSteps stage --- .../compiler/stages/FlattenStepBlocks.scala | 2 +- .../compiler/stages/FoldControlSteps.scala | 371 ++++++++++++ .../StagesSpec/FoldControlStepsSpec.scala | 539 ++++++++++++++++++ 3 files changed, 911 insertions(+), 1 deletion(-) create mode 100644 compiler/stages/src/main/scala/dfhdl/compiler/stages/FoldControlSteps.scala create mode 100644 compiler/stages/src/test/scala/StagesSpec/FoldControlStepsSpec.scala diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/FlattenStepBlocks.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/FlattenStepBlocks.scala index 554103568..2c1facf42 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/FlattenStepBlocks.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/FlattenStepBlocks.scala @@ -166,7 +166,7 @@ import scala.annotation.tailrec */ //format: on case object FlattenStepBlocks extends HierarchyStage: - def dependencies: List[Stage] = List(DropRTWaits, ExplicitNamedVars, DropLocalDcls) + def dependencies: List[Stage] = List(FoldControlSteps) def nullifies: Set[Stage] = Set() def transformSubDB(rootDB: DB)(using MemberGetSet, CompilerOptions, RefGen): DB = diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/FoldControlSteps.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/FoldControlSteps.scala new file mode 100644 index 000000000..920f6bde1 --- /dev/null +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/FoldControlSteps.scala @@ -0,0 +1,371 @@ +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.annotation.tailrec +//format: off +/** This stage removes the gratuitous clock cycle that every loop iteration costs in an RT process. + * + * == Background == + * + * This stage runs on the '''nested''' step-block form produced by [[DropRTWaits]] — before + * [[FlattenStepBlocks]] flattens it — where relative gotos (`ThisStep`/`NextStep`) are still in + * place and a loop's control step '''directly owns''' its wait step as a nested child. A loop in + * the source lowers to two kinds of steps: + * - A '''wait step''' — the innermost step whose terminus loops back to itself (`ThisStep` in the + * counting branch). It is a multi-cycle wait counter, or a single-cycle pass-through delay. It + * legitimately consumes ≥1 clock cycle and must stay. + * - A '''control step''' — the '''owner''' step that wraps a nested wait step together with the + * per-iteration loop bookkeeping (index update, wait-counter reset, condition test). Its + * dedicated FSM state burns one extra clock cycle per iteration that the source never asked for. + * + * Working on the nested form makes the control↔wait pairing trivial: the control step is the + * '''owner''' and its wait is the '''nested child''' — no flat-goto reverse-engineering needed. The + * fold dissolves the '''owner''' control step into its '''internal''' wait step. + * + * == The transform — entry-folding == + * + * For each foldable control step `C` whose iteration enters wait step `W` (and `W`'s exit loops + * back to `C`), the control step is dissolved into the wait: + * + * 1. '''Allocate a guard reg''' `C_entered : Bit <> VAR.REG init 1`, named after the control + * step. It is `1` on exactly the '''first cycle of each iteration''' and gates `C`'s one-shot + * work. + * 2. '''Fold `C`'s index update into `W`'s first cycle''', guarded — prepend to `W`'s counting + * branch: `if (C_entered) { ; C_entered.din := 0 }`. + * 3. '''Keep `C`'s wait-counter reset + condition at `W`'s exit''' (where `W` decides continue + * vs. leave): `if (C.cond) { ; C_entered.din := 1; -> W } else + * { }`. The `C_entered.din := 1` re-arms the guard for the next iteration. + * 4. '''Re-point `C`'s other predecessors''' (e.g. the loop preamble) directly to `W`, and + * '''remove `C`'''. + * + * The split matters: `C`'s '''index update''' (`i.din := i + 1`) folds into the wait's ''entry'' + * (guarded); `C`'s write to '''`W`'s own counter''' (`waitCnt.din := 0`) stays at the + * ''exit-continue'' (a separate cycle), since putting both in one cycle would double-write the + * counter. `W`'s counter is identifiable structurally (the reg `W` tests/increments); everything + * else `C` writes is iteration work that folds at entry. + * + * '''Worked example (single loop).''' Nested form after [[DropRTWaits]] — control step `S_1` owns + * its wait step `S_1_0`: + * {{{ + * def S_1: Step = // control step (owner) + * if (i < 100) + * def S_1_0: Step = // wait step (nested child) + * if (waitCnt != 49) + * waitCnt.din := waitCnt + 1 + * ThisStep + * else NextStep + * waitCnt.din := 0 // reset W's counter + * i.din := i + 1 // index update + * ThisStep + * else + * finish() + * NextStep + * }}} + * Entry-folded — `S_1` dissolved into `S_1_0` (now a direct process child), `i.din := i + 1` + * guarded by `S_1_entered`, condition `i < 100` left exactly as written: + * {{{ + * // + reg: val S_1_entered = Bit <> VAR.REG init 1 + * def S_1_0: Step = + * if (waitCnt != 49) + * if (S_1_entered) // S_1's increment, folded at entry + * i.din := i + 1 + * S_1_entered.din := 0 + * waitCnt.din := waitCnt + 1 + * ThisStep + * else if (i < 100) // S_1's condition -> continue (re-arm) + * waitCnt.din := 0 + * S_1_entered.din := 1 + * ThisStep + * else // S_1's else + * finish() + * NextStep + * }}} + * + * == Why a guard reg == + * + * The guard is a general "first cycle of this iteration" marker that works for any wait (including + * single-cycle/pass-through waits with no counter). It is the '''self-loop-surviving analog of an + * `onEntry` action''': an `onEntry` block runs once on entry but is skipped on self-transitions + * (see [[DropRTProcess]] Rule 2), and the folded wait self-loops, so an explicit reg is needed to + * fire the folded work once per iteration. Folding the index update at the wait's '''entry''' + * (rather than its exit) keeps each loop condition exactly as written — the increment (entry) and + * the condition test (exit) land in different cycles, so there is no off-by-one and no condition + * rewrite. + * + * == Nesting == + * + * One guard per control level, named after that level's step. In the nested form the levels are + * literally the ownership chain: outer control `S_1` owns inner control `S_1_0` owns the wait + * `S_1_0_0`. For a 2-deep loop, the outer guard `S_1_entered` gates the outer index update + * (`i.din := i + 1`) and the inner guard `S_1_0_entered` gates the inner one (`j.din := j + 1`); + * both control steps fold into the single innermost wait `S_1_0_0`. The outer guard is re-armed when + * a new outer pass begins (the inner loop exhausts and the outer continues); the inner guard is + * re-armed on every inner continue. The rule is uniform for any nesting depth. + * + * == Precondition (hard rule) == + * + * A control/wait pair is '''not foldable''' — left as-is — if either the control step `C` or its + * wait `W` carries any non-regular child block (`onEntry`, `onExit`, or `fallThrough`). For + * `fallThrough` the fold semantics aren't worked out yet; for `onEntry`/`onExit` no real input + * actually places them on a foldable control step (a `while`-generated control step can't carry + * them, and a hand-written step that does drops its nested wait), so folding such a step would be + * dead code. An `onEntry`/`onExit` on an '''enclosing''' step (one that merely contains a foldable + * inner loop) is neither `C` nor `W`, so it is '''preserved untouched''' — the fold only rewrites + * the inner loop. This precondition can be relaxed in the future. + * + * == Placement == + * + * The control↔wait pairing is read directly from the step '''ownership''' nesting, which is present + * only before flattening. So this stage runs '''between [[DropRTWaits]] and [[FlattenStepBlocks]]''': + * [[FlattenStepBlocks]] depends on this stage, which depends on [[DropRTWaits]] (plus the + * [[ExplicitNamedVars]] / [[DropLocalDcls]] prerequisites that [[FlattenStepBlocks]] previously + * required directly). + */ +//format: on +case object FoldControlSteps extends HierarchyStage: + def dependencies: List[Stage] = List(DropRTWaits, ExplicitNamedVars, DropLocalDcls) + def nullifies: Set[Stage] = Set() + + // One control level `C_m` of a (possibly nested) loop chain: its `if (cCond)` then-block, the loop + // condition, the index-update net (writes the reg the condition tests — folds at entry) and the + // remaining bookkeeping nets (reset of the level below — replayed on continue / exhaust). + private case class Level( + c: StepBlock, + cThen: DFConditional.DFIfElseBlock, + cCond: DFVal, + idxNet: DFNet, + resetNets: List[DFNet] + ) + // A foldable control chain `C_1` ⊃ … ⊃ `C_k` ⊃ `W` (outer→inner), down to the leaf wait `W`. + private case class Chain( + levels: List[Level], + w: StepBlock, + wThen: DFConditional.DFIfElseBlock, + wCond: DFVal, + wCountNets: List[DFNet], + c1Else: DFConditional.DFIfElseBlock // the outermost control step's else-body (the loop exit) + ) + + // direct (non-nested) gotos of a block, in order + private def directGotos(block: DFOwner)(using MemberGetSet): List[Goto] = + block.members(MemberView.Folded).collect { case g: Goto => g } + private def lastGotoIs(block: DFOwner, target: DFMember)(using MemberGetSet): Boolean = + directGotos(block).lastOption.exists(_.stepRef.get == target) + private def hasNonRegularChild(s: StepBlock)(using MemberGetSet): Boolean = + s.members(MemberView.Folded).exists { case sb: StepBlock => !sb.isRegular; case _ => false } + private def directRegularSteps(block: DFOwner)(using MemberGetSet): List[StepBlock] = + block.members(MemberView.Folded).collect { case sb: StepBlock if sb.isRegular => sb } + private def singleIfHeader(s: StepBlock)(using MemberGetSet): Option[DFConditional.DFIfHeader] = + s.members(MemberView.Folded).collect { case h: DFConditional.DFIfHeader => h } match + case List(h) => Some(h) + case _ => None + // Forward conditional-block navigation by `prevBlockOrHeaderRef` over the step's own children. + // (Deliberately avoids the `getFirstCB`/`getNextCB` analysis helpers, which build the whole-DB + // `originMemberTable` — that trips over the orphaned `for`-loop `DFRange` still present at this + // pipeline stage, which is only cleaned later by `DropUnreferenced`.) + private def thenBlockOf(step: StepBlock, h: DFConditional.DFIfHeader)( + using MemberGetSet + ): DFConditional.DFIfElseBlock = + step.members(MemberView.Folded).collectFirst { + case b: DFConditional.DFIfElseBlock if b.prevBlockOrHeaderRef.get == h => b + }.get + private def elseBlockOf(step: StepBlock, thenB: DFConditional.DFIfElseBlock)( + using MemberGetSet + ): Option[DFConditional.DFIfElseBlock] = + step.members(MemberView.Folded).collectFirst { + 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 + // the declarations read by a (condition) value + private def condDcls(cond: DFVal)(using MemberGetSet): Set[DFVal.Dcl] = + (cond :: cond.collectRelMembers(false)) + .flatMap(_.getRefs.map(_.get)) + .collect { case d: DFVal.Dcl => d } + .toSet + + // `W` is a leaf multi-cycle wait: one `if`, then-branch self-loops (ThisStep) with a counting + // assignment and no nested step, else-branch exits (NextStep). + private def isWaitStep(w: StepBlock)(using MemberGetSet): Boolean = + w.isRegular && !hasNonRegularChild(w) && singleIfHeader(w).exists { h => + val wThen = thenBlockOf(w, h) + elseBlockOf(w, wThen) match + case Some(wElse) => + directRegularSteps(wThen).isEmpty && + lastGotoIs(wThen, Goto.ThisStep) && lastGotoIs(wElse, Goto.NextStep) && + wThen.members(MemberView.Folded).exists { case _: DFNet => true; case _ => false } + case None => false + } + + // Build a `Level` from a control-step shell: one `if`, then-branch owns exactly one nested regular + // step and loops back (ThisStep), else-branch exists, no non-regular children, and exactly one + // then-net updates the loop's condition variable (the index update). Returns the level + the + // nested step + the else-block, or None if `c` is not a foldable control shell. + private def controlLevel(c: StepBlock)( + using MemberGetSet + ): Option[(Level, StepBlock, DFConditional.DFIfElseBlock)] = + if (!c.isRegular || hasNonRegularChild(c)) None + else + singleIfHeader(c).flatMap { h => + val cThen = thenBlockOf(c, h) + elseBlockOf(c, cThen).flatMap { cElse => + directRegularSteps(cThen) match + case List(nested) if lastGotoIs(cThen, Goto.ThisStep) => + val cCond = cThen.getGuardOption.get + val thenNets = cThen.members(MemberView.Folded).collect { case n: DFNet => n } + val dcls = condDcls(cCond) + thenNets.find(n => lhsDcl(n).exists(dcls.contains)).map { idxNet => + (Level(c, cThen, cCond, idxNet, thenNets.filterNot(_ == idxNet)), nested, cElse) + } + case _ => None + } + } + + // The maximal foldable control chain rooted at `c` (recursing through nested control steps down to + // a leaf wait). None if `c` is not a control shell or never reaches a wait. + private def chainOf(c: StepBlock)(using MemberGetSet): Option[Chain] = + controlLevel(c).flatMap { case (level, nested, cElse) => + if (isWaitStep(nested)) + val wThen = thenBlockOf(nested, singleIfHeader(nested).get) + Some(Chain( + List(level), nested, wThen, wThen.getGuardOption.get, + wThen.members(MemberView.Folded).collect { case n: DFNet => n }, cElse + )) + else + chainOf(nested).map(inner => inner.copy(levels = level :: inner.levels, c1Else = cElse)) + } + + def transformSubDB(rootDB: DB)(using MemberGetSet, CompilerOptions, RefGen): DB = + foldToFixpoint(subDB) + + @tailrec private def foldToFixpoint(db: DB)(using CompilerOptions, RefGen): DB = + given MemberGetSet = db.getSet + // pre-order traversal => the first foldable step is the OUTERMOST of its chain (so the whole + // chain folds at once) + db.members.iterator + .collect { case c: StepBlock if c.isInRTDomain => c } + .flatMap(chainOf) + .nextOption() match + case None => db + case Some(chain) => foldToFixpoint(db.patch(foldPatches(chain))) + + private def foldPatches(chain: Chain)(using MemberGetSet, RefGen): List[(DFMember, Patch)] = + val levels = chain.levels // outer (index 0) -> inner (index k-1) + val k = levels.size + val innermost = levels.last + val c1 = levels.head.c + + // one guard register per control level, named after that level's step (outer first) + val regDsn = + new MetaDesign(enclosingProcess(c1), Patch.Add.Config.Before, dfhdl.core.DomainType.RT): + import dfhdl.core.* + val regs = levels.map(L => (Bit <> VAR.REG).init(1)(using dfc.setName(s"${L.c.getName}_entered"))) + val enteredRegs = regDsn.regs + + def cloneNets(md: MetaDesign[?], baseOwner: DFOwner, nets: List[DFNet]): Unit = + nets.foreach { n => + md.plantClonedMembers(baseOwner, n.collectRelMembers.asInstanceOf[List[DFMember]] :+ n) + } + + val stepDsn = + new MetaDesign( + c1, + Patch.Add.Config.ReplaceWithLast(Patch.Replace.Config.ChangeRefAndRemove), + dfhdl.core.DomainType.RT + ): + import dfhdl.core.{StepBlock, DFIf, DFBool, DFUnit} + // re-arm the guard regs for control levels `from..k-1` (inner-first ordering) + def reArm(from: Int): Unit = + (k - 1 to from by -1).foreach(p => enteredRegs(p).din := 1) + // clone the wait-counter reset that re-initialises the wait for the next iteration + def resetWaitCounter(): Unit = cloneNets(this, innermost.cThen, innermost.resetNets) + def cloneElse(): Unit = plantClonedMembers(chain.c1Else, chain.c1Else.members(MemberView.Flattened)) + // the leave path for the outer control levels (k-2..0): each resets the level below's index, + // then tests its own condition (continue vs. recurse outward); the outermost else is C_1's + // else-body. For a single loop (k==1) this is just C_1's else-body. + def buildOuterExit(mi: Int): Unit = + if (mi < 0) cloneElse() + else + cloneNets(this, levels(mi).cThen, levels(mi).resetNets) + val nh = DFIf.Header(DFUnit) + val cont = DFIf.Block(Some(levels(mi).cCond.cloneAnonValueAndDepsHere.asValOf[DFBool]), nh) + dfc.enterOwner(cont) + resetWaitCounter() + reArm(mi) + ThisStep + dfc.exitOwner() + val els = DFIf.Block(None, cont) + dfc.enterOwner(els) + if (mi == 0) cloneElse() else buildOuterExit(mi - 1) + dfc.exitOwner() + + val step = StepBlock.forced(using dfc.setName(chain.w.getName)) + dfc.enterOwner(step) + val header = DFIf.Header(DFUnit) + // count branch: guarded per-level index updates (inner-first), then W's counter increment + val countBlock = DFIf.Block(Some(chain.wCond.cloneAnonValueAndDepsHere.asValOf[DFBool]), header) + dfc.enterOwner(countBlock) + levels.zipWithIndex.reverse.foreach { (lvl, idx) => + val gIf = DFIf.Block(Some(enteredRegs(idx).asValOf[DFBool]), DFIf.Header(DFUnit)) + dfc.enterOwner(gIf) + cloneNets(this, lvl.cThen, List(lvl.idxNet)) + enteredRegs(idx).din := 0 + dfc.exitOwner() + } + cloneNets(this, chain.wThen, chain.wCountNets) + ThisStep + dfc.exitOwner() + // innermost continue (part of the main chain, rendered as `else if`) + val contInner = + DFIf.Block(Some(innermost.cCond.cloneAnonValueAndDepsHere.asValOf[DFBool]), countBlock) + dfc.enterOwner(contInner) + resetWaitCounter() + reArm(k - 1) + ThisStep + dfc.exitOwner() + // leave (innermost loop exhausted): outer levels, or just C_1's else-body for a single loop + val leaveBlock = DFIf.Block(None, contInner) + dfc.enterOwner(leaveBlock) + buildOuterExit(k - 2) + dfc.exitOwner() + dfc.exitOwner() + + // Remove the whole chain. C_1 is removed by the ReplaceWithLast above; its descendants — every + // inner level, W, nets, gotos — are removed here. The original (now-cloned) conditions of + // while/for-lowered loops are owned by the *enclosing* block, not C_1, so they are added + // explicitly. BUT a condition's operands may be SHARED with surviving members — e.g. a `for` + // loop's bound const is referenced by both the loop condition and the (not-yet-cleaned) `DFRange` + // — so an external cond-member is removed only if no surviving member still references it. + val baseRemove = c1.members(MemberView.Flattened) + val baseSet = baseRemove.toSet + val condCands = + (chain.wCond :: levels.map(_.cCond)).flatMap(c => c :: c.collectRelMembers(false)).distinct + val extraConds = condCands.filterNot(baseSet.contains) + val tentativeRemove = baseSet ++ extraConds + c1 + // ref targets of everything that will survive (resolved on the intact pre-patch DB) + val externalTargets: Set[DFMember] = + getSet.designDB.members.iterator + .filterNot(tentativeRemove.contains) + .flatMap(_.getRefs.iterator.map(_.get)) + .toSet + val removeSet = (baseRemove ++ extraConds.filterNot(externalTargets.contains)).distinct + val removeChain = removeSet.map(_ -> Patch.Remove()) + + regDsn.patch :: stepDsn.patch :: removeChain + end foldPatches + + @tailrec private def enclosingProcess(m: DFMember)(using MemberGetSet): ProcessBlock = + m.getOwner match + case pb: ProcessBlock => pb + case owner => enclosingProcess(owner) +end FoldControlSteps + +extension [T: HasDB](t: T) + def foldControlSteps(using CompilerOptions): DB = + StageRunner.run(FoldControlSteps)(t.db) diff --git a/compiler/stages/src/test/scala/StagesSpec/FoldControlStepsSpec.scala b/compiler/stages/src/test/scala/StagesSpec/FoldControlStepsSpec.scala new file mode 100644 index 000000000..a49f5cc68 --- /dev/null +++ b/compiler/stages/src/test/scala/StagesSpec/FoldControlStepsSpec.scala @@ -0,0 +1,539 @@ +package StagesSpec + +import dfhdl.* +import dfhdl.compiler.stages.foldControlSteps +// scalafmt: { align.tokens = [{code = "<>"}, {code = "="}, {code = "=>"}, {code = ":="}]} + +// NOTE (Phase 2): inputs are written in the step-block syntax valid after `DropRTWaits` — explicit +// `def S: Step`, relative gotos (`NextStep`/`ThisStep`), manual `waitCnt`/index `VAR.REG`s, and +// explicit `def onEntry`/`onExit`/`fallThrough` blocks — never high-level `for`/`while`/`wait` +// (matches `DropRTWaitsSpec`/`FlattenStepBlocksSpec`). The expected strings for the *folding* cases +// are the hand-authored entry-folded contract; they fail against the Phase-1 no-op stub by design. +// The no-op cases (no enclosing loop / fallThrough-blocked / already-folded / non-RT) already pass +// against the stub, pinning the forms the stage must leave untouched. +class FoldControlStepsSpec extends StageSpec(): + + // ── No-op: a multi-cycle wait with no enclosing control step has nothing to fold ────────────── + test("single wait without a loop is left unchanged") { + class Foo extends RTDesign: + val waitCnt = UInt(6) <> VAR.REG init 0 + process: + def S_0: Step = + NextStep + end S_0 + def S_1: Step = + if (waitCnt != 49) + waitCnt.din := waitCnt + 1 + ThisStep + else NextStep + end S_1 + end Foo + val top = (new Foo).foldControlSteps + assertCodeString( + top, + """|class Foo extends RTDesign: + | val waitCnt = UInt(6) <> VAR.REG init d"6'0" + | process: + | def S_0: Step = + | NextStep + | end S_0 + | def S_1: Step = + | if (waitCnt != d"6'49") + | waitCnt.din := waitCnt + d"6'1" + | ThisStep + | else NextStep + | end S_1 + |end Foo""".stripMargin + ) + } + + // ── Fold: single loop — control `S_1` folds into wait `S_1_0`, guarded by `S_1_entered` ─────── + test("single loop folds the control step into its wait") { + class Foo extends RTDesign: + val i = Int <> VAR.REG init 0 + val waitCnt = UInt(6) <> VAR.REG init 0 + process: + def S_0: Step = + NextStep + end S_0 + def S_1: Step = + if (i < 100) + def S_1_0: Step = + if (waitCnt != 49) + waitCnt.din := waitCnt + 1 + ThisStep + else NextStep + end S_1_0 + waitCnt.din := 0 + i.din := i + 1 + ThisStep + else + finish() + NextStep + end S_1 + end Foo + val top = (new Foo).foldControlSteps + assertCodeString( + top, + """|class Foo extends RTDesign: + | val i = Int <> VAR.REG init 0 + | val waitCnt = UInt(6) <> VAR.REG init d"6'0" + | val S_1_entered = Bit <> VAR.REG init 1 + | process: + | def S_0: Step = + | NextStep + | end S_0 + | def S_1_0: Step = + | if (waitCnt != d"6'49") + | if (S_1_entered) + | i.din := i + 1 + | S_1_entered.din := 0 + | end if + | waitCnt.din := waitCnt + d"6'1" + | ThisStep + | else if (i < 100) + | waitCnt.din := d"6'0" + | S_1_entered.din := 1 + | ThisStep + | else + | finish() + | NextStep + | end if + | end S_1_0 + |end Foo""".stripMargin + ) + } + + // ── Fold: nested loops — BOTH control steps fold into the innermost wait `S_1_0_0`. The whole + // chain folds in one pass: inner index `j++` (guard `S_1_0_entered`) and outer index `i++` + // (guard `S_1_entered`) at the guarded entry; the exit weaves inner-continue (`else if j<10`), + // then the inner-exhausted path (`j := 0`) into the outer-continue/leave. ───────────────────── + test("nested loops fold both control steps into the innermost wait") { + class Foo extends RTDesign: + val i = Int <> VAR.REG init 0 + val j = Int <> VAR.REG init 0 + val waitCnt = UInt(6) <> VAR.REG init 0 + process: + def S_0: Step = + NextStep + end S_0 + def S_1: Step = + if (i < 10) + def S_1_0: Step = + if (j < 10) + def S_1_0_0: Step = + if (waitCnt != 49) + waitCnt.din := waitCnt + 1 + ThisStep + else NextStep + end S_1_0_0 + waitCnt.din := 0 + j.din := j + 1 + ThisStep + else NextStep + end S_1_0 + j.din := 0 + i.din := i + 1 + ThisStep + else + finish() + NextStep + end S_1 + end Foo + val top = (new Foo).foldControlSteps + assertCodeString( + top, + """|class Foo extends RTDesign: + | val i = Int <> VAR.REG init 0 + | val j = Int <> VAR.REG init 0 + | val waitCnt = UInt(6) <> VAR.REG init d"6'0" + | val S_1_entered = Bit <> VAR.REG init 1 + | val S_1_0_entered = Bit <> VAR.REG init 1 + | process: + | def S_0: Step = + | NextStep + | end S_0 + | def S_1_0_0: Step = + | if (waitCnt != d"6'49") + | if (S_1_0_entered) + | j.din := j + 1 + | S_1_0_entered.din := 0 + | end if + | if (S_1_entered) + | i.din := i + 1 + | S_1_entered.din := 0 + | end if + | waitCnt.din := waitCnt + d"6'1" + | ThisStep + | else if (j < 10) + | waitCnt.din := d"6'0" + | S_1_0_entered.din := 1 + | ThisStep + | else + | j.din := 0 + | if (i < 10) + | waitCnt.din := d"6'0" + | S_1_0_entered.din := 1 + | S_1_entered.din := 1 + | ThisStep + | else + | finish() + | NextStep + | end if + | end if + | end S_1_0_0 + |end Foo""".stripMargin + ) + } + + // ── Precondition: a `fallThrough` block on the wait blocks the fold (pair left unchanged) ───── + test("a fallThrough block blocks the fold") { + class Foo extends RTDesign: + val i = Int <> VAR.REG init 0 + val waitCnt = UInt(6) <> VAR.REG init 0 + process: + def S_0: Step = + NextStep + end S_0 + def S_1: Step = + if (i < 100) + def S_1_0: Step = + def fallThrough = waitCnt == 0 + if (waitCnt != 49) + waitCnt.din := waitCnt + 1 + ThisStep + else NextStep + end S_1_0 + waitCnt.din := 0 + i.din := i + 1 + ThisStep + else + finish() + NextStep + end S_1 + end Foo + val top = (new Foo).foldControlSteps + assertCodeString( + top, + """|class Foo extends RTDesign: + | val i = Int <> VAR.REG init 0 + | val waitCnt = UInt(6) <> VAR.REG init d"6'0" + | process: + | def S_0: Step = + | NextStep + | end S_0 + | def S_1: Step = + | if (i < 100) + | def S_1_0: Step = + | def fallThrough: Boolean <> VAL = + | waitCnt == d"6'0" + | end fallThrough + | if (waitCnt != d"6'49") + | waitCnt.din := waitCnt + d"6'1" + | ThisStep + | else NextStep + | end S_1_0 + | waitCnt.din := d"6'0" + | i.din := i + 1 + | ThisStep + | else + | finish() + | NextStep + | end if + | end S_1 + |end Foo""".stripMargin + ) + } + + // ── No-op: idempotency — re-running on an already-folded process changes nothing ────────────── + test("idempotency: an already-folded process is left unchanged") { + class Foo extends RTDesign: + val i = Int <> VAR.REG init 0 + val waitCnt = UInt(6) <> VAR.REG init 0 + val S_1_entered = Bit <> VAR.REG init 1 + process: + def S_0: Step = + NextStep + end S_0 + def S_1_0: Step = + if (waitCnt != 49) + if (S_1_entered) + i.din := i + 1 + S_1_entered.din := 0 + waitCnt.din := waitCnt + 1 + ThisStep + else if (i < 100) + waitCnt.din := 0 + S_1_entered.din := 1 + ThisStep + else + finish() + NextStep + end S_1_0 + end Foo + val top = (new Foo).foldControlSteps + assertCodeString( + top, + """|class Foo extends RTDesign: + | val i = Int <> VAR.REG init 0 + | val waitCnt = UInt(6) <> VAR.REG init d"6'0" + | val S_1_entered = Bit <> VAR.REG init 1 + | process: + | def S_0: Step = + | NextStep + | end S_0 + | def S_1_0: Step = + | if (waitCnt != d"6'49") + | if (S_1_entered) + | i.din := i + 1 + | S_1_entered.din := 0 + | end if + | waitCnt.din := waitCnt + d"6'1" + | ThisStep + | else if (i < 100) + | waitCnt.din := d"6'0" + | S_1_entered.din := 1 + | ThisStep + | else + | finish() + | NextStep + | end if + | end S_1_0 + |end Foo""".stripMargin + ) + } + + // ── No-op: non-RT (plain DF) design is untouched ───────────────────────────────────────────── + test("non-RT design is left unchanged") { + class Foo extends DFDesign: + val x = Bit <> OUT + val y = Bit <> IN + x := y + end Foo + val top = (new Foo).foldControlSteps + assertCodeString( + top, + """|class Foo extends DFDesign: + | val x = Bit <> OUT + | val y = Bit <> IN + | x := y + |end Foo""".stripMargin + ) + } + + // ── Fold: explicit step names — control `Count` folds into wait `Count_0`, guard `Count_entered`, + // with sequencing steps `Start`/`Done` around it (explicit, not relative) ──────────────────── + test("explicit-named loop folds with the guard named after the control step") { + class Foo extends RTDesign: + val i = Int <> VAR.REG init 0 + val waitCnt = UInt(6) <> VAR.REG init 0 + process: + def Start: Step = + NextStep + end Start + def Count: Step = + if (i < 100) + def Count_0: Step = + if (waitCnt != 49) + waitCnt.din := waitCnt + 1 + ThisStep + else NextStep + end Count_0 + waitCnt.din := 0 + i.din := i + 1 + ThisStep + else + finish() + NextStep + end Count + def Done: Step = + NextStep + end Done + end Foo + val top = (new Foo).foldControlSteps + assertCodeString( + top, + """|class Foo extends RTDesign: + | val i = Int <> VAR.REG init 0 + | val waitCnt = UInt(6) <> VAR.REG init d"6'0" + | val Count_entered = Bit <> VAR.REG init 1 + | process: + | def Start: Step = + | NextStep + | end Start + | def Count_0: Step = + | if (waitCnt != d"6'49") + | if (Count_entered) + | i.din := i + 1 + | Count_entered.din := 0 + | end if + | waitCnt.din := waitCnt + d"6'1" + | ThisStep + | else if (i < 100) + | waitCnt.din := d"6'0" + | Count_entered.din := 1 + | ThisStep + | else + | finish() + | NextStep + | end if + | end Count_0 + | def Done: Step = + | NextStep + | end Done + |end Foo""".stripMargin + ) + } + + // ── Fold: a nested foldable loop inside an enclosing step that carries `onEntry`/`onExit`, in a + // multi-step chain (`Start` → `Count` → `Done`). The fold rewrites the inner loop (`Count_0` + // folds into wait `Count_0_0`, guard `Count_0_entered`); `Count`'s `onEntry`/`onExit` — which + // fire on the real transitions into/out of `Count` — are preserved untouched. ─────────────── + test("nested fold preserves an enclosing step's onEntry/onExit in a multi-step chain") { + class Foo extends RTDesign: + val x = Bit <> OUT.REG init 0 + val i = Int <> VAR.REG init 0 + val waitCnt = UInt(6) <> VAR.REG init 0 + process: + def Start: Step = + x.din := 0 + Count + end Start + def Count: Step = + def onEntry = + x.din := 1 + def onExit = + x.din := 0 + while (i != 100) + while (waitCnt != 49) + waitCnt.din := waitCnt + 1 + waitCnt.din := 0 + i.din := i + 1 + Done + end Count + def Done: Step = + x.din := !x + Start + end Done + end Foo + val top = (new Foo).foldControlSteps + assertCodeString( + top, + """|class Foo extends RTDesign: + | val x = Bit <> OUT.REG init 0 + | val i = Int <> VAR.REG init 0 + | val waitCnt = UInt(6) <> VAR.REG init d"6'0" + | val Count_0_entered = Bit <> VAR.REG init 1 + | process: + | def Start: Step = + | x.din := 0 + | Count + | end Start + | def Count: Step = + | def onEntry: Unit = + | x.din := 1 + | end onEntry + | def onExit: Unit = + | x.din := 0 + | end onExit + | def Count_0_0: Step = + | if (waitCnt != d"6'49") + | if (Count_0_entered) + | i.din := i + 1 + | Count_0_entered.din := 0 + | end if + | waitCnt.din := waitCnt + d"6'1" + | ThisStep + | else if (i != 100) + | waitCnt.din := d"6'0" + | Count_0_entered.din := 1 + | ThisStep + | else NextStep + | end if + | end Count_0_0 + | Done + | end Count + | def Done: Step = + | x.din := !x + | Start + | end Done + |end Foo""".stripMargin + ) + } + + // ── No-op (harvested DropRTWaitsSpec form): a pure `while` loop is a *wait* step (self-loop with + // no nested wait), not a control step — nothing to fold. ───────────────────────────────────── + test("harvested: a pure while loop (wait step) is left unchanged") { + class Foo extends RTDesign: + val x = Bit <> OUT.REG + val waitCnt1 = UInt(8) <> VAR.REG init 0 + process: + while (waitCnt1 != 149) + waitCnt1.din := waitCnt1 + 1 + waitCnt1.din := 0 + x.din := !x + end Foo + val top = (new Foo).foldControlSteps + assertCodeString( + top, + """|class Foo extends RTDesign: + | val x = Bit <> OUT.REG + | val waitCnt1 = UInt(8) <> VAR.REG init d"8'0" + | process: + | def S_0: Step = + | if (waitCnt1 != d"8'149") + | waitCnt1.din := waitCnt1 + d"8'1" + | ThisStep + | else NextStep + | end S_0 + | waitCnt1.din := d"8'0" + | x.din := !x + |end Foo""".stripMargin + ) + } + + // ── No-op (harvested): two sequential pure while loops — both wait steps, neither folds ──────── + test("harvested: multiple sequential while loops are left unchanged") { + class Foo extends RTDesign: + val x = Bit <> OUT.REG + val waitCnt1 = UInt(8) <> VAR.REG init 0 + val waitCnt2 = UInt(8) <> VAR.REG init 0 + process: + while (waitCnt1 != 149) + waitCnt1.din := waitCnt1 + 1 + waitCnt1.din := 0 + x.din := !x + while (waitCnt2 != 149) + waitCnt2.din := waitCnt2 + 1 + waitCnt2.din := 0 + x.din := 1 + end Foo + val top = (new Foo).foldControlSteps + assertCodeString( + top, + """|class Foo extends RTDesign: + | val x = Bit <> OUT.REG + | val waitCnt1 = UInt(8) <> VAR.REG init d"8'0" + | val waitCnt2 = UInt(8) <> VAR.REG init d"8'0" + | process: + | def S_0: Step = + | if (waitCnt1 != d"8'149") + | waitCnt1.din := waitCnt1 + d"8'1" + | ThisStep + | else NextStep + | end S_0 + | waitCnt1.din := d"8'0" + | x.din := !x + | def S_1: Step = + | if (waitCnt2 != d"8'149") + | waitCnt2.din := waitCnt2 + d"8'1" + | ThisStep + | else NextStep + | end S_1 + | waitCnt2.din := d"8'0" + | x.din := 1 + |end Foo""".stripMargin + ) + } + +end FoldControlStepsSpec From 3990b258089764d152fa5707098bf0b54b81fb8f Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 6 Jun 2026 22:50:31 +0300 Subject: [PATCH 16/24] fix DeviceID constraint propagation --- core/src/main/scala/dfhdl/core/MutableDB.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/src/main/scala/dfhdl/core/MutableDB.scala b/core/src/main/scala/dfhdl/core/MutableDB.scala index c2583d9dd..6f4b15640 100644 --- a/core/src/main/scala/dfhdl/core/MutableDB.scala +++ b/core/src/main/scala/dfhdl/core/MutableDB.scala @@ -409,7 +409,10 @@ final class MutableDB(): val newSigConstraints = clkResource.allSigConstraints // merge the existing constraints with the new constraints val updatedSigConstraints = (existingSigConstraints ++ newSigConstraints).merge - val updatedMeta = domainOwner.meta.copy(annotations = updatedSigConstraints) + // preserve non-SigConstraint annotations (e.g. global constraints such as + // DeviceID/DeviceProperties/DeviceConfig/ToolOptions) which would otherwise be dropped + 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) From 01311988b877b3a28a0281ab92cf3dc27ae30eb1 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 6 Jun 2026 23:15:15 +0300 Subject: [PATCH 17/24] fix compiler plugin from considering null as meta context --- plugin/src/main/scala/plugin/CommonPhase.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/main/scala/plugin/CommonPhase.scala b/plugin/src/main/scala/plugin/CommonPhase.scala index ebc869cd1..63c4fe052 100755 --- a/plugin/src/main/scala/plugin/CommonPhase.scala +++ b/plugin/src/main/scala/plugin/CommonPhase.scala @@ -169,7 +169,7 @@ abstract class CommonPhase extends PluginPhase: extension (tpe: Type)(using Context) def isMetaContext: Boolean = - tpe <:< metaContextTpe + tpe <:< metaContextTpe && !(tpe <:< defn.NullType) def dfValTpeOpt: Option[Type] = tpe.dealias match case res if res.dealias.typeSymbol == dfValSym => Some(res) From 51e943d2b0d230d4958e4e2a4d0c01a09620c506 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 6 Jun 2026 23:19:00 +0300 Subject: [PATCH 18/24] fix console printout under sbtn --- .../main/scala/dfhdl/tools/toolsCore/Tool.scala | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala b/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala index a1cef10d6..e5a94084a 100644 --- a/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala +++ b/lib/src/main/scala/dfhdl/tools/toolsCore/Tool.scala @@ -147,11 +147,21 @@ trait Tool: // so we use the full tool path and resolve the executable else s"${Paths.get(this.runExecFullPath).getParent().resolve(runExec)} $cmd" - // process the output if we have a logger set. - // note that setting a logger may affect the program behavior, since it is disengaged from TTY. + // process the output. + // note that reading the output line-by-line may affect the program behavior, since it is + // disengaged from the TTY. + // when no logger is set we would like to inherit the parent's stdout/stderr so the tool keeps + // its TTY (colors, live progress). however, os.Inherit writes to the JVM's real file + // descriptors, which under `sbtn` belong to the detached build server rather than the client + // terminal, so the tool's output becomes invisible. when there is no real console (the `sbtn` + // case, and CI), fall back to reading the tool's lines and re-emitting them through + // System.out, which sbt forwards to the client. val processOutput = loggerOpt.map(logger => os.ProcessOutput.Readlines(line => logger.out(line)) - ).getOrElse(os.Inherit) + ).getOrElse( + if (System.console() != null) os.Inherit + else os.ProcessOutput.Readlines(line => println(line)) + ) // spawn the process val process = os.proc(os.Shellable(fullExec.split(" ").toSeq)).spawn( cwd = os.Path(execPath, os.pwd), From 81093d5412e73ad1e4b85c89b2327baaf2718ef5 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 6 Jun 2026 23:34:31 +0300 Subject: [PATCH 19/24] fix and add some limitations on FoldControlSteps --- .../compiler/stages/FoldControlSteps.scala | 84 ++++++++--- .../StagesSpec/FoldControlStepsSpec.scala | 139 +++++++++++++++++- 2 files changed, 193 insertions(+), 30 deletions(-) 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 920f6bde1..8ec1ca8e0 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/FoldControlSteps.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/FoldControlSteps.scala @@ -132,13 +132,16 @@ case object FoldControlSteps extends HierarchyStage: // One control level `C_m` of a (possibly nested) loop chain: its `if (cCond)` then-block, the loop // condition, the index-update net (writes the reg the condition tests — folds at entry) and the - // remaining bookkeeping nets (reset of the level below — replayed on continue / exhaust). + // nested step (the level below, excluded from the cloned bookkeeping). Everything in the then-block + // other than the index update, the nested step and the loop-back goto is the level's "rest" + // bookkeeping — any statements, including `println`/`report` and nested combinational `if`s — which + // is replayed (verbatim, deep-cloned) on continue / exhaust. private case class Level( c: StepBlock, cThen: DFConditional.DFIfElseBlock, cCond: DFVal, idxNet: DFNet, - resetNets: List[DFNet] + nested: StepBlock ) // A foldable control chain `C_1` ⊃ … ⊃ `C_k` ⊃ `W` (outer→inner), down to the leaf wait `W`. private case class Chain( @@ -146,7 +149,6 @@ case object FoldControlSteps extends HierarchyStage: w: StepBlock, wThen: DFConditional.DFIfElseBlock, wCond: DFVal, - wCountNets: List[DFNet], c1Else: DFConditional.DFIfElseBlock // the outermost control step's else-body (the loop exit) ) @@ -183,6 +185,30 @@ case object FoldControlSteps extends HierarchyStage: n.lhsRef.get match case v: DFVal => v.departialDcl.map(_._1) case _ => None + // 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 + // the nested step and everything under it + private def nestedSubtree(lvl: Level)(using MemberGetSet): Set[DFMember] = + (lvl.nested :: lvl.nested.members(MemberView.Flattened)).toSet + // transitive reference-closure of `seed` restricted to members owned within `block` — used to pull + // in values that belong to a member but are loosely owned by the enclosing block (e.g. a nested + // step's `if` condition, which `DropRTWaits` parks in the parent block rather than the step). + private def refClosureWithin(block: DFOwner, seed: Set[DFMember])(using MemberGetSet): Set[DFMember] = + val within = block.members(MemberView.Flattened).toSet + @tailrec def loop(cur: Set[DFMember]): Set[DFMember] = + val next = cur ++ cur.iterator.flatMap(_.getRefs.iterator.map(_.get)).filter(within.contains) + if (next.size == cur.size) cur else loop(next) + loop(seed) + // the members of a level's then-block that are NOT the index update, the nested step (with its + // loosely-owned condition closure), or the loop-back goto — i.e. the iteration bookkeeping (resets, + // prints, nested combinational ifs, …), in pre-order. Deep-cloned verbatim so no statement is + // dropped, while the nested step's own condition is not spuriously duplicated. + private def restMembers(lvl: Level)(using MemberGetSet): List[DFMember] = + val excl = + refClosureWithin(lvl.cThen, idxSubtree(lvl) ++ nestedSubtree(lvl)) ++ + directGotos(lvl.cThen).lastOption.toSet + lvl.cThen.members(MemberView.Flattened).filterNot(excl.contains) // the declarations read by a (condition) value private def condDcls(cond: DFVal)(using MemberGetSet): Set[DFVal.Dcl] = (cond :: cond.collectRelMembers(false)) @@ -220,8 +246,19 @@ case object FoldControlSteps extends HierarchyStage: val cCond = cThen.getGuardOption.get val thenNets = cThen.members(MemberView.Folded).collect { case n: DFNet => n } val dcls = condDcls(cCond) - thenNets.find(n => lhsDcl(n).exists(dcls.contains)).map { idxNet => - (Level(c, cThen, cCond, idxNet, thenNets.filterNot(_ == idxNet)), nested, cElse) + thenNets.find(n => lhsDcl(n).exists(dcls.contains)).flatMap { idxNet => + // Safety: entry-folding moves the index update to the wait's first cycle, so the + // index reg changes mid-wait. That is only sound if the wait body never *reads* the + // index (true for pure timing loops, where the index is condition-only). If the + // nested subtree reads the index (e.g. `println(i, …)` inside the loop body), folding + // would corrupt those reads — so this level is not foldable (leaving it unfolded is + // correct, just un-optimised; an inner safe level can still fold via the fixpoint). + val idxDcl = lhsDcl(idxNet) + val readInNested = nested.members(MemberView.Flattened).iterator + .flatMap(_.getRefs.iterator.map(_.get)) + .exists(m => idxDcl.contains(m)) + if (readInNested) None + else Some((Level(c, cThen, cCond, idxNet, nested), nested, cElse)) } case _ => None } @@ -233,10 +270,7 @@ case object FoldControlSteps extends HierarchyStage: controlLevel(c).flatMap { case (level, nested, cElse) => if (isWaitStep(nested)) val wThen = thenBlockOf(nested, singleIfHeader(nested).get) - Some(Chain( - List(level), nested, wThen, wThen.getGuardOption.get, - wThen.members(MemberView.Folded).collect { case n: DFNet => n }, cElse - )) + Some(Chain(List(level), nested, wThen, wThen.getGuardOption.get, cElse)) else chainOf(nested).map(inner => inner.copy(levels = level :: inner.levels, c1Else = cElse)) } @@ -268,11 +302,6 @@ case object FoldControlSteps extends HierarchyStage: val regs = levels.map(L => (Bit <> VAR.REG).init(1)(using dfc.setName(s"${L.c.getName}_entered"))) val enteredRegs = regDsn.regs - def cloneNets(md: MetaDesign[?], baseOwner: DFOwner, nets: List[DFNet]): Unit = - nets.foreach { n => - md.plantClonedMembers(baseOwner, n.collectRelMembers.asInstanceOf[List[DFMember]] :+ n) - } - val stepDsn = new MetaDesign( c1, @@ -280,19 +309,27 @@ case object FoldControlSteps extends HierarchyStage: dfhdl.core.DomainType.RT ): import dfhdl.core.{StepBlock, DFIf, DFBool, DFUnit} + // deep-clone a list of members (with their owner/ref structure) under the current owner + def cloneMembers(baseOwner: DFOwner, members: List[DFMember]): Unit = + if (members.nonEmpty) plantClonedMembers(baseOwner, members) + // a level's index update + its dependency subtree, in pre-order + def idxMembers(lvl: Level): List[DFMember] = + lvl.cThen.members(MemberView.Flattened).filter(idxSubtree(lvl).contains) // re-arm the guard regs for control levels `from..k-1` (inner-first ordering) def reArm(from: Int): Unit = (k - 1 to from by -1).foreach(p => enteredRegs(p).din := 1) - // clone the wait-counter reset that re-initialises the wait for the next iteration - def resetWaitCounter(): Unit = cloneNets(this, innermost.cThen, innermost.resetNets) - def cloneElse(): Unit = plantClonedMembers(chain.c1Else, chain.c1Else.members(MemberView.Flattened)) - // the leave path for the outer control levels (k-2..0): each resets the level below's index, + // clone the innermost level's bookkeeping (the wait-counter reset etc.) that re-initialises + // the wait for the next iteration + def resetWaitCounter(): Unit = cloneMembers(innermost.cThen, restMembers(innermost)) + def cloneElse(): Unit = + cloneMembers(chain.c1Else, chain.c1Else.members(MemberView.Flattened)) + // the leave path for the outer control levels (k-2..0): each replays the level's bookkeeping, // then tests its own condition (continue vs. recurse outward); the outermost else is C_1's // else-body. For a single loop (k==1) this is just C_1's else-body. def buildOuterExit(mi: Int): Unit = if (mi < 0) cloneElse() else - cloneNets(this, levels(mi).cThen, levels(mi).resetNets) + cloneMembers(levels(mi).cThen, restMembers(levels(mi))) val nh = DFIf.Header(DFUnit) val cont = DFIf.Block(Some(levels(mi).cCond.cloneAnonValueAndDepsHere.asValOf[DFBool]), nh) dfc.enterOwner(cont) @@ -308,17 +345,20 @@ case object FoldControlSteps extends HierarchyStage: val step = StepBlock.forced(using dfc.setName(chain.w.getName)) dfc.enterOwner(step) val header = DFIf.Header(DFUnit) - // count branch: guarded per-level index updates (inner-first), then W's counter increment + // count branch: guarded per-level index updates (inner-first), then W's full body val countBlock = DFIf.Block(Some(chain.wCond.cloneAnonValueAndDepsHere.asValOf[DFBool]), header) dfc.enterOwner(countBlock) levels.zipWithIndex.reverse.foreach { (lvl, idx) => val gIf = DFIf.Block(Some(enteredRegs(idx).asValOf[DFBool]), DFIf.Header(DFUnit)) dfc.enterOwner(gIf) - cloneNets(this, lvl.cThen, List(lvl.idxNet)) + cloneMembers(lvl.cThen, idxMembers(lvl)) enteredRegs(idx).din := 0 dfc.exitOwner() } - cloneNets(this, chain.wThen, chain.wCountNets) + // W's count-branch body verbatim (counter increment, prints, nested combinational ifs, …), + // everything except its loop-back goto + val wGotos: Set[DFMember] = directGotos(chain.wThen).toSet + cloneMembers(chain.wThen, chain.wThen.members(MemberView.Flattened).filterNot(wGotos.contains)) ThisStep dfc.exitOwner() // innermost continue (part of the main chain, rendered as `else if`) diff --git a/compiler/stages/src/test/scala/StagesSpec/FoldControlStepsSpec.scala b/compiler/stages/src/test/scala/StagesSpec/FoldControlStepsSpec.scala index a49f5cc68..b5725b831 100644 --- a/compiler/stages/src/test/scala/StagesSpec/FoldControlStepsSpec.scala +++ b/compiler/stages/src/test/scala/StagesSpec/FoldControlStepsSpec.scala @@ -407,8 +407,8 @@ class FoldControlStepsSpec extends StageSpec(): while (i != 100) while (waitCnt != 49) waitCnt.din := waitCnt + 1 - waitCnt.din := 0 - i.din := i + 1 + waitCnt.din := 0 + i.din := i + 1 Done end Count def Done: Step = @@ -470,8 +470,8 @@ class FoldControlStepsSpec extends StageSpec(): process: while (waitCnt1 != 149) waitCnt1.din := waitCnt1 + 1 - waitCnt1.din := 0 - x.din := !x + waitCnt1.din := 0 + x.din := !x end Foo val top = (new Foo).foldControlSteps assertCodeString( @@ -501,12 +501,12 @@ class FoldControlStepsSpec extends StageSpec(): process: while (waitCnt1 != 149) waitCnt1.din := waitCnt1 + 1 - waitCnt1.din := 0 - x.din := !x + waitCnt1.din := 0 + x.din := !x while (waitCnt2 != 149) waitCnt2.din := waitCnt2 + 1 - waitCnt2.din := 0 - x.din := 1 + waitCnt2.din := 0 + x.din := 1 end Foo val top = (new Foo).foldControlSteps assertCodeString( @@ -536,4 +536,127 @@ class FoldControlStepsSpec extends StageSpec(): ) } + // ── No-op (safety): the inner loop body READS the outer index `i` (`println(i, j)`). Entry-folding + // would move `i.din := i + 1` to the wait's first cycle, changing `i` mid-loop and corrupting + // those reads — so the `i` loop is left unfolded. Crucially, every body statement (the `println` + // and any nested combinational logic) is preserved verbatim; nothing is dropped. ────────────── + test("a loop whose body reads its index is left unfolded (statements preserved)") { + class Foo extends RTDesign: + val i = Int <> VAR.REG + val j = Int <> VAR.REG + process: + def S_0: Step = + NextStep + end S_0 + i.din := 0 + def S_1: Step = + if (i < 3) + j.din := 0 + def S_1_0: Step = + if (j < 3) + println(s"Hello i: ${i}, j: ${j}") + j.din := j + 1 + ThisStep + else NextStep + end S_1_0 + i.din := i + 1 + ThisStep + else NextStep + end if + end S_1 + finish() + end Foo + val top = (new Foo).foldControlSteps + assertCodeString( + top, + """|class Foo extends RTDesign: + | val i = Int <> VAR.REG + | val j = Int <> VAR.REG + | process: + | def S_0: Step = + | NextStep + | end S_0 + | i.din := 0 + | def S_1: Step = + | if (i < 3) + | j.din := 0 + | def S_1_0: Step = + | if (j < 3) + | println(s"Hello i: ${i}, j: ${j}") + | j.din := j + 1 + | ThisStep + | else NextStep + | end S_1_0 + | i.din := i + 1 + | ThisStep + | else NextStep + | end if + | end S_1 + | finish() + |end Foo""".stripMargin + ) + } + + // ── Fold: a wait body with an extra statement that does NOT read the folded index — the loop + // folds and the body statement is preserved in the count branch (no drop). ──────────────────── + test("fold preserves a wait-body statement that does not read the index") { + class Foo extends RTDesign: + val o = Bit <> OUT.REG init 0 + val i = Int <> VAR.REG init 0 + val waitCnt = UInt(6) <> VAR.REG init 0 + process: + def S_0: Step = + NextStep + end S_0 + def S_1: Step = + if (i < 100) + def S_1_0: Step = + if (waitCnt != 49) + o.din := !o + waitCnt.din := waitCnt + 1 + ThisStep + else NextStep + end S_1_0 + waitCnt.din := 0 + i.din := i + 1 + ThisStep + else + finish() + NextStep + end S_1 + end Foo + val top = (new Foo).foldControlSteps + assertCodeString( + top, + """|class Foo extends RTDesign: + | val o = Bit <> OUT.REG init 0 + | val i = Int <> VAR.REG init 0 + | val waitCnt = UInt(6) <> VAR.REG init d"6'0" + | val S_1_entered = Bit <> VAR.REG init 1 + | process: + | def S_0: Step = + | NextStep + | end S_0 + | def S_1_0: Step = + | if (waitCnt != d"6'49") + | if (S_1_entered) + | i.din := i + 1 + | S_1_entered.din := 0 + | end if + | o.din := !o + | waitCnt.din := waitCnt + d"6'1" + | ThisStep + | else if (i < 100) + | waitCnt.din := d"6'0" + | S_1_entered.din := 1 + | ThisStep + | else + | finish() + | NextStep + | end if + | end S_1_0 + |end Foo""".stripMargin + ) + } + end FoldControlStepsSpec From 4914252b1e94088ccb45ca69d09e29f569765043 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sat, 6 Jun 2026 23:39:15 +0300 Subject: [PATCH 20/24] disable FoldControlSteps --- .../compiler/stages/FlattenStepBlocks.scala | 3 +- .../compiler/stages/FoldControlSteps.scala | 28 +- .../StagesSpec/FoldControlStepsSpec.scala | 1294 ++++++++--------- 3 files changed, 668 insertions(+), 657 deletions(-) diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/FlattenStepBlocks.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/FlattenStepBlocks.scala index 2c1facf42..5ae0a9ef4 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/FlattenStepBlocks.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/FlattenStepBlocks.scala @@ -166,7 +166,8 @@ import scala.annotation.tailrec */ //format: on case object FlattenStepBlocks extends HierarchyStage: - def dependencies: List[Stage] = List(FoldControlSteps) + // TODO: Not running FoldControlSteps for now + def dependencies: List[Stage] = List(DropRTWaits, ExplicitNamedVars, DropLocalDcls) def nullifies: Set[Stage] = Set() def transformSubDB(rootDB: DB)(using MemberGetSet, CompilerOptions, RefGen): DB = 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 8ec1ca8e0..fe74947c1 100644 --- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/FoldControlSteps.scala +++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/FoldControlSteps.scala @@ -6,6 +6,8 @@ import dfhdl.compiler.patching.* import dfhdl.options.CompilerOptions import dfhdl.internals.* import scala.annotation.tailrec + +//// NOTICE: THIS STAGE IS DISABLED FOR NOW, SINCE IT CAUSES MORE PROBLEMS THAN IT SOLVES IN ITS CURRENT FORM. //format: off /** This stage removes the gratuitous clock cycle that every loop iteration costs in an RT process. * @@ -194,7 +196,9 @@ case object FoldControlSteps extends HierarchyStage: // transitive reference-closure of `seed` restricted to members owned within `block` — used to pull // in values that belong to a member but are loosely owned by the enclosing block (e.g. a nested // step's `if` condition, which `DropRTWaits` parks in the parent block rather than the step). - private def refClosureWithin(block: DFOwner, seed: Set[DFMember])(using MemberGetSet): Set[DFMember] = + private def refClosureWithin(block: DFOwner, seed: Set[DFMember])(using + MemberGetSet + ): Set[DFMember] = val within = block.members(MemberView.Flattened).toSet @tailrec def loop(cur: Set[DFMember]): Set[DFMember] = val next = cur ++ cur.iterator.flatMap(_.getRefs.iterator.map(_.get)).filter(within.contains) @@ -299,7 +303,8 @@ case object FoldControlSteps extends HierarchyStage: val regDsn = new MetaDesign(enclosingProcess(c1), Patch.Add.Config.Before, dfhdl.core.DomainType.RT): import dfhdl.core.* - val regs = levels.map(L => (Bit <> VAR.REG).init(1)(using dfc.setName(s"${L.c.getName}_entered"))) + val regs = + levels.map(L => (Bit <> VAR.REG).init(1)(using dfc.setName(s"${L.c.getName}_entered"))) val enteredRegs = regDsn.regs val stepDsn = @@ -316,8 +321,7 @@ case object FoldControlSteps extends HierarchyStage: def idxMembers(lvl: Level): List[DFMember] = lvl.cThen.members(MemberView.Flattened).filter(idxSubtree(lvl).contains) // re-arm the guard regs for control levels `from..k-1` (inner-first ordering) - def reArm(from: Int): Unit = - (k - 1 to from by -1).foreach(p => enteredRegs(p).din := 1) + def reArm(from: Int): Unit = (k - 1 to from by -1).foreach(p => enteredRegs(p).din := 1) // clone the innermost level's bookkeeping (the wait-counter reset etc.) that re-initialises // the wait for the next iteration def resetWaitCounter(): Unit = cloneMembers(innermost.cThen, restMembers(innermost)) @@ -331,7 +335,8 @@ case object FoldControlSteps extends HierarchyStage: else cloneMembers(levels(mi).cThen, restMembers(levels(mi))) val nh = DFIf.Header(DFUnit) - val cont = DFIf.Block(Some(levels(mi).cCond.cloneAnonValueAndDepsHere.asValOf[DFBool]), nh) + val cont = + DFIf.Block(Some(levels(mi).cCond.cloneAnonValueAndDepsHere.asValOf[DFBool]), nh) dfc.enterOwner(cont) resetWaitCounter() reArm(mi) @@ -346,7 +351,8 @@ case object FoldControlSteps extends HierarchyStage: dfc.enterOwner(step) val header = DFIf.Header(DFUnit) // count branch: guarded per-level index updates (inner-first), then W's full body - val countBlock = DFIf.Block(Some(chain.wCond.cloneAnonValueAndDepsHere.asValOf[DFBool]), header) + val countBlock = + DFIf.Block(Some(chain.wCond.cloneAnonValueAndDepsHere.asValOf[DFBool]), header) dfc.enterOwner(countBlock) levels.zipWithIndex.reverse.foreach { (lvl, idx) => val gIf = DFIf.Block(Some(enteredRegs(idx).asValOf[DFBool]), DFIf.Header(DFUnit)) @@ -358,7 +364,10 @@ case object FoldControlSteps extends HierarchyStage: // W's count-branch body verbatim (counter increment, prints, nested combinational ifs, …), // everything except its loop-back goto val wGotos: Set[DFMember] = directGotos(chain.wThen).toSet - cloneMembers(chain.wThen, chain.wThen.members(MemberView.Flattened).filterNot(wGotos.contains)) + cloneMembers( + chain.wThen, + chain.wThen.members(MemberView.Flattened).filterNot(wGotos.contains) + ) ThisStep dfc.exitOwner() // innermost continue (part of the main chain, rendered as `else if`) @@ -384,8 +393,9 @@ case object FoldControlSteps extends HierarchyStage: // — so an external cond-member is removed only if no surviving member still references it. val baseRemove = c1.members(MemberView.Flattened) val baseSet = baseRemove.toSet - val condCands = - (chain.wCond :: levels.map(_.cCond)).flatMap(c => c :: c.collectRelMembers(false)).distinct + val condCands = (chain.wCond :: levels.map(_.cCond)).flatMap(c => + c :: c.collectRelMembers(false) + ).distinct val extraConds = condCands.filterNot(baseSet.contains) val tentativeRemove = baseSet ++ extraConds + c1 // ref targets of everything that will survive (resolved on the intact pre-patch DB) diff --git a/compiler/stages/src/test/scala/StagesSpec/FoldControlStepsSpec.scala b/compiler/stages/src/test/scala/StagesSpec/FoldControlStepsSpec.scala index b5725b831..617403e45 100644 --- a/compiler/stages/src/test/scala/StagesSpec/FoldControlStepsSpec.scala +++ b/compiler/stages/src/test/scala/StagesSpec/FoldControlStepsSpec.scala @@ -1,662 +1,662 @@ -package StagesSpec +// package StagesSpec -import dfhdl.* -import dfhdl.compiler.stages.foldControlSteps -// scalafmt: { align.tokens = [{code = "<>"}, {code = "="}, {code = "=>"}, {code = ":="}]} +// import dfhdl.* +// import dfhdl.compiler.stages.foldControlSteps +// // scalafmt: { align.tokens = [{code = "<>"}, {code = "="}, {code = "=>"}, {code = ":="}]} -// NOTE (Phase 2): inputs are written in the step-block syntax valid after `DropRTWaits` — explicit -// `def S: Step`, relative gotos (`NextStep`/`ThisStep`), manual `waitCnt`/index `VAR.REG`s, and -// explicit `def onEntry`/`onExit`/`fallThrough` blocks — never high-level `for`/`while`/`wait` -// (matches `DropRTWaitsSpec`/`FlattenStepBlocksSpec`). The expected strings for the *folding* cases -// are the hand-authored entry-folded contract; they fail against the Phase-1 no-op stub by design. -// The no-op cases (no enclosing loop / fallThrough-blocked / already-folded / non-RT) already pass -// against the stub, pinning the forms the stage must leave untouched. -class FoldControlStepsSpec extends StageSpec(): +// // NOTE (Phase 2): inputs are written in the step-block syntax valid after `DropRTWaits` — explicit +// // `def S: Step`, relative gotos (`NextStep`/`ThisStep`), manual `waitCnt`/index `VAR.REG`s, and +// // explicit `def onEntry`/`onExit`/`fallThrough` blocks — never high-level `for`/`while`/`wait` +// // (matches `DropRTWaitsSpec`/`FlattenStepBlocksSpec`). The expected strings for the *folding* cases +// // are the hand-authored entry-folded contract; they fail against the Phase-1 no-op stub by design. +// // The no-op cases (no enclosing loop / fallThrough-blocked / already-folded / non-RT) already pass +// // against the stub, pinning the forms the stage must leave untouched. +// class FoldControlStepsSpec extends StageSpec(): - // ── No-op: a multi-cycle wait with no enclosing control step has nothing to fold ────────────── - test("single wait without a loop is left unchanged") { - class Foo extends RTDesign: - val waitCnt = UInt(6) <> VAR.REG init 0 - process: - def S_0: Step = - NextStep - end S_0 - def S_1: Step = - if (waitCnt != 49) - waitCnt.din := waitCnt + 1 - ThisStep - else NextStep - end S_1 - end Foo - val top = (new Foo).foldControlSteps - assertCodeString( - top, - """|class Foo extends RTDesign: - | val waitCnt = UInt(6) <> VAR.REG init d"6'0" - | process: - | def S_0: Step = - | NextStep - | end S_0 - | def S_1: Step = - | if (waitCnt != d"6'49") - | waitCnt.din := waitCnt + d"6'1" - | ThisStep - | else NextStep - | end S_1 - |end Foo""".stripMargin - ) - } +// // ── No-op: a multi-cycle wait with no enclosing control step has nothing to fold ────────────── +// test("single wait without a loop is left unchanged") { +// class Foo extends RTDesign: +// val waitCnt = UInt(6) <> VAR.REG init 0 +// process: +// def S_0: Step = +// NextStep +// end S_0 +// def S_1: Step = +// if (waitCnt != 49) +// waitCnt.din := waitCnt + 1 +// ThisStep +// else NextStep +// end S_1 +// end Foo +// val top = (new Foo).foldControlSteps +// assertCodeString( +// top, +// """|class Foo extends RTDesign: +// | val waitCnt = UInt(6) <> VAR.REG init d"6'0" +// | process: +// | def S_0: Step = +// | NextStep +// | end S_0 +// | def S_1: Step = +// | if (waitCnt != d"6'49") +// | waitCnt.din := waitCnt + d"6'1" +// | ThisStep +// | else NextStep +// | end S_1 +// |end Foo""".stripMargin +// ) +// } - // ── Fold: single loop — control `S_1` folds into wait `S_1_0`, guarded by `S_1_entered` ─────── - test("single loop folds the control step into its wait") { - class Foo extends RTDesign: - val i = Int <> VAR.REG init 0 - val waitCnt = UInt(6) <> VAR.REG init 0 - process: - def S_0: Step = - NextStep - end S_0 - def S_1: Step = - if (i < 100) - def S_1_0: Step = - if (waitCnt != 49) - waitCnt.din := waitCnt + 1 - ThisStep - else NextStep - end S_1_0 - waitCnt.din := 0 - i.din := i + 1 - ThisStep - else - finish() - NextStep - end S_1 - end Foo - val top = (new Foo).foldControlSteps - assertCodeString( - top, - """|class Foo extends RTDesign: - | val i = Int <> VAR.REG init 0 - | val waitCnt = UInt(6) <> VAR.REG init d"6'0" - | val S_1_entered = Bit <> VAR.REG init 1 - | process: - | def S_0: Step = - | NextStep - | end S_0 - | def S_1_0: Step = - | if (waitCnt != d"6'49") - | if (S_1_entered) - | i.din := i + 1 - | S_1_entered.din := 0 - | end if - | waitCnt.din := waitCnt + d"6'1" - | ThisStep - | else if (i < 100) - | waitCnt.din := d"6'0" - | S_1_entered.din := 1 - | ThisStep - | else - | finish() - | NextStep - | end if - | end S_1_0 - |end Foo""".stripMargin - ) - } +// // ── Fold: single loop — control `S_1` folds into wait `S_1_0`, guarded by `S_1_entered` ─────── +// test("single loop folds the control step into its wait") { +// class Foo extends RTDesign: +// val i = Int <> VAR.REG init 0 +// val waitCnt = UInt(6) <> VAR.REG init 0 +// process: +// def S_0: Step = +// NextStep +// end S_0 +// def S_1: Step = +// if (i < 100) +// def S_1_0: Step = +// if (waitCnt != 49) +// waitCnt.din := waitCnt + 1 +// ThisStep +// else NextStep +// end S_1_0 +// waitCnt.din := 0 +// i.din := i + 1 +// ThisStep +// else +// finish() +// NextStep +// end S_1 +// end Foo +// val top = (new Foo).foldControlSteps +// assertCodeString( +// top, +// """|class Foo extends RTDesign: +// | val i = Int <> VAR.REG init 0 +// | val waitCnt = UInt(6) <> VAR.REG init d"6'0" +// | val S_1_entered = Bit <> VAR.REG init 1 +// | process: +// | def S_0: Step = +// | NextStep +// | end S_0 +// | def S_1_0: Step = +// | if (waitCnt != d"6'49") +// | if (S_1_entered) +// | i.din := i + 1 +// | S_1_entered.din := 0 +// | end if +// | waitCnt.din := waitCnt + d"6'1" +// | ThisStep +// | else if (i < 100) +// | waitCnt.din := d"6'0" +// | S_1_entered.din := 1 +// | ThisStep +// | else +// | finish() +// | NextStep +// | end if +// | end S_1_0 +// |end Foo""".stripMargin +// ) +// } - // ── Fold: nested loops — BOTH control steps fold into the innermost wait `S_1_0_0`. The whole - // chain folds in one pass: inner index `j++` (guard `S_1_0_entered`) and outer index `i++` - // (guard `S_1_entered`) at the guarded entry; the exit weaves inner-continue (`else if j<10`), - // then the inner-exhausted path (`j := 0`) into the outer-continue/leave. ───────────────────── - test("nested loops fold both control steps into the innermost wait") { - class Foo extends RTDesign: - val i = Int <> VAR.REG init 0 - val j = Int <> VAR.REG init 0 - val waitCnt = UInt(6) <> VAR.REG init 0 - process: - def S_0: Step = - NextStep - end S_0 - def S_1: Step = - if (i < 10) - def S_1_0: Step = - if (j < 10) - def S_1_0_0: Step = - if (waitCnt != 49) - waitCnt.din := waitCnt + 1 - ThisStep - else NextStep - end S_1_0_0 - waitCnt.din := 0 - j.din := j + 1 - ThisStep - else NextStep - end S_1_0 - j.din := 0 - i.din := i + 1 - ThisStep - else - finish() - NextStep - end S_1 - end Foo - val top = (new Foo).foldControlSteps - assertCodeString( - top, - """|class Foo extends RTDesign: - | val i = Int <> VAR.REG init 0 - | val j = Int <> VAR.REG init 0 - | val waitCnt = UInt(6) <> VAR.REG init d"6'0" - | val S_1_entered = Bit <> VAR.REG init 1 - | val S_1_0_entered = Bit <> VAR.REG init 1 - | process: - | def S_0: Step = - | NextStep - | end S_0 - | def S_1_0_0: Step = - | if (waitCnt != d"6'49") - | if (S_1_0_entered) - | j.din := j + 1 - | S_1_0_entered.din := 0 - | end if - | if (S_1_entered) - | i.din := i + 1 - | S_1_entered.din := 0 - | end if - | waitCnt.din := waitCnt + d"6'1" - | ThisStep - | else if (j < 10) - | waitCnt.din := d"6'0" - | S_1_0_entered.din := 1 - | ThisStep - | else - | j.din := 0 - | if (i < 10) - | waitCnt.din := d"6'0" - | S_1_0_entered.din := 1 - | S_1_entered.din := 1 - | ThisStep - | else - | finish() - | NextStep - | end if - | end if - | end S_1_0_0 - |end Foo""".stripMargin - ) - } +// // ── Fold: nested loops — BOTH control steps fold into the innermost wait `S_1_0_0`. The whole +// // chain folds in one pass: inner index `j++` (guard `S_1_0_entered`) and outer index `i++` +// // (guard `S_1_entered`) at the guarded entry; the exit weaves inner-continue (`else if j<10`), +// // then the inner-exhausted path (`j := 0`) into the outer-continue/leave. ───────────────────── +// test("nested loops fold both control steps into the innermost wait") { +// class Foo extends RTDesign: +// val i = Int <> VAR.REG init 0 +// val j = Int <> VAR.REG init 0 +// val waitCnt = UInt(6) <> VAR.REG init 0 +// process: +// def S_0: Step = +// NextStep +// end S_0 +// def S_1: Step = +// if (i < 10) +// def S_1_0: Step = +// if (j < 10) +// def S_1_0_0: Step = +// if (waitCnt != 49) +// waitCnt.din := waitCnt + 1 +// ThisStep +// else NextStep +// end S_1_0_0 +// waitCnt.din := 0 +// j.din := j + 1 +// ThisStep +// else NextStep +// end S_1_0 +// j.din := 0 +// i.din := i + 1 +// ThisStep +// else +// finish() +// NextStep +// end S_1 +// end Foo +// val top = (new Foo).foldControlSteps +// assertCodeString( +// top, +// """|class Foo extends RTDesign: +// | val i = Int <> VAR.REG init 0 +// | val j = Int <> VAR.REG init 0 +// | val waitCnt = UInt(6) <> VAR.REG init d"6'0" +// | val S_1_entered = Bit <> VAR.REG init 1 +// | val S_1_0_entered = Bit <> VAR.REG init 1 +// | process: +// | def S_0: Step = +// | NextStep +// | end S_0 +// | def S_1_0_0: Step = +// | if (waitCnt != d"6'49") +// | if (S_1_0_entered) +// | j.din := j + 1 +// | S_1_0_entered.din := 0 +// | end if +// | if (S_1_entered) +// | i.din := i + 1 +// | S_1_entered.din := 0 +// | end if +// | waitCnt.din := waitCnt + d"6'1" +// | ThisStep +// | else if (j < 10) +// | waitCnt.din := d"6'0" +// | S_1_0_entered.din := 1 +// | ThisStep +// | else +// | j.din := 0 +// | if (i < 10) +// | waitCnt.din := d"6'0" +// | S_1_0_entered.din := 1 +// | S_1_entered.din := 1 +// | ThisStep +// | else +// | finish() +// | NextStep +// | end if +// | end if +// | end S_1_0_0 +// |end Foo""".stripMargin +// ) +// } - // ── Precondition: a `fallThrough` block on the wait blocks the fold (pair left unchanged) ───── - test("a fallThrough block blocks the fold") { - class Foo extends RTDesign: - val i = Int <> VAR.REG init 0 - val waitCnt = UInt(6) <> VAR.REG init 0 - process: - def S_0: Step = - NextStep - end S_0 - def S_1: Step = - if (i < 100) - def S_1_0: Step = - def fallThrough = waitCnt == 0 - if (waitCnt != 49) - waitCnt.din := waitCnt + 1 - ThisStep - else NextStep - end S_1_0 - waitCnt.din := 0 - i.din := i + 1 - ThisStep - else - finish() - NextStep - end S_1 - end Foo - val top = (new Foo).foldControlSteps - assertCodeString( - top, - """|class Foo extends RTDesign: - | val i = Int <> VAR.REG init 0 - | val waitCnt = UInt(6) <> VAR.REG init d"6'0" - | process: - | def S_0: Step = - | NextStep - | end S_0 - | def S_1: Step = - | if (i < 100) - | def S_1_0: Step = - | def fallThrough: Boolean <> VAL = - | waitCnt == d"6'0" - | end fallThrough - | if (waitCnt != d"6'49") - | waitCnt.din := waitCnt + d"6'1" - | ThisStep - | else NextStep - | end S_1_0 - | waitCnt.din := d"6'0" - | i.din := i + 1 - | ThisStep - | else - | finish() - | NextStep - | end if - | end S_1 - |end Foo""".stripMargin - ) - } +// // ── Precondition: a `fallThrough` block on the wait blocks the fold (pair left unchanged) ───── +// test("a fallThrough block blocks the fold") { +// class Foo extends RTDesign: +// val i = Int <> VAR.REG init 0 +// val waitCnt = UInt(6) <> VAR.REG init 0 +// process: +// def S_0: Step = +// NextStep +// end S_0 +// def S_1: Step = +// if (i < 100) +// def S_1_0: Step = +// def fallThrough = waitCnt == 0 +// if (waitCnt != 49) +// waitCnt.din := waitCnt + 1 +// ThisStep +// else NextStep +// end S_1_0 +// waitCnt.din := 0 +// i.din := i + 1 +// ThisStep +// else +// finish() +// NextStep +// end S_1 +// end Foo +// val top = (new Foo).foldControlSteps +// assertCodeString( +// top, +// """|class Foo extends RTDesign: +// | val i = Int <> VAR.REG init 0 +// | val waitCnt = UInt(6) <> VAR.REG init d"6'0" +// | process: +// | def S_0: Step = +// | NextStep +// | end S_0 +// | def S_1: Step = +// | if (i < 100) +// | def S_1_0: Step = +// | def fallThrough: Boolean <> VAL = +// | waitCnt == d"6'0" +// | end fallThrough +// | if (waitCnt != d"6'49") +// | waitCnt.din := waitCnt + d"6'1" +// | ThisStep +// | else NextStep +// | end S_1_0 +// | waitCnt.din := d"6'0" +// | i.din := i + 1 +// | ThisStep +// | else +// | finish() +// | NextStep +// | end if +// | end S_1 +// |end Foo""".stripMargin +// ) +// } - // ── No-op: idempotency — re-running on an already-folded process changes nothing ────────────── - test("idempotency: an already-folded process is left unchanged") { - class Foo extends RTDesign: - val i = Int <> VAR.REG init 0 - val waitCnt = UInt(6) <> VAR.REG init 0 - val S_1_entered = Bit <> VAR.REG init 1 - process: - def S_0: Step = - NextStep - end S_0 - def S_1_0: Step = - if (waitCnt != 49) - if (S_1_entered) - i.din := i + 1 - S_1_entered.din := 0 - waitCnt.din := waitCnt + 1 - ThisStep - else if (i < 100) - waitCnt.din := 0 - S_1_entered.din := 1 - ThisStep - else - finish() - NextStep - end S_1_0 - end Foo - val top = (new Foo).foldControlSteps - assertCodeString( - top, - """|class Foo extends RTDesign: - | val i = Int <> VAR.REG init 0 - | val waitCnt = UInt(6) <> VAR.REG init d"6'0" - | val S_1_entered = Bit <> VAR.REG init 1 - | process: - | def S_0: Step = - | NextStep - | end S_0 - | def S_1_0: Step = - | if (waitCnt != d"6'49") - | if (S_1_entered) - | i.din := i + 1 - | S_1_entered.din := 0 - | end if - | waitCnt.din := waitCnt + d"6'1" - | ThisStep - | else if (i < 100) - | waitCnt.din := d"6'0" - | S_1_entered.din := 1 - | ThisStep - | else - | finish() - | NextStep - | end if - | end S_1_0 - |end Foo""".stripMargin - ) - } +// // ── No-op: idempotency — re-running on an already-folded process changes nothing ────────────── +// test("idempotency: an already-folded process is left unchanged") { +// class Foo extends RTDesign: +// val i = Int <> VAR.REG init 0 +// val waitCnt = UInt(6) <> VAR.REG init 0 +// val S_1_entered = Bit <> VAR.REG init 1 +// process: +// def S_0: Step = +// NextStep +// end S_0 +// def S_1_0: Step = +// if (waitCnt != 49) +// if (S_1_entered) +// i.din := i + 1 +// S_1_entered.din := 0 +// waitCnt.din := waitCnt + 1 +// ThisStep +// else if (i < 100) +// waitCnt.din := 0 +// S_1_entered.din := 1 +// ThisStep +// else +// finish() +// NextStep +// end S_1_0 +// end Foo +// val top = (new Foo).foldControlSteps +// assertCodeString( +// top, +// """|class Foo extends RTDesign: +// | val i = Int <> VAR.REG init 0 +// | val waitCnt = UInt(6) <> VAR.REG init d"6'0" +// | val S_1_entered = Bit <> VAR.REG init 1 +// | process: +// | def S_0: Step = +// | NextStep +// | end S_0 +// | def S_1_0: Step = +// | if (waitCnt != d"6'49") +// | if (S_1_entered) +// | i.din := i + 1 +// | S_1_entered.din := 0 +// | end if +// | waitCnt.din := waitCnt + d"6'1" +// | ThisStep +// | else if (i < 100) +// | waitCnt.din := d"6'0" +// | S_1_entered.din := 1 +// | ThisStep +// | else +// | finish() +// | NextStep +// | end if +// | end S_1_0 +// |end Foo""".stripMargin +// ) +// } - // ── No-op: non-RT (plain DF) design is untouched ───────────────────────────────────────────── - test("non-RT design is left unchanged") { - class Foo extends DFDesign: - val x = Bit <> OUT - val y = Bit <> IN - x := y - end Foo - val top = (new Foo).foldControlSteps - assertCodeString( - top, - """|class Foo extends DFDesign: - | val x = Bit <> OUT - | val y = Bit <> IN - | x := y - |end Foo""".stripMargin - ) - } +// // ── No-op: non-RT (plain DF) design is untouched ───────────────────────────────────────────── +// test("non-RT design is left unchanged") { +// class Foo extends DFDesign: +// val x = Bit <> OUT +// val y = Bit <> IN +// x := y +// end Foo +// val top = (new Foo).foldControlSteps +// assertCodeString( +// top, +// """|class Foo extends DFDesign: +// | val x = Bit <> OUT +// | val y = Bit <> IN +// | x := y +// |end Foo""".stripMargin +// ) +// } - // ── Fold: explicit step names — control `Count` folds into wait `Count_0`, guard `Count_entered`, - // with sequencing steps `Start`/`Done` around it (explicit, not relative) ──────────────────── - test("explicit-named loop folds with the guard named after the control step") { - class Foo extends RTDesign: - val i = Int <> VAR.REG init 0 - val waitCnt = UInt(6) <> VAR.REG init 0 - process: - def Start: Step = - NextStep - end Start - def Count: Step = - if (i < 100) - def Count_0: Step = - if (waitCnt != 49) - waitCnt.din := waitCnt + 1 - ThisStep - else NextStep - end Count_0 - waitCnt.din := 0 - i.din := i + 1 - ThisStep - else - finish() - NextStep - end Count - def Done: Step = - NextStep - end Done - end Foo - val top = (new Foo).foldControlSteps - assertCodeString( - top, - """|class Foo extends RTDesign: - | val i = Int <> VAR.REG init 0 - | val waitCnt = UInt(6) <> VAR.REG init d"6'0" - | val Count_entered = Bit <> VAR.REG init 1 - | process: - | def Start: Step = - | NextStep - | end Start - | def Count_0: Step = - | if (waitCnt != d"6'49") - | if (Count_entered) - | i.din := i + 1 - | Count_entered.din := 0 - | end if - | waitCnt.din := waitCnt + d"6'1" - | ThisStep - | else if (i < 100) - | waitCnt.din := d"6'0" - | Count_entered.din := 1 - | ThisStep - | else - | finish() - | NextStep - | end if - | end Count_0 - | def Done: Step = - | NextStep - | end Done - |end Foo""".stripMargin - ) - } +// // ── Fold: explicit step names — control `Count` folds into wait `Count_0`, guard `Count_entered`, +// // with sequencing steps `Start`/`Done` around it (explicit, not relative) ──────────────────── +// test("explicit-named loop folds with the guard named after the control step") { +// class Foo extends RTDesign: +// val i = Int <> VAR.REG init 0 +// val waitCnt = UInt(6) <> VAR.REG init 0 +// process: +// def Start: Step = +// NextStep +// end Start +// def Count: Step = +// if (i < 100) +// def Count_0: Step = +// if (waitCnt != 49) +// waitCnt.din := waitCnt + 1 +// ThisStep +// else NextStep +// end Count_0 +// waitCnt.din := 0 +// i.din := i + 1 +// ThisStep +// else +// finish() +// NextStep +// end Count +// def Done: Step = +// NextStep +// end Done +// end Foo +// val top = (new Foo).foldControlSteps +// assertCodeString( +// top, +// """|class Foo extends RTDesign: +// | val i = Int <> VAR.REG init 0 +// | val waitCnt = UInt(6) <> VAR.REG init d"6'0" +// | val Count_entered = Bit <> VAR.REG init 1 +// | process: +// | def Start: Step = +// | NextStep +// | end Start +// | def Count_0: Step = +// | if (waitCnt != d"6'49") +// | if (Count_entered) +// | i.din := i + 1 +// | Count_entered.din := 0 +// | end if +// | waitCnt.din := waitCnt + d"6'1" +// | ThisStep +// | else if (i < 100) +// | waitCnt.din := d"6'0" +// | Count_entered.din := 1 +// | ThisStep +// | else +// | finish() +// | NextStep +// | end if +// | end Count_0 +// | def Done: Step = +// | NextStep +// | end Done +// |end Foo""".stripMargin +// ) +// } - // ── Fold: a nested foldable loop inside an enclosing step that carries `onEntry`/`onExit`, in a - // multi-step chain (`Start` → `Count` → `Done`). The fold rewrites the inner loop (`Count_0` - // folds into wait `Count_0_0`, guard `Count_0_entered`); `Count`'s `onEntry`/`onExit` — which - // fire on the real transitions into/out of `Count` — are preserved untouched. ─────────────── - test("nested fold preserves an enclosing step's onEntry/onExit in a multi-step chain") { - class Foo extends RTDesign: - val x = Bit <> OUT.REG init 0 - val i = Int <> VAR.REG init 0 - val waitCnt = UInt(6) <> VAR.REG init 0 - process: - def Start: Step = - x.din := 0 - Count - end Start - def Count: Step = - def onEntry = - x.din := 1 - def onExit = - x.din := 0 - while (i != 100) - while (waitCnt != 49) - waitCnt.din := waitCnt + 1 - waitCnt.din := 0 - i.din := i + 1 - Done - end Count - def Done: Step = - x.din := !x - Start - end Done - end Foo - val top = (new Foo).foldControlSteps - assertCodeString( - top, - """|class Foo extends RTDesign: - | val x = Bit <> OUT.REG init 0 - | val i = Int <> VAR.REG init 0 - | val waitCnt = UInt(6) <> VAR.REG init d"6'0" - | val Count_0_entered = Bit <> VAR.REG init 1 - | process: - | def Start: Step = - | x.din := 0 - | Count - | end Start - | def Count: Step = - | def onEntry: Unit = - | x.din := 1 - | end onEntry - | def onExit: Unit = - | x.din := 0 - | end onExit - | def Count_0_0: Step = - | if (waitCnt != d"6'49") - | if (Count_0_entered) - | i.din := i + 1 - | Count_0_entered.din := 0 - | end if - | waitCnt.din := waitCnt + d"6'1" - | ThisStep - | else if (i != 100) - | waitCnt.din := d"6'0" - | Count_0_entered.din := 1 - | ThisStep - | else NextStep - | end if - | end Count_0_0 - | Done - | end Count - | def Done: Step = - | x.din := !x - | Start - | end Done - |end Foo""".stripMargin - ) - } +// // ── Fold: a nested foldable loop inside an enclosing step that carries `onEntry`/`onExit`, in a +// // multi-step chain (`Start` → `Count` → `Done`). The fold rewrites the inner loop (`Count_0` +// // folds into wait `Count_0_0`, guard `Count_0_entered`); `Count`'s `onEntry`/`onExit` — which +// // fire on the real transitions into/out of `Count` — are preserved untouched. ─────────────── +// test("nested fold preserves an enclosing step's onEntry/onExit in a multi-step chain") { +// class Foo extends RTDesign: +// val x = Bit <> OUT.REG init 0 +// val i = Int <> VAR.REG init 0 +// val waitCnt = UInt(6) <> VAR.REG init 0 +// process: +// def Start: Step = +// x.din := 0 +// Count +// end Start +// def Count: Step = +// def onEntry = +// x.din := 1 +// def onExit = +// x.din := 0 +// while (i != 100) +// while (waitCnt != 49) +// waitCnt.din := waitCnt + 1 +// waitCnt.din := 0 +// i.din := i + 1 +// Done +// end Count +// def Done: Step = +// x.din := !x +// Start +// end Done +// end Foo +// val top = (new Foo).foldControlSteps +// assertCodeString( +// top, +// """|class Foo extends RTDesign: +// | val x = Bit <> OUT.REG init 0 +// | val i = Int <> VAR.REG init 0 +// | val waitCnt = UInt(6) <> VAR.REG init d"6'0" +// | val Count_0_entered = Bit <> VAR.REG init 1 +// | process: +// | def Start: Step = +// | x.din := 0 +// | Count +// | end Start +// | def Count: Step = +// | def onEntry: Unit = +// | x.din := 1 +// | end onEntry +// | def onExit: Unit = +// | x.din := 0 +// | end onExit +// | def Count_0_0: Step = +// | if (waitCnt != d"6'49") +// | if (Count_0_entered) +// | i.din := i + 1 +// | Count_0_entered.din := 0 +// | end if +// | waitCnt.din := waitCnt + d"6'1" +// | ThisStep +// | else if (i != 100) +// | waitCnt.din := d"6'0" +// | Count_0_entered.din := 1 +// | ThisStep +// | else NextStep +// | end if +// | end Count_0_0 +// | Done +// | end Count +// | def Done: Step = +// | x.din := !x +// | Start +// | end Done +// |end Foo""".stripMargin +// ) +// } - // ── No-op (harvested DropRTWaitsSpec form): a pure `while` loop is a *wait* step (self-loop with - // no nested wait), not a control step — nothing to fold. ───────────────────────────────────── - test("harvested: a pure while loop (wait step) is left unchanged") { - class Foo extends RTDesign: - val x = Bit <> OUT.REG - val waitCnt1 = UInt(8) <> VAR.REG init 0 - process: - while (waitCnt1 != 149) - waitCnt1.din := waitCnt1 + 1 - waitCnt1.din := 0 - x.din := !x - end Foo - val top = (new Foo).foldControlSteps - assertCodeString( - top, - """|class Foo extends RTDesign: - | val x = Bit <> OUT.REG - | val waitCnt1 = UInt(8) <> VAR.REG init d"8'0" - | process: - | def S_0: Step = - | if (waitCnt1 != d"8'149") - | waitCnt1.din := waitCnt1 + d"8'1" - | ThisStep - | else NextStep - | end S_0 - | waitCnt1.din := d"8'0" - | x.din := !x - |end Foo""".stripMargin - ) - } +// // ── No-op (harvested DropRTWaitsSpec form): a pure `while` loop is a *wait* step (self-loop with +// // no nested wait), not a control step — nothing to fold. ───────────────────────────────────── +// test("harvested: a pure while loop (wait step) is left unchanged") { +// class Foo extends RTDesign: +// val x = Bit <> OUT.REG +// val waitCnt1 = UInt(8) <> VAR.REG init 0 +// process: +// while (waitCnt1 != 149) +// waitCnt1.din := waitCnt1 + 1 +// waitCnt1.din := 0 +// x.din := !x +// end Foo +// val top = (new Foo).foldControlSteps +// assertCodeString( +// top, +// """|class Foo extends RTDesign: +// | val x = Bit <> OUT.REG +// | val waitCnt1 = UInt(8) <> VAR.REG init d"8'0" +// | process: +// | def S_0: Step = +// | if (waitCnt1 != d"8'149") +// | waitCnt1.din := waitCnt1 + d"8'1" +// | ThisStep +// | else NextStep +// | end S_0 +// | waitCnt1.din := d"8'0" +// | x.din := !x +// |end Foo""".stripMargin +// ) +// } - // ── No-op (harvested): two sequential pure while loops — both wait steps, neither folds ──────── - test("harvested: multiple sequential while loops are left unchanged") { - class Foo extends RTDesign: - val x = Bit <> OUT.REG - val waitCnt1 = UInt(8) <> VAR.REG init 0 - val waitCnt2 = UInt(8) <> VAR.REG init 0 - process: - while (waitCnt1 != 149) - waitCnt1.din := waitCnt1 + 1 - waitCnt1.din := 0 - x.din := !x - while (waitCnt2 != 149) - waitCnt2.din := waitCnt2 + 1 - waitCnt2.din := 0 - x.din := 1 - end Foo - val top = (new Foo).foldControlSteps - assertCodeString( - top, - """|class Foo extends RTDesign: - | val x = Bit <> OUT.REG - | val waitCnt1 = UInt(8) <> VAR.REG init d"8'0" - | val waitCnt2 = UInt(8) <> VAR.REG init d"8'0" - | process: - | def S_0: Step = - | if (waitCnt1 != d"8'149") - | waitCnt1.din := waitCnt1 + d"8'1" - | ThisStep - | else NextStep - | end S_0 - | waitCnt1.din := d"8'0" - | x.din := !x - | def S_1: Step = - | if (waitCnt2 != d"8'149") - | waitCnt2.din := waitCnt2 + d"8'1" - | ThisStep - | else NextStep - | end S_1 - | waitCnt2.din := d"8'0" - | x.din := 1 - |end Foo""".stripMargin - ) - } +// // ── No-op (harvested): two sequential pure while loops — both wait steps, neither folds ──────── +// test("harvested: multiple sequential while loops are left unchanged") { +// class Foo extends RTDesign: +// val x = Bit <> OUT.REG +// val waitCnt1 = UInt(8) <> VAR.REG init 0 +// val waitCnt2 = UInt(8) <> VAR.REG init 0 +// process: +// while (waitCnt1 != 149) +// waitCnt1.din := waitCnt1 + 1 +// waitCnt1.din := 0 +// x.din := !x +// while (waitCnt2 != 149) +// waitCnt2.din := waitCnt2 + 1 +// waitCnt2.din := 0 +// x.din := 1 +// end Foo +// val top = (new Foo).foldControlSteps +// assertCodeString( +// top, +// """|class Foo extends RTDesign: +// | val x = Bit <> OUT.REG +// | val waitCnt1 = UInt(8) <> VAR.REG init d"8'0" +// | val waitCnt2 = UInt(8) <> VAR.REG init d"8'0" +// | process: +// | def S_0: Step = +// | if (waitCnt1 != d"8'149") +// | waitCnt1.din := waitCnt1 + d"8'1" +// | ThisStep +// | else NextStep +// | end S_0 +// | waitCnt1.din := d"8'0" +// | x.din := !x +// | def S_1: Step = +// | if (waitCnt2 != d"8'149") +// | waitCnt2.din := waitCnt2 + d"8'1" +// | ThisStep +// | else NextStep +// | end S_1 +// | waitCnt2.din := d"8'0" +// | x.din := 1 +// |end Foo""".stripMargin +// ) +// } - // ── No-op (safety): the inner loop body READS the outer index `i` (`println(i, j)`). Entry-folding - // would move `i.din := i + 1` to the wait's first cycle, changing `i` mid-loop and corrupting - // those reads — so the `i` loop is left unfolded. Crucially, every body statement (the `println` - // and any nested combinational logic) is preserved verbatim; nothing is dropped. ────────────── - test("a loop whose body reads its index is left unfolded (statements preserved)") { - class Foo extends RTDesign: - val i = Int <> VAR.REG - val j = Int <> VAR.REG - process: - def S_0: Step = - NextStep - end S_0 - i.din := 0 - def S_1: Step = - if (i < 3) - j.din := 0 - def S_1_0: Step = - if (j < 3) - println(s"Hello i: ${i}, j: ${j}") - j.din := j + 1 - ThisStep - else NextStep - end S_1_0 - i.din := i + 1 - ThisStep - else NextStep - end if - end S_1 - finish() - end Foo - val top = (new Foo).foldControlSteps - assertCodeString( - top, - """|class Foo extends RTDesign: - | val i = Int <> VAR.REG - | val j = Int <> VAR.REG - | process: - | def S_0: Step = - | NextStep - | end S_0 - | i.din := 0 - | def S_1: Step = - | if (i < 3) - | j.din := 0 - | def S_1_0: Step = - | if (j < 3) - | println(s"Hello i: ${i}, j: ${j}") - | j.din := j + 1 - | ThisStep - | else NextStep - | end S_1_0 - | i.din := i + 1 - | ThisStep - | else NextStep - | end if - | end S_1 - | finish() - |end Foo""".stripMargin - ) - } +// // ── No-op (safety): the inner loop body READS the outer index `i` (`println(i, j)`). Entry-folding +// // would move `i.din := i + 1` to the wait's first cycle, changing `i` mid-loop and corrupting +// // those reads — so the `i` loop is left unfolded. Crucially, every body statement (the `println` +// // and any nested combinational logic) is preserved verbatim; nothing is dropped. ────────────── +// test("a loop whose body reads its index is left unfolded (statements preserved)") { +// class Foo extends RTDesign: +// val i = Int <> VAR.REG +// val j = Int <> VAR.REG +// process: +// def S_0: Step = +// NextStep +// end S_0 +// i.din := 0 +// def S_1: Step = +// if (i < 3) +// j.din := 0 +// def S_1_0: Step = +// if (j < 3) +// println(s"Hello i: ${i}, j: ${j}") +// j.din := j + 1 +// ThisStep +// else NextStep +// end S_1_0 +// i.din := i + 1 +// ThisStep +// else NextStep +// end if +// end S_1 +// finish() +// end Foo +// val top = (new Foo).foldControlSteps +// assertCodeString( +// top, +// """|class Foo extends RTDesign: +// | val i = Int <> VAR.REG +// | val j = Int <> VAR.REG +// | process: +// | def S_0: Step = +// | NextStep +// | end S_0 +// | i.din := 0 +// | def S_1: Step = +// | if (i < 3) +// | j.din := 0 +// | def S_1_0: Step = +// | if (j < 3) +// | println(s"Hello i: ${i}, j: ${j}") +// | j.din := j + 1 +// | ThisStep +// | else NextStep +// | end S_1_0 +// | i.din := i + 1 +// | ThisStep +// | else NextStep +// | end if +// | end S_1 +// | finish() +// |end Foo""".stripMargin +// ) +// } - // ── Fold: a wait body with an extra statement that does NOT read the folded index — the loop - // folds and the body statement is preserved in the count branch (no drop). ──────────────────── - test("fold preserves a wait-body statement that does not read the index") { - class Foo extends RTDesign: - val o = Bit <> OUT.REG init 0 - val i = Int <> VAR.REG init 0 - val waitCnt = UInt(6) <> VAR.REG init 0 - process: - def S_0: Step = - NextStep - end S_0 - def S_1: Step = - if (i < 100) - def S_1_0: Step = - if (waitCnt != 49) - o.din := !o - waitCnt.din := waitCnt + 1 - ThisStep - else NextStep - end S_1_0 - waitCnt.din := 0 - i.din := i + 1 - ThisStep - else - finish() - NextStep - end S_1 - end Foo - val top = (new Foo).foldControlSteps - assertCodeString( - top, - """|class Foo extends RTDesign: - | val o = Bit <> OUT.REG init 0 - | val i = Int <> VAR.REG init 0 - | val waitCnt = UInt(6) <> VAR.REG init d"6'0" - | val S_1_entered = Bit <> VAR.REG init 1 - | process: - | def S_0: Step = - | NextStep - | end S_0 - | def S_1_0: Step = - | if (waitCnt != d"6'49") - | if (S_1_entered) - | i.din := i + 1 - | S_1_entered.din := 0 - | end if - | o.din := !o - | waitCnt.din := waitCnt + d"6'1" - | ThisStep - | else if (i < 100) - | waitCnt.din := d"6'0" - | S_1_entered.din := 1 - | ThisStep - | else - | finish() - | NextStep - | end if - | end S_1_0 - |end Foo""".stripMargin - ) - } +// // ── Fold: a wait body with an extra statement that does NOT read the folded index — the loop +// // folds and the body statement is preserved in the count branch (no drop). ──────────────────── +// test("fold preserves a wait-body statement that does not read the index") { +// class Foo extends RTDesign: +// val o = Bit <> OUT.REG init 0 +// val i = Int <> VAR.REG init 0 +// val waitCnt = UInt(6) <> VAR.REG init 0 +// process: +// def S_0: Step = +// NextStep +// end S_0 +// def S_1: Step = +// if (i < 100) +// def S_1_0: Step = +// if (waitCnt != 49) +// o.din := !o +// waitCnt.din := waitCnt + 1 +// ThisStep +// else NextStep +// end S_1_0 +// waitCnt.din := 0 +// i.din := i + 1 +// ThisStep +// else +// finish() +// NextStep +// end S_1 +// end Foo +// val top = (new Foo).foldControlSteps +// assertCodeString( +// top, +// """|class Foo extends RTDesign: +// | val o = Bit <> OUT.REG init 0 +// | val i = Int <> VAR.REG init 0 +// | val waitCnt = UInt(6) <> VAR.REG init d"6'0" +// | val S_1_entered = Bit <> VAR.REG init 1 +// | process: +// | def S_0: Step = +// | NextStep +// | end S_0 +// | def S_1_0: Step = +// | if (waitCnt != d"6'49") +// | if (S_1_entered) +// | i.din := i + 1 +// | S_1_entered.din := 0 +// | end if +// | o.din := !o +// | waitCnt.din := waitCnt + d"6'1" +// | ThisStep +// | else if (i < 100) +// | waitCnt.din := d"6'0" +// | S_1_entered.din := 1 +// | ThisStep +// | else +// | finish() +// | NextStep +// | end if +// | end S_1_0 +// |end Foo""".stripMargin +// ) +// } -end FoldControlStepsSpec +// end FoldControlStepsSpec From afe1e72c836eb7dea748c9eda84910f53da57427 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sun, 7 Jun 2026 00:08:01 +0300 Subject: [PATCH 21/24] fix private/protected companion inaccessible givens warnings --- .../src/main/scala/dfhdl/options/CompilerOptions.scala | 4 ++-- core/src/main/scala/dfhdl/options/ElaborationOptions.scala | 4 ++-- core/src/main/scala/dfhdl/options/LogLevel.scala | 2 +- core/src/main/scala/dfhdl/options/OnError.scala | 2 +- lib/src/main/scala/dfhdl/options/BuilderOptions.scala | 4 ++-- lib/src/main/scala/dfhdl/options/LinterOptions.scala | 6 +++--- lib/src/main/scala/dfhdl/options/ProgrammerOptions.scala | 4 ++-- lib/src/main/scala/dfhdl/options/SimulatorOptions.scala | 6 +++--- lib/src/main/scala/dfhdl/options/ToolOptions.scala | 2 +- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/compiler/stages/src/main/scala/dfhdl/options/CompilerOptions.scala b/compiler/stages/src/main/scala/dfhdl/options/CompilerOptions.scala index d98f90637..6d076bdb3 100644 --- a/compiler/stages/src/main/scala/dfhdl/options/CompilerOptions.scala +++ b/compiler/stages/src/main/scala/dfhdl/options/CompilerOptions.scala @@ -57,7 +57,7 @@ object CompilerOptions: type Backend = dfhdl.backends.type => _Backend private[dfhdl] into opaque type _Backend <: BackendCompiler = BackendCompiler - private[dfhdl] object _Backend: + object _Backend: given Backend = _ => dfhdl.backends.verilog.sv2009 given Conversion[BackendCompiler, _Backend] = identity extension (backend: _Backend) @@ -71,7 +71,7 @@ object CompilerOptions: type LogLevel = wvlet.log.LogLevel.type => _LogLevel private[dfhdl] into opaque type _LogLevel <: dfhdl.options._LogLevel = dfhdl.options._LogLevel - private[dfhdl] object _LogLevel: + object _LogLevel: given Conversion[wvlet.log.LogLevel, _LogLevel] = x => x.asInstanceOf[_LogLevel] given (using logLevel: dfhdl.options.LogLevel): LogLevel = logLevel diff --git a/core/src/main/scala/dfhdl/options/ElaborationOptions.scala b/core/src/main/scala/dfhdl/options/ElaborationOptions.scala index c5c2c1793..4afa0792e 100644 --- a/core/src/main/scala/dfhdl/options/ElaborationOptions.scala +++ b/core/src/main/scala/dfhdl/options/ElaborationOptions.scala @@ -41,13 +41,13 @@ object ElaborationOptions: type LogLevel = wvlet.log.LogLevel.type => _LogLevel private[dfhdl] into opaque type _LogLevel <: dfhdl.options._LogLevel = dfhdl.options._LogLevel - private[dfhdl] object _LogLevel: + object _LogLevel: given Conversion[wvlet.log.LogLevel, _LogLevel] = x => x.asInstanceOf[_LogLevel] given (using logLevel: dfhdl.options.LogLevel): LogLevel = logLevel type OnError = dfhdl.options.OnError.type => _OnError private[dfhdl] into opaque type _OnError <: dfhdl.options._OnError = dfhdl.options._OnError - private[dfhdl] object _OnError: + object _OnError: given (using onError: dfhdl.options.OnError): OnError = onError given Conversion[dfhdl.options._OnError, _OnError] = x => x.asInstanceOf[_OnError] diff --git a/core/src/main/scala/dfhdl/options/LogLevel.scala b/core/src/main/scala/dfhdl/options/LogLevel.scala index 1c8d18b7e..c0aa1d840 100644 --- a/core/src/main/scala/dfhdl/options/LogLevel.scala +++ b/core/src/main/scala/dfhdl/options/LogLevel.scala @@ -3,6 +3,6 @@ import wvlet.log.LogLevel as wvletLogLevel type LogLevel = wvlet.log.LogLevel.type => _LogLevel private[dfhdl] into opaque type _LogLevel <: wvletLogLevel = wvletLogLevel -private[dfhdl] object _LogLevel: +object _LogLevel: given Conversion[wvletLogLevel, _LogLevel] = x => x.asInstanceOf[_LogLevel] given LogLevel = _ => wvlet.log.LogLevel.WARN diff --git a/core/src/main/scala/dfhdl/options/OnError.scala b/core/src/main/scala/dfhdl/options/OnError.scala index 926484285..a5610ea5a 100644 --- a/core/src/main/scala/dfhdl/options/OnError.scala +++ b/core/src/main/scala/dfhdl/options/OnError.scala @@ -5,6 +5,6 @@ type OnError = _OnError.type => _OnError val OnError = _OnError protected[dfhdl] enum _OnError derives CanEqual: case Exit, Exception -protected[dfhdl] object _OnError: +object _OnError: given OnError = _ => if (sbtShellIsRunning || sbtnIsRunning || sbtTestIsRunning) Exception else Exit diff --git a/lib/src/main/scala/dfhdl/options/BuilderOptions.scala b/lib/src/main/scala/dfhdl/options/BuilderOptions.scala index 7f5c8bb25..5d4b211f2 100644 --- a/lib/src/main/scala/dfhdl/options/BuilderOptions.scala +++ b/lib/src/main/scala/dfhdl/options/BuilderOptions.scala @@ -31,7 +31,7 @@ object BuilderOptions: type OnError = dfhdl.options.OnError.type => _OnError private[dfhdl] into opaque type _OnError <: dfhdl.options.ToolOptions._OnError = dfhdl.options.ToolOptions._OnError - private[dfhdl] object _OnError: + object _OnError: given (using onError: dfhdl.options.ToolOptions.OnError): OnError = onError given Conversion[dfhdl.options._OnError, _OnError] = x => x.asInstanceOf[_OnError] @@ -43,7 +43,7 @@ object BuilderOptions: type Tool = dfhdl.tools.builders.type => _Tool private[dfhdl] into opaque type _Tool <: dfhdl.tools.builders = dfhdl.tools.builders - private[dfhdl] object _Tool: + object _Tool: export dfhdl.tools.builders.{foss, vendor} given Tool = _.vendor given Conversion[dfhdl.tools.builders, _Tool] = identity diff --git a/lib/src/main/scala/dfhdl/options/LinterOptions.scala b/lib/src/main/scala/dfhdl/options/LinterOptions.scala index c140c51c9..ea7c8f232 100644 --- a/lib/src/main/scala/dfhdl/options/LinterOptions.scala +++ b/lib/src/main/scala/dfhdl/options/LinterOptions.scala @@ -29,7 +29,7 @@ object LinterOptions: type OnError = dfhdl.options.OnError.type => _OnError private[dfhdl] into opaque type _OnError <: dfhdl.options.ToolOptions._OnError = dfhdl.options.ToolOptions._OnError - private[dfhdl] object _OnError: + object _OnError: given (using onError: dfhdl.options.ToolOptions.OnError): OnError = onError given Conversion[dfhdl.options._OnError, _OnError] = x => x.asInstanceOf[_OnError] @@ -42,14 +42,14 @@ object LinterOptions: type VerilogLinter = dfhdl.tools.linters.verilogLinters.type => _VerilogLinter private[dfhdl] into opaque type _VerilogLinter <: dfhdl.tools.toolsCore.VerilogLinter = dfhdl.tools.toolsCore.VerilogLinter - private[dfhdl] object _VerilogLinter: + object _VerilogLinter: given VerilogLinter = _.verilator given Conversion[dfhdl.tools.toolsCore.VerilogLinter, _VerilogLinter] = identity type VHDLLinter = dfhdl.tools.linters.vhdlLinters.type => _VHDLLinter private[dfhdl] into opaque type _VHDLLinter <: dfhdl.tools.toolsCore.VHDLLinter = dfhdl.tools.toolsCore.VHDLLinter - private[dfhdl] object _VHDLLinter: + object _VHDLLinter: given VHDLLinter = _.ghdl given Conversion[dfhdl.tools.toolsCore.VHDLLinter, _VHDLLinter] = identity diff --git a/lib/src/main/scala/dfhdl/options/ProgrammerOptions.scala b/lib/src/main/scala/dfhdl/options/ProgrammerOptions.scala index f54d09781..5b2f45d57 100644 --- a/lib/src/main/scala/dfhdl/options/ProgrammerOptions.scala +++ b/lib/src/main/scala/dfhdl/options/ProgrammerOptions.scala @@ -31,7 +31,7 @@ object ProgrammerOptions: type OnError = dfhdl.options.OnError.type => _OnError private[dfhdl] into opaque type _OnError <: dfhdl.options.ToolOptions._OnError = dfhdl.options.ToolOptions._OnError - private[dfhdl] object _OnError: + object _OnError: given (using onError: dfhdl.options.ToolOptions.OnError): OnError = onError given Conversion[dfhdl.options._OnError, _OnError] = x => x.asInstanceOf[_OnError] @@ -43,7 +43,7 @@ object ProgrammerOptions: type Tool = dfhdl.tools.programmers.type => _Tool private[dfhdl] into opaque type _Tool <: dfhdl.tools.programmers = dfhdl.tools.programmers - private[dfhdl] object _Tool: + object _Tool: export dfhdl.tools.programmers.{foss, vendor} given Tool = _.vendor given Conversion[dfhdl.tools.programmers, _Tool] = identity diff --git a/lib/src/main/scala/dfhdl/options/SimulatorOptions.scala b/lib/src/main/scala/dfhdl/options/SimulatorOptions.scala index 80d8f044e..aa22f8560 100644 --- a/lib/src/main/scala/dfhdl/options/SimulatorOptions.scala +++ b/lib/src/main/scala/dfhdl/options/SimulatorOptions.scala @@ -38,7 +38,7 @@ object SimulatorOptions: type OnError = dfhdl.options.OnError.type => _OnError private[dfhdl] into opaque type _OnError <: dfhdl.options.ToolOptions._OnError = dfhdl.options.ToolOptions._OnError - private[dfhdl] object _OnError: + object _OnError: given (using onError: dfhdl.options.ToolOptions.OnError): OnError = onError given Conversion[dfhdl.options._OnError, _OnError] = x => x.asInstanceOf[_OnError] @@ -51,7 +51,7 @@ object SimulatorOptions: type VerilogSimulator = dfhdl.tools.simulators.verilogSimulators.type => _VerilogSimulator protected[dfhdl] into opaque type _VerilogSimulator <: dfhdl.tools.toolsCore.VerilogSimulator = dfhdl.tools.toolsCore.VerilogSimulator - protected[dfhdl] object _VerilogSimulator: + object _VerilogSimulator: given VerilogSimulator = _.verilator given Conversion[dfhdl.tools.toolsCore.VerilogSimulator, _VerilogSimulator] = identity given Conversion[dfhdl.tools.simulators.questa.type, VerilogSimulator] = _ => _.vlog @@ -60,7 +60,7 @@ object SimulatorOptions: type VHDLSimulator = dfhdl.tools.simulators.vhdlSimulators.type => _VHDLSimulator protected[dfhdl] into opaque type _VHDLSimulator <: dfhdl.tools.toolsCore.VHDLSimulator = dfhdl.tools.toolsCore.VHDLSimulator - protected[dfhdl] object _VHDLSimulator: + object _VHDLSimulator: given VHDLSimulator = _.ghdl given Conversion[dfhdl.tools.toolsCore.VHDLSimulator, _VHDLSimulator] = identity given Conversion[dfhdl.tools.simulators.questa.type, VHDLSimulator] = _ => _.vcom diff --git a/lib/src/main/scala/dfhdl/options/ToolOptions.scala b/lib/src/main/scala/dfhdl/options/ToolOptions.scala index 5a0908a17..348701484 100644 --- a/lib/src/main/scala/dfhdl/options/ToolOptions.scala +++ b/lib/src/main/scala/dfhdl/options/ToolOptions.scala @@ -8,7 +8,7 @@ trait ToolOptions: object ToolOptions: type OnError = dfhdl.options.OnError.type => _OnError private[dfhdl] into opaque type _OnError <: dfhdl.options._OnError = dfhdl.options._OnError - private[dfhdl] object _OnError: + object _OnError: given (using onError: dfhdl.options.OnError): OnError = onError given Conversion[dfhdl.options._OnError, _OnError] = x => x.asInstanceOf[_OnError] From 1d1262eb2695ac17367a668cbd1fe9c5e40fdaa8 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sun, 7 Jun 2026 00:40:00 +0300 Subject: [PATCH 22/24] fix naming under Scala 3.8.4 due to compiler internals change --- .../src/main/scala/plugin/MetaContextPlacerPhase.scala | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala b/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala index b166aa835..ed1040c2f 100644 --- a/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala +++ b/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala @@ -315,10 +315,11 @@ class MetaContextPlacerPhase(setting: Setting) extends CommonPhase: object DFValIdent: def unapply(tree: Tree)(using Context): Option[Tree] = tree match - case ident @ Ident(name) if !name.toString.contains("$") => Some(tree) - case Select(DFValIdent(_), _) => Some(tree) - case This(DFValIdent(_)) => Some(tree) - case _ => None + case ident @ Ident(name) + if !ident.symbol.is(InlineProxy) && !name.toString.contains("$") => Some(tree) + case Select(DFValIdent(_), _) => Some(tree) + case This(DFValIdent(_)) => Some(tree) + case _ => None end DFValIdent tree.rhs match case DFValIdent(rhs) From 9ea9cea25cdf6000d67ea7c76c904c160d8550d5 Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sun, 7 Jun 2026 02:21:54 +0300 Subject: [PATCH 23/24] even more naming fixes under Scala 3.8.4 due to https://github.com/scala/scala3/pull/25279 --- core/src/main/scala/dfhdl/core/DFBoolOrBit.scala | 16 ++++++++-------- core/src/main/scala/dfhdl/core/DFVal.scala | 12 ++++++------ .../scala/plugin/MetaContextPlacerPhase.scala | 9 ++++++--- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/core/src/main/scala/dfhdl/core/DFBoolOrBit.scala b/core/src/main/scala/dfhdl/core/DFBoolOrBit.scala index 0223dc89c..df1a96ff2 100644 --- a/core/src/main/scala/dfhdl/core/DFBoolOrBit.scala +++ b/core/src/main/scala/dfhdl/core/DFBoolOrBit.scala @@ -215,25 +215,25 @@ object DFBoolOrBit: // onTrue type has priority, except when onTrue is a DFHDL Int parameter while onFalse is not inline if (onTrueIsDFVal && !(onTrueIsDFConstInt32 && !onFalseIsDFConstInt32)) inline onTrue match - case onTrueDFVal: DFValTP[tt, tp] => + case ___onTrueDFVal: DFValTP[tt, tp] => val tc = compiletime.summonInline[DFVal.TC[tt, OF]] - val dfType = onTrueDFVal.dfType + val dfType = ___onTrueDFVal.dfType inline if (isConstCheck[OF]) - DFVal.Func(dfType, FuncOp.sel, List(lhs, onTrueDFVal, tc(dfType, onFalse))) + DFVal.Func(dfType, FuncOp.sel, List(lhs, ___onTrueDFVal, tc(dfType, onFalse))) .asValTP[tt, P | tp] else - DFVal.Func(dfType, FuncOp.sel, List(lhs, onTrueDFVal, tc(dfType, onFalse))) + DFVal.Func(dfType, FuncOp.sel, List(lhs, ___onTrueDFVal, tc(dfType, onFalse))) .asValOf[tt] else if (onFalseIsDFVal) inline onFalse match - case onFalseDFVal: DFValTP[ft, fp] => + case ___onFalseDFVal: DFValTP[ft, fp] => val tc = compiletime.summonInline[DFVal.TC[ft, OT]] - val dfType = onFalseDFVal.dfType + val dfType = ___onFalseDFVal.dfType inline if (isConstCheck[OT]) - DFVal.Func(dfType, FuncOp.sel, List(lhs, tc(dfType, onTrue), onFalseDFVal)) + DFVal.Func(dfType, FuncOp.sel, List(lhs, tc(dfType, onTrue), ___onFalseDFVal)) .asValTP[ft, P | fp] else - DFVal.Func(dfType, FuncOp.sel, List(lhs, tc(dfType, onTrue), onFalseDFVal)) + DFVal.Func(dfType, FuncOp.sel, List(lhs, tc(dfType, onTrue), ___onFalseDFVal)) .asValOf[ft] else BoolSelWrapper[P, OT, OF](lhs, onTrue, onFalse) diff --git a/core/src/main/scala/dfhdl/core/DFVal.scala b/core/src/main/scala/dfhdl/core/DFVal.scala index afdd30e01..76f8bc51c 100644 --- a/core/src/main/scala/dfhdl/core/DFVal.scala +++ b/core/src/main/scala/dfhdl/core/DFVal.scala @@ -1403,9 +1403,9 @@ object DFVal extends DFValLP: // connection in either direction where both implicit directions are available inline if (lhsIsDFVal && rhsIsDFVal) inline lhs match - case lhs: DFVal[lt, lm] => inline rhs match - case rhs: DFVal[rt, rm] => - ConnectOps.specialConnect[lt, lm, rt, rm](lhs, rhs) + case ___lhs: DFVal[lt, lm] => inline rhs match + case ___rhs: DFVal[rt, rm] => + ConnectOps.specialConnect[lt, lm, rt, rm](___lhs, ___rhs) // if the RHS is a modifier, this is a port/variable constructor, // so we invoke the the implicit given operation only in one way else if (rhsIsModifier) exactOp2["<>", DFC, Any](lhs, rhs) @@ -1437,9 +1437,9 @@ object DFVal extends DFValLP: case _ => false inline if (lhsIsDFVal && rhsIsDFVal) inline lhs match - case lhs: DFValTP[lt, lp] => inline rhs match - case rhs: DFValTP[rt, rp] => - specialCompare[Op, lt, lp, rt, rp](lhs, rhs) + case ___lhs: DFValTP[lt, lp] => inline rhs match + case ___rhs: DFValTP[rt, rp] => + specialCompare[Op, lt, lp, rt, rp](___lhs, ___rhs) else exactOp2[Op, DFC, DFValOf[DFBool]](lhs, rhs) end compare diff --git a/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala b/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala index ed1040c2f..29638a664 100644 --- a/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala +++ b/plugin/src/main/scala/plugin/MetaContextPlacerPhase.scala @@ -316,15 +316,18 @@ class MetaContextPlacerPhase(setting: Setting) extends CommonPhase: def unapply(tree: Tree)(using Context): Option[Tree] = tree match case ident @ Ident(name) - if !ident.symbol.is(InlineProxy) && !name.toString.contains("$") => Some(tree) + if !ident.symbol.isOneOf(InlineProxy | Case) && !name.toString.contains("$") => + Some(tree) case Select(DFValIdent(_), _) => Some(tree) case This(DFValIdent(_)) => Some(tree) case _ => None end DFValIdent + def skipName(name: String): Boolean = + name.contains("$") || name.startsWith("___") tree.rhs match case DFValIdent(rhs) - if !tree.name.toString.contains("$") && - !tree.symbol.flags.is(InlineProxy) && tree.tpt.tpe.dfValTpeOpt.nonEmpty => + if !tree.symbol.flags.isOneOf(InlineProxy | Case) && !skipName(tree.name.toString) && + tree.tpt.tpe.dfValTpeOpt.nonEmpty => val dfc = dfcArgStack.headOption.getOrElse(ref(emptyNoEODFCSym)) val updatedRHS = ref(dfhdlDFValIdentSym) From a6f7a628fb78b446f8ea65f60dbf4629d458422d Mon Sep 17 00:00:00 2001 From: Oron Port Date: Sun, 7 Jun 2026 02:24:08 +0300 Subject: [PATCH 24/24] remove DFHDL warning from issue example 131 --- lib/src/test/scala/issues/i131.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/test/scala/issues/i131.scala b/lib/src/test/scala/issues/i131.scala index bfbd67240..fe9601592 100644 --- a/lib/src/test/scala/issues/i131.scala +++ b/lib/src/test/scala/issues/i131.scala @@ -22,5 +22,5 @@ import hw.flag.scalaRanges addr_r.din := b"12'0" for (i <- 0 until fetch_count.toScalaInt) - if ((dict_in(dict_entry_size * (i+1) - 1, dict_entry_size * i) == (idx_r, sym_r)) && (addr_r + i < entry_count - 1)) + if ((dict_in(dict_entry_size * (i+1) - 1, dict_entry_size * i) == (idx_r, sym_r)) && (addr_r + d"$i" < entry_count - d"1")) matching(i) := 1 \ No newline at end of file