quint_driver 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.ja.md +865 -0
- data/README.md +648 -0
- data/examples/loans/loans.qnt +57 -0
- data/examples/loans/loans.rb +62 -0
- data/lib/quint_driver/hegel.rb +141 -0
- data/lib/quint_driver/model.rb +289 -0
- data/lib/quint_driver/quint.rb +376 -0
- data/lib/quint_driver/spec.rb +110 -0
- data/lib/quint_driver/trace.rb +129 -0
- data/lib/quint_driver/version.rb +3 -0
- data/lib/quint_driver.rb +199 -0
- data/skills/quint_driver/SKILL.md +925 -0
- metadata +55 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// A lending shelf: a member borrows one book at a time, and gives it back.
|
|
2
|
+
// Each command has a refused twin, so a trace checks the refusals too.
|
|
3
|
+
module loans {
|
|
4
|
+
type Outcome = Done | Refused(str)
|
|
5
|
+
|
|
6
|
+
pure val BOOKS = Set("b1", "b2", "b3")
|
|
7
|
+
pure val MEMBERS = Set("m1", "m2")
|
|
8
|
+
pure val NOBODY = ""
|
|
9
|
+
|
|
10
|
+
var holder: str -> str
|
|
11
|
+
var outcome: Outcome
|
|
12
|
+
|
|
13
|
+
pure def onLoan(h: str -> str, b: str): bool = h.get(b) != NOBODY
|
|
14
|
+
pure def holds(h: str -> str, m: str): bool = BOOKS.exists(b => h.get(b) == m)
|
|
15
|
+
pure def borrowRefusal(h: str -> str, m: str, b: str): str =
|
|
16
|
+
if (onLoan(h, b)) "on_loan" else if (holds(h, m)) "limit_reached" else ""
|
|
17
|
+
|
|
18
|
+
action init = all {
|
|
19
|
+
holder' = Map("b1" -> NOBODY, "b2" -> NOBODY, "b3" -> NOBODY),
|
|
20
|
+
outcome' = Done,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
action borrow(m: str, b: str): bool = all {
|
|
24
|
+
borrowRefusal(holder, m, b) == "",
|
|
25
|
+
holder' = holder.put(b, m),
|
|
26
|
+
outcome' = Done,
|
|
27
|
+
}
|
|
28
|
+
action borrow_refused(m: str, b: str): bool = all {
|
|
29
|
+
borrowRefusal(holder, m, b) != "",
|
|
30
|
+
holder' = holder,
|
|
31
|
+
outcome' = Refused(borrowRefusal(holder, m, b)),
|
|
32
|
+
}
|
|
33
|
+
action give_back(b: str): bool = all {
|
|
34
|
+
onLoan(holder, b),
|
|
35
|
+
holder' = holder.put(b, NOBODY),
|
|
36
|
+
outcome' = Done,
|
|
37
|
+
}
|
|
38
|
+
action give_back_refused(b: str): bool = all {
|
|
39
|
+
not(onLoan(holder, b)),
|
|
40
|
+
holder' = holder,
|
|
41
|
+
outcome' = Refused("not_on_loan"),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
action step = any {
|
|
45
|
+
nondet m = MEMBERS.oneOf()
|
|
46
|
+
nondet b = BOOKS.oneOf()
|
|
47
|
+
any { borrow(m, b), borrow_refused(m, b) },
|
|
48
|
+
nondet b = BOOKS.oneOf()
|
|
49
|
+
any { give_back(b), give_back_refused(b) },
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
val one_book_each = MEMBERS.forall(m => BOOKS.filter(b => holder.get(b) == m).size() <= 1)
|
|
53
|
+
// False once a book is lent: the tests use it to see a broken invariant reported.
|
|
54
|
+
val nothing_on_loan = BOOKS.forall(b => not(onLoan(holder, b)))
|
|
55
|
+
// True once two books are lent: the tests ask quint for traces that reach it.
|
|
56
|
+
val two_on_loan = BOOKS.filter(b => onLoan(holder, b)).size() == 2
|
|
57
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# The system the tests hold to loans.qnt, and a driver that runs the spec's
|
|
2
|
+
# actions on it. BrokenLoans forgets the one-book limit.
|
|
3
|
+
class Loans
|
|
4
|
+
class Refused < StandardError
|
|
5
|
+
attr_reader :reason
|
|
6
|
+
|
|
7
|
+
def initialize(reason)
|
|
8
|
+
@reason = reason
|
|
9
|
+
super(reason.to_s)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize = @holders = {}
|
|
14
|
+
|
|
15
|
+
def borrow(member, book)
|
|
16
|
+
raise Refused.new(:on_loan) if @holders.key?(book)
|
|
17
|
+
raise Refused.new(:limit_reached) if limited? && @holders.value?(member)
|
|
18
|
+
|
|
19
|
+
@holders[book] = member
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def give_back(book)
|
|
23
|
+
@holders.delete(book) || raise(Refused.new(:not_on_loan))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def holder(book) = @holders[book]
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
def limited? = true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class BrokenLoans < Loans
|
|
33
|
+
private
|
|
34
|
+
def limited? = false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class LoansDriver
|
|
38
|
+
SPEC = File.expand_path("loans.qnt", __dir__)
|
|
39
|
+
BOOKS = %w[b1 b2 b3].freeze
|
|
40
|
+
|
|
41
|
+
def self.spec = @spec ||= QuintDriver::Spec.new(SPEC, main: "loans")
|
|
42
|
+
|
|
43
|
+
def initialize(system = Loans)
|
|
44
|
+
@system = system
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def step(action, picks)
|
|
48
|
+
@outcome = "Done"
|
|
49
|
+
case action.delete_suffix("_refused")
|
|
50
|
+
when "init" then @loans = @system.new
|
|
51
|
+
when "borrow" then @loans.borrow(picks.fetch("m"), picks.fetch("b"))
|
|
52
|
+
when "give_back" then @loans.give_back(picks.fetch("b"))
|
|
53
|
+
else raise ArgumentError, "no command for the action #{action}"
|
|
54
|
+
end
|
|
55
|
+
rescue Loans::Refused => refusal
|
|
56
|
+
@outcome = [ "Refused", refusal.reason.to_s ]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def state
|
|
60
|
+
{ "holder" => BOOKS.to_h { [ _1, @loans.holder(_1) || "" ] }, "outcome" => @outcome }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# The Hegel adapter. It is the one file that loads hegeltest, which needs
|
|
2
|
+
# Ruby 3.4, so trace replay and the model run without it.
|
|
3
|
+
require "hegel"
|
|
4
|
+
require_relative "../quint_driver"
|
|
5
|
+
|
|
6
|
+
module QuintDriver
|
|
7
|
+
Agreement.hide(File.dirname(Hegel::Stateful.method(:run).source_location.first))
|
|
8
|
+
|
|
9
|
+
# A Hegel::StateMachine for +spec+'s +step+ action. Hegel picks each move,
|
|
10
|
+
# the model takes it, the driver runs the same command on the system, and
|
|
11
|
+
# the two states have to be equal afterwards. Hegel owns every choice, so a
|
|
12
|
+
# failure shrinks to a short sequence of commands.
|
|
13
|
+
#
|
|
14
|
+
# +choose+ is how a move is drawn:
|
|
15
|
+
# - :rule, Hegel picks a rule and a disabled one is rejected (tc.assume).
|
|
16
|
+
# Shrinks to the shortest sequence; deep states take more test cases.
|
|
17
|
+
# - :rotate, a disabled rule passes the turn to the next enabled one.
|
|
18
|
+
# Nothing is rejected, so deep states come sooner; shrinking leaves extra
|
|
19
|
+
# moves, because a draw means a different rule once the state differs.
|
|
20
|
+
#
|
|
21
|
+
# +observe+ names definitions of the spec, as Trace.replay takes them: the
|
|
22
|
+
# driver reports each one in its state, held to the model's value.
|
|
23
|
+
# +ignore+ names variables the driver does not report, as Trace.replay
|
|
24
|
+
# takes them.
|
|
25
|
+
def self.machine(spec, driver:, step: "step", invariants: [], choose: :rule, observe: [], ignore: [])
|
|
26
|
+
rules = spec.rules(step)
|
|
27
|
+
observe, ignore = observe.map(&:to_s), ignore.map(&:to_s)
|
|
28
|
+
spec.observable!(observe)
|
|
29
|
+
Agreement.ignorable!(ignore, spec.declarations.select { _1["kind"] == "var" }.map { _1["name"] }, "the spec's variables")
|
|
30
|
+
# Many draws map to one sequence of moves, so the shrinker asks for the
|
|
31
|
+
# same sequence thousands of times: on one failure, 8818 test cases held
|
|
32
|
+
# 1342 distinct sequences. A sequence the system already agreed on is not
|
|
33
|
+
# run again. That assumes the system answers a sequence the same way each
|
|
34
|
+
# time, which the shrinker assumes too. A disagreement is never kept: it
|
|
35
|
+
# runs on the system every time, so the failure reported is one it
|
|
36
|
+
# reproduced.
|
|
37
|
+
agreed = Set.new
|
|
38
|
+
# A fixed domain (Spec::Rule#fixed) is the same list in every test case,
|
|
39
|
+
# so it is evaluated and sorted once per machine, not once per draw.
|
|
40
|
+
fixed = {}.compare_by_identity
|
|
41
|
+
|
|
42
|
+
init = [ "init", {} ].freeze
|
|
43
|
+
# A replay names a step by its index in the trace, init being step 0; the
|
|
44
|
+
# machine names a move by the number Hegel's report gives it.
|
|
45
|
+
at = ->(number, (action, picks)) { "step #{number} `#{action}` #{Agreement.show(picks)}" }
|
|
46
|
+
|
|
47
|
+
Class.new(Hegel::StateMachine) do
|
|
48
|
+
# The driver starts with the machine, before Hegel's first step, so a
|
|
49
|
+
# failure at init is reported with no step Hegel took. Once init agreed,
|
|
50
|
+
# the empty sequence is kept like any other, and a later machine starts
|
|
51
|
+
# its driver only on a move that is not.
|
|
52
|
+
define_method(:initialize) do
|
|
53
|
+
@model = spec.model(step)
|
|
54
|
+
@model.step(*init)
|
|
55
|
+
@start = @model.state.merge(@model.observe(observe))
|
|
56
|
+
@moves = []
|
|
57
|
+
@numbers = [] # the Hegel step of each move
|
|
58
|
+
@step = 0 # every rule Hegel ran, a rejected one too, as its report counts
|
|
59
|
+
@ran = 0 # how many of @moves the driver has run
|
|
60
|
+
next if agreed.include?(@moves)
|
|
61
|
+
|
|
62
|
+
start_driver
|
|
63
|
+
agreed << []
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Every driver is held to the model at init, also one started after an
|
|
67
|
+
# earlier machine agreed on init: a test database keeps the rows of an
|
|
68
|
+
# earlier test case unless the driver's init deletes them, and without
|
|
69
|
+
# this check the first move would take the blame for them.
|
|
70
|
+
define_method(:start_driver) do
|
|
71
|
+
actual = Agreement.drive(at.(0, init), steps: [ init ]) do
|
|
72
|
+
@driver = driver.call
|
|
73
|
+
@driver.step(*init)
|
|
74
|
+
@driver.state
|
|
75
|
+
end
|
|
76
|
+
Agreement.check!(at.(0, init), @start, actual, by: "model", steps: [ init ], observe:, spec:, ignore:)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
define_method(:move!) do |action, picks, after|
|
|
80
|
+
@model.enter(after)
|
|
81
|
+
@moves << [ action, Model.plain(picks) ]
|
|
82
|
+
@numbers << @step
|
|
83
|
+
return if agreed.include?(@moves)
|
|
84
|
+
|
|
85
|
+
start_driver unless @driver
|
|
86
|
+
# Every move the driver has not run yet, which is more than the last
|
|
87
|
+
# one when the driver skipped a sequence it agreed on.
|
|
88
|
+
while @ran < @moves.size
|
|
89
|
+
move = @moves[@ran]
|
|
90
|
+
Agreement.drive(at.(@numbers[@ran], move), steps: [ init, *@moves[..@ran] ]) { @driver.step(*move) }
|
|
91
|
+
@ran += 1
|
|
92
|
+
end
|
|
93
|
+
last = at.(@numbers.last, @moves.last)
|
|
94
|
+
actual = Agreement.drive(last, steps: [ init, *@moves ]) { @driver.state }
|
|
95
|
+
Agreement.check!(last, @model.state.merge(@model.observe(observe)), actual, by: "model", steps: [ init, *@moves ], observe:, spec:, ignore:)
|
|
96
|
+
agreed << @moves.dup
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
case choose
|
|
100
|
+
when :rule
|
|
101
|
+
rules.each do |rule|
|
|
102
|
+
rule(rule.action) do |tc|
|
|
103
|
+
@step += 1
|
|
104
|
+
picks = rule.domains.each_with_object({}) do |(name, domain), chosen|
|
|
105
|
+
choices = fixed.fetch(domain) do
|
|
106
|
+
sorted = @model.value(domain, chosen).to_a.sort_by(&:to_s).freeze
|
|
107
|
+
rule.fixed.include?(name) ? fixed[domain] = sorted : sorted
|
|
108
|
+
end
|
|
109
|
+
# any? would reject a domain whose one element is false.
|
|
110
|
+
tc.assume(!choices.empty?)
|
|
111
|
+
chosen[name] = tc.draw(sampled_from(choices), label: name)
|
|
112
|
+
end
|
|
113
|
+
after = @model.try(rule.action, rule.args.map { @model.value(_1, picks) })
|
|
114
|
+
tc.assume(after)
|
|
115
|
+
tc.note { "#{rule.action} #{Model.plain(picks)}" }
|
|
116
|
+
move!(rule.action, picks, after)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
when :rotate
|
|
120
|
+
rule(step) do |tc|
|
|
121
|
+
@step += 1
|
|
122
|
+
start = tc.draw(integers(min_value: 0, max_value: rules.size - 1), label: "rule")
|
|
123
|
+
moves = rules.rotate(start).lazy.map { @model.moves(_1) }.find(&:any?)
|
|
124
|
+
tc.assume(moves)
|
|
125
|
+
action, picks, after = tc.draw(sampled_from(moves), label: "move")
|
|
126
|
+
tc.note { "#{action} #{Model.plain(picks)}" }
|
|
127
|
+
move!(action, picks, after)
|
|
128
|
+
end
|
|
129
|
+
else Agreement.refuse "quint: choose is :rule or :rotate"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Hegel samples an invariant between moves unless always_run is set;
|
|
133
|
+
# an invariant here holds after every move.
|
|
134
|
+
invariants.each do |name|
|
|
135
|
+
invariant(name, always_run: true) do
|
|
136
|
+
raise Mismatch, "quint: the model broke `#{name}`", Agreement.outside(caller_locations) unless @model.value(name.to_s) == true
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
module QuintDriver
|
|
2
|
+
# Evaluates a spec's JSON IR inside the Ruby process, so a Hegel step costs
|
|
3
|
+
# no call to another process. It covers the operators a command-per-action
|
|
4
|
+
# spec uses and raises Unsupported on any other, so a spec that outgrows it
|
|
5
|
+
# fails loudly instead of being judged wrongly. It answers the driver
|
|
6
|
+
# protocol too, which is how it is held to Quint: replay Quint's own traces
|
|
7
|
+
# through it.
|
|
8
|
+
class Model
|
|
9
|
+
Variant = Struct.new(:tag, :value)
|
|
10
|
+
BOOL = Set[false, true].freeze
|
|
11
|
+
|
|
12
|
+
# +within+ names the definition that holds the lambda, so an operator its
|
|
13
|
+
# body cannot evaluate is reported where the spec writes it.
|
|
14
|
+
Closure = Struct.new(:params, :body, :env, :within)
|
|
15
|
+
|
|
16
|
+
# Spec#model is the one way to build a model: the evaluator trusts the
|
|
17
|
+
# shape of quint's compiled IR and checks none of it.
|
|
18
|
+
private_class_method :new
|
|
19
|
+
|
|
20
|
+
# +owners+ names the definition that writes each IR node, by its id.
|
|
21
|
+
def initialize(declarations, rules, owners)
|
|
22
|
+
@owners = owners
|
|
23
|
+
@defs = declarations.select { _1["kind"] == "def" }.to_h { [ _1["name"], _1 ] }
|
|
24
|
+
@vars = declarations.select { _1["kind"] == "var" }.map { _1["name"] }
|
|
25
|
+
@rules = rules.to_h { [ _1.action, _1 ] }
|
|
26
|
+
@state = {}
|
|
27
|
+
@constant = {}
|
|
28
|
+
@within = nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The driver protocol. Picks from a trace are plain values; each one
|
|
32
|
+
# becomes the element of its domain that reads as the same plain value,
|
|
33
|
+
# so a variant pick is a variant again.
|
|
34
|
+
def step(action, picks)
|
|
35
|
+
args = if action == "init" then []
|
|
36
|
+
else
|
|
37
|
+
# Quint names a move after step itself when step is one action,
|
|
38
|
+
# or a nondet pick, that no `any` wraps.
|
|
39
|
+
rule = @rules.fetch(action) do
|
|
40
|
+
raise Unsupported, "quint: no action of step is named #{action}; write step as any { ... } so each move names its action"
|
|
41
|
+
end
|
|
42
|
+
typed = rule.domains.each_with_object({}) do |(name, domain), chosen|
|
|
43
|
+
pick = picks.fetch(name) { raise Unsupported, "quint: #{action} needs a pick for #{name}, and the move gives none" }
|
|
44
|
+
# A pick can be false, so the lookup asks whether it found
|
|
45
|
+
# an element, never whether the element is truthy.
|
|
46
|
+
elements = value(domain, chosen).to_a
|
|
47
|
+
at = elements.index { Model.plain(_1) == pick }
|
|
48
|
+
raise Mismatch, "quint: the model cannot pick #{name} = #{pick.inspect} for #{action}" unless at
|
|
49
|
+
|
|
50
|
+
chosen[name] = elements[at]
|
|
51
|
+
end
|
|
52
|
+
rule.args.map { value(_1, typed) }
|
|
53
|
+
end
|
|
54
|
+
enter(try(action, args) || raise(Mismatch, "quint: the model refused #{action} #{Agreement.show(picks)}"))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def state = Model.plain(@state)
|
|
58
|
+
|
|
59
|
+
# The state the action would reach, or nil when it is disabled. The model
|
|
60
|
+
# stays where it is, so a caller can ask which actions are enabled.
|
|
61
|
+
def try(name, args = [])
|
|
62
|
+
expr = @defs.fetch(name)["expr"]
|
|
63
|
+
env = {}
|
|
64
|
+
if expr["kind"] == "lambda"
|
|
65
|
+
env = expr["params"].map { _1["name"] }.zip(args).to_h
|
|
66
|
+
expr = expr["expr"]
|
|
67
|
+
end
|
|
68
|
+
@next = {}
|
|
69
|
+
return nil unless inside(name) { eval(expr, env) }
|
|
70
|
+
|
|
71
|
+
missing = @vars - @next.keys
|
|
72
|
+
raise Unsupported, "quint: #{name} leaves #{missing.join(', ')} unassigned" if missing.any?
|
|
73
|
+
|
|
74
|
+
@next
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def enter(state) = @state = state
|
|
78
|
+
|
|
79
|
+
# Every [action, picks, next state] that +rule+, a Spec::Rule, allows
|
|
80
|
+
# now, in a fixed order, so the same draw means the same move from the
|
|
81
|
+
# same state.
|
|
82
|
+
def moves(rule)
|
|
83
|
+
picks_of(rule, rule.domains.to_a, {}).filter_map do |picks|
|
|
84
|
+
after = try(rule.action, rule.args.map { value(_1, picks) })
|
|
85
|
+
[ rule.action, picks, after ] if after
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# A refusal names the definition that writes +node_or_name+, such as the
|
|
90
|
+
# action a domain or an argument of a rule is written in, or says the node
|
|
91
|
+
# is outside any definition when no definition of the spec holds it.
|
|
92
|
+
def value(node_or_name, env = {})
|
|
93
|
+
node = node_or_name.is_a?(String) ? { "kind" => "name", "name" => node_or_name } : node_or_name
|
|
94
|
+
inside(@owners[node["id"]]) { eval(node, env) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Each definition +names+ names, by name, as a plain value.
|
|
98
|
+
def observe(names) = names.to_h { [ _1, Model.plain(value(_1)) ] }
|
|
99
|
+
|
|
100
|
+
# A value as Trace.decode gives it, so a driver reads an Option the same
|
|
101
|
+
# on a replay and under the machine: nil for None, the value for Some.
|
|
102
|
+
def self.plain(value)
|
|
103
|
+
case value
|
|
104
|
+
when Variant
|
|
105
|
+
case value.tag
|
|
106
|
+
when "None" then nil
|
|
107
|
+
when "Some" then plain(value.value)
|
|
108
|
+
else value.value == [] ? value.tag : [ value.tag, plain(value.value) ]
|
|
109
|
+
end
|
|
110
|
+
when Set then value.to_set { plain(_1) }
|
|
111
|
+
when Hash then value.to_h { |k, v| [ plain(k), plain(v) ] }
|
|
112
|
+
when Array then value.map { plain(_1) }
|
|
113
|
+
else value
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
def picks_of(rule, domains, picks)
|
|
119
|
+
return [ picks ] if domains.empty?
|
|
120
|
+
|
|
121
|
+
(name, domain), *rest = domains
|
|
122
|
+
value(domain, picks).to_a.sort_by(&:to_s).flat_map { picks_of(rule, rest, picks.merge(name => _1)) }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def eval(node, env)
|
|
126
|
+
case node["kind"]
|
|
127
|
+
when "str", "int", "bool" then node["value"]
|
|
128
|
+
when "name" then lookup(node["name"], env)
|
|
129
|
+
when "lambda" then Closure.new(node["params"].map { _1["name"] }, node["expr"], env, @within)
|
|
130
|
+
when "let"
|
|
131
|
+
definition = node["opdef"]
|
|
132
|
+
eval(node["expr"], env.merge(definition["name"] => eval(definition["expr"], env)))
|
|
133
|
+
when "app" then apply(node["opcode"], node["args"], env)
|
|
134
|
+
else refuse("IR kind #{node['kind']}")
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Quint reserves Bool, Int and Nat, so no definition shadows them.
|
|
139
|
+
def lookup(name, env)
|
|
140
|
+
return env[name] if env.key?(name)
|
|
141
|
+
return @state.fetch(name) if @vars.include?(name)
|
|
142
|
+
return BOOL if name == "Bool"
|
|
143
|
+
|
|
144
|
+
defined(name) do
|
|
145
|
+
if %w[Int Nat].include?(name)
|
|
146
|
+
refuse("the name #{name}", "#{name} is infinite, so no move can list its picks; draw from a finite set such as 0.to(9)")
|
|
147
|
+
end
|
|
148
|
+
refuse("the name #{name}", "no definition, variable or parameter has it")
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# A pure val or pure def reads no variable, and a lambda reads none
|
|
153
|
+
# until it is called, so their value is the same in every state and is
|
|
154
|
+
# evaluated once. Every other definition is evaluated on each read.
|
|
155
|
+
def defined(name, &missing)
|
|
156
|
+
@constant.fetch(name) do
|
|
157
|
+
definition = @defs.fetch(name, &missing)
|
|
158
|
+
value = inside(name) { eval(definition["expr"], {}) }
|
|
159
|
+
constant = %w[pureval puredef].include?(definition["qualifier"]) || definition["expr"]["kind"] == "lambda"
|
|
160
|
+
constant ? @constant[name] = value.freeze : value
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def call(closure, args)
|
|
165
|
+
inside(closure.within) { eval(closure.body, closure.env.merge(closure.params.zip(args).to_h)) }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def inside(name)
|
|
169
|
+
outer, @within = @within, name
|
|
170
|
+
yield
|
|
171
|
+
ensure
|
|
172
|
+
@within = outer
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def refuse(what, why = nil)
|
|
176
|
+
raise Unsupported, "quint: no evaluation for #{what} #{@within ? "in #{@within}" : 'outside any definition'}#{"; #{why}" if why}"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Quint's builtin docs leave these undefined, and its two evaluators do
|
|
180
|
+
# not always agree on them (range(3, 1) is [] in one and [3, 2] in the
|
|
181
|
+
# other), so the model refuses them rather than pick an answer.
|
|
182
|
+
def undefined(what) = refuse(what, "Quint leaves it undefined")
|
|
183
|
+
|
|
184
|
+
def index!(list, i, op)
|
|
185
|
+
return i if i.between?(0, list.size - 1)
|
|
186
|
+
|
|
187
|
+
undefined("#{op}(_, #{i}) on a list of length #{list.size}")
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def nonempty!(list, op) = list.empty? ? undefined("#{op} of an empty list") : list
|
|
191
|
+
|
|
192
|
+
def keyed!(map, key, op) = map.key?(key) || refuse("#{op}(_, #{key.inspect})", "the map has no such key")
|
|
193
|
+
|
|
194
|
+
# Quint documents no rounding: its evaluators truncate / and % toward
|
|
195
|
+
# zero, and Ruby's Integer#/ and #% floor, so only operands where the
|
|
196
|
+
# two agree are evaluated.
|
|
197
|
+
def divide(a, b, op)
|
|
198
|
+
refuse("#{a} #{op} #{b}", "Quint documents no rounding for a negative operand") if a.negative? || b.negative?
|
|
199
|
+
b.zero? ? undefined("#{a} #{op} 0") : a.public_send(op, b)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def apply(op, args, env)
|
|
203
|
+
case op
|
|
204
|
+
when "actionAll", "and" then args.all? { eval(_1, env) }
|
|
205
|
+
when "or" then args.any? { eval(_1, env) }
|
|
206
|
+
when "implies" then !eval(args[0], env) || eval(args[1], env)
|
|
207
|
+
when "ite" then eval(args[eval(args[0], env) ? 1 : 2], env)
|
|
208
|
+
when "assign" then @next[args[0]["name"]] = eval(args[1], env); true
|
|
209
|
+
else operate(op, args.map { eval(_1, env) })
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Every other operator reads all of its arguments, so they are evaluated
|
|
214
|
+
# once, before it runs.
|
|
215
|
+
def operate(op, values)
|
|
216
|
+
a, b, c = values
|
|
217
|
+
case op
|
|
218
|
+
when "not" then !a
|
|
219
|
+
when "eq", "iff" then a == b
|
|
220
|
+
when "neq" then a != b
|
|
221
|
+
when "igt" then a > b
|
|
222
|
+
when "igte" then a >= b
|
|
223
|
+
when "ilt" then a < b
|
|
224
|
+
when "ilte" then a <= b
|
|
225
|
+
when "iadd" then a + b
|
|
226
|
+
when "isub" then a - b
|
|
227
|
+
when "imul" then a * b
|
|
228
|
+
when "idiv" then divide(a, b, "/")
|
|
229
|
+
when "imod" then divide(a, b, "%")
|
|
230
|
+
when "iuminus" then -a
|
|
231
|
+
when "to" then a <= b ? (a..b).to_set : undefined("#{a}.to(#{b})")
|
|
232
|
+
# A set's fold order is unspecified, so Quint leaves a fold whose
|
|
233
|
+
# operator is not associative and commutative undefined.
|
|
234
|
+
when "fold", "foldl" then a.reduce(b) { |acc, x| call(c, [ acc, x ]) }
|
|
235
|
+
# The builtin docs leave foldr out, but both evaluators fold from the
|
|
236
|
+
# right and pass the element first.
|
|
237
|
+
when "foldr" then a.reverse.reduce(b) { |acc, x| call(c, [ x, acc ]) }
|
|
238
|
+
when "List", "Tup" then values
|
|
239
|
+
when "append" then a + [ b ]
|
|
240
|
+
when "concat" then a + b
|
|
241
|
+
when "head" then nonempty!(a, "head").first
|
|
242
|
+
when "tail" then nonempty!(a, "tail").drop(1)
|
|
243
|
+
when "length" then a.size
|
|
244
|
+
when "nth" then a[index!(a, b, "nth")]
|
|
245
|
+
when "indices" then (0...a.size).to_set
|
|
246
|
+
when "replaceAt" then a.dup.tap { _1[index!(a, b, "replaceAt")] = c }
|
|
247
|
+
when "slice"
|
|
248
|
+
# The docs also leave i == l.length() undefined, but both evaluators
|
|
249
|
+
# answer slice(l, l.length(), l.length()) with [], which the docs'
|
|
250
|
+
# own reading (from i inclusive to j exclusive) gives, and a spec
|
|
251
|
+
# that drops a list's last element needs it.
|
|
252
|
+
0 <= b && b <= c && c <= a.size ? a[b...c] : undefined("slice(_, #{b}, #{c}) on a list of length #{a.size}")
|
|
253
|
+
when "range" then a <= b ? (a...b).to_a : undefined("range(#{a}, #{b})")
|
|
254
|
+
when "select" then a.select { call(b, [ _1 ]) }
|
|
255
|
+
when "Set" then values.to_set
|
|
256
|
+
when "contains" then a.include?(b)
|
|
257
|
+
when "in" then b.include?(a)
|
|
258
|
+
when "union" then a | b
|
|
259
|
+
when "intersect" then a & b
|
|
260
|
+
when "exclude" then a - b
|
|
261
|
+
when "subseteq" then a.subset?(b)
|
|
262
|
+
when "size" then a.size
|
|
263
|
+
when "exists" then a.any? { call(b, [ _1 ]) }
|
|
264
|
+
when "forall" then a.all? { call(b, [ _1 ]) }
|
|
265
|
+
when "filter" then a.select { call(b, [ _1 ]) }.to_set
|
|
266
|
+
when "map" then a.to_set { call(b, [ _1 ]) }
|
|
267
|
+
when "Map" then values.to_h
|
|
268
|
+
when "put", "with" then a.merge(b => c)
|
|
269
|
+
when "mapBy" then a.to_h { [ _1, call(b, [ _1 ]) ] }
|
|
270
|
+
when "set" then a.merge(b => c) if keyed!(a, b, "set")
|
|
271
|
+
when "setBy" then a.merge(b => call(c, [ a[b] ])) if keyed!(a, b, "setBy")
|
|
272
|
+
# Which of two pairs with one key wins depends on the set's order.
|
|
273
|
+
when "setToMap" then a.to_h.tap { undefined("setToMap of pairs that share a key") if _1.size < a.size }
|
|
274
|
+
when "get" then a.fetch(b) { refuse("get(_, #{b.inspect})", "the map has no such key") }
|
|
275
|
+
when "keys" then a.keys.to_set
|
|
276
|
+
when "item" then a.fetch(b - 1)
|
|
277
|
+
when "Rec" then values.each_slice(2).to_h
|
|
278
|
+
when "field" then a.fetch(b)
|
|
279
|
+
when "variant" then Variant.new(a, b)
|
|
280
|
+
when "matchVariant"
|
|
281
|
+
arm = values.drop(1).each_slice(2).find { |label, _| [ a.tag, "_" ].include?(label) }
|
|
282
|
+
call(arm[1], [ a.value ])
|
|
283
|
+
else
|
|
284
|
+
# A definition is a lambda, or another operator's name (`def doubleOf = twice`).
|
|
285
|
+
call(defined(op) { refuse("operator #{op}") }, values)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|