|
| 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