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,728 @@
|
|
|
1
|
+
% Module: model_check
|
|
2
|
+
% implements REQ-kibi-non-numeric-proof-kinds
|
|
3
|
+
%
|
|
4
|
+
% Decides a claim's obligation against the finite model a rule or
|
|
5
|
+
% state_machine certificate records, by enumerating every case. Nothing is
|
|
6
|
+
% sampled: a rule holds only when it holds for every assignment of its
|
|
7
|
+
% variables over their domains, and a failure names the assignment that
|
|
8
|
+
% breaks it.
|
|
9
|
+
%
|
|
10
|
+
% rule The model is the certificate's domains (one list of values
|
|
11
|
+
% per argument type) and its predicate tables (the value the
|
|
12
|
+
% code's function returned for every combination of its
|
|
13
|
+
% argument domains). Kibi evaluated both from the code at
|
|
14
|
+
% ingest, so the tables are the code's own decisions. A
|
|
15
|
+
% domain's type is also a predicate of one argument that
|
|
16
|
+
% holds for its members, so a rule can range its variables
|
|
17
|
+
% with document_status(D).
|
|
18
|
+
% state_machine The model is the machine's states and transitions. Its
|
|
19
|
+
% predicates are transition(From, To), reachable(From, To)
|
|
20
|
+
% (one or more steps), initial(State), terminal(State) and
|
|
21
|
+
% state(State); every variable ranges over the states.
|
|
22
|
+
%
|
|
23
|
+
% Obligations (claim_obligations.pl derives them from the claim's fact):
|
|
24
|
+
%
|
|
25
|
+
% rule_holds kibi.logic.v1 rule: for every assignment, when the body
|
|
26
|
+
% holds and no exception applies, the head has the value
|
|
27
|
+
% the modality requires (true for assert, oblige and
|
|
28
|
+
% permit; false for deny and forbid). An atom rule's head
|
|
29
|
+
% must have it for every assignment; a constraint's body
|
|
30
|
+
% must.
|
|
31
|
+
% predicate_holds A ground predicate fact: the code's value is the fact's
|
|
32
|
+
% polarity (assert true, deny false).
|
|
33
|
+
% every_member A set fact whose property names a predicate: the
|
|
34
|
+
% predicate is true for every member.
|
|
35
|
+
% machine_set A set fact naming a machine projection (transitions,
|
|
36
|
+
% states, initial_states, terminal_states,
|
|
37
|
+
% reachable_states, reachable_pairs): the projection is
|
|
38
|
+
% exactly the set.
|
|
39
|
+
% machine_relation The projection stands in the fact operator's relation
|
|
40
|
+
% to the set (includes, subset_of, excludes, eq).
|
|
41
|
+
% reachable_pairs are "from->to" pairs joined by a path
|
|
42
|
+
% of one or more transitions; a required waypoint
|
|
43
|
+
% "from->to via mid" holds when the target is reachable
|
|
44
|
+
% from the start and every such path passes through mid.
|
|
45
|
+
% source The model is what Kibi read from a project source (the
|
|
46
|
+
% source kind): its members, and the tables, objects or
|
|
47
|
+
% routes. set_relation compares the members with the
|
|
48
|
+
% fact's set; required_fields and nullable_fields check
|
|
49
|
+
% each selected table or object; route_middleware checks
|
|
50
|
+
% each selected route's middleware.
|
|
51
|
+
%
|
|
52
|
+
% Rules with explicit exists quantifiers, count or temporal expressions, or
|
|
53
|
+
% durations, timestamps and intervals are outside what Kibi enumerates and
|
|
54
|
+
% are refused with a reason.
|
|
55
|
+
|
|
56
|
+
:- module(model_check, [
|
|
57
|
+
model_check_obligation/4,
|
|
58
|
+
machine_projection/3,
|
|
59
|
+
edge_text/3
|
|
60
|
+
]).
|
|
61
|
+
|
|
62
|
+
:- use_module(library(apply)).
|
|
63
|
+
:- use_module(library(lists)).
|
|
64
|
+
:- use_module(library(assoc)).
|
|
65
|
+
:- use_module(library(pairs)).
|
|
66
|
+
:- use_module(library(ordsets)).
|
|
67
|
+
:- use_module(library(yall)).
|
|
68
|
+
:- use_module('kb.pl', [kb_entity/3]).
|
|
69
|
+
:- use_module('logic_ir.pl', [logic_rule_from_props/2]).
|
|
70
|
+
|
|
71
|
+
% The most assignments one rule may need; beyond it Kibi refuses to decide.
|
|
72
|
+
max_assignments(100000).
|
|
73
|
+
|
|
74
|
+
%% model_check_obligation(+Kind, +Obligation, +Certificate, -Result)
|
|
75
|
+
% Kind is rule or state_machine, Obligation and Certificate are dicts.
|
|
76
|
+
% Result is ok or problem(Reason) with Reason a string.
|
|
77
|
+
model_check_obligation(Kind, Obligation, Certificate, Result) :-
|
|
78
|
+
catch(check_obligation(Kind, Obligation, Certificate, Result0),
|
|
79
|
+
model_error(Reason),
|
|
80
|
+
Result0 = problem(Reason)),
|
|
81
|
+
!,
|
|
82
|
+
Result = Result0.
|
|
83
|
+
model_check_obligation(_, _, _, problem("Kibi could not check the obligation against the certificate's model")).
|
|
84
|
+
|
|
85
|
+
check_obligation(Kind, Obligation, Certificate, Result) :-
|
|
86
|
+
certificate_model(Kind, Certificate, Model),
|
|
87
|
+
dict_text(Obligation, kind, ObligationKind),
|
|
88
|
+
obligation_result(ObligationKind, Obligation, Model, Result).
|
|
89
|
+
|
|
90
|
+
% ---------------------------------------------------------------------------
|
|
91
|
+
% Models
|
|
92
|
+
|
|
93
|
+
certificate_model(rule, Certificate, model(Domains, Table, none)) :-
|
|
94
|
+
!,
|
|
95
|
+
required(Certificate, domains, DomainDict),
|
|
96
|
+
required(Certificate, predicates, PredicateDict),
|
|
97
|
+
dict_pairs(DomainDict, _, DomainPairs),
|
|
98
|
+
maplist(domain_entry, DomainPairs, Domains0),
|
|
99
|
+
list_to_assoc(Domains0, Domains),
|
|
100
|
+
dict_pairs(PredicateDict, _, PredicatePairs),
|
|
101
|
+
foldl(predicate_rows, PredicatePairs, [], Rows),
|
|
102
|
+
list_to_assoc(Rows, Table).
|
|
103
|
+
certificate_model(state_machine, Certificate, model(any(States), Table, machine(States, Edges, Initial))) :-
|
|
104
|
+
!,
|
|
105
|
+
required(Certificate, machine, Machine),
|
|
106
|
+
required(Machine, states, RawStates),
|
|
107
|
+
maplist(text_atom, RawStates, States0),
|
|
108
|
+
sort(States0, States),
|
|
109
|
+
required(Machine, transitions, RawEdges),
|
|
110
|
+
maplist(edge_pair, RawEdges, Edges0),
|
|
111
|
+
sort(Edges0, Edges),
|
|
112
|
+
( get_dict(initial, Machine, RawInitial)
|
|
113
|
+
-> maplist(text_atom, RawInitial, Initial0),
|
|
114
|
+
sort(Initial0, Initial)
|
|
115
|
+
; Initial = none
|
|
116
|
+
),
|
|
117
|
+
machine_table(States, Edges, Initial, Table).
|
|
118
|
+
certificate_model(source, Certificate, model(none, none, source(Members, Entities, Routes))) :-
|
|
119
|
+
!,
|
|
120
|
+
required(Certificate, model, Model),
|
|
121
|
+
required(Model, members, RawMembers),
|
|
122
|
+
maplist(text_string, RawMembers, Members0),
|
|
123
|
+
sort(Members0, Members),
|
|
124
|
+
( get_dict(entities, Model, Entities) -> true ; Entities = none ),
|
|
125
|
+
( get_dict(routes, Model, Routes) -> true ; Routes = none ).
|
|
126
|
+
certificate_model(Kind, _, _) :-
|
|
127
|
+
format(string(Reason), "a ~w certificate has no finite model to check", [Kind]),
|
|
128
|
+
throw(model_error(Reason)).
|
|
129
|
+
|
|
130
|
+
required(Dict, Key, Value) :-
|
|
131
|
+
( is_dict(Dict), get_dict(Key, Dict, Value)
|
|
132
|
+
-> true
|
|
133
|
+
; format(string(Reason), "the certificate has no ~w", [Key]),
|
|
134
|
+
throw(model_error(Reason))
|
|
135
|
+
).
|
|
136
|
+
|
|
137
|
+
domain_entry(Type0-Entry, Type-Members) :-
|
|
138
|
+
text_atom(Type0, Type),
|
|
139
|
+
required(Entry, members, Raw),
|
|
140
|
+
maplist(text_atom, Raw, Members).
|
|
141
|
+
|
|
142
|
+
predicate_rows(Name0-Entry, Acc0, Acc) :-
|
|
143
|
+
text_atom(Name0, Name),
|
|
144
|
+
required(Entry, rows, Rows),
|
|
145
|
+
foldl(predicate_row(Name), Rows, Acc0, Acc).
|
|
146
|
+
|
|
147
|
+
predicate_row(Name, Row, Acc, [(Name-Args)-Value|Acc]) :-
|
|
148
|
+
required(Row, args, RawArgs),
|
|
149
|
+
maplist(text_atom, RawArgs, Args),
|
|
150
|
+
required(Row, value, RawValue),
|
|
151
|
+
boolean_value(RawValue, Value).
|
|
152
|
+
|
|
153
|
+
boolean_value(true, true) :- !.
|
|
154
|
+
boolean_value(false, false) :- !.
|
|
155
|
+
boolean_value("true", true) :- !.
|
|
156
|
+
boolean_value("false", false) :- !.
|
|
157
|
+
boolean_value(_, _) :-
|
|
158
|
+
throw(model_error("a predicate table holds a value that is not a boolean")).
|
|
159
|
+
|
|
160
|
+
edge_pair(Raw, From-To) :-
|
|
161
|
+
( is_list(Raw), Raw = [RawFrom, RawTo]
|
|
162
|
+
-> text_atom(RawFrom, From),
|
|
163
|
+
text_atom(RawTo, To)
|
|
164
|
+
; throw(model_error("a transition is not a [from, to] pair"))
|
|
165
|
+
).
|
|
166
|
+
|
|
167
|
+
% The machine's predicates as one table over its states.
|
|
168
|
+
machine_table(States, Edges, Initial, Table) :-
|
|
169
|
+
closure_pairs(States, Edges, Reachable),
|
|
170
|
+
findall((transition-[A, B])-V,
|
|
171
|
+
( member(A, States), member(B, States), truth(memberchk(A-B, Edges), V) ),
|
|
172
|
+
TransitionRows),
|
|
173
|
+
findall((reachable-[A, B])-V,
|
|
174
|
+
( member(A, States), member(B, States), truth(memberchk(A-B, Reachable), V) ),
|
|
175
|
+
ReachableRows),
|
|
176
|
+
findall((terminal-[S])-V,
|
|
177
|
+
( member(S, States), truth(\+ memberchk(S-_, Edges), V) ),
|
|
178
|
+
TerminalRows),
|
|
179
|
+
findall((state-[S])-true, member(S, States), StateRows),
|
|
180
|
+
( Initial == none
|
|
181
|
+
-> InitialRows = []
|
|
182
|
+
; findall((initial-[S])-V, ( member(S, States), truth(memberchk(S, Initial), V) ), InitialRows)
|
|
183
|
+
),
|
|
184
|
+
append([TransitionRows, ReachableRows, TerminalRows, StateRows, InitialRows], Rows),
|
|
185
|
+
list_to_assoc(Rows, Table).
|
|
186
|
+
|
|
187
|
+
truth(Goal, Value) :- ( call(Goal) -> Value = true ; Value = false ).
|
|
188
|
+
|
|
189
|
+
% Pairs connected by a path of one or more transitions.
|
|
190
|
+
closure_pairs(States, Edges, Pairs) :-
|
|
191
|
+
findall(A-B, ( member(A, States), reach_from(A, Edges, Reached), member(B, Reached) ), Pairs0),
|
|
192
|
+
sort(Pairs0, Pairs).
|
|
193
|
+
|
|
194
|
+
reach_from(Start, Edges, Reached) :-
|
|
195
|
+
findall(Next, member(Start-Next, Edges), First0),
|
|
196
|
+
sort(First0, First),
|
|
197
|
+
reach_frontier(First, Edges, First, Reached).
|
|
198
|
+
|
|
199
|
+
reach_frontier([], _, Seen, Seen).
|
|
200
|
+
reach_frontier([State|Rest], Edges, Seen0, Seen) :-
|
|
201
|
+
findall(Next, ( member(State-Next, Edges), \+ memberchk(Next, Seen0) ), New0),
|
|
202
|
+
sort(New0, New),
|
|
203
|
+
ord_union(Seen0, New, Seen1),
|
|
204
|
+
append(Rest, New, Frontier),
|
|
205
|
+
reach_frontier(Frontier, Edges, Seen1, Seen).
|
|
206
|
+
|
|
207
|
+
%% machine_projection(+Projection, +Machine, -Members)
|
|
208
|
+
% The machine's states or transitions a set fact's property names, as
|
|
209
|
+
% sorted strings (a transition spelled "from->to").
|
|
210
|
+
machine_projection(transitions, machine(_, Edges, _), Members) :- !,
|
|
211
|
+
maplist([From-To, Text]>>edge_text(From, To, Text), Edges, Members0),
|
|
212
|
+
sort(Members0, Members).
|
|
213
|
+
machine_projection(states, machine(States, _, _), Members) :- !,
|
|
214
|
+
maplist(atom_string, States, Members).
|
|
215
|
+
machine_projection(initial_states, machine(_, _, Initial), Members) :- !,
|
|
216
|
+
( Initial == none
|
|
217
|
+
-> throw(model_error("the certificate names no initial state; set proof_contract.initial_source"))
|
|
218
|
+
; maplist(atom_string, Initial, Members)
|
|
219
|
+
).
|
|
220
|
+
machine_projection(terminal_states, machine(States, Edges, _), Members) :- !,
|
|
221
|
+
findall(S, ( member(S, States), \+ memberchk(S-_, Edges) ), Terminal),
|
|
222
|
+
maplist(atom_string, Terminal, Members).
|
|
223
|
+
machine_projection(reachable_states, machine(States, Edges, Initial), Members) :- !,
|
|
224
|
+
( Initial == none
|
|
225
|
+
-> throw(model_error("the certificate names no initial state; set proof_contract.initial_source"))
|
|
226
|
+
; true
|
|
227
|
+
),
|
|
228
|
+
findall(S, ( member(I, Initial), reach_from(I, Edges, Reached), member(S, Reached) ), Reached0),
|
|
229
|
+
append(Initial, Reached0, All0),
|
|
230
|
+
sort(All0, All),
|
|
231
|
+
include(member_of(States), All, Kept),
|
|
232
|
+
maplist(atom_string, Kept, Members).
|
|
233
|
+
|
|
234
|
+
machine_projection(reachable_pairs, machine(States, Edges, _), Members) :- !,
|
|
235
|
+
closure_pairs(States, Edges, Pairs),
|
|
236
|
+
maplist([From-To, Text]>>edge_text(From, To, Text), Pairs, Members0),
|
|
237
|
+
sort(Members0, Members).
|
|
238
|
+
|
|
239
|
+
edge_text(From, To, Text) :-
|
|
240
|
+
format(string(Text), "~w->~w", [From, To]).
|
|
241
|
+
|
|
242
|
+
% implements REQ-kibi-source-proof-kinds
|
|
243
|
+
% Whether "from->to via mid" holds: to is reachable from from, and not once
|
|
244
|
+
% mid is taken out of the machine.
|
|
245
|
+
waypoint_holds(Member, machine(States, Edges, _), Holds, Why) :-
|
|
246
|
+
( sub_string(Member, Before, _, After, " via "),
|
|
247
|
+
sub_string(Member, 0, Before, _, EdgeText),
|
|
248
|
+
sub_string(Member, _, After, 0, ViaText),
|
|
249
|
+
sub_string(EdgeText, B2, _, A2, "->")
|
|
250
|
+
-> sub_string(EdgeText, 0, B2, _, FromText),
|
|
251
|
+
sub_string(EdgeText, _, A2, 0, ToText),
|
|
252
|
+
maplist([T, A]>>atom_string(A, T), [FromText, ToText, ViaText], [From, To, Via])
|
|
253
|
+
; format(string(Reason), "~w is not spelled \"from->to via state\"", [Member]),
|
|
254
|
+
throw(model_error(Reason))
|
|
255
|
+
),
|
|
256
|
+
forall(member(S, [From, To, Via]),
|
|
257
|
+
( memberchk(S, States)
|
|
258
|
+
-> true
|
|
259
|
+
; format(string(Reason), "~w is not a state of the machine", [S]),
|
|
260
|
+
throw(model_error(Reason))
|
|
261
|
+
)),
|
|
262
|
+
reach_from(From, Edges, Reached),
|
|
263
|
+
( \+ memberchk(To, Reached)
|
|
264
|
+
-> Holds = false,
|
|
265
|
+
format(string(Why), "~w is never reachable from ~w, so no path passes through ~w", [To, From, Via])
|
|
266
|
+
; memberchk(Via, [From, To])
|
|
267
|
+
-> Holds = true, Why = ""
|
|
268
|
+
; exclude(edge_touches(Via), Edges, Without),
|
|
269
|
+
reach_from(From, Without, Avoiding),
|
|
270
|
+
( memberchk(To, Avoiding)
|
|
271
|
+
-> Holds = false,
|
|
272
|
+
format(string(Why), "a path from ~w reaches ~w without passing through ~w", [From, To, Via])
|
|
273
|
+
; Holds = true, Why = ""
|
|
274
|
+
)
|
|
275
|
+
).
|
|
276
|
+
|
|
277
|
+
edge_touches(State, From-To) :- ( From == State ; To == State ).
|
|
278
|
+
|
|
279
|
+
% relation_problem(+Operator, +Actual, +Expected, +What, -Reason) fails when
|
|
280
|
+
% the relation holds; Actual and Expected are sorted lists of strings.
|
|
281
|
+
relation_problem("eq", Actual, Expected, What, Reason) :-
|
|
282
|
+
Actual \== Expected,
|
|
283
|
+
subtract(Expected, Actual, Missing),
|
|
284
|
+
subtract(Actual, Expected, Extra),
|
|
285
|
+
set_difference_text(Missing, Extra, Difference),
|
|
286
|
+
format(string(Reason), "~w are not the fact's set (~w)", [What, Difference]).
|
|
287
|
+
relation_problem("includes", Actual, Expected, What, Reason) :-
|
|
288
|
+
subtract(Expected, Actual, Missing),
|
|
289
|
+
Missing \== [],
|
|
290
|
+
atomic_list_concat(Missing, ', ', Text),
|
|
291
|
+
format(string(Reason), "~w do not include ~w", [What, Text]).
|
|
292
|
+
relation_problem("subset_of", Actual, Expected, What, Reason) :-
|
|
293
|
+
subtract(Actual, Expected, Extra),
|
|
294
|
+
Extra \== [],
|
|
295
|
+
atomic_list_concat(Extra, ', ', Text),
|
|
296
|
+
format(string(Reason), "~w include ~w, which the fact's set does not allow", [What, Text]).
|
|
297
|
+
relation_problem("excludes", Actual, Expected, What, Reason) :-
|
|
298
|
+
intersection(Actual, Expected, Shared),
|
|
299
|
+
Shared \== [],
|
|
300
|
+
atomic_list_concat(Shared, ', ', Text),
|
|
301
|
+
format(string(Reason), "~w include ~w, which the fact excludes", [What, Text]).
|
|
302
|
+
|
|
303
|
+
% ---------------------------------------------------------------------------
|
|
304
|
+
% Obligations
|
|
305
|
+
|
|
306
|
+
obligation_result("rule_holds", Obligation, Model, Result) :- !,
|
|
307
|
+
dict_text(Obligation, fact_id, FactText),
|
|
308
|
+
atom_string(FactId, FactText),
|
|
309
|
+
( kb_entity(FactId, fact, Props),
|
|
310
|
+
logic_rule_from_props(Props, Rule)
|
|
311
|
+
-> rule_result(Rule, Model, Result)
|
|
312
|
+
; format(string(Reason), "the rule_ir of ~w cannot be read", [FactId]),
|
|
313
|
+
Result = problem(Reason)
|
|
314
|
+
).
|
|
315
|
+
obligation_result("predicate_holds", Obligation, model(_, Table, _), Result) :- !,
|
|
316
|
+
dict_text(Obligation, predicate, NameText),
|
|
317
|
+
atom_string(Name, NameText),
|
|
318
|
+
get_dict(args, Obligation, RawArgs),
|
|
319
|
+
maplist(text_atom, RawArgs, Args),
|
|
320
|
+
get_dict(value, Obligation, RawExpected),
|
|
321
|
+
boolean_value(RawExpected, Expected),
|
|
322
|
+
table_value(Table, Name, Args, Actual),
|
|
323
|
+
( Actual == Expected
|
|
324
|
+
-> Result = ok
|
|
325
|
+
; atomic_list_concat(Args, ', ', ArgText),
|
|
326
|
+
format(string(Reason), "the code decides ~w(~w) is ~w, but the fact says ~w",
|
|
327
|
+
[Name, ArgText, Actual, Expected]),
|
|
328
|
+
Result = problem(Reason)
|
|
329
|
+
).
|
|
330
|
+
obligation_result("every_member", Obligation, model(_, Table, _), Result) :- !,
|
|
331
|
+
dict_text(Obligation, predicate, NameText),
|
|
332
|
+
atom_string(Name, NameText),
|
|
333
|
+
get_dict(members, Obligation, RawMembers),
|
|
334
|
+
maplist(text_atom, RawMembers, Members),
|
|
335
|
+
( member(Member, Members),
|
|
336
|
+
table_value(Table, Name, [Member], Value),
|
|
337
|
+
Value \== true
|
|
338
|
+
-> format(string(Reason), "the code decides ~w(~w) is false, so not every member of the set satisfies ~w",
|
|
339
|
+
[Name, Member, Name]),
|
|
340
|
+
Result = problem(Reason)
|
|
341
|
+
; Result = ok
|
|
342
|
+
).
|
|
343
|
+
obligation_result("machine_set", Obligation, model(_, _, Machine), Result) :- !,
|
|
344
|
+
( Machine == none
|
|
345
|
+
-> throw(model_error("a machine_set obligation needs a state_machine certificate"))
|
|
346
|
+
; true
|
|
347
|
+
),
|
|
348
|
+
dict_text(Obligation, projection, ProjectionText),
|
|
349
|
+
atom_string(Projection, ProjectionText),
|
|
350
|
+
get_dict(members, Obligation, RawMembers),
|
|
351
|
+
maplist(text_string, RawMembers, Expected0),
|
|
352
|
+
sort(Expected0, Expected),
|
|
353
|
+
machine_projection(Projection, Machine, Actual0),
|
|
354
|
+
sort(Actual0, Actual),
|
|
355
|
+
( Actual == Expected
|
|
356
|
+
-> Result = ok
|
|
357
|
+
; subtract(Expected, Actual, Missing),
|
|
358
|
+
subtract(Actual, Expected, Extra),
|
|
359
|
+
set_difference_text(Missing, Extra, Difference),
|
|
360
|
+
format(string(Reason), "the machine's ~w are not the fact's set (~w)", [Projection, Difference]),
|
|
361
|
+
Result = problem(Reason)
|
|
362
|
+
).
|
|
363
|
+
obligation_result("machine_relation", Obligation, model(_, _, Machine), Result) :- !,
|
|
364
|
+
( Machine = machine(_, _, _)
|
|
365
|
+
-> true
|
|
366
|
+
; throw(model_error("a machine_relation obligation needs a state_machine certificate"))
|
|
367
|
+
),
|
|
368
|
+
dict_text(Obligation, projection, ProjectionText),
|
|
369
|
+
atom_string(Projection, ProjectionText),
|
|
370
|
+
dict_text(Obligation, operator, Operator),
|
|
371
|
+
get_dict(members, Obligation, RawMembers),
|
|
372
|
+
maplist(text_string, RawMembers, Expected0),
|
|
373
|
+
sort(Expected0, Expected),
|
|
374
|
+
( Projection == required_waypoints
|
|
375
|
+
-> ( member(Member, Expected),
|
|
376
|
+
waypoint_holds(Member, Machine, Holds, Why),
|
|
377
|
+
( Operator == "excludes"
|
|
378
|
+
-> Holds == true,
|
|
379
|
+
format(string(Reason), "every path ~w, which the fact excludes", [Member])
|
|
380
|
+
; Holds == false,
|
|
381
|
+
Reason = Why
|
|
382
|
+
)
|
|
383
|
+
-> Result = problem(Reason)
|
|
384
|
+
; Result = ok
|
|
385
|
+
)
|
|
386
|
+
; machine_projection(Projection, Machine, Actual0),
|
|
387
|
+
sort(Actual0, Actual),
|
|
388
|
+
format(string(What), "the machine's ~w", [Projection]),
|
|
389
|
+
( relation_problem(Operator, Actual, Expected, What, Reason)
|
|
390
|
+
-> Result = problem(Reason)
|
|
391
|
+
; Result = ok
|
|
392
|
+
)
|
|
393
|
+
).
|
|
394
|
+
obligation_result("set_relation", Obligation, model(_, _, source(Actual, _, _)), Result) :- !,
|
|
395
|
+
dict_text(Obligation, operator, Operator),
|
|
396
|
+
obligation_strings(Obligation, Expected),
|
|
397
|
+
( relation_problem(Operator, Actual, Expected, "the members Kibi read from the source", Reason)
|
|
398
|
+
-> Result = problem(Reason)
|
|
399
|
+
; Result = ok
|
|
400
|
+
).
|
|
401
|
+
obligation_result(FieldKind, Obligation, model(_, _, source(_, Entities, _)), Result) :-
|
|
402
|
+
memberchk(FieldKind, ["required_fields", "nullable_fields"]),
|
|
403
|
+
!,
|
|
404
|
+
( is_list(Entities), Entities \== []
|
|
405
|
+
-> true
|
|
406
|
+
; throw(model_error("the source has no tables or objects; read a schema or validator for a field claim"))
|
|
407
|
+
),
|
|
408
|
+
obligation_strings(Obligation, Expected),
|
|
409
|
+
( member(Member, Expected),
|
|
410
|
+
field_problem(FieldKind, Member, Entities, Reason)
|
|
411
|
+
-> Result = problem(Reason)
|
|
412
|
+
; Result = ok
|
|
413
|
+
).
|
|
414
|
+
obligation_result("route_middleware", Obligation, model(_, _, source(_, _, Routes)), Result) :- !,
|
|
415
|
+
( is_list(Routes), Routes \== []
|
|
416
|
+
-> true
|
|
417
|
+
; throw(model_error("the source has no routes; read a route table for a middleware claim"))
|
|
418
|
+
),
|
|
419
|
+
dict_text(Obligation, operator, Operator),
|
|
420
|
+
obligation_strings(Obligation, Expected),
|
|
421
|
+
( member(Route, Routes),
|
|
422
|
+
get_dict(middleware, Route, Raw),
|
|
423
|
+
maplist(text_string, Raw, Actual0),
|
|
424
|
+
sort(Actual0, Actual),
|
|
425
|
+
get_dict(method, Route, Method),
|
|
426
|
+
get_dict(path, Route, Path),
|
|
427
|
+
format(string(What), "the middleware of ~w ~w", [Method, Path]),
|
|
428
|
+
relation_problem(Operator, Actual, Expected, What, Reason)
|
|
429
|
+
-> Result = problem(Reason)
|
|
430
|
+
; Result = ok
|
|
431
|
+
).
|
|
432
|
+
obligation_result(Kind, _, _, problem(Reason)) :-
|
|
433
|
+
format(string(Reason), "a ~w obligation cannot be checked against a finite model", [Kind]).
|
|
434
|
+
|
|
435
|
+
obligation_strings(Obligation, Members) :-
|
|
436
|
+
get_dict(members, Obligation, Raw),
|
|
437
|
+
maplist(text_string, Raw, Members0),
|
|
438
|
+
sort(Members0, Members).
|
|
439
|
+
|
|
440
|
+
% A field member "name" applies to every selected table or object, and
|
|
441
|
+
% "table.name" to that table only.
|
|
442
|
+
field_problem(FieldKind, Member, Entities, Reason) :-
|
|
443
|
+
( sub_string(Member, Before, _, After, "."),
|
|
444
|
+
sub_string(Member, 0, Before, _, Table),
|
|
445
|
+
sub_string(Member, _, After, 0, Field),
|
|
446
|
+
\+ sub_string(Field, _, _, _, ".")
|
|
447
|
+
-> ( member(Entity, Entities), get_dict(name, Entity, Name), text_string(Name, Table)
|
|
448
|
+
-> Targets = [Entity]
|
|
449
|
+
; format(string(Reason), "the source has no table or object ~w", [Table]),
|
|
450
|
+
Targets = []
|
|
451
|
+
)
|
|
452
|
+
; Field = Member,
|
|
453
|
+
Targets = Entities
|
|
454
|
+
),
|
|
455
|
+
( nonvar(Reason)
|
|
456
|
+
-> true
|
|
457
|
+
; member(Entity, Targets),
|
|
458
|
+
get_dict(name, Entity, EntityName),
|
|
459
|
+
get_dict(columns, Entity, Columns),
|
|
460
|
+
( member(Column, Columns), get_dict(name, Column, ColumnName), text_string(ColumnName, Field)
|
|
461
|
+
-> get_dict(nullable, Column, Nullable),
|
|
462
|
+
get_dict(optional, Column, Optional),
|
|
463
|
+
( FieldKind == "required_fields",
|
|
464
|
+
( Nullable == true ; Optional == true )
|
|
465
|
+
-> ( Nullable == true -> How = nullable ; How = optional ),
|
|
466
|
+
format(string(Reason), "~w.~w is ~w, not required", [EntityName, Field, How])
|
|
467
|
+
; FieldKind == "nullable_fields",
|
|
468
|
+
Nullable \== true
|
|
469
|
+
-> format(string(Reason), "~w.~w is not nullable", [EntityName, Field])
|
|
470
|
+
)
|
|
471
|
+
; format(string(Reason), "~w has no field ~w", [EntityName, Field])
|
|
472
|
+
)
|
|
473
|
+
).
|
|
474
|
+
|
|
475
|
+
set_difference_text(Missing, Extra, Text) :-
|
|
476
|
+
findall(Part,
|
|
477
|
+
( Missing \== [], atomic_list_concat(Missing, ', ', M), format(string(Part), "missing ~w", [M])
|
|
478
|
+
; Extra \== [], atomic_list_concat(Extra, ', ', E), format(string(Part), "not in the set ~w", [E])
|
|
479
|
+
),
|
|
480
|
+
Parts),
|
|
481
|
+
atomic_list_concat(Parts, '; ', Text).
|
|
482
|
+
|
|
483
|
+
table_value(Table, Name, Args, Value) :-
|
|
484
|
+
( get_assoc(Name-Args, Table, Value0)
|
|
485
|
+
-> Value = Value0
|
|
486
|
+
; atomic_list_concat(Args, ', ', ArgText),
|
|
487
|
+
format(string(Reason),
|
|
488
|
+
"~w(~w) is not in the certificate's model: name the function behind ~w in proof_contract.predicate_source and make each argument's domain list ~w",
|
|
489
|
+
[Name, ArgText, Name, ArgText]),
|
|
490
|
+
throw(model_error(Reason))
|
|
491
|
+
).
|
|
492
|
+
|
|
493
|
+
% ---------------------------------------------------------------------------
|
|
494
|
+
% Rules
|
|
495
|
+
|
|
496
|
+
rule_result(rule(Kind, Modality, Head, Body, Exceptions, _Scope, _From, _To, _Schema, Declared), Model, Result) :-
|
|
497
|
+
( member(variable(Name, _, exists), Declared)
|
|
498
|
+
-> format(string(Reason), "the rule quantifies ~w existentially, which Kibi does not enumerate", [Name]),
|
|
499
|
+
throw(model_error(Reason))
|
|
500
|
+
; true
|
|
501
|
+
),
|
|
502
|
+
expected_value(Modality, Expected),
|
|
503
|
+
rule_variables(Head, Body, Exceptions, Declared, Variables),
|
|
504
|
+
Model = model(Domains, _, _),
|
|
505
|
+
maplist(variable_domain(Domains), Variables, Ranges),
|
|
506
|
+
assignment_count(Ranges, Count),
|
|
507
|
+
max_assignments(Max),
|
|
508
|
+
( Count > Max
|
|
509
|
+
-> format(string(Reason), "the rule has ~D assignments, more than the ~D Kibi enumerates", [Count, Max]),
|
|
510
|
+
throw(model_error(Reason))
|
|
511
|
+
; true
|
|
512
|
+
),
|
|
513
|
+
( assignment(Ranges, Env),
|
|
514
|
+
\+ rule_case_holds(Kind, Head, Body, Exceptions, Expected, Env, Model)
|
|
515
|
+
-> counterexample_text(Kind, Head, Body, Exceptions, Expected, Env, Model, Reason),
|
|
516
|
+
Result = problem(Reason)
|
|
517
|
+
; Result = ok
|
|
518
|
+
).
|
|
519
|
+
|
|
520
|
+
expected_value(Modality, true) :- memberchk(Modality, [assert, oblige, permit]), !.
|
|
521
|
+
expected_value(Modality, false) :- memberchk(Modality, [deny, forbid]), !.
|
|
522
|
+
|
|
523
|
+
rule_case_holds(atom, Head, _Body, Exceptions, Expected, Env, Model) :- !,
|
|
524
|
+
( any_exception(Exceptions, Env, Model)
|
|
525
|
+
-> true
|
|
526
|
+
; eval_atom(Head, Env, Model, Expected)
|
|
527
|
+
).
|
|
528
|
+
rule_case_holds(rule, Head, Body, Exceptions, Expected, Env, Model) :- !,
|
|
529
|
+
( Head == none
|
|
530
|
+
-> throw(model_error("the rule has no head"))
|
|
531
|
+
; true
|
|
532
|
+
),
|
|
533
|
+
( applies(Body, Exceptions, Env, Model)
|
|
534
|
+
-> eval_atom(Head, Env, Model, Expected)
|
|
535
|
+
; true
|
|
536
|
+
).
|
|
537
|
+
rule_case_holds(constraint, _Head, Body, Exceptions, Expected, Env, Model) :- !,
|
|
538
|
+
( Body == none
|
|
539
|
+
-> throw(model_error("the constraint has no body"))
|
|
540
|
+
; true
|
|
541
|
+
),
|
|
542
|
+
( any_exception(Exceptions, Env, Model)
|
|
543
|
+
-> true
|
|
544
|
+
; eval_expression(Body, Env, Model, Expected)
|
|
545
|
+
).
|
|
546
|
+
rule_case_holds(Kind, _, _, _, _, _, _) :-
|
|
547
|
+
format(string(Reason), "a ~w rule cannot be model checked", [Kind]),
|
|
548
|
+
throw(model_error(Reason)).
|
|
549
|
+
|
|
550
|
+
applies(Body, Exceptions, Env, Model) :-
|
|
551
|
+
( Body == none
|
|
552
|
+
-> true
|
|
553
|
+
; eval_expression(Body, Env, Model, true)
|
|
554
|
+
),
|
|
555
|
+
\+ any_exception(Exceptions, Env, Model).
|
|
556
|
+
|
|
557
|
+
any_exception(Exceptions, Env, Model) :-
|
|
558
|
+
member(Exception, Exceptions),
|
|
559
|
+
eval_expression(Exception, Env, Model, true),
|
|
560
|
+
!.
|
|
561
|
+
|
|
562
|
+
counterexample_text(_Kind, Head, _Body, _Exceptions, Expected, Env, Model, Reason) :-
|
|
563
|
+
assignment_text(Env, EnvText),
|
|
564
|
+
( Head \== none,
|
|
565
|
+
catch(eval_atom(Head, Env, Model, Actual), model_error(_), fail),
|
|
566
|
+
Actual \== Expected
|
|
567
|
+
-> atom_text(Head, Env, HeadText),
|
|
568
|
+
format(string(Reason), "the rule fails for ~w: the code decides ~w is ~w, but the rule requires ~w",
|
|
569
|
+
[EnvText, HeadText, Actual, Expected])
|
|
570
|
+
; format(string(Reason), "the rule fails for ~w: the constraint does not have the value ~w the rule requires",
|
|
571
|
+
[EnvText, Expected])
|
|
572
|
+
).
|
|
573
|
+
|
|
574
|
+
assignment_text([], "the only case") :- !.
|
|
575
|
+
assignment_text(Env, Text) :-
|
|
576
|
+
findall(Part, ( member(Name-Value, Env), format(string(Part), "~w=~w", [Name, Value]) ), Parts),
|
|
577
|
+
atomic_list_concat(Parts, ', ', Text).
|
|
578
|
+
|
|
579
|
+
atom_text(atom(_, Name, Args, Polarity, _), Env, Text) :-
|
|
580
|
+
maplist(resolve_atom_arg(Env), Args, Values),
|
|
581
|
+
atomic_list_concat(Values, ', ', ArgText),
|
|
582
|
+
( Polarity == negative
|
|
583
|
+
-> format(string(Text), "not ~w(~w)", [Name, ArgText])
|
|
584
|
+
; format(string(Text), "~w(~w)", [Name, ArgText])
|
|
585
|
+
).
|
|
586
|
+
|
|
587
|
+
% Variables, each with its declared or first-seen type, in name order.
|
|
588
|
+
rule_variables(Head, Body, Exceptions, Declared, Variables) :-
|
|
589
|
+
findall(Name-Type, ( member(variable(Name, Type, _), Declared) ), FromDeclared),
|
|
590
|
+
findall(Name-Type, ( member(Term, [Head, Body|Exceptions]), term_var(Term, Name, Type) ), FromTerms),
|
|
591
|
+
append(FromDeclared, FromTerms, All),
|
|
592
|
+
first_per_name(All, [], Variables0),
|
|
593
|
+
keysort(Variables0, Variables).
|
|
594
|
+
|
|
595
|
+
first_per_name([], Acc, Acc).
|
|
596
|
+
first_per_name([Name-Type|Rest], Acc, Out) :-
|
|
597
|
+
( memberchk(Name-_, Acc)
|
|
598
|
+
-> first_per_name(Rest, Acc, Out)
|
|
599
|
+
; first_per_name(Rest, [Name-Type|Acc], Out)
|
|
600
|
+
).
|
|
601
|
+
|
|
602
|
+
term_var(var(Name, Type), Name, Type) :- !.
|
|
603
|
+
term_var(Term, Name, Type) :-
|
|
604
|
+
compound(Term),
|
|
605
|
+
Term =.. [_|Args],
|
|
606
|
+
member(Arg, Args),
|
|
607
|
+
( is_list(Arg) -> member(Item, Arg), term_var(Item, Name, Type)
|
|
608
|
+
; term_var(Arg, Name, Type)
|
|
609
|
+
).
|
|
610
|
+
|
|
611
|
+
variable_domain(Domains, Name-Type, Name-Members) :-
|
|
612
|
+
( Domains = any(Members0)
|
|
613
|
+
-> Members = Members0
|
|
614
|
+
; get_assoc(Type, Domains, Members0)
|
|
615
|
+
-> Members = Members0
|
|
616
|
+
; format(string(Reason),
|
|
617
|
+
"variable ~w has type ~w, which has no finite domain: name the export listing its values in proof_contract.domains",
|
|
618
|
+
[Name, Type]),
|
|
619
|
+
throw(model_error(Reason))
|
|
620
|
+
).
|
|
621
|
+
|
|
622
|
+
assignment_count(Ranges, Count) :-
|
|
623
|
+
foldl([_-Members, A0, A]>>(length(Members, N), A is A0 * N), Ranges, 1, Count).
|
|
624
|
+
|
|
625
|
+
assignment([], []).
|
|
626
|
+
assignment([Name-Members|Rest], [Name-Value|Env]) :-
|
|
627
|
+
member(Value, Members),
|
|
628
|
+
assignment(Rest, Env).
|
|
629
|
+
|
|
630
|
+
eval_expression(atom(NS, Name, Args, Polarity, CW), Env, Model, Value) :- !,
|
|
631
|
+
eval_atom(atom(NS, Name, Args, Polarity, CW), Env, Model, Value).
|
|
632
|
+
eval_expression(all(Items), Env, Model, Value) :- !,
|
|
633
|
+
( member(Item, Items), eval_expression(Item, Env, Model, false)
|
|
634
|
+
-> Value = false
|
|
635
|
+
; Value = true
|
|
636
|
+
).
|
|
637
|
+
eval_expression(any(Items), Env, Model, Value) :- !,
|
|
638
|
+
( member(Item, Items), eval_expression(Item, Env, Model, true)
|
|
639
|
+
-> Value = true
|
|
640
|
+
; Value = false
|
|
641
|
+
).
|
|
642
|
+
eval_expression(not(Atom), Env, Model, Value) :- !,
|
|
643
|
+
eval_atom(Atom, Env, Model, Inner),
|
|
644
|
+
negate(Inner, Value).
|
|
645
|
+
eval_expression(compare(Op, Left, Right), Env, _Model, Value) :- !,
|
|
646
|
+
resolve_value(Left, Env, L),
|
|
647
|
+
resolve_value(Right, Env, R),
|
|
648
|
+
truth(compare_values(Op, L, R), Value).
|
|
649
|
+
eval_expression(Expression, _, _, _) :-
|
|
650
|
+
functor(Expression, Kind, _),
|
|
651
|
+
format(string(Reason), "a ~w expression is outside what Kibi model checks", [Kind]),
|
|
652
|
+
throw(model_error(Reason)).
|
|
653
|
+
|
|
654
|
+
eval_atom(atom(_NS, Name, Args, Polarity, _CW), Env, model(Domains, Table, _), Value) :-
|
|
655
|
+
maplist(resolve_atom_arg(Env), Args, Values),
|
|
656
|
+
( Values = [Member],
|
|
657
|
+
\+ get_assoc(Name-Values, Table, _),
|
|
658
|
+
Domains \= any(_),
|
|
659
|
+
get_assoc(Name, Domains, Members)
|
|
660
|
+
-> truth(memberchk(Member, Members), Positive)
|
|
661
|
+
; table_value(Table, Name, Values, Positive)
|
|
662
|
+
),
|
|
663
|
+
( Polarity == negative
|
|
664
|
+
-> negate(Positive, Value0)
|
|
665
|
+
; Value0 = Positive
|
|
666
|
+
),
|
|
667
|
+
Value = Value0.
|
|
668
|
+
|
|
669
|
+
negate(true, false).
|
|
670
|
+
negate(false, true).
|
|
671
|
+
|
|
672
|
+
resolve_atom_arg(Env, var(Name, _), Value) :- !,
|
|
673
|
+
( memberchk(Name-Value, Env)
|
|
674
|
+
-> true
|
|
675
|
+
; format(string(Reason), "variable ~w is unbound", [Name]),
|
|
676
|
+
throw(model_error(Reason))
|
|
677
|
+
).
|
|
678
|
+
resolve_atom_arg(_, const(Value, _), Value) :- !.
|
|
679
|
+
resolve_atom_arg(_, number(Number, _), Value) :- !,
|
|
680
|
+
format(atom(Value), "~w", [Number]).
|
|
681
|
+
resolve_atom_arg(_, Term, _) :-
|
|
682
|
+
functor(Term, Kind, _),
|
|
683
|
+
format(string(Reason), "a ~w argument is outside what Kibi model checks", [Kind]),
|
|
684
|
+
throw(model_error(Reason)).
|
|
685
|
+
|
|
686
|
+
resolve_value(var(Name, _), Env, Value) :- !,
|
|
687
|
+
resolve_atom_arg(Env, var(Name, _), Atom),
|
|
688
|
+
atom_or_number(Atom, Value).
|
|
689
|
+
resolve_value(const(Atom, _), _, Value) :- !, atom_or_number(Atom, Value).
|
|
690
|
+
resolve_value(number(Number, _), _, Number) :- !.
|
|
691
|
+
resolve_value(Term, Env, Value) :- resolve_atom_arg(Env, Term, Value).
|
|
692
|
+
|
|
693
|
+
atom_or_number(Atom, Value) :-
|
|
694
|
+
( atom(Atom), atom_number(Atom, Number) -> Value = Number ; Value = Atom ).
|
|
695
|
+
|
|
696
|
+
compare_values(eq, L, R) :- number(L), number(R), !, L =:= R.
|
|
697
|
+
compare_values(eq, L, R) :- !, L == R.
|
|
698
|
+
compare_values(neq, L, R) :- !, \+ compare_values(eq, L, R).
|
|
699
|
+
compare_values(Op, L, R) :-
|
|
700
|
+
( number(L), number(R)
|
|
701
|
+
-> numeric_compare(Op, L, R)
|
|
702
|
+
; format(string(Reason), "~w compares ~w and ~w, which are not both numbers", [Op, L, R]),
|
|
703
|
+
throw(model_error(Reason))
|
|
704
|
+
).
|
|
705
|
+
|
|
706
|
+
numeric_compare(lt, L, R) :- L < R.
|
|
707
|
+
numeric_compare(lte, L, R) :- L =< R.
|
|
708
|
+
numeric_compare(gt, L, R) :- L > R.
|
|
709
|
+
numeric_compare(gte, L, R) :- L >= R.
|
|
710
|
+
|
|
711
|
+
% ---------------------------------------------------------------------------
|
|
712
|
+
% Text
|
|
713
|
+
|
|
714
|
+
dict_text(Dict, Key, String) :-
|
|
715
|
+
is_dict(Dict),
|
|
716
|
+
get_dict(Key, Dict, Raw),
|
|
717
|
+
text_string(Raw, String).
|
|
718
|
+
|
|
719
|
+
text_string(Raw, String) :- string(Raw), !, String = Raw.
|
|
720
|
+
text_string(Raw, String) :- atom(Raw), !, atom_string(Raw, String).
|
|
721
|
+
text_string(Raw, String) :- number(Raw), !, number_string(Raw, String).
|
|
722
|
+
|
|
723
|
+
text_atom(Raw, Atom) :- atom(Raw), !, Atom = Raw.
|
|
724
|
+
text_atom(Raw, Atom) :- string(Raw), !, atom_string(Atom, Raw).
|
|
725
|
+
text_atom(Raw, Atom) :- number(Raw), !, format(atom(Atom), "~w", [Raw]).
|
|
726
|
+
text_atom(_, _) :- throw(model_error("a model value is not text")).
|
|
727
|
+
|
|
728
|
+
member_of(List, Element) :- memberchk(Element, List).
|