Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions compiler/src/main/scala/edg/ExprBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ object ExprBuilder {
def Literal(valueMin: Double, valueMax: Double): expr.ValueExpr = // convenience method
Literal(valueMin.toFloat, valueMax.toFloat)

def LiteralRangeEmpty(): expr.ValueExpr =
Literal(Float.NaN, Float.NaN)

def BinOp(op: expr.BinaryExpr.Op, lhs: expr.ValueExpr, rhs: expr.ValueExpr): expr.ValueExpr = expr.ValueExpr(
expr = expr.ValueExpr.Expr.Binary(expr.BinaryExpr(op = op, lhs = Some(lhs), rhs = Some(rhs)))
)
Expand Down
24 changes: 13 additions & 11 deletions compiler/src/main/scala/edg/compiler/ExprEvaluate.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ object ExprEvaluate {
case (RangeValue(lhsMin, lhsMax), RangeValue(rhsMin, rhsMax)) =>
val all = Seq(lhsMin + rhsMin, lhsMin + rhsMax, lhsMax + rhsMin, lhsMax + rhsMax)
RangeValue(all.min, all.max)
case (RangeEmpty, RangeEmpty) => RangeEmpty
case (lhs: RangeValue, RangeEmpty) => lhs
case (RangeEmpty, rhs: RangeValue) => rhs
case (_: RangeType, RangeEmpty) => RangeEmpty
case (RangeEmpty, _: RangeType) => RangeEmpty
Comment on lines +24 to +25
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Op.ADD now treats RangeEmpty as an absorbing element (result is always RangeEmpty). This is a behavior change from the previous identity-like behavior and should be covered by a targeted unit test (eg, RangeEmpty + RangeValue, RangeValue + RangeEmpty, and RangeEmpty + RangeEmpty) so downstream users don't silently see different results without test signal.

Copilot uses AI. Check for mistakes.
case (RangeValue(lhsMin, lhsMax), FloatPromotable(rhs)) =>
RangeValue(lhsMin + rhs, lhsMax + rhs)
case (FloatPromotable(lhs), RangeValue(rhsMin, rhsMax)) =>
Expand All @@ -39,9 +38,8 @@ object ExprEvaluate {
case (RangeValue(lhsMin, lhsMax), RangeValue(rhsMin, rhsMax)) =>
val all = Seq(lhsMin * rhsMin, lhsMin * rhsMax, lhsMax * rhsMin, lhsMax * rhsMax)
RangeValue(all.min, all.max)
case (RangeEmpty, RangeEmpty) => RangeEmpty
case (lhs: RangeValue, RangeEmpty) => RangeEmpty
case (RangeEmpty, rhs: RangeValue) => RangeEmpty
case (_: RangeType, RangeEmpty) => RangeEmpty
case (RangeEmpty, _: RangeType) => RangeEmpty
Comment on lines +41 to +42
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Op.MULT empty-range handling was changed to make RangeEmpty absorbing. Please add/extend a unit test for RangeEmpty * RangeValue, RangeValue * RangeEmpty, and RangeEmpty * RangeEmpty to lock in the intended semantics.

Copilot uses AI. Check for mistakes.
case (RangeValue(lhsMin, lhsMax), FloatPromotable(rhs)) if rhs >= 0 =>
RangeValue(lhsMin * rhs, lhsMax * rhs)
case (RangeValue(lhsMin, lhsMax), FloatPromotable(rhs)) if rhs < 0 =>
Expand Down Expand Up @@ -170,8 +168,8 @@ object ExprEvaluate {
}

case Op.INTERSECTION => (lhs, rhs) match {
case (RangeEmpty, _) => RangeEmpty // anything intersecting with empty is empty
case (_, RangeEmpty) => RangeEmpty
case (RangeEmpty, _: RangeType) => RangeEmpty // anything intersecting with empty is empty
case (_: RangeType, RangeEmpty) => RangeEmpty
case (RangeValue(lhsMin, lhsMax), RangeValue(rhsMin, rhsMax)) =>
val (minMax, maxMin) = (math.min(lhsMax, rhsMax), math.max(lhsMin, rhsMin))
if (maxMin <= minMax) {
Expand Down Expand Up @@ -305,7 +303,10 @@ object ExprEvaluate {
case (Op.SUM, ArrayValue.ExtractBoolean(vals)) => IntValue(vals.count(_ == true))
case (Op.SUM, ArrayValue.UnpackRange(extracted)) => extracted match {
case ArrayValue.UnpackRange.FullRange(valMins, valMaxs) => RangeValue(valMins.sum, valMaxs.sum)
case _ => ErrorValue("unpack_range(empty) is undefined")
case ArrayValue.UnpackRange.RangeWithEmpty(_, _) => RangeEmpty
case ArrayValue.UnpackRange.EmptyRange() => RangeEmpty
case ArrayValue.UnpackRange.EmptyArray() =>
RangeValue(0, 0) // unreachable in practice, superseded by float 0 case
}
Comment on lines 305 to 310
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes alter empty-range propagation semantics (eg, SUM returning RangeEmpty when any element is empty, and defining behavior for empty/all-empty arrays). There are existing evaluation tests in compiler/src/test/scala/edg/compiler/ExprEvaluateTest.scala with TODOs for empty-range cases, but no assertions covering these new behaviors; please add tests for the updated empty-range cases to prevent regressions.

Copilot uses AI. Check for mistakes.

case (Op.ALL_TRUE, ArrayValue.Empty(_)) => BooleanValue(true)
Expand Down Expand Up @@ -340,8 +341,9 @@ object ExprEvaluate {
} else { // does not intersect, null set
ErrorValue(f"intersection($extracted) produces empty set")
}
case ArrayValue.UnpackRange.RangeWithEmpty(_, _) => RangeEmpty
case ArrayValue.UnpackRange.EmptyRange() => RangeEmpty
Comment on lines +344 to +345
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intersection over an array of ranges now returns RangeEmpty when the input contains empty values (RangeWithEmpty) or is all-empty (EmptyRange). Since this previously fell through to an error, please add a unit test covering these cases to ensure the new behavior is intentional and stable.

Copilot uses AI. Check for mistakes.
// The implicit initial value of intersect is the full range
// TODO are these good semantics?
case ArrayValue.UnpackRange.EmptyArray() => RangeValue(Float.NegativeInfinity, Float.PositiveInfinity)
case _ => ErrorValue(f"intersection($vals) is undefined")
}
Expand All @@ -350,8 +352,8 @@ object ExprEvaluate {
case (Op.HULL, ArrayValue.UnpackRange(extracted)) => extracted match {
case ArrayValue.UnpackRange.FullRange(valMins, valMaxs) => RangeValue(valMins.min, valMaxs.max)
case ArrayValue.UnpackRange.RangeWithEmpty(valMins, valMaxs) => RangeValue(valMins.min, valMaxs.max)
case ArrayValue.UnpackRange.EmptyArray() => RangeEmpty // TODO: should this be an error?
case ArrayValue.UnpackRange.EmptyRange() => RangeEmpty
case ArrayValue.UnpackRange.EmptyArray() => RangeEmpty // TODO: should this be an error?
}
case (Op.HULL, ArrayValue.ExtractFloat(vals)) => RangeValue(vals.min, vals.max)

Expand Down
34 changes: 30 additions & 4 deletions compiler/src/test/scala/edg/compiler/ExprEvaluateTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ class ExprEvaluateTest extends AnyFlatSpec {
val constProp = new ConstProp()
val evalTest = new ExprEvaluate(constProp, DesignPath())

// TODO: add array tests once there is an array literal

it should "handle literals" in {
evalTest.map(ValueExpr.Literal(Literal.Floating(2.0))) should equal(FloatValue(2.0))
evalTest.map(ValueExpr.Literal(Literal.Integer(42))) should equal(IntValue(42))
Expand Down Expand Up @@ -97,6 +95,17 @@ class ExprEvaluateTest extends AnyFlatSpec {

it should "handle binary range ops" in {
import edgir.expr.expr.BinaryExpr.Op

evalTest.map(
ValueExpr.BinOp(Op.ADD, ValueExpr.Literal(4.0, 6.0), ValueExpr.Literal(5.0, 7.0))
) should equal(RangeValue(9.0, 13.0))
evalTest.map(
ValueExpr.BinOp(Op.ADD, ValueExpr.Literal(5.0, 7.0), ValueExpr.LiteralRangeEmpty())
) should equal(RangeEmpty)
evalTest.map(
ValueExpr.BinOp(Op.ADD, ValueExpr.LiteralRangeEmpty(), ValueExpr.LiteralRangeEmpty())
) should equal(RangeEmpty)

evalTest.map(
ValueExpr.BinOp(Op.INTERSECTION, ValueExpr.Literal(4.0, 6.0), ValueExpr.Literal(5.0, 7.0))
) should equal(RangeValue(5.0, 6.0))
Expand All @@ -109,7 +118,12 @@ class ExprEvaluateTest extends AnyFlatSpec {
evalTest.map(
ValueExpr.BinOp(Op.INTERSECTION, ValueExpr.Literal(5.0, 7.0), ValueExpr.Literal(8.0, 10.0))
) should equal(RangeEmpty)
// TODO test with empty ranges?
evalTest.map(
ValueExpr.BinOp(Op.INTERSECTION, ValueExpr.Literal(5.0, 7.0), ValueExpr.LiteralRangeEmpty())
) should equal(RangeEmpty)
evalTest.map(
ValueExpr.BinOp(Op.INTERSECTION, ValueExpr.LiteralRangeEmpty(), ValueExpr.LiteralRangeEmpty())
) should equal(RangeEmpty)

evalTest.map(ValueExpr.BinOp(Op.HULL, ValueExpr.Literal(4.0, 6.0), ValueExpr.Literal(5.0, 7.0))) should equal(
RangeValue(4.0, 7.0)
Expand All @@ -123,7 +137,12 @@ class ExprEvaluateTest extends AnyFlatSpec {
evalTest.map(ValueExpr.BinOp(Op.HULL, ValueExpr.Literal(5.0, 7.0), ValueExpr.Literal(8.0, 10.0))) should equal(
RangeValue(5.0, 10.0)
)
// TODO test with empty ranges?
evalTest.map(
ValueExpr.BinOp(Op.HULL, ValueExpr.Literal(5.0, 7.0), ValueExpr.LiteralRangeEmpty())
) should equal(RangeValue(5.0, 7.0))
evalTest.map(
ValueExpr.BinOp(Op.HULL, ValueExpr.LiteralRangeEmpty(), ValueExpr.LiteralRangeEmpty())
) should equal(RangeEmpty)

evalTest.map(ValueExpr.BinOp(Op.WITHIN, ValueExpr.Literal(6.0), ValueExpr.Literal(5.0, 7.0))) should equal(
BooleanValue(true)
Expand All @@ -150,6 +169,13 @@ class ExprEvaluateTest extends AnyFlatSpec {
evalTest.map(ValueExpr.BinOp(Op.WITHIN, ValueExpr.Literal(5.5, 7.5), ValueExpr.Literal(5.0, 7.0))) should equal(
BooleanValue(false)
)

evalTest.map(ValueExpr.BinOp(Op.WITHIN, ValueExpr.Literal(5.5, 7.5), ValueExpr.LiteralRangeEmpty())) should equal(
BooleanValue(false)
)
evalTest.map(ValueExpr.BinOp(Op.WITHIN, ValueExpr.LiteralRangeEmpty(), ValueExpr.Literal(5.0, 7.0))) should equal(
BooleanValue(true)
)
}

it should "handle arithmetic promotions" in {
Expand Down
Binary file modified edg/core/resources/edg-compiler-precompiled.jar
Binary file not shown.
Loading