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.
@@ -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).