-
Notifications
You must be signed in to change notification settings - Fork 265
/
JSONExamples.dfy
280 lines (258 loc) · 10.2 KB
/
JSONExamples.dfy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
/**
This library offers two APIs: a high-level one (giving abstract value trees
with no concrete syntactic details) and a low-level one (including all
information about blanks, separator positions, character escapes, etc.).
*/
/** High-level API (JSON values) */
@Options("-functionSyntax:4")
module AbstractSyntax {
import Std.JSON.API
import opened Std.JSON.Values
import opened Std.Wrappers
import opened Std.Unicode.UnicodeStringsWithUnicodeChar
/**
The high-level API works with fairly simple datatype values that contain
native Dafny strings.
*/
@Test
@ResourceLimit("100e6")
method Test() {
/**
Use `API.Deserialize` to deserialize a byte string.
For example, here is how to decode the JSON test `"[true]"`. (We need to
convert from Dafny's native strings to byte strings because Dafny does not
have syntax for byte strings; in a real application, we would be reading and
writing raw bytes directly from disk or from the network instead).
*/
var SIMPLE_JS :- expect ToUTF8Checked("[true]");
var SIMPLE_VALUE := Array([Bool(true)]);
expect API.Deserialize(SIMPLE_JS) == Success(SIMPLE_VALUE);
/**
Here is a larger object, written using a verbatim string (with `@"`).
In verbatim strings `""` represents a single double-quote character):
*/
var CITIES_JS :- expect ToUTF8Checked(@"{
""Cities"": [
{
""Name"": ""Boston"",
""Founded"": 1630,
""Population"": 689386,
""Area (km2)"": 4584.2
}, {
""Name"": ""Rome"",
""Founded"": -753,
""Population"": 2.873e6,
""Area (km2)"": 1285
}, {
""Name"": ""Paris"",
""Founded"": null,
""Population"": 2.161e6,
""Area (km2)"": 2383.5
}
]
}");
var CITIES_VALUE :=
Object([
("Cities", Array([
Object([
("Name", String("Boston")),
("Founded", Number(Int(1630))),
("Population", Number(Int(689386))),
("Area (km2)", Number(Decimal(45842, -1)))
]),
Object([
("Name", String("Rome")),
("Founded", Number(Int(-753))),
("Population", Number(Decimal(2873, 3))),
("Area (km2)", Number(Int(1285)))
]),
Object([
("Name", String("Paris")),
("Founded", Null),
("Population", Number(Decimal(2161, 3))),
("Area (km2)", Number(Decimal(23835, -1)))
])
]))
]);
expect API.Deserialize(CITIES_JS) == Success(CITIES_VALUE);
/**
Serialization works similarly, with `API.Serialize`. For this first example
the generated string matches what we started with exactly:
*/
expect API.Serialize(SIMPLE_VALUE) == Success(SIMPLE_JS);
/**
For more complex object, the generated layout may not be exactly the same;
note in particular how the representation of numbers and the whitespace have changed.
*/
var EXPECTED :- expect ToUTF8Checked(
@"{""Cities"":[{""Name"":""Boston"",""Founded"":1630,""Population"":689386,""Area (km2)"":45842e-1},{""Name"":""Rome"",""Founded"":-753,""Population"":2873e3,""Area (km2)"":1285},{""Name"":""Paris"",""Founded"":null,""Population"":2161e3,""Area (km2)"":23835e-1}]}"
);
expect API.Serialize(CITIES_VALUE) == Success(EXPECTED);
/**
Additional methods are defined in `API.dfy` to serialize an object into an
existing buffer or into an array. Below is the smaller example as a sanity check:
*/
var CITY_JS :- expect ToUTF8Checked(@"{""Cities"": [{
""Name"": ""Boston"",
""Founded"": 1630,
""Population"": 689386,
""Area (km2)"": 4584.2}]}");
var CITY_VALUE :=
Object([("Cities", Array([
Object([
("Name", String("Boston")),
("Founded", Number(Int(1630))),
("Population", Number(Int(689386))),
("Area (km2)", Number(Decimal(45842, -1)))])]))]);
expect API.Deserialize(CITY_JS) == Success(CITY_VALUE);
var EXPECTED' :- expect ToUTF8Checked(
@"{""Cities"":[{""Name"":""Boston"",""Founded"":1630,""Population"":689386,""Area (km2)"":45842e-1}]}"
);
expect API.Serialize(CITY_VALUE) == Success(EXPECTED');
}
}
/** Low-level API (concrete syntax) */
/**
If you care about low-level performance, or about preserving existing
formatting as much as possible, you may prefer to use the lower-level API:
*/
@Options("-functionSyntax:4")
module ConcreteSyntax {
import Std.JSON.ZeroCopy.API
import opened Std.Unicode.UnicodeStringsWithUnicodeChar
import opened Std.JSON.Grammar
import opened Std.Wrappers
import Std.Collections.Seq
/**
The low-level API works with ASTs that record all details of formatting and
encoding: each node contains pointers to parts of a string, such that
concatenating the fields of all nodes reconstructs the serialized value.
*/
@Test
@ResourceLimit("100e6")
method Test() {
/**
The low-level API exposes the same functions and methods as the high-level
one, but the type that they consume and produce is `Grammar.JSON` (defined
in `Grammar.dfy` as a `Grammar.Value` surrounded by optional whitespace)
instead of `Values.JSON` (defined in `Values.dfy`). Since `Grammar.JSON` contains
all formatting information, re-serializing an object produces the original value:
*/
var CITIES :- expect ToUTF8Checked(@"{
""Cities"": [
{
""Name"": ""Boston"",
""Founded"": 1630,
""Population"": 689386,
""Area (km2)"": 4600
}, {
""Name"": ""Rome"",
""Founded"": -753,
""Population"": 2.873e6,
""Area (km2)"": 1285
}, {
""Name"": ""Paris"",
""Founded"": null,
""Population"": 2.161e6,
""Area (km2)"": 2383.5
}
]
}");
var deserialized :- expect API.Deserialize(CITIES);
expect API.Serialize(deserialized) == Success(CITIES);
/**
Since the formatting is preserved, it is also possible to write
minimally-invasive transformations over an AST. For example, let's replace
`null` in the object above with `"Unknown"`. First, we construct a JSON
value for the string `"Unknown"`; this could be done by hand using
`View.OfBytes()`, but using `API.Deserialize` is even simpler:
*/
var UNKNOWN_JS :- expect ToUTF8Checked(@"""Unknown""");
var UNKNOWN :- expect API.Deserialize(UNKNOWN_JS);
/**
`UNKNOWN` is of type `Grammar.JSON`, which contains optional whitespace and
a `Grammar.Value` under the name `UNKNOWN.t`, which we can use in the
replacement:
*/
var without_null := deserialized.(t := ReplaceNull(deserialized.t, UNKNOWN.t));
/**
Then, if we reserialize, we see that all formatting (and, in fact, all of
the serialization work) has been reused:
*/
var expected_js :- expect ToUTF8Checked(@"{
""Cities"": [
{
""Name"": ""Boston"",
""Founded"": 1630,
""Population"": 689386,
""Area (km2)"": 4600
}, {
""Name"": ""Rome"",
""Founded"": -753,
""Population"": 2.873e6,
""Area (km2)"": 1285
}, {
""Name"": ""Paris"",
""Founded"": ""Unknown"",
""Population"": 2.161e6,
""Area (km2)"": 2383.5
}
]
}");
var actual_js :- expect API.Serialize(without_null);
expect actual_js == expected_js;
}
/** All that remains is to write the recursive traversal: */
function ReplaceNull(js: Value, replacement: Value): Value {
match js
/** Non-recursive cases are untouched: */
case Bool(_) => js
case String(_) => js
case Number(_) => js
/** `Null` is replaced with the new `replacement` value: */
case Null(_) => replacement
/**
… and objects and arrays are traversed recursively (only the data part of is
traversed: other fields record information about the formatting of braces,
square brackets, and whitespace, and can thus be reused without
modifications):
*/
case Object(obj) =>
Object(obj.(data := MapSuffixedSequence(obj.data, (s: Suffixed<jKeyValue, jcomma>) requires s in obj.data =>
s.t.(v := ReplaceNull(s.t.v, replacement)))))
case Array(arr) =>
Array(arr.(data := MapSuffixedSequence(arr.data, (s: Suffixed<Value, jcomma>) requires s in arr.data =>
ReplaceNull(s.t, replacement))))
}
/**
Note that well-formedness criteria on the low-level AST are enforced using
subset types, which is why we need a bit more work to iterate over the
sequences of key-value paris and of values in objects and arrays.
Specifically, we need to prove that mapping over these sequences doesn't
introduce dangling punctuation (`NoTrailingSuffix`). We package this
reasoning into a `MapSuffixedSequence` function:
*/
function MapSuffixedSequence<D, S>(sq: SuffixedSequence<D, S>, fn: Suffixed<D, S> --> D)
: SuffixedSequence<D, S>
requires forall suffixed | suffixed in sq :: fn.requires(suffixed)
{
// BUG(https://github.com/dafny-lang/dafny/issues/2184)
// BUG(https://github.com/dafny-lang/dafny/issues/2690)
var fn' := (sf: Suffixed<D, S>) requires (ghost var in_sq := sf => sf in sq; in_sq(sf)) => sf.(t := fn(sf));
Seq.Map(fn', sq)
}
lemma MapSuffixedSequenceNoTrailingSuffix<D, S>(sq: SuffixedSequence<D, S>, fn: Suffixed<D, S> --> D)
requires forall suffixed | suffixed in sq :: fn.requires(suffixed)
ensures NoTrailingSuffix(MapSuffixedSequence<D, S>(sq, fn))
{
var sq' := MapSuffixedSequence<D, S>(sq, fn);
forall idx | 0 <= idx < |sq'| ensures sq'[idx].suffix.Empty? <==> idx == |sq'| - 1 {
calc {
sq'[idx].suffix.Empty?;
sq[idx].suffix.Empty?;
idx == |sq| - 1;
idx == |sq'| - 1;
}
}
}
}