Skip to content

Commit b56bebf

Browse files
committed
SPICE-0026: Power Assertions
1 parent 585598c commit b56bebf

File tree

2 files changed

+307
-0
lines changed

2 files changed

+307
-0
lines changed

images/power-assertions.png

68.8 KB
Loading
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
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

Comments
 (0)