Skip to content

Commit a02576e

Browse files
committed
chore: test both aggressiveOptimization
1 parent a20ea87 commit a02576e

4 files changed

Lines changed: 1168 additions & 213 deletions

File tree

sjsonnet/src/sjsonnet/StaticOptimizer.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import ScopedExprTransform.*
99
* StaticOptimizer performs necessary transformations for the evaluator (assigning ValScope indices)
1010
* plus additional optimizations (post-order) and static checking (pre-order).
1111
*
12-
* When `aggressiveStaticOptimization` is enabled, the optimizer additionally performs during
13-
* the optimization phase:
12+
* When `aggressiveStaticOptimization` is enabled, the optimizer additionally performs during the
13+
* optimization phase:
1414
* - Constant folding for arithmetic (+, -, *, /, %), comparison (<, >, <=, >=, ==, !=), bitwise
1515
* (&, ^, |), shift (<<, >>), and unary (!, -, ~, +) operators.
1616
* - Branch elimination for if-else with constant conditions.
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
package sjsonnet
2+
3+
import utest._
4+
import TestUtils.{eval, evalErr}
5+
6+
/**
7+
* Tests for `aggressiveStaticOptimization = true`.
8+
*
9+
* Covers every optimization branch in [[StaticOptimizer.tryAggressiveOptimize]], which runs during
10+
* the optimization phase (not parse time):
11+
* - Constant folding: arithmetic (+, -, *, /, %), comparison (<, >, <=, >=, ==, !=), bitwise (&,
12+
* ^, |), shift (<<, >>), unary (!, -, ~, +), string/array concatenation.
13+
* - Branch elimination: if-else with constant condition.
14+
* - Short-circuit elimination: And/Or with constant lhs.
15+
*
16+
* Each case is verified to produce the same result as without the optimization, confirming that the
17+
* optimization is semantics-preserving.
18+
*/
19+
object AggressiveStaticOptimizationTests extends TestSuite {
20+
21+
/** Shorthand: evaluate with aggressiveStaticOptimization enabled. */
22+
def evalOpt(s: String): ujson.Value =
23+
eval(s, aggressiveStaticOptimization = true)
24+
25+
/** Shorthand: evaluate and expect an error with aggressiveStaticOptimization enabled. */
26+
def evalErrOpt(s: String): String =
27+
evalErr(s, aggressiveStaticOptimization = true)
28+
29+
def tests: Tests = Tests {
30+
31+
// -------------------------------------------------------------------------
32+
// Arithmetic constant folding
33+
// -------------------------------------------------------------------------
34+
test("constantFolding") {
35+
test("addNumbers") {
36+
evalOpt("1 + 2") ==> ujson.Num(3)
37+
evalOpt("1.5 + 2.5") ==> ujson.Num(4)
38+
}
39+
test("subtractNumbers") {
40+
evalOpt("10 - 3") ==> ujson.Num(7)
41+
evalOpt("0 - 5") ==> ujson.Num(-5)
42+
}
43+
test("multiplyNumbers") {
44+
evalOpt("3 * 4") ==> ujson.Num(12)
45+
evalOpt("2.5 * 2") ==> ujson.Num(5)
46+
}
47+
test("divideNumbers") {
48+
evalOpt("10 / 4") ==> ujson.Num(2.5)
49+
evalOpt("9 / 3") ==> ujson.Num(3)
50+
}
51+
test("moduloNumbers") {
52+
evalOpt("10 % 3") ==> ujson.Num(1)
53+
evalOpt("7 % 7") ==> ujson.Num(0)
54+
}
55+
test("addStrings") {
56+
evalOpt(""" "hello" + " world" """) ==> ujson.Str("hello world")
57+
evalOpt(""" "foo" + "bar" """) ==> ujson.Str("foobar")
58+
}
59+
test("addArrays") {
60+
evalOpt("[1, 2] + [3, 4]") ==> ujson.Arr(1, 2, 3, 4)
61+
evalOpt("[] + [1]") ==> ujson.Arr(1)
62+
}
63+
}
64+
65+
// -------------------------------------------------------------------------
66+
// Unary operator constant folding
67+
// -------------------------------------------------------------------------
68+
test("unaryConstantFolding") {
69+
test("logicalNot") {
70+
evalOpt("!true") ==> ujson.False
71+
evalOpt("!false") ==> ujson.True
72+
}
73+
test("negateNumber") {
74+
evalOpt("-5") ==> ujson.Num(-5)
75+
evalOpt("-(-3)") ==> ujson.Num(3)
76+
}
77+
test("bitwiseNot") {
78+
evalOpt("~0") ==> ujson.Num(-1)
79+
evalOpt("~(-1)") ==> ujson.Num(0)
80+
}
81+
test("unaryPlus") {
82+
evalOpt("+7") ==> ujson.Num(7)
83+
}
84+
}
85+
86+
// -------------------------------------------------------------------------
87+
// Comparison constant folding
88+
// -------------------------------------------------------------------------
89+
test("comparisonConstantFolding") {
90+
test("lessThan") {
91+
evalOpt("1 < 2") ==> ujson.True
92+
evalOpt("2 < 1") ==> ujson.False
93+
evalOpt("1 < 1") ==> ujson.False
94+
}
95+
test("greaterThan") {
96+
evalOpt("2 > 1") ==> ujson.True
97+
evalOpt("1 > 2") ==> ujson.False
98+
evalOpt("1 > 1") ==> ujson.False
99+
}
100+
test("lessThanOrEqual") {
101+
evalOpt("1 <= 1") ==> ujson.True
102+
evalOpt("1 <= 2") ==> ujson.True
103+
evalOpt("2 <= 1") ==> ujson.False
104+
}
105+
test("greaterThanOrEqual") {
106+
evalOpt("1 >= 1") ==> ujson.True
107+
evalOpt("2 >= 1") ==> ujson.True
108+
evalOpt("1 >= 2") ==> ujson.False
109+
}
110+
test("equalNumbers") {
111+
evalOpt("1 == 1") ==> ujson.True
112+
evalOpt("1 == 2") ==> ujson.False
113+
}
114+
test("notEqualNumbers") {
115+
evalOpt("1 != 2") ==> ujson.True
116+
evalOpt("1 != 1") ==> ujson.False
117+
}
118+
test("equalStrings") {
119+
evalOpt(""" "abc" == "abc" """) ==> ujson.True
120+
evalOpt(""" "abc" == "def" """) ==> ujson.False
121+
}
122+
test("notEqualStrings") {
123+
evalOpt(""" "abc" != "def" """) ==> ujson.True
124+
evalOpt(""" "abc" != "abc" """) ==> ujson.False
125+
}
126+
test("equalBooleans") {
127+
evalOpt("true == true") ==> ujson.True
128+
evalOpt("false == false") ==> ujson.True
129+
evalOpt("true == false") ==> ujson.False
130+
}
131+
test("equalNull") {
132+
evalOpt("null == null") ==> ujson.True
133+
evalOpt("null != null") ==> ujson.False
134+
}
135+
test("stringComparison") {
136+
evalOpt(""" "abc" < "abd" """) ==> ujson.True
137+
evalOpt(""" "b" > "a" """) ==> ujson.True
138+
evalOpt(""" "abc" <= "abc" """) ==> ujson.True
139+
evalOpt(""" "abc" >= "abc" """) ==> ujson.True
140+
}
141+
}
142+
143+
// -------------------------------------------------------------------------
144+
// Bitwise and shift constant folding
145+
// -------------------------------------------------------------------------
146+
test("bitwiseConstantFolding") {
147+
test("bitwiseAnd") {
148+
evalOpt("12 & 10") ==> ujson.Num(8)
149+
evalOpt("0 & 255") ==> ujson.Num(0)
150+
}
151+
test("bitwiseXor") {
152+
evalOpt("12 ^ 10") ==> ujson.Num(6)
153+
evalOpt("0 ^ 0") ==> ujson.Num(0)
154+
}
155+
test("bitwiseOr") {
156+
evalOpt("12 | 10") ==> ujson.Num(14)
157+
evalOpt("0 | 0") ==> ujson.Num(0)
158+
}
159+
test("shiftLeft") {
160+
evalOpt("1 << 3") ==> ujson.Num(8)
161+
evalOpt("3 << 2") ==> ujson.Num(12)
162+
}
163+
test("shiftRight") {
164+
evalOpt("8 >> 2") ==> ujson.Num(2)
165+
evalOpt("16 >> 4") ==> ujson.Num(1)
166+
}
167+
}
168+
169+
// -------------------------------------------------------------------------
170+
// Branch elimination: if-else with constant condition
171+
// -------------------------------------------------------------------------
172+
test("branchElimination") {
173+
test("trueConditionSelectsThenBranch") {
174+
evalOpt("if true then 42 else 0") ==> ujson.Num(42)
175+
evalOpt("if true then 'yes' else 'no'") ==> ujson.Str("yes")
176+
}
177+
test("falseConditionSelectsElseBranch") {
178+
evalOpt("if false then 42 else 0") ==> ujson.Num(0)
179+
evalOpt("if false then 'yes' else 'no'") ==> ujson.Str("no")
180+
}
181+
test("falseConditionWithNoElseYieldsNull") {
182+
// `if false then expr` with no else branch should yield null
183+
evalOpt("if false then 42") ==> ujson.Null
184+
}
185+
test("nestedBranchElimination") {
186+
evalOpt("if true then (if false then 1 else 2) else 3") ==> ujson.Num(2)
187+
}
188+
}
189+
190+
// -------------------------------------------------------------------------
191+
// Short-circuit elimination for And / Or
192+
// -------------------------------------------------------------------------
193+
test("shortCircuitElimination") {
194+
test("trueAndTrue") {
195+
evalOpt("true && true") ==> ujson.True
196+
}
197+
test("trueAndFalse") {
198+
evalOpt("true && false") ==> ujson.False
199+
}
200+
test("falseAndAnything") {
201+
// false && rhs should short-circuit to false regardless of rhs
202+
evalOpt("false && true") ==> ujson.False
203+
evalOpt("false && false") ==> ujson.False
204+
}
205+
test("trueOrAnything") {
206+
// true || rhs should short-circuit to true regardless of rhs
207+
evalOpt("true || false") ==> ujson.True
208+
evalOpt("true || true") ==> ujson.True
209+
}
210+
test("falseOrTrue") {
211+
evalOpt("false || true") ==> ujson.True
212+
}
213+
test("falseOrFalse") {
214+
evalOpt("false || false") ==> ujson.False
215+
}
216+
}
217+
218+
// -------------------------------------------------------------------------
219+
// Semantics-preserving: results must match the non-optimized evaluator
220+
// -------------------------------------------------------------------------
221+
test("semanticsMatch") {
222+
val expressions = Seq(
223+
"1 + 2",
224+
"10 - 3",
225+
"3 * 4",
226+
"10 / 4",
227+
"10 % 3",
228+
"!true",
229+
"!false",
230+
"-5",
231+
"~0",
232+
"+7",
233+
"1 < 2",
234+
"2 > 1",
235+
"1 <= 1",
236+
"2 >= 1",
237+
"1 == 1",
238+
"1 != 2",
239+
"12 & 10",
240+
"12 ^ 10",
241+
"12 | 10",
242+
"1 << 3",
243+
"8 >> 2",
244+
"if true then 1 else 0",
245+
"if false then 1 else 0",
246+
"if false then 1",
247+
"true && true",
248+
"true && false",
249+
"false && true",
250+
"true || false",
251+
"false || true",
252+
"false || false"
253+
)
254+
for (expr <- expressions) {
255+
val withOpt = eval(expr, aggressiveStaticOptimization = true)
256+
val withoutOpt = eval(expr, aggressiveStaticOptimization = false)
257+
withOpt ==> withoutOpt
258+
}
259+
}
260+
261+
// -------------------------------------------------------------------------
262+
// Edge cases: optimizations that must NOT fire (non-constant operands)
263+
// -------------------------------------------------------------------------
264+
test("nonConstantOperandsNotFolded") {
265+
// Variables are not statically known values; the optimizer must not fold these.
266+
evalOpt("local x = 3; local y = 4; x + y") ==> ujson.Num(7)
267+
evalOpt("local b = true; if b then 1 else 0") ==> ujson.Num(1)
268+
evalOpt("local b = false; b || true") ==> ujson.True
269+
}
270+
271+
// -------------------------------------------------------------------------
272+
// Error cases: runtime errors must still be raised correctly
273+
// -------------------------------------------------------------------------
274+
test("runtimeErrorsPreserved") {
275+
test("divisionByZeroNotFolded") {
276+
// Division by zero: the optimizer must NOT fold `1 / 0` into a value;
277+
// it should fall back to the runtime error path.
278+
val err = evalErrOpt("1 / 0")
279+
assert(err.contains("sjsonnet.Error"))
280+
}
281+
test("negativeShiftNotFolded") {
282+
// Negative shift amounts must not be constant-folded; runtime error expected.
283+
val err = evalErrOpt("1 << -1")
284+
assert(err.contains("sjsonnet.Error"))
285+
}
286+
test("andWithNonBoolRhsStillErrors") {
287+
// `true && "hello"` must still error: the optimizer only short-circuits when
288+
// rhs is a Val.Bool. If rhs is not a Bool, the BinaryOp is left intact and
289+
// the runtime type-check fires.
290+
val err = evalErrOpt(""" true && "hello" """)
291+
assert(err.contains("binary operator &&"))
292+
}
293+
test("orWithNonBoolRhsStillErrors") {
294+
val err = evalErrOpt(""" false || "hello" """)
295+
assert(err.contains("binary operator ||"))
296+
}
297+
}
298+
299+
// -------------------------------------------------------------------------
300+
// Interaction with other settings: aggressiveStaticOptimization + useNewEvaluator
301+
// -------------------------------------------------------------------------
302+
test("withNewEvaluator") {
303+
def evalBoth(s: String): ujson.Value =
304+
eval(s, aggressiveStaticOptimization = true, useNewEvaluator = true)
305+
306+
evalBoth("1 + 2") ==> ujson.Num(3)
307+
evalBoth("if true then 'yes' else 'no'") ==> ujson.Str("yes")
308+
evalBoth("true && false") ==> ujson.False
309+
evalBoth("false || true") ==> ujson.True
310+
evalBoth("~0") ==> ujson.Num(-1)
311+
evalBoth("12 & 10") ==> ujson.Num(8)
312+
}
313+
}
314+
}

0 commit comments

Comments
 (0)