Skip to content

Commit 3f6711d

Browse files
authored
Non-fatal shrink-multiply operation in compiler (#403)
Adds general infrastructure for non-fatal expr errors in the compiler, folding the over-assign errors into this mechanism. Also bumps expected proto version. Some error semantics changed: - max/min(RangeEmpty) now returns a nonfatal error (instead of NaN) - unpack_range(empty) now returns a nonfatal error (instead of RangeEmpty) - set_extract(empty | different) now returns a nonfatal error (instead of a fatal compiler exception) - intersection(non-intersecting range) now returns a nonfatal error (instead of range-empty) - is this the right answer? Infrastructural changes: - Change expr eval ref arg to a refResolver instead of the whole constProp object - ConstProp has a clearReadyNode that removes a node from ready without setting a value / propagating it, to handle these non-propagating error values - Add errors-should-be-empty tests on some unit tests TODO: - [x] remove error type from IR - [x] add overassign cause to overassign error
1 parent 810c86d commit 3f6711d

File tree

14 files changed

+154
-131
lines changed

14 files changed

+154
-131
lines changed

compiler/src/main/scala/edg/compiler/Compiler.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ class AssignNamer() {
117117
}
118118

119119
object Compiler {
120-
final val kExpectedProtoVersion = 6
120+
final val kExpectedProtoVersion = 7
121121
}
122122

123123
/** Compiler for a particular design, with an associated library to elaborate references from.
@@ -1695,7 +1695,7 @@ class Compiler private (
16951695
}
16961696

16971697
def evaluateExpr(root: DesignPath, value: expr.ValueExpr): ExprResult = {
1698-
new ExprEvaluatePartial(constProp, root).map(value)
1698+
new ExprEvaluatePartial(constProp.getValue, root).map(value)
16991699
}
17001700

17011701
// Primarily used for unit tests, TODO clean up this API?

compiler/src/main/scala/edg/compiler/CompilerError.scala

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,16 +104,15 @@ object CompilerError {
104104
}
105105
}
106106

107-
case class OverAssign(target: IndirectDesignPath, causes: Seq[OverAssignCause]) extends CompilerError {
108-
override def toString: String = s"Overassign to $target:\n" +
109-
s"${causes.map(x => s"- $x").mkString("\n")}"
107+
case class ExprError(target: IndirectDesignPath, msg: String) extends CompilerError {
108+
override def toString: String = s"Expr error at $target: $msg"
110109

111110
override def toIr: edgcompiler.ErrorRecord = {
112111
edgcompiler.ErrorRecord(
113112
path = Some(target.toLocalPath),
114-
kind = "Overassign",
113+
kind = "Expr",
115114
name = "",
116-
details = causes.map(_.toString).mkString(", ")
115+
details = msg
117116
)
118117
}
119118
}

compiler/src/main/scala/edg/compiler/ConstProp.scala

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
package edg.compiler
22

3-
import scala.collection.mutable
4-
import scala.collection.Set
3+
import edg.ExprBuilder
4+
import edg.compiler.CompilerError.ExprError
5+
import edg.util.DependencyGraph
6+
import edg.wir._
57
import edgir.expr.expr
68
import edgir.init.init
7-
import edg.wir._
8-
import edg.util.{DependencyGraph, MutableBiMap}
9-
import edg.ExprBuilder
109
import edgir.ref.ref.LocalPath
1110

12-
case class AssignRecord(target: IndirectDesignPath, root: DesignPath, value: expr.ValueExpr)
11+
import scala.collection.{Set, mutable}
1312

14-
case class OverassignRecord(assigns: mutable.Set[(DesignPath, String, expr.ValueExpr)] = mutable.Set())
13+
case class AssignRecord(target: IndirectDesignPath, root: DesignPath, value: expr.ValueExpr)
1514

1615
sealed trait ConnectedLinkRecord // a record in the connected link directed graph
1716
object ConnectedLinkRecord {
@@ -53,21 +52,20 @@ class ConstProp() {
5352
// Params that have a forced/override value, so they arent over-assigned.
5453
private val forcedParams = mutable.Set[IndirectDesignPath]()
5554

56-
// Overassigns, for error tracking
57-
// This only tracks overassigns that were discarded, not including assigns that took effect.
58-
// Additional analysis is needed to get the full set of conflicting assigns.
59-
private val discardOverassigns = mutable.HashMap[IndirectDesignPath, OverassignRecord]()
55+
// Errors that were generated during the process of resolving parameters, including overassigns
56+
// A value may or may not exist (and may or not have been propagated) in the param dependency graph
57+
private val paramErrors = mutable.HashMap[IndirectDesignPath, mutable.ListBuffer[ErrorValue]]()
6058

6159
def initFrom(that: ConstProp): Unit = {
6260
require(paramAssign.isEmpty && paramSource.isEmpty && paramTypes.isEmpty && forcedParams.isEmpty
63-
&& discardOverassigns.isEmpty)
61+
&& paramErrors.isEmpty)
6462
paramAssign.addAll(that.paramAssign)
6563
paramSource.addAll(that.paramSource)
6664
params.initFrom(that.params)
6765
paramTypes.addAll(that.paramTypes)
6866
connectedLink.initFrom(that.connectedLink)
6967
forcedParams.addAll(that.forcedParams)
70-
discardOverassigns.addAll(that.discardOverassigns)
68+
paramErrors.addAll(that.paramErrors)
7169
}
7270

7371
//
@@ -120,9 +118,15 @@ class ConstProp() {
120118
}
121119
readyList.foreach { constrTarget =>
122120
val assign = paramAssign(constrTarget)
123-
new ExprEvaluatePartial(this, assign.root).map(assign.value) match {
121+
new ExprEvaluatePartial(getValue, assign.root).map(assign.value) match {
124122
case ExprResult.Result(result) =>
125-
params.setValue(constrTarget, result)
123+
result match {
124+
case result @ ErrorValue(_) =>
125+
paramErrors.getOrElseUpdate(constrTarget, mutable.ListBuffer()).append(result)
126+
params.clearReadyNode(constrTarget)
127+
case result => params.setValue(constrTarget, result)
128+
}
129+
126130
onParamSolved(constrTarget, result)
127131
case ExprResult.Missing(missing) => // account for CONNECTED_LINK prefix
128132
val missingCorrected = missing.map { path =>
@@ -202,9 +206,11 @@ class ConstProp() {
202206
params.addNode(target, Seq(), overwrite = true) // forced can overwrite other records
203207
} else {
204208
if (!forcedParams.contains(target)) {
205-
if (params.nodeDefinedAt(target)) {
206-
val record = discardOverassigns.getOrElseUpdate(target, OverassignRecord())
207-
record.assigns.add(paramSourceRecord)
209+
if (params.nodeDefinedAt(target)) { // TODO add propagated assign
210+
val (prevRoot, prevConstr, _) = paramSource.get(target).getOrElse("?", "?", "")
211+
paramErrors.getOrElseUpdate(target, mutable.ListBuffer()).append(
212+
ErrorValue(s"over-assign from $root.$constrName, prev assigned from $prevRoot.$prevConstr")
213+
)
208214
return // first set "wins"
209215
}
210216
params.addNode(target, Seq())
@@ -254,7 +260,7 @@ class ConstProp() {
254260
}
255261

256262
/** Returns the value of a parameter, or None if it does not have a value (yet?). Can be used to check if parameters
257-
* are resolved yet by testing against None.
263+
* are resolved yet by testing against None. Cannot return an ErrorValue
258264
*/
259265
def getValue(param: IndirectDesignPath): Option[ExprValue] = {
260266
resolveConnectedLink(param) match {
@@ -264,7 +270,6 @@ class ConstProp() {
264270

265271
}
266272
def getValue(param: DesignPath): Option[ExprValue] = {
267-
// TODO should this be an implicit conversion?
268273
getValue(param.asIndirect)
269274
}
270275

@@ -282,20 +287,14 @@ class ConstProp() {
282287
* references.
283288
*/
284289
def getUnsolved: Set[IndirectDesignPath] = {
285-
paramTypes.keySet.toSet -- params.knownValueKeys
290+
paramTypes.keySet.toSet -- params.knownValueKeys -- paramErrors.keys
286291
}
287292

288293
def getAllSolved: Map[IndirectDesignPath, ExprValue] = params.toMap
289294

290-
def getErrors: Seq[CompilerError] = {
291-
discardOverassigns.map { case (target, record) =>
292-
val propagatedAssign = paramSource.get(target).map { case (root, constrName, value) =>
293-
CompilerError.OverAssignCause.Assign(target, root, constrName, value)
294-
}.toSeq
295-
val discardedAssigns = record.assigns.map { case (root, constrName, value) =>
296-
CompilerError.OverAssignCause.Assign(target, root, constrName, value)
297-
}
298-
CompilerError.OverAssign(target, propagatedAssign ++ discardedAssigns)
295+
def getErrors: Seq[ExprError] = {
296+
paramErrors.flatMap { case (target, errors) =>
297+
errors.map(error => ExprError(target, error.msg))
299298
}.toSeq
300299
}
301300
}

compiler/src/main/scala/edg/compiler/ExprEvaluate.scala

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ object ExprEvaluate {
6060
case (RangeValue(targetMin, targetMax), RangeValue(contribMin, contribMax)) =>
6161
val lower = contribMax * targetMin
6262
val upper = contribMin * targetMax
63-
if (lower > upper) { // TODO this should store a nonfatal error result instead of crashing on-the-spot
64-
throw new ExprEvaluateException(s"Empty range result in $lhs ${binary.op} $rhs from $binary")
63+
if (lower > upper) {
64+
ErrorValue(s"shrink_mult($lhs, $rhs) produces empty range, target $lhs tighter tol than contrib $rhs")
6565
} else {
6666
RangeValue(lower, upper)
6767
}
@@ -285,10 +285,8 @@ object ExprEvaluate {
285285
case (Op.MIN, RangeValue(valMin, _)) => FloatValue(valMin)
286286
case (Op.MAX, RangeValue(_, valMax)) => FloatValue(valMax)
287287

288-
// TODO can we have stricter semantics to avoid min(RangeEmpty) and max(RangeEmpty)?
289-
// This just NaNs out so at least it propagates
290-
case (Op.MAX, RangeEmpty) => FloatValue(Float.NaN)
291-
case (Op.MIN, RangeEmpty) => FloatValue(Float.NaN)
288+
case (Op.MAX, RangeEmpty) => ErrorValue("max(RangeEmpty) is undefined")
289+
case (Op.MIN, RangeEmpty) => ErrorValue("min(RangeEmpty) is undefined")
292290

293291
case (Op.CENTER, RangeValue(valMin, valMax)) => FloatValue((valMin + valMax) / 2)
294292
case (Op.WIDTH, RangeValue(valMin, valMax)) => FloatValue(math.abs(valMax - valMin))
@@ -307,7 +305,7 @@ object ExprEvaluate {
307305
case (Op.SUM, ArrayValue.ExtractBoolean(vals)) => IntValue(vals.count(_ == true))
308306
case (Op.SUM, ArrayValue.UnpackRange(extracted)) => extracted match {
309307
case ArrayValue.UnpackRange.FullRange(valMins, valMaxs) => RangeValue(valMins.sum, valMaxs.sum)
310-
case _ => RangeEmpty // TODO how should sum behave on empty ranges?
308+
case _ => ErrorValue("unpack_range(empty) is undefined")
311309
}
312310

313311
case (Op.ALL_TRUE, ArrayValue.Empty(_)) => BooleanValue(true)
@@ -326,13 +324,11 @@ object ExprEvaluate {
326324
case (Op.MINIMUM, ArrayValue.ExtractFloat(vals)) => FloatValue(vals.min)
327325
case (Op.MINIMUM, ArrayValue.ExtractInt(vals)) => IntValue(vals.min)
328326

329-
// TODO this should be a user-level assertion instead of a compiler error
330-
case (Op.SET_EXTRACT, ArrayValue.Empty(_)) =>
331-
throw new ExprEvaluateException(s"SetExtract with empty values from $unarySet")
327+
case (Op.SET_EXTRACT, ArrayValue.Empty(_)) => ErrorValue("set_extract(empty) is undefined")
332328
case (Op.SET_EXTRACT, ArrayValue(vals)) => if (vals.forall(_ == vals.head)) {
333329
vals.head
334330
} else {
335-
throw new ExprEvaluateException(s"SetExtract with non-equal values $vals from $unarySet")
331+
ErrorValue(f"set_extract($vals) with non-equal values")
336332
}
337333

338334
// Any empty value means the expression result is empty
@@ -342,19 +338,20 @@ object ExprEvaluate {
342338
if (maxMin <= minMax) {
343339
RangeValue(maxMin, minMax)
344340
} else { // does not intersect, null set
345-
RangeEmpty
341+
ErrorValue(f"intersection($extracted) produces empty set")
346342
}
347343
// The implicit initial value of intersect is the full range
348344
// TODO are these good semantics?
349345
case ArrayValue.UnpackRange.EmptyArray() => RangeValue(Float.NegativeInfinity, Float.PositiveInfinity)
350-
case _ => RangeEmpty
346+
case _ => ErrorValue(f"intersection($vals) is undefined")
351347
}
352348

353349
// Any value is used (empty effectively discarded)
354350
case (Op.HULL, ArrayValue.UnpackRange(extracted)) => extracted match {
355351
case ArrayValue.UnpackRange.FullRange(valMins, valMaxs) => RangeValue(valMins.min, valMaxs.max)
356352
case ArrayValue.UnpackRange.RangeWithEmpty(valMins, valMaxs) => RangeValue(valMins.min, valMaxs.max)
357-
case _ => RangeEmpty
353+
case ArrayValue.UnpackRange.EmptyArray() => RangeEmpty // TODO: should this be an error?
354+
case ArrayValue.UnpackRange.EmptyRange() => RangeEmpty
358355
}
359356
case (Op.HULL, ArrayValue.ExtractFloat(vals)) => RangeValue(vals.min, vals.max)
360357

@@ -397,7 +394,7 @@ object ExprEvaluate {
397394
case (FloatPromotable(lhs), FloatPromotable(rhs)) => if (lhs <= rhs) {
398395
RangeValue(lhs, rhs)
399396
} else {
400-
throw new ExprEvaluateException(s"Range with min $minimum not <= max $maximum from $range")
397+
ErrorValue(s"range($minimum, $maximum) is malformed, $minimum > $maximum")
401398
}
402399
case _ => throw new ExprEvaluateException(s"Unknown range operands types $minimum $maximum from $range")
403400
}

compiler/src/main/scala/edg/compiler/ExprEvaluatePartial.scala

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ object ExprResult {
1616
* missing set may increase as more parameters are solved, because some expressions have dynamic references. For
1717
* example, if-then-else additionally depends on either true or false subexprs once the condition is known.
1818
*/
19-
class ExprEvaluatePartial(refs: ConstProp, root: DesignPath) extends ValueExprMap[ExprResult] {
19+
class ExprEvaluatePartial(refResolver: IndirectDesignPath => Option[ExprValue], root: DesignPath)
20+
extends ValueExprMap[ExprResult] {
2021
override def mapLiteral(literal: lit.ValueLit): ExprResult =
2122
ExprResult.Result(ExprEvaluate.evalLiteral(literal))
2223

@@ -126,11 +127,11 @@ class ExprEvaluatePartial(refs: ConstProp, root: DesignPath) extends ValueExprMa
126127
val container = mapExtract.getContainer.expr.ref.getOrElse( // TODO restrict allowed types in proto
127128
throw new ExprEvaluateException(s"Non-ref container type in mapExtract $mapExtract"))
128129
val containerPath = root ++ container
129-
refs.getValue(containerPath.asIndirect + IndirectStep.Elements) match {
130+
refResolver(containerPath.asIndirect + IndirectStep.Elements) match {
130131
case Some(paramValue) =>
131132
val eltsVals = ArrayValue.ExtractText(paramValue).map { elt =>
132133
val eltPath = containerPath.asIndirect + elt ++ mapExtract.getPath
133-
refs.getValue(eltPath) match {
134+
refResolver(eltPath) match {
134135
case Some(value) => ExprResult.Result(value)
135136
case None => ExprResult.Missing(Set(eltPath))
136137
}
@@ -151,7 +152,7 @@ class ExprEvaluatePartial(refs: ConstProp, root: DesignPath) extends ValueExprMa
151152

152153
override def mapRef(path: ref.LocalPath): ExprResult = {
153154
val refPath = root.asIndirect ++ path
154-
refs.getValue(refPath) match {
155+
refResolver(refPath) match {
155156
case Some(value) => ExprResult.Result(value)
156157
case None => ExprResult.Missing(Set(refPath))
157158
}

compiler/src/main/scala/edg/compiler/ExprToString.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class ExprToString() extends ValueExprMap[String] {
2929
case lit.ValueLit.Type.Array(array) =>
3030
val arrayElts = array.elts.map(mapLiteral)
3131
s"[${arrayElts.mkString(", ")}]"
32-
case lit.ValueLit.Type.Struct(value) => "unsupported struct"
32+
case lit.ValueLit.Type.Struct(_) => "unsupported struct"
3333
case lit.ValueLit.Type.Empty => "(empty)"
3434
}
3535

compiler/src/main/scala/edg/compiler/ExprValue.scala

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ object ExprValue {
2020
case init.ValInit.Val.Text(_) => classOf[TextValue]
2121
case init.ValInit.Val.Range(_) => classOf[RangeType]
2222
case init.ValInit.Val.Array(_) => classOf[ArrayValue[Nothing]]
23-
case _ => throw new IllegalArgumentException(s"Unknown valinit $initObj")
23+
case init.ValInit.Val.Empty | init.ValInit.Val.Set(_) | init.ValInit.Val.Struct(_) =>
24+
throw new IllegalArgumentException(s"unsupported valInit $initObj")
2425
}
2526

2627
def fromValueLit(literal: lit.ValueLit): ExprValue = literal.`type` match {
@@ -41,7 +42,8 @@ object ExprValue {
4142
}
4243
case lit.ValueLit.Type.Array(arrayLiteral) =>
4344
ArrayValue(arrayLiteral.elts.map { lit => fromValueLit(lit) })
44-
case _ => throw new IllegalArgumentException(s"Unknown literal $literal")
45+
case lit.ValueLit.Type.Empty | lit.ValueLit.Type.Struct(_) =>
46+
throw new IllegalArgumentException(s"unsupported literal $literal")
4547
}
4648
}
4749

@@ -205,3 +207,9 @@ case class ArrayValue[T <: ExprValue](values: Seq[T]) extends ExprValue {
205207
s"[$valuesString]"
206208
}
207209
}
210+
211+
// a special value not in the IR that indicates and error
212+
case class ErrorValue(msg: String) extends ExprValue {
213+
override def toLit: lit.ValueLit = throw new IllegalArgumentException("cannot convert error value to literal")
214+
override def toStringValue: String = msg
215+
}

compiler/src/main/scala/edg/util/DependencyGraph.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,17 @@ class DependencyGraph[KeyType, ValueType] {
6262
// Node must exist, or this will exception out
6363
def nodeMissing(node: KeyType): Set[KeyType] = deps(node).toSet
6464

65+
// Clears a node from ready without setting a value in the graph.
66+
// Useful to stop propagation at some point, but without crashing.
67+
// The node may be marked ready again by other sources.
68+
def clearReadyNode(node: KeyType): Unit = {
69+
require(ready.contains(node), s"attempt to clear ready node $node that is not ready")
70+
ready -= node
71+
}
72+
6573
// Sets the value of a node. May not overwrite values.
74+
// If stop is true, propagation will not continue:
75+
// while the node will have a value, dependents will not be marked as ready
6676
def setValue(node: KeyType, value: ValueType): Unit = {
6777
require(!values.isDefinedAt(node), s"redefinition of $node (prior value ${values(node)}, new value $value)")
6878
deps.put(node, mutable.Set())

compiler/src/test/scala/edg/compiler/ConstPropArrayTest.scala

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import org.scalatest.flatspec.AnyFlatSpec
55
import matchers.should.Matchers._
66
import edg.wir.{DesignPath, IndirectDesignPath, IndirectStep}
77
import edg.ExprBuilder._
8+
import edg.compiler.CompilerError.ExprError
89

910
class ConstPropArrayTest extends AnyFlatSpec {
1011
behavior.of("ConstProp with array paths")
@@ -89,12 +90,12 @@ class ConstPropArrayTest extends AnyFlatSpec {
8990
val constProp = new ConstProp()
9091
addArray(constProp, Seq(), 4, { i => ValueExpr.Literal(i + 2) }, Seq("ports"), Seq("param"))
9192
constProp.addDeclaration(DesignPath() + "reduce", ValInit.Integer)
92-
an[ExprEvaluateException] should be thrownBy {
93-
constProp.addAssignExpr(
94-
IndirectDesignPath() + "reduce",
95-
ValueExpr.UnarySetOp(Op.SET_EXTRACT, ValueExpr.MapExtract(Ref("ports"), "param"))
96-
)
97-
}
93+
constProp.addAssignExpr(
94+
IndirectDesignPath() + "reduce",
95+
ValueExpr.UnarySetOp(Op.SET_EXTRACT, ValueExpr.MapExtract(Ref("ports"), "param"))
96+
)
97+
constProp.getValue(IndirectDesignPath() + "reduce") should equal(None)
98+
constProp.getErrors should not be empty
9899
}
99100

100101
it should "SetExtract for same values" in {

0 commit comments

Comments
 (0)