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.
Files changed (97) hide show
  1. openscript/__init__.py +40 -0
  2. openscript/__main__.py +62 -0
  3. openscript/accounting/__init__.py +74 -0
  4. openscript/accounting/analysis.py +174 -0
  5. openscript/accounting/charges.py +397 -0
  6. openscript/accounting/equity.py +234 -0
  7. openscript/accounting/report.py +82 -0
  8. openscript/accounting/shapes.py +74 -0
  9. openscript/accounting/statistics.py +300 -0
  10. openscript/accounting/trades.py +294 -0
  11. openscript/adapter/__init__.py +32 -0
  12. openscript/adapter/answers.py +215 -0
  13. openscript/adapter/channels.py +137 -0
  14. openscript/adapter/expectations.py +67 -0
  15. openscript/adapter/facts.py +127 -0
  16. openscript/adapter/matching.py +257 -0
  17. openscript/adapter/ordering.py +187 -0
  18. openscript/adapter/page.py +130 -0
  19. openscript/adapter/reading.py +357 -0
  20. openscript/adapter/reporting.py +244 -0
  21. openscript/adapter/running.py +449 -0
  22. openscript/adapter/serving.py +229 -0
  23. openscript/adapter/sessions.py +168 -0
  24. openscript/adapter/spellings.py +184 -0
  25. openscript/bars.py +157 -0
  26. openscript/budget.py +342 -0
  27. openscript/canonical.py +192 -0
  28. openscript/civil.py +196 -0
  29. openscript/contracts.py +165 -0
  30. openscript/dates.py +302 -0
  31. openscript/diagnostics.py +104 -0
  32. openscript/hours.py +165 -0
  33. openscript/inputs.py +239 -0
  34. openscript/intervals.py +60 -0
  35. openscript/library/__init__.py +76 -0
  36. openscript/library/arithmetic.py +128 -0
  37. openscript/library/averages.py +133 -0
  38. openscript/library/bars.py +60 -0
  39. openscript/library/bookkeeping.py +166 -0
  40. openscript/library/code_points.py +85 -0
  41. openscript/library/colour.py +202 -0
  42. openscript/library/composites.py +208 -0
  43. openscript/library/counting.py +218 -0
  44. openscript/library/deviation.py +155 -0
  45. openscript/library/elementary.py +206 -0
  46. openscript/library/extremes.py +122 -0
  47. openscript/library/flows.py +220 -0
  48. openscript/library/momentum.py +203 -0
  49. openscript/library/number_text.py +223 -0
  50. openscript/library/prices.py +36 -0
  51. openscript/library/ranges.py +105 -0
  52. openscript/library/rounding.py +123 -0
  53. openscript/library/series.py +213 -0
  54. openscript/library/stateful.py +442 -0
  55. openscript/library/stateless.py +261 -0
  56. openscript/library/strength.py +180 -0
  57. openscript/library/strings.py +228 -0
  58. openscript/library/trend.py +260 -0
  59. openscript/library/values.py +91 -0
  60. openscript/logbook.py +119 -0
  61. openscript/machine.py +499 -0
  62. openscript/memory.py +204 -0
  63. openscript/opcodes.py +166 -0
  64. openscript/program.py +146 -0
  65. openscript/run.py +368 -0
  66. openscript/strategy/__init__.py +78 -0
  67. openscript/strategy/calls.py +201 -0
  68. openscript/strategy/closable.py +182 -0
  69. openscript/strategy/fills.py +131 -0
  70. openscript/strategy/holdings.py +277 -0
  71. openscript/strategy/intents.py +162 -0
  72. openscript/strategy/ledger.py +270 -0
  73. openscript/strategy/placing.py +206 -0
  74. openscript/strategy/positions.py +124 -0
  75. openscript/strategy/refusals.py +293 -0
  76. openscript/strategy/rows.py +219 -0
  77. openscript/strategy/sizing.py +229 -0
  78. openscript/strategy/statuses.py +65 -0
  79. openscript/surface/__init__.py +115 -0
  80. openscript/surface/bands.py +103 -0
  81. openscript/surface/levels.py +44 -0
  82. openscript/surface/marks.py +52 -0
  83. openscript/surface/paints.py +58 -0
  84. openscript/surface/plots.py +44 -0
  85. openscript/surface/published.py +119 -0
  86. openscript/values.py +210 -0
  87. openscript/verify.py +301 -0
  88. openscript/verify_code.py +290 -0
  89. openscript/verify_requests.py +271 -0
  90. openscript/verify_shape.py +162 -0
  91. openscript/verify_tables.py +256 -0
  92. openscript/version.py +39 -0
  93. openscript/zones.py +118 -0
  94. openscript-0.4.0.dist-info/METADATA +82 -0
  95. openscript-0.4.0.dist-info/RECORD +97 -0
  96. openscript-0.4.0.dist-info/WHEEL +5 -0
  97. 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