-
Notifications
You must be signed in to change notification settings - Fork 0
/
engine.cr
326 lines (285 loc) · 10.4 KB
/
engine.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
module Novika
module IExhaustTracker
# Invoked before *engine* opens the given *form*.
def on_form_begin(engine : Engine, form : Form)
end
# Invoked after *engine* opened the given *form*.
def on_form_end(engine : Engine, form : Form)
end
end
# An engine object is responsible for managing a *continuations block*.
#
# Continuations block consists of *continuatio**n** blocks*.
#
# Canonical continuation blocks themselves contain two blocks
# (see `Engine.cont`):
#
# - *Code block*, more commonly known as simply the block
# (and as *active* block when its continuation is active,
# i.e., when it's being evaluated),
#
# - *Stack block*, more commonly known as simply the stack
# (and as *active* stack when its continuation is active,
# i.e., when it's being evaluated).
#
# `Engine#schedule` is used to create a continuation block
# given a `Schedulable` object (usually a `Form`, and in rarer
# cases an `Entry`) and a stack block. It then adds the
# continuation block to the continuations block -- effectively
# scheduling it for execution *on the next exhaust loop cycle*.
#
# Note that there are two other methods linked with execution
# and implemented by all forms: `on_open`, and `on_parent_open`.
# They *perform* whatever action the form wants rather than
# simply *scheduling* it to be performed some time in the
# future. Namely, `on_open` is invoked whenever the form at
# hand is itself the target of opening (aka execution, aka
# evaluation), and `on_parent_open` is invoked when a block
# containing the form at hand (its parent block) is the target
# of opening.
#
# An engine's *exhaust loop* is where most of the magic happens.
# It is organized very much like the fetch-decode-execute cycle
# in CPUs.
#
# For *fetch*, the engine finds the top (see `Block#top`)
# continuation block, then finds the top form on the code
# block, and invokes the `on_parent_open` method on it.
#
# This method is analogous to *decoding* followed by *execution*.
# The form is free to choose how it wants to make sense of itself,
# given an engine. Some forms (e.g. words) end up scheduling
# new continuation blocks `on_parent_open`, making the engine
# go through them first.
#
# After the cursor of the active block hits the end, `Engine`
# drops (see `Block#drop`) the continuation block (thereby
# *closing* the code block).
#
# ```
# caps = CapabilityCollection.with_default.enable_all
# block = Block.new(caps.block).slurp("1 2 +")
# stack = Block.new
#
# engine = Engine.new(caps)
# engine.schedule(block, stack)
# engine.exhaust
#
# puts stack # [ 3 ]
#
# # Or, shorter:
#
# caps = CapabilityCollection.with_default.enable_all
# block = Block.new(caps.block).slurp("1 2 +")
#
# puts Engine.exhaust(caps, block) # [ 3 ]
# ```
class Engine
# Maximum amount of scheduled continuations in `conts`. After
# passing this number, `Error` is raised to bring attention
# to such dangerous depth.
MAX_CONTS = 1024
# Maximum number of engines that can be created.
#
# This is for safety reasons only, particularly to prevent
# infinite recursion in e.g. asserts which are called from
# Crystal rather than Novika, thereby circumventing `MAX_CONTS`
# checks. See `Engine.count`.
MAX_ENGINES = 1024
# Index of the code block in a continuation block.
C_BLOCK_AT = 0
# Index of the stack block in a continuation block.
C_STACK_AT = 1
# Holds an array of exhaust tracker objects associated with
# all instances of `Engine`. These objects intercept forms
# before/after opening in `Engine#exhaust`. This e.g. allows
# frontends to analyze/track forms and/or matching blocks.
class_getter trackers = [] of IExhaustTracker
@@stack = [] of Engine
# Returns the current engine. Raises a BUG exception if
# there is no current engine.
def self.current
@@stack.last? || raise "BUG: there is no current engine"
end
# Pushes *engine* onto the engine stack.
def self.push(engine : Engine) : Engine
unless @@stack.size.in?(0..MAX_ENGINES)
raise Error.new("bad engine stack depth: deep recursion in a __metaword__?")
end
@@stack << engine
engine
end
# Pushes a new engine with the given capability collection *caps*.
#
# Make sure that you `pop` it yourself or that you know what
# you're doing!
def self.push(caps : CapabilityCollection)
push new(caps)
end
# Pops *engine* from the engine stack. Raises a BUG exception
# (and does not pop!) if the current engine is not *engine*
# (or if it is absent).
def self.pop(engine : Engine) : Engine?
unless current.same?(engine)
raise "BUG: lost track of the engine stack: unexpected engine on top!"
end
@@stack.pop
end
# Returns the capability collection used by this engine.
getter capabilities : CapabilityCollection
# Holds the continuations block (aka continuations stack).
property conts = Block.new
private def initialize(@capabilities : CapabilityCollection)
end
# Yields an instance of `Engine`.
def self.new(capabilities : CapabilityCollection, &)
engine = new(capabilities)
Engine.push(engine)
begin
yield engine
ensure
Engine.pop(engine)
end
end
# Creates and returns a canonical continuation block.
#
# A continuation block must include two blocks: the first is
# called simply the *block* (found at `C_BLOCK_AT`), and the
# second is called the *stack* block (found at `C_STACK_AT`).
def self.cont(*, block, stack)
Block.with([block, stack] of Form, leaf: false)
end
{% for name, schedule in {:exhaust => :schedule, :exhaust! => :schedule!} %}
# Schedules *schedulable* and exhausts immediately. Returns the
# resulting *stack* (creates one if `nil`).
#
# Useful for when you need the result of *schedulable*
# immediately.
#
# For details see `Engine#{{schedule.id}}`.
#
# ```
# caps = CapabilityCollection.with_default.enable_all
# result = Engine.exhaust(caps, Block.new(caps.block).slurp("1 2 +"))
# result.top # 3 : Novika::Decimal
# ```
def self.{{name.id}}(capabilities : CapabilityCollection, schedulable, stack = nil) : Block
stack ||= Block.new
Engine.new(capabilities) do |engine|
engine.{{schedule.id}}(schedulable, stack)
engine.exhaust
end
stack
end
{% end %}
# Returns the active continuation.
def cont
conts.top.a(Block)
end
# Returns the block of the active continuation.
def block
cont.at(C_BLOCK_AT).a(Block)
end
# Returns the stack block of the active continuation.
def stack
cont.at(C_STACK_AT).a(Block)
end
# Yields active blocks, starting from the oldest active block
# up to the current active block.
def each_active_block
conts.each do |cont|
yield cont.a(Block).at(C_BLOCK_AT).a(Block)
end
end
# See `Form#die`.
delegate :die, to: block
# Main authorized point for adding continuations unsafely.
# Returns self.
#
# Provides protection from continuations stack overflow.
#
# Adding to `conts` (the unauthorized way) does not protect
# one from continuations stack overflow, and therefore from
# a memory usage explosion.
def schedule!(other : Block)
if conts.count > MAX_CONTS
die("recursion or block open is too deep (> #{MAX_CONTS})")
end
tap { conts.add(other) }
end
# Schedules a continuation with the given *block* and *stack*.
def schedule!(*, block : Block, stack : Block)
schedule! Engine.cont(block: block, stack: stack)
end
# See `Schedulable#schedule`.
def schedule(schedulable : Schedulable, stack : Block)
schedulable.schedule(self, stack)
end
# See `Schedulable#schedule!`.
def schedule!(schedulable : Schedulable, stack : Block)
schedulable.schedule!(self, stack)
end
# Converts value form of a death handler *entry* to a
# block, if it's not a block already. Returns the block.
private def entry_to_death_handler_block(entry : Entry) : Block
unless form = entry.form.as?(Block)
form = Block.new(block, prototype: block.prototype).add(entry.form)
end
form
end
# Returns the relevant death handler, or nil. Avoids
# handlers whose prototype is *avoid_prototype*.
#
# To find the relevant death handler, the continuations
# block is inspected right-to-left (back-to-front); each
# code block is then asked to retrieve `Word::DIED`
# using `Block#at?`. Regardless of the result, the
# continuation block is then dropped.
#
# If succeeded in retrieving `Word::DIED`, converts the
# resulting entry to block (does not distinguish between
# openers and pushers). Returns that block.
#
# If all continuations were exhausted and no `Hook.died`
# had been found, returns nil.
def drop_until_death_handler?(avoid_prototype = nil)
until conts.tape.empty?
entry = block.entry_for?(Hook.died)
conts.drop
next unless entry
handler = entry_to_death_handler_block(entry)
unless avoid_prototype && handler.prototype.same?(avoid_prototype)
return handler
end
end
end
def execute(form : Form)
form.on_parent_open(self)
rescue error : Error
error.conts ||= conts.instance
# Re-raise if no user-defined death handler ANYWHERE.
# Death handler lookup is the broadest kind of lookup
# in Novika, it traverses *all* running blocks and
# their relatives.
#
# Avoid current block because that would case
# infinite death loop.
unless handler = drop_until_death_handler?(avoid_prototype: block.prototype)
raise error
end
schedule(handler, stack: conts.count.zero? ? Block.with(error) : stack.add(error))
end
# Exhausts all scheduled continuations, starting from the
# topmost (see `Block#top`) continuation in `conts`.
def exhaust
until conts.tape.empty?
while form = block.next?
Engine.trackers.each &.on_form_begin(self, form)
execute(form)
Engine.trackers.each &.on_form_end(self, form)
end
conts.drop
end
end
end
end