openscript 0.4.0__py3-none-any.whl
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.
- openscript/__init__.py +40 -0
- openscript/__main__.py +62 -0
- openscript/accounting/__init__.py +74 -0
- openscript/accounting/analysis.py +174 -0
- openscript/accounting/charges.py +397 -0
- openscript/accounting/equity.py +234 -0
- openscript/accounting/report.py +82 -0
- openscript/accounting/shapes.py +74 -0
- openscript/accounting/statistics.py +300 -0
- openscript/accounting/trades.py +294 -0
- openscript/adapter/__init__.py +32 -0
- openscript/adapter/answers.py +215 -0
- openscript/adapter/channels.py +137 -0
- openscript/adapter/expectations.py +67 -0
- openscript/adapter/facts.py +127 -0
- openscript/adapter/matching.py +257 -0
- openscript/adapter/ordering.py +187 -0
- openscript/adapter/page.py +130 -0
- openscript/adapter/reading.py +357 -0
- openscript/adapter/reporting.py +244 -0
- openscript/adapter/running.py +449 -0
- openscript/adapter/serving.py +229 -0
- openscript/adapter/sessions.py +168 -0
- openscript/adapter/spellings.py +184 -0
- openscript/bars.py +157 -0
- openscript/budget.py +342 -0
- openscript/canonical.py +192 -0
- openscript/civil.py +196 -0
- openscript/contracts.py +165 -0
- openscript/dates.py +302 -0
- openscript/diagnostics.py +104 -0
- openscript/hours.py +165 -0
- openscript/inputs.py +239 -0
- openscript/intervals.py +60 -0
- openscript/library/__init__.py +76 -0
- openscript/library/arithmetic.py +128 -0
- openscript/library/averages.py +133 -0
- openscript/library/bars.py +60 -0
- openscript/library/bookkeeping.py +166 -0
- openscript/library/code_points.py +85 -0
- openscript/library/colour.py +202 -0
- openscript/library/composites.py +208 -0
- openscript/library/counting.py +218 -0
- openscript/library/deviation.py +155 -0
- openscript/library/elementary.py +206 -0
- openscript/library/extremes.py +122 -0
- openscript/library/flows.py +220 -0
- openscript/library/momentum.py +203 -0
- openscript/library/number_text.py +223 -0
- openscript/library/prices.py +36 -0
- openscript/library/ranges.py +105 -0
- openscript/library/rounding.py +123 -0
- openscript/library/series.py +213 -0
- openscript/library/stateful.py +442 -0
- openscript/library/stateless.py +261 -0
- openscript/library/strength.py +180 -0
- openscript/library/strings.py +228 -0
- openscript/library/trend.py +260 -0
- openscript/library/values.py +91 -0
- openscript/logbook.py +119 -0
- openscript/machine.py +499 -0
- openscript/memory.py +204 -0
- openscript/opcodes.py +166 -0
- openscript/program.py +146 -0
- openscript/run.py +368 -0
- openscript/strategy/__init__.py +78 -0
- openscript/strategy/calls.py +201 -0
- openscript/strategy/closable.py +182 -0
- openscript/strategy/fills.py +131 -0
- openscript/strategy/holdings.py +277 -0
- openscript/strategy/intents.py +162 -0
- openscript/strategy/ledger.py +270 -0
- openscript/strategy/placing.py +206 -0
- openscript/strategy/positions.py +124 -0
- openscript/strategy/refusals.py +293 -0
- openscript/strategy/rows.py +219 -0
- openscript/strategy/sizing.py +229 -0
- openscript/strategy/statuses.py +65 -0
- openscript/surface/__init__.py +115 -0
- openscript/surface/bands.py +103 -0
- openscript/surface/levels.py +44 -0
- openscript/surface/marks.py +52 -0
- openscript/surface/paints.py +58 -0
- openscript/surface/plots.py +44 -0
- openscript/surface/published.py +119 -0
- openscript/values.py +210 -0
- openscript/verify.py +301 -0
- openscript/verify_code.py +290 -0
- openscript/verify_requests.py +271 -0
- openscript/verify_shape.py +162 -0
- openscript/verify_tables.py +256 -0
- openscript/version.py +39 -0
- openscript/zones.py +118 -0
- openscript-0.4.0.dist-info/METADATA +82 -0
- openscript-0.4.0.dist-info/RECORD +97 -0
- openscript-0.4.0.dist-info/WHEEL +5 -0
- openscript-0.4.0.dist-info/top_level.txt +1 -0
openscript/machine.py
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
"""The instruction loop: forty-one arms and nothing clever in any of them.
|
|
2
|
+
|
|
3
|
+
Section 4 is deliberately small and deliberately dull. There is one way to do
|
|
4
|
+
each thing, no instruction is a shorthand for two others, and a new one cannot be
|
|
5
|
+
added without a major format bump. So this is a loop over a list, with a table of
|
|
6
|
+
handlers, and the table is data: a name in a compiled program never becomes a
|
|
7
|
+
function here, and nothing on this path builds anything out of text.
|
|
8
|
+
|
|
9
|
+
**Verification is what lets this be plain.** A verified program cannot underflow
|
|
10
|
+
the stack, cannot jump out of bounds, cannot address a slot that does not exist
|
|
11
|
+
and cannot loop without charging the budget (section 3.5), so none of that is
|
|
12
|
+
tested again per instruction. What is tested per instruction is the one thing
|
|
13
|
+
verification cannot decide: the tags of the values, which is where absence lives.
|
|
14
|
+
|
|
15
|
+
**Absence has a rule in every arm and the rule is the page's.** It propagates
|
|
16
|
+
through arithmetic and ordering comparison, is total under equality, is three
|
|
17
|
+
valued under the logical operators, and is false at a branch. Those five rules
|
|
18
|
+
are ``values.py``'s, and an engine that special cased absence anywhere else would
|
|
19
|
+
have a bug rather than an optimisation.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import math
|
|
23
|
+
from typing import Any, Callable, Dict, List, Optional, Sequence
|
|
24
|
+
|
|
25
|
+
from .budget import Budget
|
|
26
|
+
from .contracts import CallContext, Library
|
|
27
|
+
from .diagnostics import NO_POSITION, Position, raise_at
|
|
28
|
+
from .memory import Cells, Channels, Frame, Register, States, read_history
|
|
29
|
+
from .program import LoadedProgram
|
|
30
|
+
from .values import (
|
|
31
|
+
ABSENT,
|
|
32
|
+
ArrayValue,
|
|
33
|
+
conjunction,
|
|
34
|
+
disjunction,
|
|
35
|
+
equal,
|
|
36
|
+
finite,
|
|
37
|
+
is_number,
|
|
38
|
+
is_whole,
|
|
39
|
+
negation,
|
|
40
|
+
ordered,
|
|
41
|
+
stored,
|
|
42
|
+
truthy,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PendingEffect:
|
|
47
|
+
"""One library call that reaches the outside world, held until step 9.
|
|
48
|
+
|
|
49
|
+
A function with an effect does not perform it when it executes: it leaves a
|
|
50
|
+
record here and pushes absent. The list is discarded at step 3 of every
|
|
51
|
+
execution and applied at step 9 only when the bar is decided, which is what
|
|
52
|
+
makes a condition that was true halfway through a bar and false when it
|
|
53
|
+
closed place no order at all.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
__slots__ = ("fn", "name", "effect", "arguments", "position")
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self, fn: int, name: str, effect: str, arguments: List[Any], position: Position
|
|
60
|
+
) -> None:
|
|
61
|
+
self.fn = fn
|
|
62
|
+
self.name = name
|
|
63
|
+
self.effect = effect
|
|
64
|
+
self.arguments = arguments
|
|
65
|
+
self.position = position
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Machine:
|
|
69
|
+
"""One program, its memory, and the loop that walks its instructions."""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
program: LoadedProgram,
|
|
74
|
+
cells: Cells,
|
|
75
|
+
states: States,
|
|
76
|
+
registers: List[Register],
|
|
77
|
+
channels: Channels,
|
|
78
|
+
budget: Budget,
|
|
79
|
+
library: Library,
|
|
80
|
+
) -> None:
|
|
81
|
+
self.program = program
|
|
82
|
+
self.cells = cells
|
|
83
|
+
self.states = states
|
|
84
|
+
self.registers = registers
|
|
85
|
+
self.channels = channels
|
|
86
|
+
self.budget = budget
|
|
87
|
+
self.library = library
|
|
88
|
+
self.pending: List[PendingEffect] = []
|
|
89
|
+
self.context = CallContext()
|
|
90
|
+
self.bar_index = 0
|
|
91
|
+
self._frames: List[Frame] = []
|
|
92
|
+
self._loop_line: Optional[int] = None
|
|
93
|
+
self._answer: Any = ABSENT
|
|
94
|
+
self._running = False
|
|
95
|
+
self._arms: Dict[str, Callable[[Frame, Sequence[Any]], None]] = {
|
|
96
|
+
"CONST": self._const,
|
|
97
|
+
"DUP": self._dup,
|
|
98
|
+
"POP": self._pop_top,
|
|
99
|
+
"LOAD": self._load,
|
|
100
|
+
"STORE": self._store,
|
|
101
|
+
"CELL_INIT": self._cell_init,
|
|
102
|
+
"LOADC": self._load_cell,
|
|
103
|
+
"STOREC": self._store_cell,
|
|
104
|
+
"SLOAD": self._series_load,
|
|
105
|
+
"SSTORE": self._series_store,
|
|
106
|
+
"HIST": self._history,
|
|
107
|
+
"HISTP": self._history_parameter,
|
|
108
|
+
"ADD": self._add,
|
|
109
|
+
"SUB": self._subtract,
|
|
110
|
+
"MUL": self._multiply,
|
|
111
|
+
"DIV": self._divide,
|
|
112
|
+
"MOD": self._modulo,
|
|
113
|
+
"NEG": self._negate,
|
|
114
|
+
"LT": self._less,
|
|
115
|
+
"LE": self._less_equal,
|
|
116
|
+
"GT": self._greater,
|
|
117
|
+
"GE": self._greater_equal,
|
|
118
|
+
"EQ": self._equals,
|
|
119
|
+
"NE": self._differs,
|
|
120
|
+
"NOT": self._not,
|
|
121
|
+
"AND": self._and,
|
|
122
|
+
"OR": self._or,
|
|
123
|
+
"AND_SHORT": self._and_short,
|
|
124
|
+
"OR_SHORT": self._or_short,
|
|
125
|
+
"JUMP": self._jump,
|
|
126
|
+
"JUMP_FALSE": self._jump_false,
|
|
127
|
+
"TICK": self._tick,
|
|
128
|
+
"FOR_INIT": self._for_init,
|
|
129
|
+
"FOR_NEXT": self._for_next,
|
|
130
|
+
"ARRAY": self._array,
|
|
131
|
+
"ELEM": self._element,
|
|
132
|
+
"CALL_LIB": self._call_library,
|
|
133
|
+
"CALL_FN": self._call_function,
|
|
134
|
+
"RET": self._return,
|
|
135
|
+
"EMIT": self._emit,
|
|
136
|
+
"HALT": self._halt,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# -- the loop ---------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
def begin(self, bar_index: int, context: CallContext) -> None:
|
|
142
|
+
"""Step 3's share: a fresh frame 0 with every slot absent, and the counters.
|
|
143
|
+
|
|
144
|
+
The frame exists before step 6 rather than at it, because step 5 writes
|
|
145
|
+
each input's effective value into a slot of this frame and the two steps
|
|
146
|
+
are in that order.
|
|
147
|
+
"""
|
|
148
|
+
self.bar_index = bar_index
|
|
149
|
+
self.context = context
|
|
150
|
+
self.pending = []
|
|
151
|
+
self._loop_line = None
|
|
152
|
+
self.budget.begin()
|
|
153
|
+
top = self.program.top
|
|
154
|
+
self._frames = [Frame(top.code, top.slots, positions=top.positions)]
|
|
155
|
+
|
|
156
|
+
def write_slot(self, slot: int, value: Any) -> None:
|
|
157
|
+
"""Step 5: one input's effective value, into frame 0."""
|
|
158
|
+
self._frames[0].slots[slot] = stored(value)
|
|
159
|
+
|
|
160
|
+
def execute(self) -> Any:
|
|
161
|
+
"""Step 6: run the top-level list from instruction 0 until ``HALT``."""
|
|
162
|
+
return self._walk()
|
|
163
|
+
|
|
164
|
+
def _walk(self) -> Any:
|
|
165
|
+
self._answer = ABSENT
|
|
166
|
+
self._running = True
|
|
167
|
+
while self._running:
|
|
168
|
+
current = self._frames[-1]
|
|
169
|
+
if self.budget.step():
|
|
170
|
+
self.budget.spent(self.here(), self._loop_line)
|
|
171
|
+
instruction = current.code[current.pc]
|
|
172
|
+
current.pc += 1
|
|
173
|
+
self._arms[instruction[0]](current, instruction)
|
|
174
|
+
return self._answer
|
|
175
|
+
|
|
176
|
+
def here(self) -> Position:
|
|
177
|
+
"""Where the instruction that is executing came from in the source."""
|
|
178
|
+
if not self._frames:
|
|
179
|
+
return NO_POSITION
|
|
180
|
+
frame = self._frames[-1]
|
|
181
|
+
at = frame.pc - 1
|
|
182
|
+
positions = frame.positions
|
|
183
|
+
if positions is None or at < 0 or at >= len(positions):
|
|
184
|
+
return NO_POSITION
|
|
185
|
+
return positions[at]
|
|
186
|
+
|
|
187
|
+
# -- constants and stack ----------------------------------------------
|
|
188
|
+
|
|
189
|
+
def _const(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
190
|
+
frame.stack.append(self.program.consts[instruction[1]])
|
|
191
|
+
|
|
192
|
+
def _dup(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
193
|
+
frame.stack.append(frame.stack[-1])
|
|
194
|
+
|
|
195
|
+
def _pop_top(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
196
|
+
frame.stack.pop()
|
|
197
|
+
|
|
198
|
+
# -- slots -------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
def _load(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
201
|
+
frame.stack.append(frame.slots[instruction[1]])
|
|
202
|
+
|
|
203
|
+
def _store(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
204
|
+
frame.slots[instruction[1]] = stored(frame.stack.pop())
|
|
205
|
+
|
|
206
|
+
# -- cells --------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
def _cell_init(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
209
|
+
cell = frame.cell_base + instruction[1]
|
|
210
|
+
if self.cells.initialised(cell):
|
|
211
|
+
frame.pc = instruction[2]
|
|
212
|
+
return
|
|
213
|
+
# Marked before the initialiser runs, not after: the two differ only if
|
|
214
|
+
# the initialiser could reach this instruction again, which needs
|
|
215
|
+
# recursion, which is an error.
|
|
216
|
+
self.cells.mark(cell)
|
|
217
|
+
|
|
218
|
+
def _load_cell(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
219
|
+
frame.stack.append(self.cells.read(frame.cell_base + instruction[1]))
|
|
220
|
+
|
|
221
|
+
def _store_cell(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
222
|
+
self.cells.write(frame.cell_base + instruction[1], frame.stack.pop())
|
|
223
|
+
|
|
224
|
+
# -- series registers ---------------------------------------------------
|
|
225
|
+
|
|
226
|
+
def _series_load(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
227
|
+
frame.stack.append(self.registers[instruction[1]].current)
|
|
228
|
+
|
|
229
|
+
def _series_store(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
230
|
+
self.registers[instruction[1]].current = stored(frame.stack.pop())
|
|
231
|
+
|
|
232
|
+
def _read_back(self, frame: Frame, register: int) -> None:
|
|
233
|
+
back = frame.stack.pop()
|
|
234
|
+
frame.stack.append(
|
|
235
|
+
read_history(
|
|
236
|
+
self.registers[register],
|
|
237
|
+
back,
|
|
238
|
+
self.bar_index,
|
|
239
|
+
self.program.retained,
|
|
240
|
+
self.here(),
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def _history(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
245
|
+
self._read_back(frame, instruction[1])
|
|
246
|
+
|
|
247
|
+
def _history_parameter(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
248
|
+
# A parameter whose call site bound -1 cannot be reached by a HISTP: the
|
|
249
|
+
# compiler binds a register only for a parameter whose history the body
|
|
250
|
+
# reads, and a HISTP against -1 is a corrupt program the verifier caught.
|
|
251
|
+
self._read_back(frame, frame.bindings[instruction[1]])
|
|
252
|
+
|
|
253
|
+
# -- arithmetic ---------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
def _two(self, frame: Frame):
|
|
256
|
+
right = frame.stack.pop()
|
|
257
|
+
return frame.stack.pop(), right
|
|
258
|
+
|
|
259
|
+
def _add(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
260
|
+
left, right = self._two(frame)
|
|
261
|
+
if left is ABSENT or right is ABSENT:
|
|
262
|
+
frame.stack.append(ABSENT)
|
|
263
|
+
return
|
|
264
|
+
if is_number(left) and is_number(right):
|
|
265
|
+
frame.stack.append(finite(left + right))
|
|
266
|
+
return
|
|
267
|
+
if isinstance(left, str) and isinstance(right, str):
|
|
268
|
+
# The one operator that grows a string, and so the one that meets
|
|
269
|
+
# the ceiling: a log appended to on every bar is what OS5008 is for.
|
|
270
|
+
frame.stack.append(self.budget.text(self.here(), left + right))
|
|
271
|
+
return
|
|
272
|
+
# Any other combination is a program the checker should have rejected,
|
|
273
|
+
# and an engine that invented a conversion here would become the only
|
|
274
|
+
# engine that runs that script.
|
|
275
|
+
frame.stack.append(ABSENT)
|
|
276
|
+
|
|
277
|
+
def _numeric(self, frame: Frame, of: Callable[[float, float], Any]) -> None:
|
|
278
|
+
left, right = self._two(frame)
|
|
279
|
+
if not is_number(left) or not is_number(right):
|
|
280
|
+
frame.stack.append(ABSENT)
|
|
281
|
+
return
|
|
282
|
+
frame.stack.append(of(float(left), float(right)))
|
|
283
|
+
|
|
284
|
+
def _subtract(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
285
|
+
self._numeric(frame, lambda a, b: finite(a - b))
|
|
286
|
+
|
|
287
|
+
def _multiply(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
288
|
+
self._numeric(frame, lambda a, b: finite(a * b))
|
|
289
|
+
|
|
290
|
+
def _divide(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
291
|
+
# Absent when the divisor is zero, zero divided by zero included: the
|
|
292
|
+
# language has no infinity and no not-a-number to hand back.
|
|
293
|
+
self._numeric(frame, lambda a, b: ABSENT if b == 0 else finite(a / b))
|
|
294
|
+
|
|
295
|
+
def _modulo(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
296
|
+
# The remainder of truncated division, so the sign follows the left
|
|
297
|
+
# operand rather than the divisor.
|
|
298
|
+
self._numeric(frame, lambda a, b: ABSENT if b == 0 else finite(math.fmod(a, b)))
|
|
299
|
+
|
|
300
|
+
def _negate(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
301
|
+
value = frame.stack.pop()
|
|
302
|
+
frame.stack.append(finite(-float(value)) if is_number(value) else ABSENT)
|
|
303
|
+
|
|
304
|
+
# -- comparison ---------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
def _order(self, frame: Frame, wanted: Callable[[float], bool]) -> None:
|
|
307
|
+
left, right = self._two(frame)
|
|
308
|
+
found = ordered(left, right)
|
|
309
|
+
frame.stack.append(ABSENT if found is ABSENT else wanted(found))
|
|
310
|
+
|
|
311
|
+
def _less(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
312
|
+
self._order(frame, lambda found: found < 0)
|
|
313
|
+
|
|
314
|
+
def _less_equal(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
315
|
+
self._order(frame, lambda found: found <= 0)
|
|
316
|
+
|
|
317
|
+
def _greater(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
318
|
+
self._order(frame, lambda found: found > 0)
|
|
319
|
+
|
|
320
|
+
def _greater_equal(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
321
|
+
self._order(frame, lambda found: found >= 0)
|
|
322
|
+
|
|
323
|
+
def _equals(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
324
|
+
left, right = self._two(frame)
|
|
325
|
+
frame.stack.append(equal(left, right))
|
|
326
|
+
|
|
327
|
+
def _differs(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
328
|
+
left, right = self._two(frame)
|
|
329
|
+
frame.stack.append(not equal(left, right))
|
|
330
|
+
|
|
331
|
+
# -- logic ---------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
def _not(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
334
|
+
frame.stack.append(negation(frame.stack.pop()))
|
|
335
|
+
|
|
336
|
+
def _and(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
337
|
+
left, right = self._two(frame)
|
|
338
|
+
frame.stack.append(conjunction(left, right))
|
|
339
|
+
|
|
340
|
+
def _or(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
341
|
+
left, right = self._two(frame)
|
|
342
|
+
frame.stack.append(disjunction(left, right))
|
|
343
|
+
|
|
344
|
+
def _and_short(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
345
|
+
# The value that decides the answer by itself stays on the stack as the
|
|
346
|
+
# answer. Absence decides neither operator, so it does not short circuit.
|
|
347
|
+
if frame.stack[-1] is False:
|
|
348
|
+
frame.pc = instruction[1]
|
|
349
|
+
|
|
350
|
+
def _or_short(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
351
|
+
if frame.stack[-1] is True:
|
|
352
|
+
frame.pc = instruction[1]
|
|
353
|
+
|
|
354
|
+
# -- branching and loops --------------------------------------------------
|
|
355
|
+
|
|
356
|
+
def _jump(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
357
|
+
frame.pc = instruction[1]
|
|
358
|
+
|
|
359
|
+
def _jump_false(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
360
|
+
if not truthy(frame.stack.pop()):
|
|
361
|
+
frame.pc = instruction[1]
|
|
362
|
+
|
|
363
|
+
def _tick(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
364
|
+
line = self.program.loop_lines[instruction[1]]
|
|
365
|
+
self._loop_line = line
|
|
366
|
+
self.budget.tick(self.here(), line)
|
|
367
|
+
|
|
368
|
+
def _for_init(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
369
|
+
step = frame.stack.pop()
|
|
370
|
+
limit = frame.stack.pop()
|
|
371
|
+
start = frame.stack.pop()
|
|
372
|
+
bounds = ((start, "start"), (limit, "limit"), (step, "step"))
|
|
373
|
+
for value, bound in bounds:
|
|
374
|
+
if value is ABSENT:
|
|
375
|
+
# An absent bound means the number of iterations is unknown, and
|
|
376
|
+
# running zero times would hide that.
|
|
377
|
+
raise_at("OS4013", self.here(), bound=bound)
|
|
378
|
+
for value, _bound in bounds:
|
|
379
|
+
if not is_number(value):
|
|
380
|
+
raise_at("OS4001", self.here(), index=str(value))
|
|
381
|
+
if step == 0:
|
|
382
|
+
raise_at(
|
|
383
|
+
"OS3004",
|
|
384
|
+
self.here(),
|
|
385
|
+
name="this loop",
|
|
386
|
+
argument="step",
|
|
387
|
+
range="anything but zero",
|
|
388
|
+
found=0,
|
|
389
|
+
)
|
|
390
|
+
frame.slots[instruction[2]] = start
|
|
391
|
+
frame.slots[instruction[3]] = limit
|
|
392
|
+
frame.slots[instruction[4]] = step
|
|
393
|
+
# A descending range with a positive step runs zero times and is never
|
|
394
|
+
# silently reversed.
|
|
395
|
+
if (start > limit) if step > 0 else (start < limit):
|
|
396
|
+
frame.pc = instruction[5]
|
|
397
|
+
|
|
398
|
+
def _for_next(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
399
|
+
limit = frame.slots[instruction[3]]
|
|
400
|
+
step = frame.slots[instruction[4]]
|
|
401
|
+
moved = finite(float(frame.slots[instruction[2]]) + float(step))
|
|
402
|
+
frame.slots[instruction[2]] = moved
|
|
403
|
+
if moved is ABSENT:
|
|
404
|
+
return
|
|
405
|
+
if (moved <= limit) if step > 0 else (moved >= limit):
|
|
406
|
+
frame.pc = instruction[5]
|
|
407
|
+
|
|
408
|
+
# -- arrays ----------------------------------------------------------------
|
|
409
|
+
|
|
410
|
+
def _array(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
411
|
+
count = instruction[1]
|
|
412
|
+
held = frame.stack[len(frame.stack) - count :] if count else []
|
|
413
|
+
del frame.stack[len(frame.stack) - count :]
|
|
414
|
+
# A new array every time it executes, which is why an array literal is
|
|
415
|
+
# not a constant pool entry.
|
|
416
|
+
frame.stack.append(ArrayValue(list(held)))
|
|
417
|
+
|
|
418
|
+
def _element(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
419
|
+
index = frame.stack.pop()
|
|
420
|
+
array = frame.stack.pop()
|
|
421
|
+
size = len(array.elements) if isinstance(array, ArrayValue) else 0
|
|
422
|
+
usable = (
|
|
423
|
+
isinstance(array, ArrayValue)
|
|
424
|
+
and is_whole(index)
|
|
425
|
+
and 0 <= index < size
|
|
426
|
+
)
|
|
427
|
+
if not usable:
|
|
428
|
+
# The opposite of a history read past the start of the dataset, and
|
|
429
|
+
# the difference is the point: an array has an extent the script
|
|
430
|
+
# chose, so an index outside it is a mistake.
|
|
431
|
+
raise_at(
|
|
432
|
+
"OS4004",
|
|
433
|
+
self.here(),
|
|
434
|
+
index="none" if index is ABSENT else str(index),
|
|
435
|
+
name="the array",
|
|
436
|
+
size=size,
|
|
437
|
+
)
|
|
438
|
+
frame.stack.append(array.elements[int(index)])
|
|
439
|
+
|
|
440
|
+
# -- calls -------------------------------------------------------------------
|
|
441
|
+
|
|
442
|
+
def _call_library(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
443
|
+
which, count, state = instruction[1], instruction[2], instruction[3]
|
|
444
|
+
arguments = frame.stack[len(frame.stack) - count :] if count else []
|
|
445
|
+
del frame.stack[len(frame.stack) - count :]
|
|
446
|
+
entry = self.program.entries[which]
|
|
447
|
+
if entry.effect != "none":
|
|
448
|
+
self.pending.append(
|
|
449
|
+
PendingEffect(which, entry.name, entry.effect, list(arguments), self.here())
|
|
450
|
+
)
|
|
451
|
+
frame.stack.append(ABSENT)
|
|
452
|
+
return
|
|
453
|
+
region = None if state < 0 else self.states.region(frame.state_base + state)
|
|
454
|
+
# The string ceiling, both halves: the length a call can be asked for
|
|
455
|
+
# before a character of it exists, and the string it did build. A call of
|
|
456
|
+
# that passes a string through, or answers a fact the host stated, hands
|
|
457
|
+
# back a string it did not make, and refusing that would refuse the host
|
|
458
|
+
# its own symbol for being long.
|
|
459
|
+
self.budget.measured(self.here(), self.library.length_of(entry.name, arguments))
|
|
460
|
+
answer = self.library.call(entry.name, list(arguments), region, self.context)
|
|
461
|
+
if isinstance(answer, str) and self.library.builds_a_string(entry.name):
|
|
462
|
+
self.budget.text(self.here(), answer)
|
|
463
|
+
frame.stack.append(stored(answer))
|
|
464
|
+
|
|
465
|
+
def _call_function(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
466
|
+
site = self.program.call_sites[instruction[1]]
|
|
467
|
+
body = self.program.functions[site["fn"]]
|
|
468
|
+
count = site["argc"]
|
|
469
|
+
arguments = frame.stack[len(frame.stack) - count :] if count else []
|
|
470
|
+
del frame.stack[len(frame.stack) - count :]
|
|
471
|
+
called = Frame(
|
|
472
|
+
body.code,
|
|
473
|
+
body.slots,
|
|
474
|
+
cell_base=site["cellBase"],
|
|
475
|
+
state_base=site["stateBase"],
|
|
476
|
+
bindings=site["series"],
|
|
477
|
+
positions=body.positions,
|
|
478
|
+
)
|
|
479
|
+
for at, value in enumerate(arguments):
|
|
480
|
+
called.slots[at] = value
|
|
481
|
+
self._frames.append(called)
|
|
482
|
+
|
|
483
|
+
def _return(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
484
|
+
value = frame.stack.pop()
|
|
485
|
+
self._frames.pop()
|
|
486
|
+
if not self._frames:
|
|
487
|
+
self._answer = value
|
|
488
|
+
self._running = False
|
|
489
|
+
return
|
|
490
|
+
self._frames[-1].stack.append(value)
|
|
491
|
+
|
|
492
|
+
# -- output and termination -----------------------------------------------------
|
|
493
|
+
|
|
494
|
+
def _emit(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
495
|
+
self.channels.write(instruction[1], frame.stack.pop())
|
|
496
|
+
|
|
497
|
+
def _halt(self, frame: Frame, instruction: Sequence[Any]) -> None:
|
|
498
|
+
self._answer = ABSENT
|
|
499
|
+
self._running = False
|
openscript/memory.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""The regions section 3.2 tabulates, each with the lifetime that table gives it.
|
|
2
|
+
|
|
3
|
+
Three lifetimes and one table. Slots, the operand stack and the channels last one
|
|
4
|
+
execution of a bar; cells, library state regions, register histories and the
|
|
5
|
+
object heap last across bars; and the third and fourth columns of that table,
|
|
6
|
+
what rolls back and what a checkpoint holds, are section 6 and are ``run.py``'s.
|
|
7
|
+
|
|
8
|
+
**A register is the only thing with history.** ``HIST`` and ``HISTP`` are the
|
|
9
|
+
only way to read one, and the resolution order below is the page's own numbered
|
|
10
|
+
list, in that order, because case 3 and case 4 are different on purpose: in case
|
|
11
|
+
3 the value never existed, and in case 4 it existed and the engine threw it away.
|
|
12
|
+
Returning absence for both would hide a real bug behind a plausible gap.
|
|
13
|
+
|
|
14
|
+
**Trimming moves the storage and not the bar numbering.** A register that has
|
|
15
|
+
dropped its oldest entries still answers about bar 300 when asked for bar 300, so
|
|
16
|
+
the count of what was dropped is kept beside the entries rather than the caller
|
|
17
|
+
being asked to subtract.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import Any, Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
from .canonical import canonical_number
|
|
23
|
+
from .diagnostics import Position, raise_at
|
|
24
|
+
from .values import ABSENT, is_number, is_whole, stored
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Register:
|
|
28
|
+
"""One series register: a history of one value per bar, plus this bar's cell."""
|
|
29
|
+
|
|
30
|
+
__slots__ = ("history", "current", "dropped")
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
self.history: List[Any] = []
|
|
34
|
+
self.current: Any = ABSENT
|
|
35
|
+
#: How many of the oldest entries trimming has thrown away.
|
|
36
|
+
self.dropped: int = 0
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def length(self) -> int:
|
|
40
|
+
"""The number of bars this register has entries for, trimmed or not."""
|
|
41
|
+
return self.dropped + len(self.history)
|
|
42
|
+
|
|
43
|
+
def truncate(self, length: int) -> None:
|
|
44
|
+
"""Step 2: discard any entry a previous execution of this bar wrote."""
|
|
45
|
+
keep = max(0, length - self.dropped)
|
|
46
|
+
if keep < len(self.history):
|
|
47
|
+
del self.history[keep:]
|
|
48
|
+
|
|
49
|
+
def close(self) -> None:
|
|
50
|
+
"""Step 7: the current cell becomes this bar's entry."""
|
|
51
|
+
self.history.append(self.current)
|
|
52
|
+
|
|
53
|
+
def at(self, bar: int) -> Any:
|
|
54
|
+
"""The value this register held on that bar, or absence where it is gone."""
|
|
55
|
+
where = bar - self.dropped
|
|
56
|
+
if where < 0 or where >= len(self.history):
|
|
57
|
+
return ABSENT
|
|
58
|
+
return self.history[where]
|
|
59
|
+
|
|
60
|
+
def trim(self, depth: int) -> None:
|
|
61
|
+
"""Step 10: drop entries older than the retained depth.
|
|
62
|
+
|
|
63
|
+
``depth`` entries plus the bar just closed, because a read at the depth
|
|
64
|
+
itself is the last one section 4.4's case 4 does not refuse.
|
|
65
|
+
"""
|
|
66
|
+
keep = depth + 1
|
|
67
|
+
if len(self.history) <= keep:
|
|
68
|
+
return
|
|
69
|
+
self.dropped += len(self.history) - keep
|
|
70
|
+
del self.history[: len(self.history) - keep]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def read_history(
|
|
74
|
+
register: Register,
|
|
75
|
+
offset: Any,
|
|
76
|
+
bar_index: int,
|
|
77
|
+
retained: Optional[int],
|
|
78
|
+
position: Position,
|
|
79
|
+
) -> Any:
|
|
80
|
+
"""``HIST``, resolved in the order section 4.4 gives and no other."""
|
|
81
|
+
if offset is ABSENT:
|
|
82
|
+
return ABSENT
|
|
83
|
+
if not is_whole(offset) or offset < 0:
|
|
84
|
+
raise_at("OS4001", position, index=_written(offset))
|
|
85
|
+
back = int(offset)
|
|
86
|
+
if back > bar_index:
|
|
87
|
+
return ABSENT
|
|
88
|
+
if retained is not None and back > retained:
|
|
89
|
+
raise_at("OS4002", position, index=back, depth=retained, suggested=back)
|
|
90
|
+
# Offset zero is the bar being executed, which is the current cell and not a
|
|
91
|
+
# history entry: the entry for this bar is appended at step 7, after the
|
|
92
|
+
# code that is asking has finished running.
|
|
93
|
+
if back == 0:
|
|
94
|
+
return register.current
|
|
95
|
+
return register.at(bar_index - back)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _written(value: Any) -> str:
|
|
99
|
+
"""The offending value as the message shows it."""
|
|
100
|
+
if value is ABSENT:
|
|
101
|
+
return "none"
|
|
102
|
+
if is_number(value):
|
|
103
|
+
return canonical_number(value)
|
|
104
|
+
return str(value)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Cells:
|
|
108
|
+
"""The persistent values of section 2.11: one entry per ``var`` declaration.
|
|
109
|
+
|
|
110
|
+
Both kinds survive from bar to bar and both start uninitialised, which is a
|
|
111
|
+
state of its own rather than a value: a cell inside an ``if`` that is false
|
|
112
|
+
for a hundred bars is absent for a hundred bars, and ``CELL_INIT`` is what
|
|
113
|
+
ends that.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
__slots__ = ("values", "ready")
|
|
117
|
+
|
|
118
|
+
def __init__(self, count: int) -> None:
|
|
119
|
+
self.values: List[Any] = [ABSENT] * count
|
|
120
|
+
self.ready: List[bool] = [False] * count
|
|
121
|
+
|
|
122
|
+
def initialised(self, cell: int) -> bool:
|
|
123
|
+
return self.ready[cell]
|
|
124
|
+
|
|
125
|
+
def mark(self, cell: int) -> None:
|
|
126
|
+
self.ready[cell] = True
|
|
127
|
+
|
|
128
|
+
def read(self, cell: int) -> Any:
|
|
129
|
+
return self.values[cell]
|
|
130
|
+
|
|
131
|
+
def write(self, cell: int, value: Any) -> None:
|
|
132
|
+
self.values[cell] = stored(value)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class States:
|
|
136
|
+
"""The per-call-site regions of the library functions that hold state.
|
|
137
|
+
|
|
138
|
+
What a region contains is the library's, section 20, and not this engine's.
|
|
139
|
+
What this engine requires of it is that it be snapshottable by a mechanical
|
|
140
|
+
copy, because the rollback and replay rules apply to every region at once and
|
|
141
|
+
an engine must be able to copy one without knowing which function owns it.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
__slots__ = ("regions",)
|
|
145
|
+
|
|
146
|
+
def __init__(self, count: int) -> None:
|
|
147
|
+
self.regions: List[Dict[str, Any]] = [{} for _ in range(count)]
|
|
148
|
+
|
|
149
|
+
def region(self, at: int) -> Dict[str, Any]:
|
|
150
|
+
return self.regions[at]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class Channels:
|
|
154
|
+
"""Everything a script draws for a bar, one value per channel per bar.
|
|
155
|
+
|
|
156
|
+
``EMIT`` is the only way to write one and a second write on a bar replaces
|
|
157
|
+
the first, so the last write wins and no drawing surface needs a rule of its
|
|
158
|
+
own. A channel nothing wrote is absent, and absence reaching a surface is a
|
|
159
|
+
gap in a plot, no marker, no alert, a bar left its own colour: never a zero.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
__slots__ = ("values",)
|
|
163
|
+
|
|
164
|
+
def __init__(self, count: int) -> None:
|
|
165
|
+
self.values: List[Any] = [ABSENT] * count
|
|
166
|
+
|
|
167
|
+
def clear(self) -> None:
|
|
168
|
+
for at in range(len(self.values)):
|
|
169
|
+
self.values[at] = ABSENT
|
|
170
|
+
|
|
171
|
+
def write(self, channel: int, value: Any) -> None:
|
|
172
|
+
self.values[channel] = stored(value)
|
|
173
|
+
|
|
174
|
+
def read(self, channel: int) -> Any:
|
|
175
|
+
return self.values[channel]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class Frame:
|
|
179
|
+
"""One frame, section 3.3: a list, a counter and the bases a body reads through.
|
|
180
|
+
|
|
181
|
+
A frame's stack region is its own. A called function cannot see or disturb
|
|
182
|
+
the caller's operands, which is what makes the verifier's job local and what
|
|
183
|
+
lets the depth walk of check 5 stop at a call.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
__slots__ = ("code", "pc", "slots", "cell_base", "state_base", "bindings", "stack", "positions")
|
|
187
|
+
|
|
188
|
+
def __init__(
|
|
189
|
+
self,
|
|
190
|
+
code: List[Any],
|
|
191
|
+
slots: int,
|
|
192
|
+
cell_base: int = 0,
|
|
193
|
+
state_base: int = 0,
|
|
194
|
+
bindings: Optional[List[int]] = None,
|
|
195
|
+
positions: Optional[List[Any]] = None,
|
|
196
|
+
) -> None:
|
|
197
|
+
self.code = code
|
|
198
|
+
self.pc = 0
|
|
199
|
+
self.slots: List[Any] = [ABSENT] * slots
|
|
200
|
+
self.cell_base = cell_base
|
|
201
|
+
self.state_base = state_base
|
|
202
|
+
self.bindings: List[int] = [] if bindings is None else bindings
|
|
203
|
+
self.stack: List[Any] = []
|
|
204
|
+
self.positions = positions
|