kibi-core 0.24.0 → 0.27.0

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