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
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""Checks 2 to 7 of ``compiled-program.md`` section 3.5, over one instruction list.
|
|
2
|
+
|
|
3
|
+
Verification is not optional and not a debug mode. An engine that skips it can
|
|
4
|
+
be handed a malformed list and will read past the end of an array or jump into
|
|
5
|
+
the middle of an expression, and the failure will look like a wrong number
|
|
6
|
+
rather than a broken program. What these checks buy is what the interpreter is
|
|
7
|
+
then allowed to assume and therefore not to test: a verified program cannot
|
|
8
|
+
underflow the stack, cannot jump out of bounds, cannot address a slot that does
|
|
9
|
+
not exist and cannot loop without charging the budget.
|
|
10
|
+
|
|
11
|
+
Check 6 is the one that looks small and is not. The target of every backward
|
|
12
|
+
jump must be a ``TICK``, so no cycle in the control flow graph can run without
|
|
13
|
+
charging the loop budget, and the whole of that guarantee is checkable by
|
|
14
|
+
looking at one instruction rather than by analysing the graph.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import Any, Callable, Dict, List, Optional, Sequence
|
|
19
|
+
|
|
20
|
+
from .diagnostics import at_instruction
|
|
21
|
+
from .opcodes import (
|
|
22
|
+
INSTRUCTION_TAGS,
|
|
23
|
+
TABLE_NAMES,
|
|
24
|
+
depth_change,
|
|
25
|
+
is_opcode,
|
|
26
|
+
operand_count,
|
|
27
|
+
tables_for,
|
|
28
|
+
targets,
|
|
29
|
+
)
|
|
30
|
+
from .verify_shape import ShapeCheck
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ListLimits:
|
|
35
|
+
"""How big each table an operand may index is, for the list being walked."""
|
|
36
|
+
|
|
37
|
+
slots: int
|
|
38
|
+
cells: int
|
|
39
|
+
states: int
|
|
40
|
+
registers: int
|
|
41
|
+
channels: int
|
|
42
|
+
libFunctions: int
|
|
43
|
+
callSites: int
|
|
44
|
+
loops: int
|
|
45
|
+
consts: int
|
|
46
|
+
bindings: int
|
|
47
|
+
argc_of: Callable[[int], int] = field(default=lambda site: 0)
|
|
48
|
+
|
|
49
|
+
def size(self, table: str) -> Optional[int]:
|
|
50
|
+
return getattr(self, table, None)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def check_list(
|
|
54
|
+
shape: ShapeCheck,
|
|
55
|
+
listing: str,
|
|
56
|
+
code: Sequence[Any],
|
|
57
|
+
limits: ListLimits,
|
|
58
|
+
terminator: str,
|
|
59
|
+
) -> bool:
|
|
60
|
+
"""Walks one instruction list: checks 2, 3, 4, 5 and 6.
|
|
61
|
+
|
|
62
|
+
``terminator`` is ``HALT`` for the bar's own code and ``RET`` for a function
|
|
63
|
+
body or a read's body, which is check 4, and the depth at it must be zero,
|
|
64
|
+
which is check 5.
|
|
65
|
+
"""
|
|
66
|
+
if len(code) == 0:
|
|
67
|
+
return shape.fail(listing, "an instruction list cannot be empty")
|
|
68
|
+
|
|
69
|
+
# Check 2, and every operand in range.
|
|
70
|
+
for at, instruction in enumerate(code):
|
|
71
|
+
where = at_instruction(listing, at)
|
|
72
|
+
if not shape.array(instruction, where):
|
|
73
|
+
return False
|
|
74
|
+
opcode = instruction[0] if instruction else None
|
|
75
|
+
if not is_opcode(opcode):
|
|
76
|
+
return shape.fail(where, f"{opcode} is not an opcode this engine implements")
|
|
77
|
+
operands = instruction[1:]
|
|
78
|
+
if len(operands) != operand_count(opcode):
|
|
79
|
+
return shape.fail(
|
|
80
|
+
where,
|
|
81
|
+
f"{opcode} takes {operand_count(opcode)} operands and carries {len(operands)}",
|
|
82
|
+
)
|
|
83
|
+
if not _check_operands(shape, where, opcode, operands, limits, len(code)):
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
# Check 6: the target of a backward jump is a TICK.
|
|
87
|
+
for position in targets(opcode):
|
|
88
|
+
target = operands[position]
|
|
89
|
+
if target > at:
|
|
90
|
+
continue
|
|
91
|
+
landing = code[target]
|
|
92
|
+
if not isinstance(landing, list) or not landing or landing[0] != "TICK":
|
|
93
|
+
return shape.fail(
|
|
94
|
+
where,
|
|
95
|
+
f"it jumps back to instruction {target}, which is not a TICK, so the loop "
|
|
96
|
+
"would run without charging the budget",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Check 4.
|
|
100
|
+
last = code[-1]
|
|
101
|
+
if not isinstance(last, list) or not last or last[0] != terminator:
|
|
102
|
+
return shape.fail(
|
|
103
|
+
at_instruction(listing, len(code) - 1),
|
|
104
|
+
f"the last instruction of this list must be {terminator}",
|
|
105
|
+
)
|
|
106
|
+
for at in range(len(code) - 1):
|
|
107
|
+
if code[at][0] == "HALT":
|
|
108
|
+
return shape.fail(at_instruction(listing, at), "HALT is only ever the last instruction")
|
|
109
|
+
|
|
110
|
+
return _check_depths(shape, listing, code, limits, terminator)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _check_operands(
|
|
114
|
+
shape: ShapeCheck,
|
|
115
|
+
where: str,
|
|
116
|
+
opcode: str,
|
|
117
|
+
operands: Sequence[Any],
|
|
118
|
+
limits: ListLimits,
|
|
119
|
+
length: int,
|
|
120
|
+
) -> bool:
|
|
121
|
+
tables = tables_for(opcode)
|
|
122
|
+
for at, operand in enumerate(operands):
|
|
123
|
+
if not shape.whole(operand, f"{where} operand {at}"):
|
|
124
|
+
return False
|
|
125
|
+
table = tables[at] if at < len(tables) else None
|
|
126
|
+
if table is None or table == "count":
|
|
127
|
+
if operand < 0:
|
|
128
|
+
return shape.fail(f"{where} operand {at}", "a count cannot be negative")
|
|
129
|
+
continue
|
|
130
|
+
if table == "target":
|
|
131
|
+
# Check 3. A target past the terminator is out of the list.
|
|
132
|
+
if operand < 0 or operand >= length:
|
|
133
|
+
return shape.fail(where, f"the jump target {operand} is outside a list of {length}")
|
|
134
|
+
continue
|
|
135
|
+
# A CALL_LIB with no state carries -1 rather than an index.
|
|
136
|
+
if opcode == "CALL_LIB" and table == "states" and operand == -1:
|
|
137
|
+
continue
|
|
138
|
+
size = limits.size(table)
|
|
139
|
+
if size is None:
|
|
140
|
+
continue
|
|
141
|
+
if operand < 0 or operand >= size:
|
|
142
|
+
named = TABLE_NAMES.get(table, table)
|
|
143
|
+
return shape.fail(
|
|
144
|
+
f"{where} operand {at}", f"{operand} is outside {named}, which holds {size}"
|
|
145
|
+
)
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _check_depths(
|
|
150
|
+
shape: ShapeCheck,
|
|
151
|
+
listing: str,
|
|
152
|
+
code: Sequence[Any],
|
|
153
|
+
limits: ListLimits,
|
|
154
|
+
terminator: str,
|
|
155
|
+
) -> bool:
|
|
156
|
+
"""Check 5: the depth agrees on every path, never goes below zero, is zero at the end.
|
|
157
|
+
|
|
158
|
+
A disagreement at a join is a corrupt program and not a program with an
|
|
159
|
+
unusual shape: every instruction has a fixed effect, so two paths reaching
|
|
160
|
+
one instruction with different depths means one of them was built wrong.
|
|
161
|
+
"""
|
|
162
|
+
depths: List[Optional[int]] = [None] * len(code)
|
|
163
|
+
depths[0] = 0
|
|
164
|
+
pending = [0]
|
|
165
|
+
|
|
166
|
+
def reach(index: int, depth: int, origin: int) -> bool:
|
|
167
|
+
if depth < 0:
|
|
168
|
+
return shape.fail(at_instruction(listing, origin), "it takes the stack below zero")
|
|
169
|
+
known = depths[index]
|
|
170
|
+
if known is None:
|
|
171
|
+
depths[index] = depth
|
|
172
|
+
pending.append(index)
|
|
173
|
+
return True
|
|
174
|
+
if known != depth:
|
|
175
|
+
return shape.fail(
|
|
176
|
+
at_instruction(listing, index),
|
|
177
|
+
f"two paths reach it with the stack {known} and {depth} deep",
|
|
178
|
+
)
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
while pending:
|
|
182
|
+
index = pending.pop()
|
|
183
|
+
instruction = code[index]
|
|
184
|
+
depth = depths[index]
|
|
185
|
+
opcode = instruction[0]
|
|
186
|
+
operands = instruction[1:]
|
|
187
|
+
after = depth + depth_change(opcode, operands, limits.argc_of)
|
|
188
|
+
if opcode == terminator:
|
|
189
|
+
if after != 0:
|
|
190
|
+
return shape.fail(
|
|
191
|
+
at_instruction(listing, index),
|
|
192
|
+
f"the stack is {after} deep at {terminator} and must be empty",
|
|
193
|
+
)
|
|
194
|
+
continue
|
|
195
|
+
for position in targets(opcode):
|
|
196
|
+
if not reach(operands[position], after, index):
|
|
197
|
+
return False
|
|
198
|
+
if opcode not in ("JUMP", "RET", "HALT"):
|
|
199
|
+
if not reach(index + 1, after, index):
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
return True
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
#: How high a write count is carried before it stops mattering.
|
|
206
|
+
#:
|
|
207
|
+
#: "More than once" is the whole of what check 7 needs to know, and without a
|
|
208
|
+
#: ceiling a loop around an ``EMIT`` would never settle.
|
|
209
|
+
_CAP = 2
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def check_once(shape: ShapeCheck, code: Sequence[Any], once: Sequence[bool]) -> bool:
|
|
213
|
+
"""Check 7: every channel declared ``once`` is written exactly once on every path.
|
|
214
|
+
|
|
215
|
+
That is how a plot column is guaranteed a value, or an explicit absence, for
|
|
216
|
+
every bar.
|
|
217
|
+
"""
|
|
218
|
+
channels = len(once)
|
|
219
|
+
if channels == 0:
|
|
220
|
+
return True
|
|
221
|
+
least: List[Optional[List[int]]] = [None] * len(code)
|
|
222
|
+
most: List[Optional[List[int]]] = [None] * len(code)
|
|
223
|
+
least[0] = [0] * channels
|
|
224
|
+
most[0] = [0] * channels
|
|
225
|
+
pending = [0]
|
|
226
|
+
|
|
227
|
+
def merge(index: int, low: Sequence[int], high: Sequence[int]) -> None:
|
|
228
|
+
if index < 0 or index >= len(code):
|
|
229
|
+
return
|
|
230
|
+
known_low, known_high = least[index], most[index]
|
|
231
|
+
if known_low is None or known_high is None:
|
|
232
|
+
least[index] = list(low)
|
|
233
|
+
most[index] = list(high)
|
|
234
|
+
pending.append(index)
|
|
235
|
+
return
|
|
236
|
+
changed = False
|
|
237
|
+
for channel in range(channels):
|
|
238
|
+
lower = min(known_low[channel], low[channel])
|
|
239
|
+
upper = min(_CAP, max(known_high[channel], high[channel]))
|
|
240
|
+
if lower != known_low[channel]:
|
|
241
|
+
known_low[channel] = lower
|
|
242
|
+
changed = True
|
|
243
|
+
if upper != known_high[channel]:
|
|
244
|
+
known_high[channel] = upper
|
|
245
|
+
changed = True
|
|
246
|
+
if changed:
|
|
247
|
+
pending.append(index)
|
|
248
|
+
|
|
249
|
+
end = -1
|
|
250
|
+
while pending:
|
|
251
|
+
index = pending.pop()
|
|
252
|
+
instruction = code[index]
|
|
253
|
+
low, high = least[index], most[index]
|
|
254
|
+
opcode = instruction[0]
|
|
255
|
+
if opcode == "HALT":
|
|
256
|
+
end = index
|
|
257
|
+
continue
|
|
258
|
+
next_low, next_high = list(low), list(high)
|
|
259
|
+
if opcode == "EMIT":
|
|
260
|
+
channel = instruction[1]
|
|
261
|
+
next_low[channel] = min(_CAP, next_low[channel] + 1)
|
|
262
|
+
next_high[channel] = min(_CAP, next_high[channel] + 1)
|
|
263
|
+
for position in targets(opcode):
|
|
264
|
+
merge(instruction[position + 1], next_low, next_high)
|
|
265
|
+
if opcode not in ("JUMP", "RET"):
|
|
266
|
+
merge(index + 1, next_low, next_high)
|
|
267
|
+
|
|
268
|
+
if end < 0:
|
|
269
|
+
return shape.fail("code", "no path reaches HALT")
|
|
270
|
+
low, high = least[end], most[end]
|
|
271
|
+
for channel in range(channels):
|
|
272
|
+
if not once[channel]:
|
|
273
|
+
continue
|
|
274
|
+
if low[channel] != 1 or high[channel] != 1:
|
|
275
|
+
return shape.fail(
|
|
276
|
+
f"channels[{channel}]",
|
|
277
|
+
f"it is declared once and is written {low[channel]} to {high[channel]} times "
|
|
278
|
+
"on a path to HALT",
|
|
279
|
+
)
|
|
280
|
+
return True
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def missing_tag(lists: Sequence[Sequence[Any]], requires: Sequence[str]) -> Optional[Dict[str, str]]:
|
|
284
|
+
"""Check 8's program half: an instruction whose tag the program never declared."""
|
|
285
|
+
for code in lists:
|
|
286
|
+
for instruction in code:
|
|
287
|
+
tag = INSTRUCTION_TAGS.get(instruction[0])
|
|
288
|
+
if tag is not None and tag not in requires:
|
|
289
|
+
return {"opcode": instruction[0], "tag": tag}
|
|
290
|
+
return None
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Check 1 over the tables a machine indexes, and over ``requests`` and its bodies.
|
|
2
|
+
|
|
3
|
+
A read's body (section 2.16.1) holds the tables a program holds for the machine
|
|
4
|
+
of section 3, counted from zero, so it needs the same walk the program's own
|
|
5
|
+
tables get and for the same reason: every later step indexes into them, and an
|
|
6
|
+
index out of range that is not caught here is a read past the end of an array
|
|
7
|
+
during a bar.
|
|
8
|
+
|
|
9
|
+
**A body that was not walked is the hole verification exists to close.** It is
|
|
10
|
+
reached by the same interpreter, with the same instruction list, over another
|
|
11
|
+
instrument's history. Verifying the program and not the expression it carries
|
|
12
|
+
would leave exactly the failure section 3.5 is written to prevent, one level
|
|
13
|
+
down where nobody would look for it.
|
|
14
|
+
|
|
15
|
+
The walk is shared rather than written twice, because two copies would be two
|
|
16
|
+
places for a table to gain a field and only one of them to learn about it.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import Any, List, Sequence, Set
|
|
21
|
+
|
|
22
|
+
from .verify_shape import ShapeCheck
|
|
23
|
+
|
|
24
|
+
READS = ("timeframe", "symbol")
|
|
25
|
+
MODES = ("confirmed", "developing", "lookahead")
|
|
26
|
+
CHART_FACTS = ("symbol", "exchange", "interval")
|
|
27
|
+
|
|
28
|
+
#: The five kinds section 2.10 declares, not the ones an engine happens to run.
|
|
29
|
+
#:
|
|
30
|
+
#: A verifier that listed only the kinds it executes would refuse a well formed
|
|
31
|
+
#: program with a message blaming the compiler that wrote it, and would pre-empt
|
|
32
|
+
#: the capability refusal that exists to say which feature is missing.
|
|
33
|
+
#: Verification answers whether this is a valid program; the capability check
|
|
34
|
+
#: answers whether this engine can run it.
|
|
35
|
+
REGISTER_KINDS = ("bar", "computed", "argument", "request", "input")
|
|
36
|
+
CELL_KINDS = ("var", "live")
|
|
37
|
+
LOOP_KINDS = ("for", "forIn", "while")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class MachineTables:
|
|
42
|
+
"""The tables one machine indexes, wherever in the program they sit."""
|
|
43
|
+
|
|
44
|
+
series: List[Any]
|
|
45
|
+
cells: List[Any]
|
|
46
|
+
states: List[Any]
|
|
47
|
+
functions: List[Any]
|
|
48
|
+
call_sites: List[Any]
|
|
49
|
+
loops: List[Any]
|
|
50
|
+
slots: int
|
|
51
|
+
lib_functions: int
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def check_machine_tables(shape: ShapeCheck, prefix: str, tables: MachineTables) -> bool:
|
|
55
|
+
for at, register in enumerate(tables.series):
|
|
56
|
+
path = f"{prefix}series[{at}]"
|
|
57
|
+
if not shape.object(register, path):
|
|
58
|
+
return False
|
|
59
|
+
if register.get("id") != at:
|
|
60
|
+
return shape.fail(f"{path}.id", "a register id is its position")
|
|
61
|
+
if not shape.one(register.get("kind"), f"{path}.kind", REGISTER_KINDS):
|
|
62
|
+
return False
|
|
63
|
+
if register["kind"] == "bar" and not shape.string(register.get("field"), f"{path}.field"):
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
for at, cell in enumerate(tables.cells):
|
|
67
|
+
path = f"{prefix}cells[{at}]"
|
|
68
|
+
if not shape.object(cell, path):
|
|
69
|
+
return False
|
|
70
|
+
if not shape.one(cell.get("kind"), f"{path}.kind", CELL_KINDS):
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
for at, state in enumerate(tables.states):
|
|
74
|
+
path = f"{prefix}states[{at}]"
|
|
75
|
+
if not shape.object(state, path):
|
|
76
|
+
return False
|
|
77
|
+
if not shape.index(state.get("fn"), f"{path}.fn", tables.lib_functions, "lib.functions"):
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
for at, one in enumerate(tables.functions):
|
|
81
|
+
path = f"{prefix}functions[{at}]"
|
|
82
|
+
if not shape.object(one, path):
|
|
83
|
+
return False
|
|
84
|
+
if not shape.string(one.get("name"), f"{path}.name"):
|
|
85
|
+
return False
|
|
86
|
+
if not shape.whole(one.get("params"), f"{path}.params"):
|
|
87
|
+
return False
|
|
88
|
+
if not shape.whole(one.get("slots"), f"{path}.slots"):
|
|
89
|
+
return False
|
|
90
|
+
if not shape.array(one.get("code"), f"{path}.code"):
|
|
91
|
+
return False
|
|
92
|
+
if one["params"] > one["slots"]:
|
|
93
|
+
return shape.fail(f"{path}.slots", "a frame cannot be smaller than its parameter list")
|
|
94
|
+
|
|
95
|
+
for at, site in enumerate(tables.call_sites):
|
|
96
|
+
path = f"{prefix}callSites[{at}]"
|
|
97
|
+
if not shape.object(site, path):
|
|
98
|
+
return False
|
|
99
|
+
if not shape.index(site.get("fn"), f"{path}.fn", len(tables.functions), "functions"):
|
|
100
|
+
return False
|
|
101
|
+
for name in ("argc", "cellBase", "stateBase"):
|
|
102
|
+
if not shape.whole(site.get(name), f"{path}.{name}"):
|
|
103
|
+
return False
|
|
104
|
+
if not shape.array(site.get("series"), f"{path}.series"):
|
|
105
|
+
return False
|
|
106
|
+
if site["argc"] != tables.functions[site["fn"]]["params"]:
|
|
107
|
+
return shape.fail(
|
|
108
|
+
f"{path}.argc", "a site passes exactly the function's parameter count"
|
|
109
|
+
)
|
|
110
|
+
for which, bound in enumerate(site["series"]):
|
|
111
|
+
if bound == -1:
|
|
112
|
+
continue
|
|
113
|
+
if not shape.index(bound, f"{path}.series[{which}]", len(tables.series), "series"):
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
for at, loop in enumerate(tables.loops):
|
|
117
|
+
path = f"{prefix}loops[{at}]"
|
|
118
|
+
if not shape.object(loop, path):
|
|
119
|
+
return False
|
|
120
|
+
if not shape.one(loop.get("kind"), f"{path}.kind", LOOP_KINDS):
|
|
121
|
+
return False
|
|
122
|
+
for name in ("line", "col"):
|
|
123
|
+
if not shape.whole(loop.get(name), f"{path}.{name}"):
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def check_requests(
|
|
130
|
+
shape: ShapeCheck,
|
|
131
|
+
prefix: str,
|
|
132
|
+
requests: Sequence[Any],
|
|
133
|
+
registers: int,
|
|
134
|
+
lib_functions: int,
|
|
135
|
+
keys: Set[str],
|
|
136
|
+
) -> bool:
|
|
137
|
+
"""``requests`` and every body inside it, section 2.16.
|
|
138
|
+
|
|
139
|
+
``series`` is the register of the enclosing machine that the read's value
|
|
140
|
+
lands in, so it is checked against the table the caller passes; everything
|
|
141
|
+
inside ``body`` is the body's own and is checked against the body's.
|
|
142
|
+
"""
|
|
143
|
+
return _walk(shape, prefix, requests, registers, lib_functions, keys, set())
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _walk(
|
|
147
|
+
shape: ShapeCheck,
|
|
148
|
+
prefix: str,
|
|
149
|
+
requests: Sequence[Any],
|
|
150
|
+
registers: int,
|
|
151
|
+
lib_functions: int,
|
|
152
|
+
keys: Set[str],
|
|
153
|
+
seen: Set[int],
|
|
154
|
+
) -> bool:
|
|
155
|
+
for at, request in enumerate(requests):
|
|
156
|
+
path = f"{prefix}requests[{at}]"
|
|
157
|
+
if not shape.object(request, path):
|
|
158
|
+
return False
|
|
159
|
+
if not shape.whole(request.get("id"), f"{path}.id"):
|
|
160
|
+
return False
|
|
161
|
+
# The id is this read's handle and is what req.isReady is handed, so two
|
|
162
|
+
# reads sharing one would answer each other's question.
|
|
163
|
+
if request["id"] in seen:
|
|
164
|
+
handle = request["id"]
|
|
165
|
+
return shape.fail(f"{path}.id", f"{handle} is already another read's handle")
|
|
166
|
+
seen.add(request["id"])
|
|
167
|
+
if not shape.one(request.get("read"), f"{path}.read", READS):
|
|
168
|
+
return False
|
|
169
|
+
if not shape.one(request.get("mode"), f"{path}.mode", MODES):
|
|
170
|
+
return False
|
|
171
|
+
if not shape.index(request.get("series"), f"{path}.series", registers, "series"):
|
|
172
|
+
return False
|
|
173
|
+
warmup = request.get("warmup")
|
|
174
|
+
if warmup is not None and not shape.whole(warmup, f"{path}.warmup"):
|
|
175
|
+
return False
|
|
176
|
+
for name in ("symbol", "exchange", "timeframe"):
|
|
177
|
+
if not _check_identity(shape, request.get(name), f"{path}.{name}", keys):
|
|
178
|
+
return False
|
|
179
|
+
if not _check_body(shape, f"{path}.body.", request.get("body"), lib_functions, keys, seen):
|
|
180
|
+
return False
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
#: The tables a read's body carries, section 2.16.1. Not the program's tables:
|
|
185
|
+
#: a body holds neither the constant pool nor the library manifest, because
|
|
186
|
+
#: neither holds anything that depends on a bar.
|
|
187
|
+
BODY_TABLES = (
|
|
188
|
+
"inputs",
|
|
189
|
+
"series",
|
|
190
|
+
"cells",
|
|
191
|
+
"states",
|
|
192
|
+
"functions",
|
|
193
|
+
"callSites",
|
|
194
|
+
"loops",
|
|
195
|
+
"requests",
|
|
196
|
+
"code",
|
|
197
|
+
"pos",
|
|
198
|
+
"fnPos",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _check_body(
|
|
203
|
+
shape: ShapeCheck,
|
|
204
|
+
prefix: str,
|
|
205
|
+
raw: Any,
|
|
206
|
+
lib_functions: int,
|
|
207
|
+
keys: Set[str],
|
|
208
|
+
seen: Set[int],
|
|
209
|
+
) -> bool:
|
|
210
|
+
at = prefix[:-1]
|
|
211
|
+
if not shape.object(raw, at):
|
|
212
|
+
return False
|
|
213
|
+
for name in BODY_TABLES:
|
|
214
|
+
if not shape.array(raw.get(name), f"{prefix}{name}"):
|
|
215
|
+
return False
|
|
216
|
+
if not shape.object(raw.get("frame"), f"{prefix}frame"):
|
|
217
|
+
return False
|
|
218
|
+
if not shape.whole(raw["frame"].get("slots"), f"{prefix}frame.slots"):
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
series = raw["series"]
|
|
222
|
+
tables = MachineTables(
|
|
223
|
+
series=series,
|
|
224
|
+
cells=raw["cells"],
|
|
225
|
+
states=raw["states"],
|
|
226
|
+
functions=raw["functions"],
|
|
227
|
+
call_sites=raw["callSites"],
|
|
228
|
+
loops=raw["loops"],
|
|
229
|
+
slots=raw["frame"]["slots"],
|
|
230
|
+
lib_functions=lib_functions,
|
|
231
|
+
)
|
|
232
|
+
if not check_machine_tables(shape, prefix, tables):
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
for which, one in enumerate(raw["inputs"]):
|
|
236
|
+
path = f"{prefix}inputs[{which}]"
|
|
237
|
+
if not shape.object(one, path):
|
|
238
|
+
return False
|
|
239
|
+
if not shape.string(one.get("input"), f"{path}.input"):
|
|
240
|
+
return False
|
|
241
|
+
if one["input"] not in keys:
|
|
242
|
+
named = one["input"]
|
|
243
|
+
return shape.fail(path, f"it names the input {named}, which inputs[] does not declare")
|
|
244
|
+
if not shape.index(one.get("series"), f"{path}.series", len(series), "the body's series"):
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
return _walk(shape, prefix, raw["requests"], len(series), lib_functions, keys, seen)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _check_identity(shape: ShapeCheck, value: Any, path: str, keys: Set[str]) -> bool:
|
|
251
|
+
"""One of the three forms a request's identity field takes, section 2.16.
|
|
252
|
+
|
|
253
|
+
A value, the setting that supplies it, or one of the three chart facts that
|
|
254
|
+
identify a chart rather than describe it. The rest of the chart namespace
|
|
255
|
+
describes the chart, and a request built from one of those would name
|
|
256
|
+
nothing.
|
|
257
|
+
"""
|
|
258
|
+
if value is None or isinstance(value, str):
|
|
259
|
+
return True
|
|
260
|
+
if not shape.object(value, path):
|
|
261
|
+
return False
|
|
262
|
+
if "input" in value:
|
|
263
|
+
if not shape.string(value["input"], f"{path}.input"):
|
|
264
|
+
return False
|
|
265
|
+
if value["input"] not in keys:
|
|
266
|
+
named = value["input"]
|
|
267
|
+
return shape.fail(path, f"it names the input {named}, which inputs[] does not declare")
|
|
268
|
+
return True
|
|
269
|
+
if "chart" not in value:
|
|
270
|
+
return shape.fail(path, "an object here names an input or a chart fact")
|
|
271
|
+
return shape.one(value["chart"], f"{path}.chart", CHART_FACTS)
|