|
| 1 | += Power assertions |
| 2 | + |
| 3 | +* Proposal: link:./SPICE-0021-binary-renderer-and-parser.adoc[SPICE-0021] |
| 4 | +* Author: https://github.com/bioball[Dan Chao] |
| 5 | +* Status: Accepted or Rejected |
| 6 | +* Implemented in: TBD |
| 7 | +* Category: Language |
| 8 | +
|
| 9 | +== Introduction |
| 10 | + |
| 11 | +We will enhance Pkl's error messages with power assertions. |
| 12 | + |
| 13 | +Power assertions are error messages that display the values produced through executing source code in a visual diagram. |
| 14 | + |
| 15 | +== Motivation |
| 16 | + |
| 17 | +Pkl has some places that are assertions: |
| 18 | + |
| 19 | +1. Type constraint expressions |
| 20 | +2. The `facts` block of `pkl:test` |
| 21 | + |
| 22 | +When these assertions fail, Pkl shows a limited amount of information about the failure. |
| 23 | + |
| 24 | +For example, given the following program: |
| 25 | + |
| 26 | +[source,pkl] |
| 27 | +---- |
| 28 | +class Person { |
| 29 | + name: String |
| 30 | +} |
| 31 | +
|
| 32 | +passenger: Person(name.endsWith(lastName)) |
| 33 | + = new { name = "Bub Johnson" } |
| 34 | +
|
| 35 | +lastName: String = "Smith" |
| 36 | +---- |
| 37 | + |
| 38 | +A failing typecheck simply tells that the type constraint check failed: |
| 39 | + |
| 40 | +[source,text] |
| 41 | +---- |
| 42 | +–– Pkl Error –– |
| 43 | +Type constraint `name.endsWith(lastName)` violated. |
| 44 | +Value: new Person { name = "Bub Johnson" } |
| 45 | +
|
| 46 | +5 | passenger: Person(name.endsWith(lastName)) |
| 47 | +---- |
| 48 | + |
| 49 | +This doesn't tell explain _why_ the type constraint failed. |
| 50 | +For example, what is `lastName`? |
| 51 | +What is the expectation? |
| 52 | + |
| 53 | +== Proposed Solution |
| 54 | + |
| 55 | +These assertions will be decorated with values produced by the AST nodes during execution. |
| 56 | + |
| 57 | +With power assertions, the above error becomes: |
| 58 | + |
| 59 | +[source,text] |
| 60 | +---- |
| 61 | +–– Pkl Error –– |
| 62 | +Type constraint `name.endsWith(lastName)` violated. |
| 63 | +Value: new Person { name = "Bub Johnson" } |
| 64 | +
|
| 65 | + name.endsWith(lastName) |
| 66 | + | | | |
| 67 | + | false "Smith" |
| 68 | + "Bub Johnson" |
| 69 | +
|
| 70 | +5 | passenger: Person(name.endsWith(lastName)) |
| 71 | + ^^^^^^^^^^^^^^^^^^^^^^^ |
| 72 | +---- |
| 73 | + |
| 74 | +== Detailed design |
| 75 | + |
| 76 | +=== How the diagram works |
| 77 | + |
| 78 | +The design of the diagram follows prior art: |
| 79 | + |
| 80 | +* https://docs.groovy-lang.org/next/html/documentation/core-testing-guide.html#_power_assertions[Groovy] |
| 81 | +* https://kotlinlang.org/docs/power-assert.html[Kotlin] |
| 82 | +* https://github.com/kishikawakatsumi/swift-power-assert[swift-power-assert] |
| 83 | +* https://github.com/power-assert-js/power-assert[power-assert-js] |
| 84 | + |
| 85 | +The rules for displaying power asserts are: |
| 86 | + |
| 87 | +Values are appended to the source graph using the `|` character. |
| 88 | + |
| 89 | +[source,text] |
| 90 | +---- |
| 91 | + myName.startsWith(prefix) |
| 92 | + | | | |
| 93 | + "bub" false "g" |
| 94 | +---- |
| 95 | + |
| 96 | +If two values cannot fit, the right-most value wins, and the left-most value gets moved down one line. |
| 97 | + |
| 98 | +[source,text] |
| 99 | +---- |
| 100 | + num1 == num2 |
| 101 | + | | | |
| 102 | + 5 | 6 |
| 103 | + false |
| 104 | +---- |
| 105 | + |
| 106 | +Continually overlapping values will cascade. |
| 107 | + |
| 108 | +[source,text] |
| 109 | +---- |
| 110 | + foo == bar |
| 111 | + | | | |
| 112 | + | | "barrey" |
| 113 | + | false |
| 114 | + "foooey" |
| 115 | +---- |
| 116 | + |
| 117 | +Literal values are omitted from the source. |
| 118 | + |
| 119 | +[source,text] |
| 120 | +---- |
| 121 | + foo == "barrey" |
| 122 | + | | |
| 123 | + | false |
| 124 | + "foooey" |
| 125 | +---- |
| 126 | + |
| 127 | +Literal values include stdlib types like IntSeq, List, Map whose members are also literal. |
| 128 | +Here, `List(1, 2, 3)` is excluded because it's considered a literal: |
| 129 | + |
| 130 | +[%nowrap] |
| 131 | +[source,text] |
| 132 | +---- |
| 133 | +–– Pkl Error –– |
| 134 | +Type constraint `isInMyList` violated. |
| 135 | +Value: "four" |
| 136 | +
|
| 137 | + (it) -> myList.contains(it) |
| 138 | + | | | |
| 139 | + | false "four" |
| 140 | + List("one", "two", "three") |
| 141 | +
|
| 142 | +1 | foo: String(isInMyList) = "four" |
| 143 | +---- |
| 144 | + |
| 145 | +Nodes from within things that can be executed multiple times are excluded. |
| 146 | +These are: |
| 147 | + |
| 148 | + * Lambdas |
| 149 | + * For-generators |
| 150 | + * Member predicates |
| 151 | + |
| 152 | +Here, the nodes within the lambda are not part of the diagram. |
| 153 | + |
| 154 | +[%nowrap] |
| 155 | +[source,text] |
| 156 | +---- |
| 157 | +myList.fold(0, (a, b) -> a + b) == 5 |
| 158 | +| | | |
| 159 | +| 6 false |
| 160 | +List(1, 2, 3) |
| 161 | +---- |
| 162 | + |
| 163 | +Expressions that span multiple lines have a blank line inserted in between. |
| 164 | + |
| 165 | +[%nowrap] |
| 166 | +[source,text] |
| 167 | +---- |
| 168 | + one |
| 169 | + | |
| 170 | + 1 |
| 171 | +
|
| 172 | + + two |
| 173 | + | | |
| 174 | + | 2 |
| 175 | + 1.6666666666666665 |
| 176 | +
|
| 177 | + / three |
| 178 | + | | |
| 179 | + | 3 |
| 180 | + 0.6666666666666666 |
| 181 | +
|
| 182 | + == four |
| 183 | + | | |
| 184 | + | 4 |
| 185 | + false |
| 186 | +---- |
| 187 | + |
| 188 | +=== Lambda constraints |
| 189 | + |
| 190 | +Type constraints accept either a boolean, or a `Function1` lambda. |
| 191 | + |
| 192 | +In the case of lambdas, the lambda's source section is diagrammed instead. |
| 193 | + |
| 194 | +For example: |
| 195 | + |
| 196 | +[%nowrap] |
| 197 | +[source] |
| 198 | +---- |
| 199 | +–– Pkl Error –– |
| 200 | +Type constraint `isInMyList` violated. |
| 201 | +Value: "four" |
| 202 | +
|
| 203 | + (it) -> myList.contains(it) |
| 204 | + | | | |
| 205 | + | false "four" |
| 206 | + List("one", "two", "three") |
| 207 | +
|
| 208 | +1 | foo: String(isInMyList) = "four" |
| 209 | +---- |
| 210 | + |
| 211 | +If the lambda recurses, only nodes that are executed once are shown in the diagram. |
| 212 | + |
| 213 | +The following code: |
| 214 | + |
| 215 | +[%nowrap] |
| 216 | +[source,pkl] |
| 217 | +---- |
| 218 | +foo: String(endsWithA) = "bar" |
| 219 | +
|
| 220 | +local endsWithA = (it) -> if (it.length == 1) it == "a" else endsWithA.apply(it.drop(1)) |
| 221 | +---- |
| 222 | + |
| 223 | +Throws this error: |
| 224 | + |
| 225 | +[%nowrap] |
| 226 | +[source] |
| 227 | +---- |
| 228 | +–– Pkl Error –– |
| 229 | +Type constraint `endsWithA` violated. |
| 230 | +Value: "bar" |
| 231 | +
|
| 232 | + (it) -> if (it.length == 1) it == "a" else endsWithA.apply(it.drop(1)) |
| 233 | + | | |
| 234 | + | false |
| 235 | + "r" |
| 236 | +
|
| 237 | +1 | foo: String(endsWithA) = "bar" |
| 238 | +---- |
| 239 | + |
| 240 | +=== Colors |
| 241 | + |
| 242 | +If colors are enabled, the source nodes are syntax highlighted, and the `|` character is emitted with ANSI code 2 (faint). |
| 243 | + |
| 244 | +Sample: |
| 245 | + |
| 246 | +image::../images/power-assertions.png[] |
| 247 | + |
| 248 | +=== Runtime implementation |
| 249 | + |
| 250 | +Values are collected through truffle instrumentation, which is machinery to wrap AST nodes to observe their execution. |
| 251 | + |
| 252 | +Instrumentation is disabled by default, and only enabled if an assertion fails. |
| 253 | + |
| 254 | +Essentially, failing assertions are executed twice. |
| 255 | +The algorithm works as follows: |
| 256 | + |
| 257 | +1. Run the assertion; assertion fails |
| 258 | +2. Enable instrumentation |
| 259 | +3. Run the assertion again |
| 260 | +4. Disable instrumentation |
| 261 | + |
| 262 | +Running instrumentation has a runtime cost. |
| 263 | +By only enabling instrumentation for failing assertions, we only pay this cost for the error path. |
| 264 | + |
| 265 | +=== `test.catch` |
| 266 | + |
| 267 | +Power assertions are hidden from the error passed to `test.catch` and `test.catchOrNull`; these two APIs work exactly like they do today. |
| 268 | + |
| 269 | +== Compatibility |
| 270 | + |
| 271 | +There is no impact on compatibility. |
| 272 | +This design only impacts error messages, which is not considered an API. |
| 273 | + |
| 274 | +== Future directions |
| 275 | + |
| 276 | +=== Assertions as an API |
| 277 | + |
| 278 | +Possibly, we can provide an in-language assertion API. |
| 279 | +For example: |
| 280 | + |
| 281 | +[source,pkl] |
| 282 | +---- |
| 283 | +assert(1 == 2) |
| 284 | +---- |
| 285 | + |
| 286 | +The assertion would also run display power assertions in the resulting thrown error. |
| 287 | + |
| 288 | +This might play nicely with custom error messages: |
| 289 | + |
| 290 | +[source,pkl] |
| 291 | +---- |
| 292 | +local startingStartsWithBub = (it: String) -> |
| 293 | + assert(it.starsWith("bub"), "Foo should start with bub.") |
| 294 | +---- |
| 295 | + |
| 296 | +=== Richer power assertions |
| 297 | + |
| 298 | +We can possibly provide richer diagrams. |
| 299 | +For example, in the case of failing string comparison, to display a unified diff. |
| 300 | + |
| 301 | +== Alternatives considered |
| 302 | + |
| 303 | +N/A |
| 304 | + |
| 305 | +== Acknowledgements |
| 306 | + |
| 307 | +Power assertions was initially introduced in https://spockframework.org[Spock Framework] by Peter Niederwieser. |
0 commit comments