kibi-core 0.23.0 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/schema/entities.pl +6 -0
- package/schema/validation.pl +7 -2
- package/src/certificate_check.pl +62 -1
- package/src/certificates.pl +17 -1
- package/src/checks.pl +4 -1
- package/src/claim_obligations.pl +706 -0
- package/src/code_certificates.pl +532 -0
- package/src/discovery.pl +160 -2
- package/src/intervals.pl +11 -0
- package/src/kb.pl +55 -1
- package/src/model_check.pl +728 -0
- package/src/requirement_proof.pl +561 -42
- package/src/rule_registry.pl +8 -0
- package/src/semantic_quality.pl +1 -1
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
% Module: code_certificates
|
|
2
|
+
% implements REQ-kibi-code-proof-kinds
|
|
3
|
+
%
|
|
4
|
+
% Re-checks the kibi.code-certificate.v1 a certificate-kind proof contract
|
|
5
|
+
% leaves in its receipt. A receipt counts as proved only when its
|
|
6
|
+
% certificate re-checks here, so a test that claims to have covered every
|
|
7
|
+
% case cannot earn proved by saying so. The checks never search:
|
|
8
|
+
%
|
|
9
|
+
% exhaustive The domain lists at least one member, no member twice, and
|
|
10
|
+
% the results name every member exactly once, each passed.
|
|
11
|
+
% (Kibi checks at ingest that the proof imported the domain
|
|
12
|
+
% from its source instead of hand-listing it.)
|
|
13
|
+
% typecheck The witness names a file and its sha256 and the compiler
|
|
14
|
+
% that accepted it; the outcome is passed. (Kibi checks at
|
|
15
|
+
% ingest that the witness is the file in the workspace and
|
|
16
|
+
% imports the domain.)
|
|
17
|
+
% arithmetic The assumptions on one variable are satisfiable and entail
|
|
18
|
+
% the claim, decided exactly by intervals.pl: the assumptions
|
|
19
|
+
% plus the negated claim admit no value.
|
|
20
|
+
% smt The query asks (check-sat) and the solver answered unsat.
|
|
21
|
+
% (Kibi re-runs the solver on the query at ingest.)
|
|
22
|
+
% rule Every domain lists distinct values, every predicate table
|
|
23
|
+
% holds exactly one boolean for every combination of its
|
|
24
|
+
% argument domains, and no call failed. (Kibi recomputes the
|
|
25
|
+
% domains and tables from the code at ingest.)
|
|
26
|
+
% state_machine
|
|
27
|
+
% The machine lists distinct states, and its transitions and
|
|
28
|
+
% initial states are pairs and states of that list. (Kibi
|
|
29
|
+
% reads the machine from the code at ingest.)
|
|
30
|
+
% regex The certificate holds the code's pattern (source, flags and
|
|
31
|
+
% flavor) or string constant. (Kibi reads it from the code
|
|
32
|
+
% at ingest and decides the claim's language exactly.)
|
|
33
|
+
% source The certificate names a project source (a reader and its
|
|
34
|
+
% spec) and holds what Kibi read from it: the members as a
|
|
35
|
+
% sorted list of distinct strings, and for schema, validator
|
|
36
|
+
% and route readers the tables, objects or routes. (Kibi
|
|
37
|
+
% reads the source itself at ingest and stores its own read.)
|
|
38
|
+
%
|
|
39
|
+
% The digest a receipt records is the sha256 of the certificate's canonical
|
|
40
|
+
% JSON as canonical_json/2 below spells it. The CLI records the digest this
|
|
41
|
+
% module returns at ingest, so ingest and every later re-check hash the
|
|
42
|
+
% certificate the same way.
|
|
43
|
+
|
|
44
|
+
:- module(code_certificates, [
|
|
45
|
+
code_certificate_check/2,
|
|
46
|
+
code_certificate_digest/2,
|
|
47
|
+
code_certificate_check_json/2
|
|
48
|
+
]).
|
|
49
|
+
|
|
50
|
+
:- use_module(library(http/json)).
|
|
51
|
+
:- use_module(library(crypto)).
|
|
52
|
+
:- use_module(library(apply)).
|
|
53
|
+
:- use_module(library(lists)).
|
|
54
|
+
:- use_module(library(yall)).
|
|
55
|
+
:- use_module('intervals.pl', [
|
|
56
|
+
numeric_constraints_satisfiable/2,
|
|
57
|
+
numeric_constraint_entailed/3
|
|
58
|
+
]).
|
|
59
|
+
|
|
60
|
+
code_certificate_version("kibi.code-certificate.v1").
|
|
61
|
+
|
|
62
|
+
%% code_certificate_check(+Certificate, -Result)
|
|
63
|
+
% Result is ok or rejected(Reason) with Reason a string naming what failed.
|
|
64
|
+
code_certificate_check(Certificate, Result) :-
|
|
65
|
+
( catch(certificate_problem(Certificate, Reason), _, (Reason = "the certificate is malformed"))
|
|
66
|
+
-> Result = rejected(Reason)
|
|
67
|
+
; Result = ok
|
|
68
|
+
).
|
|
69
|
+
|
|
70
|
+
%% code_certificate_digest(+Certificate, -Digest)
|
|
71
|
+
code_certificate_digest(Certificate, Digest) :-
|
|
72
|
+
canonical_json(Certificate, Json),
|
|
73
|
+
crypto_data_hash(Json, Digest, [algorithm(sha256), encoding(utf8)]).
|
|
74
|
+
|
|
75
|
+
%% code_certificate_check_json(+CertificateJson, -ResultJson)
|
|
76
|
+
% Entry point for the CLI: {"accepted": true, "digest": D} or
|
|
77
|
+
% {"accepted": false, "reason": R}.
|
|
78
|
+
code_certificate_check_json(CertificateJson, ResultJson) :-
|
|
79
|
+
( catch(atom_json_dict(CertificateJson, Certificate, [value_string_as(string)]), _, fail),
|
|
80
|
+
is_dict(Certificate)
|
|
81
|
+
-> code_certificate_check(Certificate, Result),
|
|
82
|
+
code_certificate_digest(Certificate, Digest),
|
|
83
|
+
( Result == ok
|
|
84
|
+
-> Out = _{accepted: true, digest: Digest}
|
|
85
|
+
; Result = rejected(Reason),
|
|
86
|
+
Out = _{accepted: false, digest: Digest, reason: Reason}
|
|
87
|
+
)
|
|
88
|
+
; Out = _{accepted: false, reason: "the certificate is not a JSON object"}
|
|
89
|
+
),
|
|
90
|
+
with_output_to(string(ResultJson), json_write_dict(current_output, Out, [width(0)])).
|
|
91
|
+
|
|
92
|
+
% certificate_problem(+Certificate, -Reason) succeeds with the first problem.
|
|
93
|
+
certificate_problem(Certificate, Reason) :-
|
|
94
|
+
\+ is_dict(Certificate), !,
|
|
95
|
+
Reason = "the certificate is not an object".
|
|
96
|
+
certificate_problem(Certificate, Reason) :-
|
|
97
|
+
code_certificate_version(Version),
|
|
98
|
+
\+ get_dict(version, Certificate, Version), !,
|
|
99
|
+
format(string(Reason), "the certificate version is not ~w", [Version]).
|
|
100
|
+
certificate_problem(Certificate, Reason) :-
|
|
101
|
+
\+ (get_dict(test_id, Certificate, TestId), nonempty_string(TestId)), !,
|
|
102
|
+
Reason = "the certificate names no test_id".
|
|
103
|
+
certificate_problem(Certificate, Reason) :-
|
|
104
|
+
( get_dict(kind, Certificate, KindString), string(KindString)
|
|
105
|
+
-> atom_string(Kind, KindString)
|
|
106
|
+
; Kind = none
|
|
107
|
+
),
|
|
108
|
+
( kind_problem(Kind, Certificate, Reason0)
|
|
109
|
+
-> Reason = Reason0
|
|
110
|
+
; \+ memberchk(Kind, [exhaustive, typecheck, arithmetic, smt, rule, state_machine, regex, source])
|
|
111
|
+
-> Reason = "the certificate kind is not exhaustive, typecheck, arithmetic, smt, rule, state_machine, regex or source"
|
|
112
|
+
).
|
|
113
|
+
|
|
114
|
+
kind_problem(exhaustive, Certificate, Reason) :-
|
|
115
|
+
field(Certificate, domain, Domain),
|
|
116
|
+
field(Domain, symbol_id, Symbol),
|
|
117
|
+
field(Domain, members, Members),
|
|
118
|
+
field(Certificate, results, Results),
|
|
119
|
+
( \+ is_dict(Domain)
|
|
120
|
+
-> Reason = "the certificate has no domain"
|
|
121
|
+
; \+ nonempty_string(Symbol)
|
|
122
|
+
-> Reason = "the domain names no symbol_id"
|
|
123
|
+
; \+ (is_list(Members), Members \== [])
|
|
124
|
+
-> Reason = "the domain lists no members"
|
|
125
|
+
; \+ maplist(nonempty_string, Members)
|
|
126
|
+
-> Reason = "every domain member must be a non-empty string"
|
|
127
|
+
; msort(Members, Sorted), sort(Members, Unique), Sorted \== Unique
|
|
128
|
+
-> Reason = "the domain lists a member twice"
|
|
129
|
+
; \+ is_list(Results)
|
|
130
|
+
-> Reason = "the certificate has no results"
|
|
131
|
+
; \+ maplist(result_row, Results)
|
|
132
|
+
-> Reason = "every result must name a member and an outcome"
|
|
133
|
+
; findall(M, (member(Row, Results), get_dict(member, Row, M)), Named),
|
|
134
|
+
msort(Named, NamedSorted),
|
|
135
|
+
msort(Members, MembersSorted),
|
|
136
|
+
NamedSorted \== MembersSorted
|
|
137
|
+
-> Reason = "the results do not name every domain member exactly once"
|
|
138
|
+
; member(Row, Results),
|
|
139
|
+
get_dict(outcome, Row, Outcome),
|
|
140
|
+
Outcome \== "passed"
|
|
141
|
+
-> get_dict(member, Row, Failed),
|
|
142
|
+
format(string(Reason), "member ~w did not pass", [Failed])
|
|
143
|
+
).
|
|
144
|
+
kind_problem(typecheck, Certificate, Reason) :-
|
|
145
|
+
field(Certificate, witness, Witness),
|
|
146
|
+
field(Witness, path, Path),
|
|
147
|
+
field(Witness, sha256, Hash),
|
|
148
|
+
field(Certificate, domain, Domain),
|
|
149
|
+
field(Domain, symbol_id, Symbol),
|
|
150
|
+
field(Certificate, compiler, Compiler),
|
|
151
|
+
field(Compiler, name, Name),
|
|
152
|
+
field(Compiler, version, CompilerVersion),
|
|
153
|
+
field(Certificate, outcome, Outcome),
|
|
154
|
+
( \+ nonempty_string(Path)
|
|
155
|
+
-> Reason = "the certificate names no witness path"
|
|
156
|
+
; \+ sha256_string(Hash)
|
|
157
|
+
-> Reason = "the witness has no sha256"
|
|
158
|
+
; \+ nonempty_string(Symbol)
|
|
159
|
+
-> Reason = "the certificate names no domain symbol_id"
|
|
160
|
+
; \+ (nonempty_string(Name), nonempty_string(CompilerVersion))
|
|
161
|
+
-> Reason = "the certificate names no compiler and version"
|
|
162
|
+
; Outcome \== "passed"
|
|
163
|
+
-> Reason = "the type checker did not accept the witness"
|
|
164
|
+
).
|
|
165
|
+
kind_problem(arithmetic, Certificate, Reason) :-
|
|
166
|
+
field(Certificate, variable, Variable),
|
|
167
|
+
field(Certificate, value_type, Type),
|
|
168
|
+
field(Certificate, assumptions, Assumptions),
|
|
169
|
+
field(Certificate, claim, Claim),
|
|
170
|
+
( \+ nonempty_string(Variable)
|
|
171
|
+
-> Reason = "the certificate names no variable"
|
|
172
|
+
; \+ memberchk(Type, ["int", "real"])
|
|
173
|
+
-> Reason = "value_type must be int or real"
|
|
174
|
+
; \+ (is_list(Assumptions), Assumptions \== [])
|
|
175
|
+
-> Reason = "the certificate states no assumptions"
|
|
176
|
+
; \+ maplist(arithmetic_constraint(_), Assumptions, _)
|
|
177
|
+
-> Reason = "every assumption must be {operator, value} with a comparison operator and a number"
|
|
178
|
+
; \+ arithmetic_constraint(_, Claim, _)
|
|
179
|
+
-> Reason = "the claim must be {operator, value} with a comparison operator and a number"
|
|
180
|
+
; maplist(arithmetic_constraint(X), Assumptions, Premises),
|
|
181
|
+
arithmetic_constraint(X, Claim, Goal),
|
|
182
|
+
integer_variables(Type, X, Integers),
|
|
183
|
+
( \+ numeric_constraints_satisfiable(Premises, Integers)
|
|
184
|
+
-> maplist(constraint_text(Variable), Assumptions, Texts),
|
|
185
|
+
atomic_list_concat(Texts, ' and ', Joined),
|
|
186
|
+
format(string(Reason),
|
|
187
|
+
"the assumptions contradict each other (~w), so no value satisfies them and they prove nothing about the claim",
|
|
188
|
+
[Joined])
|
|
189
|
+
; \+ numeric_constraint_entailed(Premises, Goal, Integers)
|
|
190
|
+
-> ( arithmetic_counterexample(Type, Assumptions, Claim, Value)
|
|
191
|
+
-> format(string(Reason),
|
|
192
|
+
"the assumptions do not entail the claim: ~w = ~w satisfies them and breaks it",
|
|
193
|
+
[Variable, Value])
|
|
194
|
+
; Reason = "the assumptions do not entail the claim: some value satisfies them and breaks it"
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
).
|
|
198
|
+
kind_problem(smt, Certificate, Reason) :-
|
|
199
|
+
field(Certificate, solver, Solver),
|
|
200
|
+
field(Solver, name, Name),
|
|
201
|
+
field(Solver, version, SolverVersion),
|
|
202
|
+
field(Certificate, query, Query),
|
|
203
|
+
field(Certificate, result, Result),
|
|
204
|
+
( \+ (nonempty_string(Name), nonempty_string(SolverVersion))
|
|
205
|
+
-> Reason = "the certificate names no solver and version"
|
|
206
|
+
; \+ nonempty_string(Query)
|
|
207
|
+
-> Reason = "the certificate has no query"
|
|
208
|
+
; \+ sub_string(Query, _, _, _, "(check-sat)")
|
|
209
|
+
-> Reason = "the query never asks (check-sat)"
|
|
210
|
+
; Result \== "unsat"
|
|
211
|
+
-> Reason = "the solver did not answer unsat"
|
|
212
|
+
).
|
|
213
|
+
|
|
214
|
+
kind_problem(rule, Certificate, Reason) :-
|
|
215
|
+
field(Certificate, domains, Domains),
|
|
216
|
+
field(Certificate, predicates, Predicates),
|
|
217
|
+
( \+ (is_dict(Domains), dict_pairs(Domains, _, DomainPairs), DomainPairs \== [])
|
|
218
|
+
-> Reason = "the certificate has no domains"
|
|
219
|
+
; dict_pairs(Domains, _, DomainPairs),
|
|
220
|
+
member(Type-Domain, DomainPairs),
|
|
221
|
+
field(Domain, members, Members),
|
|
222
|
+
\+ distinct_strings(Members)
|
|
223
|
+
-> format(string(Reason), "the domain ~w must list distinct values, at least one", [Type])
|
|
224
|
+
; \+ (is_dict(Predicates), dict_pairs(Predicates, _, PredicatePairs), PredicatePairs \== [])
|
|
225
|
+
-> Reason = "the certificate has no predicate tables"
|
|
226
|
+
; dict_pairs(Predicates, _, PredicatePairs),
|
|
227
|
+
member(Name-Table, PredicatePairs),
|
|
228
|
+
table_problem(Name, Table, Domains, Reason0)
|
|
229
|
+
-> Reason = Reason0
|
|
230
|
+
).
|
|
231
|
+
kind_problem(state_machine, Certificate, Reason) :-
|
|
232
|
+
field(Certificate, machine, Machine),
|
|
233
|
+
field(Machine, symbol_id, Symbol),
|
|
234
|
+
field(Machine, states, States),
|
|
235
|
+
field(Machine, transitions, Transitions),
|
|
236
|
+
( \+ is_dict(Machine)
|
|
237
|
+
-> Reason = "the certificate has no machine"
|
|
238
|
+
; \+ nonempty_string(Symbol)
|
|
239
|
+
-> Reason = "the machine names no symbol_id"
|
|
240
|
+
; \+ distinct_strings(States)
|
|
241
|
+
-> Reason = "the machine must list distinct states, at least one"
|
|
242
|
+
; \+ is_list(Transitions)
|
|
243
|
+
-> Reason = "the machine has no transitions list"
|
|
244
|
+
; member(Edge, Transitions),
|
|
245
|
+
\+ ( is_list(Edge), Edge = [From, To], memberchk(From, States), memberchk(To, States) )
|
|
246
|
+
-> Reason = "every transition must be a [from, to] pair of the machine's states"
|
|
247
|
+
; msort(Transitions, SortedEdges), sort(Transitions, UniqueEdges), SortedEdges \== UniqueEdges
|
|
248
|
+
-> Reason = "the machine lists a transition twice"
|
|
249
|
+
; get_dict(initial, Machine, Initial),
|
|
250
|
+
\+ ( is_list(Initial), forall(member(S, Initial), memberchk(S, States)) )
|
|
251
|
+
-> Reason = "the machine's initial states must be states of the machine"
|
|
252
|
+
).
|
|
253
|
+
kind_problem(regex, Certificate, Reason) :-
|
|
254
|
+
field(Certificate, pattern, Pattern),
|
|
255
|
+
field(Pattern, symbol_id, Symbol),
|
|
256
|
+
field(Pattern, type, Type),
|
|
257
|
+
( \+ is_dict(Pattern)
|
|
258
|
+
-> Reason = "the certificate has no pattern"
|
|
259
|
+
; \+ nonempty_string(Symbol)
|
|
260
|
+
-> Reason = "the pattern names no symbol_id"
|
|
261
|
+
; Type == "regexp"
|
|
262
|
+
-> field(Pattern, source, Source),
|
|
263
|
+
field(Pattern, flags, Flags),
|
|
264
|
+
field(Pattern, flavor, Flavor),
|
|
265
|
+
( \+ (string(Source), string(Flags))
|
|
266
|
+
-> Reason = "a regexp pattern must give its source and flags"
|
|
267
|
+
; \+ memberchk(Flavor, ["js", "python"])
|
|
268
|
+
-> Reason = "a regexp pattern's flavor must be js or python"
|
|
269
|
+
)
|
|
270
|
+
; Type == "string"
|
|
271
|
+
-> field(Pattern, value, Value),
|
|
272
|
+
\+ string(Value),
|
|
273
|
+
Reason = "a string pattern must give its value"
|
|
274
|
+
; Reason = "the pattern's type must be regexp or string"
|
|
275
|
+
).
|
|
276
|
+
|
|
277
|
+
% implements REQ-kibi-source-proof-kinds
|
|
278
|
+
kind_problem(source, Certificate, Reason) :-
|
|
279
|
+
field(Certificate, source, Source),
|
|
280
|
+
field(Certificate, model, Model),
|
|
281
|
+
field(Model, members, Members),
|
|
282
|
+
( \+ ( is_dict(Source), get_dict(reader, Source, Reader), nonempty_string(Reader) )
|
|
283
|
+
-> Reason = "the certificate names no source reader"
|
|
284
|
+
; \+ is_dict(Model)
|
|
285
|
+
-> Reason = "the certificate has no model of what Kibi read"
|
|
286
|
+
; \+ sorted_distinct_strings(Members)
|
|
287
|
+
-> Reason = "the source model's members must be a sorted list of distinct strings"
|
|
288
|
+
; get_dict(entities, Model, Entities),
|
|
289
|
+
\+ ( is_list(Entities), maplist(source_entity_ok, Entities) )
|
|
290
|
+
-> Reason = "every source entity must have a name and columns of {name, nullable, optional}"
|
|
291
|
+
; get_dict(routes, Model, Routes),
|
|
292
|
+
\+ ( is_list(Routes), maplist(source_route_ok, Routes) )
|
|
293
|
+
-> Reason = "every source route must have a method, a path and a middleware list"
|
|
294
|
+
).
|
|
295
|
+
|
|
296
|
+
sorted_distinct_strings(List) :-
|
|
297
|
+
is_list(List),
|
|
298
|
+
forall(member(Item, List), string(Item)),
|
|
299
|
+
sort(List, Sorted),
|
|
300
|
+
Sorted == List.
|
|
301
|
+
|
|
302
|
+
source_entity_ok(Entity) :-
|
|
303
|
+
is_dict(Entity),
|
|
304
|
+
get_dict(name, Entity, Name), nonempty_string(Name),
|
|
305
|
+
get_dict(columns, Entity, Columns), is_list(Columns),
|
|
306
|
+
forall(member(Column, Columns),
|
|
307
|
+
( is_dict(Column),
|
|
308
|
+
get_dict(name, Column, ColumnName), nonempty_string(ColumnName),
|
|
309
|
+
get_dict(nullable, Column, Nullable), memberchk(Nullable, [true, false]),
|
|
310
|
+
get_dict(optional, Column, Optional), memberchk(Optional, [true, false]) )),
|
|
311
|
+
( get_dict(foreign_keys, Entity, Keys) -> is_list(Keys), forall(member(K, Keys), string(K)) ; true ).
|
|
312
|
+
|
|
313
|
+
source_route_ok(Route) :-
|
|
314
|
+
is_dict(Route),
|
|
315
|
+
get_dict(method, Route, Method), nonempty_string(Method),
|
|
316
|
+
get_dict(path, Route, Path), nonempty_string(Path),
|
|
317
|
+
get_dict(middleware, Route, Middleware), is_list(Middleware),
|
|
318
|
+
forall(member(M, Middleware), string(M)).
|
|
319
|
+
|
|
320
|
+
% table_problem(+Name, +Table, +Domains, -Reason): the first problem with
|
|
321
|
+
% one predicate table.
|
|
322
|
+
table_problem(Name, Table, Domains, Reason) :-
|
|
323
|
+
field(Table, symbol_id, Symbol),
|
|
324
|
+
field(Table, arg_types, Types),
|
|
325
|
+
field(Table, rows, Rows),
|
|
326
|
+
( \+ nonempty_string(Symbol)
|
|
327
|
+
-> format(string(Reason), "the table for ~w names no symbol_id", [Name])
|
|
328
|
+
; \+ (is_list(Types), Types \== [], length(Types, Arity), Arity =< 8,
|
|
329
|
+
forall(member(T, Types), (string(T), atom_string(TA, T), get_dict(TA, Domains, _))))
|
|
330
|
+
-> format(string(Reason), "the table for ~w must name 1 to 8 argument types, each with a domain", [Name])
|
|
331
|
+
; \+ is_list(Rows)
|
|
332
|
+
-> format(string(Reason), "the table for ~w has no rows", [Name])
|
|
333
|
+
; member(Row, Rows),
|
|
334
|
+
get_dict(error, Row, Error)
|
|
335
|
+
-> get_dict(args, Row, Args),
|
|
336
|
+
atomic_list_concat(Args, ', ', ArgText),
|
|
337
|
+
format(string(Reason), "~w(~w) failed: ~w", [Name, ArgText, Error])
|
|
338
|
+
; member(Row, Rows),
|
|
339
|
+
\+ table_row(Row, Types, Domains)
|
|
340
|
+
-> format(string(Reason), "every row of ~w must give one value per argument from its domain and a boolean", [Name])
|
|
341
|
+
; findall(Args, (member(Row, Rows), get_dict(args, Row, Args)), AllArgs),
|
|
342
|
+
msort(AllArgs, SortedArgs),
|
|
343
|
+
sort(AllArgs, UniqueArgs),
|
|
344
|
+
SortedArgs \== UniqueArgs
|
|
345
|
+
-> format(string(Reason), "the table for ~w lists a combination twice", [Name])
|
|
346
|
+
; length(Rows, Count),
|
|
347
|
+
foldl(domain_product(Domains), Types, 1, Expected),
|
|
348
|
+
Count =\= Expected
|
|
349
|
+
-> format(string(Reason), "the table for ~w has ~d rows, not one per combination of its domains (~d)", [Name, Count, Expected])
|
|
350
|
+
).
|
|
351
|
+
|
|
352
|
+
domain_product(Domains, Type, Count0, Count) :-
|
|
353
|
+
atom_string(TypeKey, Type),
|
|
354
|
+
get_dict(TypeKey, Domains, Domain),
|
|
355
|
+
get_dict(members, Domain, Members),
|
|
356
|
+
length(Members, N),
|
|
357
|
+
Count is Count0 * N.
|
|
358
|
+
|
|
359
|
+
table_row(Row, Types, Domains) :-
|
|
360
|
+
is_dict(Row),
|
|
361
|
+
get_dict(args, Row, Args),
|
|
362
|
+
is_list(Args),
|
|
363
|
+
same_length(Args, Types),
|
|
364
|
+
forall(nth1(I, Args, Arg),
|
|
365
|
+
( nth1(I, Types, T), atom_string(TA, T), get_dict(TA, Domains, D),
|
|
366
|
+
get_dict(members, D, Members), memberchk(Arg, Members) )),
|
|
367
|
+
get_dict(value, Row, Value),
|
|
368
|
+
memberchk(Value, [true, false]).
|
|
369
|
+
|
|
370
|
+
distinct_strings(Values) :-
|
|
371
|
+
is_list(Values),
|
|
372
|
+
Values \== [],
|
|
373
|
+
maplist(string, Values),
|
|
374
|
+
msort(Values, Sorted),
|
|
375
|
+
sort(Values, Unique),
|
|
376
|
+
Sorted == Unique.
|
|
377
|
+
|
|
378
|
+
% field(+Dict, +Key, -Value): the value, or '$missing' when absent.
|
|
379
|
+
field(Dict, Key, Value) :-
|
|
380
|
+
( is_dict(Dict), get_dict(Key, Dict, Value0)
|
|
381
|
+
-> Value = Value0
|
|
382
|
+
; Value = '$missing'
|
|
383
|
+
).
|
|
384
|
+
|
|
385
|
+
result_row(Row) :-
|
|
386
|
+
is_dict(Row),
|
|
387
|
+
get_dict(member, Row, _),
|
|
388
|
+
get_dict(outcome, Row, _).
|
|
389
|
+
|
|
390
|
+
arithmetic_constraint(X, Row, c(Op, X, Value)) :-
|
|
391
|
+
is_dict(Row),
|
|
392
|
+
get_dict(operator, Row, OpString),
|
|
393
|
+
string(OpString),
|
|
394
|
+
atom_string(Op, OpString),
|
|
395
|
+
memberchk(Op, [eq, neq, lt, lte, gt, gte]),
|
|
396
|
+
get_dict(value, Row, Value),
|
|
397
|
+
number(Value).
|
|
398
|
+
|
|
399
|
+
% "count = 4", for naming an assumption in a rejection.
|
|
400
|
+
constraint_text(Variable, Row, Text) :-
|
|
401
|
+
get_dict(operator, Row, OpString),
|
|
402
|
+
get_dict(value, Row, Value),
|
|
403
|
+
atom_string(Op, OpString),
|
|
404
|
+
operator_symbol(Op, Symbol),
|
|
405
|
+
format(atom(Text), "~w ~w ~w", [Variable, Symbol, Value]).
|
|
406
|
+
|
|
407
|
+
operator_symbol(eq, '=').
|
|
408
|
+
operator_symbol(neq, '!=').
|
|
409
|
+
operator_symbol(lt, '<').
|
|
410
|
+
operator_symbol(lte, '<=').
|
|
411
|
+
operator_symbol(gt, '>').
|
|
412
|
+
operator_symbol(gte, '>=').
|
|
413
|
+
|
|
414
|
+
% arithmetic_counterexample(+Type, +Assumptions, +Claim, -Value): a value
|
|
415
|
+
% next to one of the constants that satisfies every assumption and breaks the
|
|
416
|
+
% claim, so a rejection can name it (a count of 5 against "exactly 4").
|
|
417
|
+
arithmetic_counterexample(Type, Assumptions, Claim, Value) :-
|
|
418
|
+
findall(C, (member(Row, [Claim|Assumptions]), get_dict(value, Row, C)), Constants),
|
|
419
|
+
( Type == "int" -> Step = 1 ; Step = 0.5 ),
|
|
420
|
+
findall(V,
|
|
421
|
+
( member(C, [0|Constants]),
|
|
422
|
+
member(D, [0, -1, 1]),
|
|
423
|
+
V is C + D*Step,
|
|
424
|
+
( Type == "int" -> integer(V) ; true )
|
|
425
|
+
),
|
|
426
|
+
Candidates0),
|
|
427
|
+
sort(Candidates0, Candidates),
|
|
428
|
+
member(Value, Candidates),
|
|
429
|
+
forall(member(Row, Assumptions), concrete_holds(Row, Value)),
|
|
430
|
+
\+ concrete_holds(Claim, Value),
|
|
431
|
+
!.
|
|
432
|
+
|
|
433
|
+
concrete_holds(Row, X) :-
|
|
434
|
+
arithmetic_constraint(Y, Row, c(Op, Y, V)),
|
|
435
|
+
concrete_op(Op, X, V).
|
|
436
|
+
|
|
437
|
+
concrete_op(eq, X, V) :- X =:= V.
|
|
438
|
+
concrete_op(neq, X, V) :- X =\= V.
|
|
439
|
+
concrete_op(lt, X, V) :- X < V.
|
|
440
|
+
concrete_op(lte, X, V) :- X =< V.
|
|
441
|
+
concrete_op(gt, X, V) :- X > V.
|
|
442
|
+
concrete_op(gte, X, V) :- X >= V.
|
|
443
|
+
|
|
444
|
+
integer_variables("int", X, [X]).
|
|
445
|
+
integer_variables("real", _, []).
|
|
446
|
+
|
|
447
|
+
nonempty_string(Value) :-
|
|
448
|
+
string(Value),
|
|
449
|
+
Value \== "".
|
|
450
|
+
|
|
451
|
+
sha256_string(Value) :-
|
|
452
|
+
string(Value),
|
|
453
|
+
string_length(Value, 64),
|
|
454
|
+
string_codes(Value, Codes),
|
|
455
|
+
forall(member(C, Codes), (code_type(C, digit) ; between(0'a, 0'f, C))).
|
|
456
|
+
|
|
457
|
+
% The canonical JSON is spelled out here rather than left to json_write, so
|
|
458
|
+
% it does not depend on the stream encoding or on how a key was read (a YAML
|
|
459
|
+
% key and a JSON key for the same text are the same key): object keys sorted
|
|
460
|
+
% by code point, no whitespace, ", \ and the JSON control escapes, other
|
|
461
|
+
% control characters and every non-ASCII character as \u escapes of their
|
|
462
|
+
% UTF-16 code units, integers in decimal.
|
|
463
|
+
canonical_json(Value, Json) :-
|
|
464
|
+
is_dict(Value), !,
|
|
465
|
+
dict_pairs(Value, _, Pairs0),
|
|
466
|
+
maplist(string_keyed, Pairs0, Pairs1),
|
|
467
|
+
keysort(Pairs1, Pairs),
|
|
468
|
+
maplist(canonical_pair, Pairs, Parts),
|
|
469
|
+
atomics_to_string(Parts, ",", Body),
|
|
470
|
+
format(string(Json), "{~s}", [Body]).
|
|
471
|
+
canonical_json(Value, Json) :-
|
|
472
|
+
is_list(Value), !,
|
|
473
|
+
maplist(canonical_json, Value, Parts),
|
|
474
|
+
atomics_to_string(Parts, ",", Body),
|
|
475
|
+
format(string(Json), "[~s]", [Body]).
|
|
476
|
+
canonical_json(Value, Json) :-
|
|
477
|
+
string(Value), !,
|
|
478
|
+
canonical_string(Value, Json).
|
|
479
|
+
canonical_json(Value, Json) :-
|
|
480
|
+
integer(Value), !,
|
|
481
|
+
format(string(Json), "~d", [Value]).
|
|
482
|
+
canonical_json(Value, Json) :-
|
|
483
|
+
memberchk(Value, [true, false, null]), !,
|
|
484
|
+
atom_string(Value, Json).
|
|
485
|
+
canonical_json(Value, Json) :-
|
|
486
|
+
atom(Value), !,
|
|
487
|
+
atom_string(Value, String),
|
|
488
|
+
canonical_string(String, Json).
|
|
489
|
+
canonical_json(Value, Json) :-
|
|
490
|
+
with_output_to(string(Json), json_write(current_output, Value, [width(0)])).
|
|
491
|
+
|
|
492
|
+
% Keys as code lists, so keysort orders them by code point whatever the
|
|
493
|
+
% reader made of them (an atom, or a small integer for a numeric key).
|
|
494
|
+
string_keyed(Key-Value, Codes-Value) :-
|
|
495
|
+
format(string(String), "~w", [Key]),
|
|
496
|
+
string_codes(String, Codes).
|
|
497
|
+
|
|
498
|
+
canonical_pair(Codes-Value, Json) :-
|
|
499
|
+
string_codes(Key, Codes),
|
|
500
|
+
canonical_string(Key, KeyJson),
|
|
501
|
+
canonical_json(Value, ValueJson),
|
|
502
|
+
format(string(Json), "~s:~s", [KeyJson, ValueJson]).
|
|
503
|
+
|
|
504
|
+
canonical_string(String, Json) :-
|
|
505
|
+
string_codes(String, Codes),
|
|
506
|
+
phrase(escaped_codes(Codes), Escaped),
|
|
507
|
+
string_codes(Body, Escaped),
|
|
508
|
+
format(string(Json), "\"~s\"", [Body]).
|
|
509
|
+
|
|
510
|
+
escaped_codes([]) --> [].
|
|
511
|
+
escaped_codes([C|Cs]) --> escaped_code(C), escaped_codes(Cs).
|
|
512
|
+
|
|
513
|
+
escaped_code(0'") --> !, "\\\"".
|
|
514
|
+
escaped_code(0'\\) --> !, "\\\\".
|
|
515
|
+
escaped_code(0'\b) --> !, "\\b".
|
|
516
|
+
escaped_code(0'\f) --> !, "\\f".
|
|
517
|
+
escaped_code(0'\n) --> !, "\\n".
|
|
518
|
+
escaped_code(0'\r) --> !, "\\r".
|
|
519
|
+
escaped_code(0'\t) --> !, "\\t".
|
|
520
|
+
escaped_code(C) --> { C >= 0x20, C =< 0x7e }, !, [C].
|
|
521
|
+
escaped_code(C) -->
|
|
522
|
+
{ C > 0xffff }, !,
|
|
523
|
+
{ V is C - 0x10000,
|
|
524
|
+
High is 0xd800 + (V >> 10),
|
|
525
|
+
Low is 0xdc00 + (V /\ 0x3ff) },
|
|
526
|
+
unit_escape(High),
|
|
527
|
+
unit_escape(Low).
|
|
528
|
+
escaped_code(C) --> unit_escape(C).
|
|
529
|
+
|
|
530
|
+
unit_escape(Unit) -->
|
|
531
|
+
{ format(codes(Codes), "\\u~|~`0t~16r~4+", [Unit]) },
|
|
532
|
+
Codes.
|