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/opcodes.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""The forty-one instructions, as a table rather than as forty-one functions.
|
|
2
|
+
|
|
3
|
+
``compiled-program.md`` section 4.13 prints the whole set with its operands and
|
|
4
|
+
its stack effect, and this is that table in the one form an engine needs it:
|
|
5
|
+
what each opcode's operands are called, how deep the stack goes after it, which
|
|
6
|
+
operand is a jump target, and which table each operand indexes into.
|
|
7
|
+
|
|
8
|
+
It is a table because verification needs the same facts the interpreter needs. A
|
|
9
|
+
dispatch written as forty-one functions with the operand counts implied by their
|
|
10
|
+
signatures would leave the verifier to state them a second time, and the two
|
|
11
|
+
would drift the day an instruction changed.
|
|
12
|
+
|
|
13
|
+
**The page is the source and this is a copy, so a test compares them.** That is
|
|
14
|
+
the standing habit of this repository: a figure the specification prints is
|
|
15
|
+
measured by a test rather than quoted by one. ``tests/test_opcodes.py`` reads
|
|
16
|
+
section 4.13 out of the document by pattern and holds every row here to it, so a
|
|
17
|
+
row the page gains, loses or changes fails this engine's tests naming the
|
|
18
|
+
opcode. Nothing here may be corrected without the page being read again.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import Mapping, Sequence, Tuple
|
|
22
|
+
|
|
23
|
+
#: Depth changes that are not a fixed number, written as the page writes them.
|
|
24
|
+
BY_COUNT = "1 - n"
|
|
25
|
+
BY_ARGC = "1 - argc"
|
|
26
|
+
|
|
27
|
+
#: Opcode, operand names in order, depth change, and the group the page files it under.
|
|
28
|
+
TABLE: Mapping[str, Tuple[Sequence[str], object, str]] = {
|
|
29
|
+
"CONST": (("k",), 1, "Constants and stack"),
|
|
30
|
+
"DUP": ((), 1, "Constants and stack"),
|
|
31
|
+
"POP": ((), -1, "Constants and stack"),
|
|
32
|
+
"LOAD": (("s",), 1, "Slots"),
|
|
33
|
+
"STORE": (("s",), -1, "Slots"),
|
|
34
|
+
"CELL_INIT": (("c", "t"), 0, "Cells"),
|
|
35
|
+
"LOADC": (("c",), 1, "Cells"),
|
|
36
|
+
"STOREC": (("c",), -1, "Cells"),
|
|
37
|
+
"SLOAD": (("r",), 1, "Series"),
|
|
38
|
+
"SSTORE": (("r",), -1, "Series"),
|
|
39
|
+
"HIST": (("r",), 0, "Series"),
|
|
40
|
+
"HISTP": (("p",), 0, "Series"),
|
|
41
|
+
"ADD": ((), -1, "Arithmetic"),
|
|
42
|
+
"SUB": ((), -1, "Arithmetic"),
|
|
43
|
+
"MUL": ((), -1, "Arithmetic"),
|
|
44
|
+
"DIV": ((), -1, "Arithmetic"),
|
|
45
|
+
"MOD": ((), -1, "Arithmetic"),
|
|
46
|
+
"NEG": ((), 0, "Arithmetic"),
|
|
47
|
+
"LT": ((), -1, "Comparison"),
|
|
48
|
+
"LE": ((), -1, "Comparison"),
|
|
49
|
+
"GT": ((), -1, "Comparison"),
|
|
50
|
+
"GE": ((), -1, "Comparison"),
|
|
51
|
+
"EQ": ((), -1, "Comparison"),
|
|
52
|
+
"NE": ((), -1, "Comparison"),
|
|
53
|
+
"NOT": ((), 0, "Logic"),
|
|
54
|
+
"AND": ((), -1, "Logic"),
|
|
55
|
+
"OR": ((), -1, "Logic"),
|
|
56
|
+
"AND_SHORT": (("t",), 0, "Logic"),
|
|
57
|
+
"OR_SHORT": (("t",), 0, "Logic"),
|
|
58
|
+
"JUMP": (("t",), 0, "Control"),
|
|
59
|
+
"JUMP_FALSE": (("t",), -1, "Control"),
|
|
60
|
+
"TICK": (("l",), 0, "Control"),
|
|
61
|
+
"FOR_INIT": (("l", "s", "sLim", "sStep", "t"), -3, "Control"),
|
|
62
|
+
"FOR_NEXT": (("l", "s", "sLim", "sStep", "t"), 0, "Control"),
|
|
63
|
+
"ARRAY": (("n",), BY_COUNT, "Arrays"),
|
|
64
|
+
"ELEM": ((), -1, "Arrays"),
|
|
65
|
+
"CALL_LIB": (("f", "n", "st"), BY_COUNT, "Calls"),
|
|
66
|
+
"CALL_FN": (("site",), BY_ARGC, "Calls"),
|
|
67
|
+
"RET": ((), -1, "Calls"),
|
|
68
|
+
"EMIT": (("ch",), -1, "Output"),
|
|
69
|
+
"HALT": ((), 0, "Termination"),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#: Which operand of an instruction is a jump target, by position.
|
|
73
|
+
#:
|
|
74
|
+
#: Read off the operand names rather than written down again: ``t`` is the name
|
|
75
|
+
#: section 4's operand table gives a jump target and nothing else carries it.
|
|
76
|
+
TARGETS: Mapping[str, Sequence[int]] = {
|
|
77
|
+
opcode: tuple(at for at, name in enumerate(names) if name == "t")
|
|
78
|
+
for opcode, (names, _depth, _group) in TABLE.items()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#: Which table each operand indexes into, for the range half of check 1.
|
|
82
|
+
#:
|
|
83
|
+
#: ``target`` is an instruction index in the same list and ``count`` is a number
|
|
84
|
+
#: the instruction carries rather than an index into anything.
|
|
85
|
+
OPERAND_TABLES: Mapping[str, Sequence[str]] = {
|
|
86
|
+
"CONST": ("consts",),
|
|
87
|
+
"LOAD": ("slots",),
|
|
88
|
+
"STORE": ("slots",),
|
|
89
|
+
"CELL_INIT": ("cells", "target"),
|
|
90
|
+
"LOADC": ("cells",),
|
|
91
|
+
"STOREC": ("cells",),
|
|
92
|
+
"SLOAD": ("registers",),
|
|
93
|
+
"SSTORE": ("registers",),
|
|
94
|
+
"HIST": ("registers",),
|
|
95
|
+
"HISTP": ("bindings",),
|
|
96
|
+
"AND_SHORT": ("target",),
|
|
97
|
+
"OR_SHORT": ("target",),
|
|
98
|
+
"JUMP": ("target",),
|
|
99
|
+
"JUMP_FALSE": ("target",),
|
|
100
|
+
"TICK": ("loops",),
|
|
101
|
+
"FOR_INIT": ("loops", "slots", "slots", "slots", "target"),
|
|
102
|
+
"FOR_NEXT": ("loops", "slots", "slots", "slots", "target"),
|
|
103
|
+
"ARRAY": ("count",),
|
|
104
|
+
"CALL_LIB": ("libFunctions", "count", "states"),
|
|
105
|
+
"CALL_FN": ("callSites",),
|
|
106
|
+
"EMIT": ("channels",),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#: What a reader is told a table is called when an operand is outside it.
|
|
110
|
+
TABLE_NAMES: Mapping[str, str] = {
|
|
111
|
+
"slots": "the frame",
|
|
112
|
+
"cells": "cells",
|
|
113
|
+
"states": "states",
|
|
114
|
+
"registers": "series",
|
|
115
|
+
"channels": "channels",
|
|
116
|
+
"libFunctions": "lib.functions",
|
|
117
|
+
"callSites": "callSites",
|
|
118
|
+
"loops": "loops",
|
|
119
|
+
"consts": "consts",
|
|
120
|
+
"bindings": "the call site's series bindings",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#: Check 8's program half: an instruction that needs a tag in ``requires``.
|
|
124
|
+
INSTRUCTION_TAGS: Mapping[str, str] = {
|
|
125
|
+
"ARRAY": "arrays",
|
|
126
|
+
"ELEM": "arrays",
|
|
127
|
+
"CALL_FN": "functions",
|
|
128
|
+
"TICK": "loops",
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def is_opcode(name: object) -> bool:
|
|
133
|
+
return isinstance(name, str) and name in TABLE
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def operand_count(opcode: str) -> int:
|
|
137
|
+
return len(TABLE[opcode][0])
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def operand_names(opcode: str) -> Sequence[str]:
|
|
141
|
+
return TABLE[opcode][0]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def depth_change(opcode: str, operands: Sequence[int], argc_of) -> int:
|
|
145
|
+
"""How far the stack moves, with the two formulas resolved.
|
|
146
|
+
|
|
147
|
+
``CALL_FN`` reads its call site's ``argc``, which is fixed at load, so the
|
|
148
|
+
depth stays statically computable and the verifier can walk the list.
|
|
149
|
+
"""
|
|
150
|
+
written = TABLE[opcode][1]
|
|
151
|
+
if written == BY_COUNT:
|
|
152
|
+
return 1 - int(operands[1] if opcode == "CALL_LIB" else operands[0])
|
|
153
|
+
if written == BY_ARGC:
|
|
154
|
+
return 1 - int(argc_of(int(operands[0])))
|
|
155
|
+
return int(written)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def targets(opcode: str) -> Sequence[int]:
|
|
159
|
+
return TARGETS.get(opcode, ())
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
COUNT: int = len(TABLE)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def tables_for(opcode: str) -> Sequence[str]:
|
|
166
|
+
return OPERAND_TABLES.get(opcode, ())
|
openscript/program.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""A verified program, with the parts a bar reads worked out once.
|
|
2
|
+
|
|
3
|
+
Everything here is a table read from the program rather than a decision about
|
|
4
|
+
it: the constant pool as machine values, the source position of each
|
|
5
|
+
instruction, the line of each loop, and what the library manifest says about
|
|
6
|
+
each entry the program calls. None of it changes between bars, so working it out
|
|
7
|
+
per bar would be the same answer computed forty thousand times.
|
|
8
|
+
|
|
9
|
+
**Nothing is built out of text.** The program is data, this is that data in the
|
|
10
|
+
shape the loop reads it in, and there is no step between the two where a name
|
|
11
|
+
becomes a function or a string becomes an instruction. That is the rule the
|
|
12
|
+
whole format exists to keep, and it is what lets a platform run many people's
|
|
13
|
+
scripts in one process.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
|
18
|
+
|
|
19
|
+
from .contracts import LibraryEntry
|
|
20
|
+
from .diagnostics import NO_POSITION, Position
|
|
21
|
+
from .values import ABSENT, Colour
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def constant_value(entry: Sequence[Any]) -> Any:
|
|
25
|
+
"""The machine value a constant pool entry denotes, section 2.9."""
|
|
26
|
+
tag = entry[0]
|
|
27
|
+
if tag == "z":
|
|
28
|
+
return ABSENT
|
|
29
|
+
if tag == "b":
|
|
30
|
+
return bool(entry[1])
|
|
31
|
+
if tag == "n":
|
|
32
|
+
return float(entry[1])
|
|
33
|
+
if tag == "s":
|
|
34
|
+
return entry[1]
|
|
35
|
+
red, green, blue, alpha = entry[1]
|
|
36
|
+
return Colour(float(red), float(green), float(blue), float(alpha))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def positions_for(triples: Sequence[Sequence[int]], length: int) -> List[Position]:
|
|
40
|
+
"""One position per instruction, from the triples ``debug.pos`` holds.
|
|
41
|
+
|
|
42
|
+
Section 2.15: the position of an instruction is the triple with the greatest
|
|
43
|
+
index at or below it, so a run of instructions from one expression costs one
|
|
44
|
+
triple. Spread once here rather than searched per instruction, because a
|
|
45
|
+
diagnostic is rare and the search would be on the path of every bar.
|
|
46
|
+
"""
|
|
47
|
+
found: List[Position] = [NO_POSITION] * length
|
|
48
|
+
current = NO_POSITION
|
|
49
|
+
at = 0
|
|
50
|
+
ordered = sorted(
|
|
51
|
+
(one for one in triples if isinstance(one, list) and len(one) >= 3),
|
|
52
|
+
key=lambda one: one[0],
|
|
53
|
+
)
|
|
54
|
+
for index, line, column in ((one[0], one[1], one[2]) for one in ordered):
|
|
55
|
+
while at < length and at < index:
|
|
56
|
+
found[at] = current
|
|
57
|
+
at += 1
|
|
58
|
+
current = Position(int(line), int(column))
|
|
59
|
+
if 0 <= index < length:
|
|
60
|
+
found[index] = current
|
|
61
|
+
at = index + 1
|
|
62
|
+
while at < length:
|
|
63
|
+
found[at] = current
|
|
64
|
+
at += 1
|
|
65
|
+
return found
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class Body:
|
|
70
|
+
"""One instruction list with the tables a frame running it needs."""
|
|
71
|
+
|
|
72
|
+
code: List[Any]
|
|
73
|
+
positions: List[Position]
|
|
74
|
+
slots: int
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class LoadedProgram:
|
|
79
|
+
"""The verified program and the tables a bar reads, worked out once at load."""
|
|
80
|
+
|
|
81
|
+
raw: Dict[str, Any]
|
|
82
|
+
consts: List[Any] = field(default_factory=list)
|
|
83
|
+
top: Body = None
|
|
84
|
+
functions: List[Body] = field(default_factory=list)
|
|
85
|
+
call_sites: List[Dict[str, Any]] = field(default_factory=list)
|
|
86
|
+
loop_lines: List[int] = field(default_factory=list)
|
|
87
|
+
entries: List[LibraryEntry] = field(default_factory=list)
|
|
88
|
+
defer: List[bool] = field(default_factory=list)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def loop_budget(self) -> int:
|
|
92
|
+
return int(self.raw["limits"]["loops"])
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def retained(self) -> Optional[int]:
|
|
96
|
+
held = self.raw["limits"]["history"]
|
|
97
|
+
return None if held is None else int(held)
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def on_unconfirmed(self) -> bool:
|
|
101
|
+
return bool(self.raw["meta"]["onUnconfirmed"])
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def kind(self) -> str:
|
|
105
|
+
return self.raw["meta"]["kind"]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def loaded(raw: Dict[str, Any], entries: Sequence[LibraryEntry]) -> LoadedProgram:
|
|
109
|
+
"""Spread a verified program into the tables above, and nothing more."""
|
|
110
|
+
functions = [
|
|
111
|
+
Body(
|
|
112
|
+
code=one["code"],
|
|
113
|
+
positions=_function_positions(raw, at, len(one["code"])),
|
|
114
|
+
slots=int(one["slots"]),
|
|
115
|
+
)
|
|
116
|
+
for at, one in enumerate(raw["functions"])
|
|
117
|
+
]
|
|
118
|
+
return LoadedProgram(
|
|
119
|
+
raw=raw,
|
|
120
|
+
consts=[constant_value(one) for one in raw["consts"]],
|
|
121
|
+
top=Body(
|
|
122
|
+
code=raw["code"],
|
|
123
|
+
positions=positions_for(raw["debug"]["pos"], len(raw["code"])),
|
|
124
|
+
slots=int(raw["frame"]["slots"]),
|
|
125
|
+
),
|
|
126
|
+
functions=functions,
|
|
127
|
+
call_sites=list(raw["callSites"]),
|
|
128
|
+
loop_lines=[int(one["line"]) for one in raw["loops"]],
|
|
129
|
+
entries=list(entries),
|
|
130
|
+
defer=[bool(one["defer"]) for one in raw["channels"]],
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _function_positions(raw: Dict[str, Any], which: int, length: int) -> List[Position]:
|
|
135
|
+
for pair in raw["debug"]["fnPos"]:
|
|
136
|
+
if isinstance(pair, list) and len(pair) == 2 and pair[0] == which:
|
|
137
|
+
return positions_for(pair[1], length)
|
|
138
|
+
return [NO_POSITION] * length
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def manifest_entries(raw: Dict[str, Any]) -> List[Tuple[str, int, bool, str]]:
|
|
142
|
+
"""What the program says about each function it calls, section 2.5."""
|
|
143
|
+
return [
|
|
144
|
+
(one["name"], int(one["arity"]), bool(one["state"]), one["effect"])
|
|
145
|
+
for one in raw["lib"]["functions"]
|
|
146
|
+
]
|
openscript/run.py
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""The eleven steps of section 5.1, and the checkpoint that makes a moving bar idempotent.
|
|
2
|
+
|
|
3
|
+
Everything else in this package is a detail of one of these steps. They run in
|
|
4
|
+
this order, on every execution of every bar, and the order is not an
|
|
5
|
+
implementation's choice: steps 1 and 2 are what make a still-moving bar
|
|
6
|
+
idempotent, and steps 8 and 9 are what separate what a script may do on a moving
|
|
7
|
+
bar from what it may not.
|
|
8
|
+
|
|
9
|
+
**The checkpoint is taken before a bar rather than after one.** Section 6.1
|
|
10
|
+
describes it as taken at the end of bar ``i - 1``, and the state at the start of
|
|
11
|
+
the first execution of bar ``i`` *is* that state, so taking it there is the same
|
|
12
|
+
checkpoint reached by the cheaper road: nothing is copied for a bar that is
|
|
13
|
+
executed once, and a re-execution restores what the bar began with. Section 6.2
|
|
14
|
+
says outright that an engine may implement a checkpoint however it likes, and
|
|
15
|
+
that the specification requires the semantics rather than the representation.
|
|
16
|
+
|
|
17
|
+
**A live cell is written into the checkpoint and is not rolled back.** That is
|
|
18
|
+
the whole of ``live var``, and it has one consequence worth stating rather than
|
|
19
|
+
discovering: where a live cell and an ordinary cell held the same array, the
|
|
20
|
+
restore gives the ordinary cell the copy it recorded and leaves the live cell
|
|
21
|
+
holding what it has now, so the two stop being the same array. That follows from
|
|
22
|
+
the exception rather than from this implementation of it, and a script using
|
|
23
|
+
``live var`` is not reproducible by design.
|
|
24
|
+
|
|
25
|
+
**An error during step 6 stops the bar.** Steps 7 to 11 do not run, the bar's
|
|
26
|
+
columns keep whatever the previous execution published or stay absent, and the
|
|
27
|
+
engine reports the diagnostic. It does not carry a half executed state into the
|
|
28
|
+
next bar.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import copy
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple
|
|
34
|
+
|
|
35
|
+
from .bars import BarFacts, bar_field, facts_for
|
|
36
|
+
from .budget import Budget, DEFAULT_LIMITS, EngineLimits, step_bound
|
|
37
|
+
from .canonical import parse
|
|
38
|
+
from .contracts import Bar, BarState, CallContext, Library, NoLibrary
|
|
39
|
+
from .diagnostics import Diagnostic, ScriptError
|
|
40
|
+
from .inputs import ResolvedInput, TimeReader, field_value, resolve_inputs
|
|
41
|
+
from .machine import Machine, PendingEffect
|
|
42
|
+
from .memory import Cells, Channels, Register, States
|
|
43
|
+
from .program import LoadedProgram, loaded
|
|
44
|
+
from .values import ABSENT
|
|
45
|
+
from .verify import MACHINE_CAPABILITIES, VerifyOptions, verify
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class Checkpoint:
|
|
50
|
+
"""What section 6.1 holds, in the one form a mechanical copy can restore."""
|
|
51
|
+
|
|
52
|
+
kept: Tuple[Any, Any, Any]
|
|
53
|
+
lengths: Sequence[int]
|
|
54
|
+
bar: int
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class Alert:
|
|
59
|
+
"""What a raised alert carries, section 5.4, and nothing else.
|
|
60
|
+
|
|
61
|
+
Fixed there rather than left to a host, because a host cannot compose a
|
|
62
|
+
notification out of fields that differ between engines. The message is
|
|
63
|
+
whatever the channel held, absence included: one built out of a value that
|
|
64
|
+
was absent during warmup is absent, and an engine that substituted an empty
|
|
65
|
+
string would hide the one thing the author needs to see.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
key: str
|
|
69
|
+
title: Any
|
|
70
|
+
message: Any
|
|
71
|
+
bar: int
|
|
72
|
+
time: Any
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class BarResult:
|
|
77
|
+
"""What one execution of one bar produced."""
|
|
78
|
+
|
|
79
|
+
index: int
|
|
80
|
+
columns: List[Any] = field(default_factory=list)
|
|
81
|
+
#: The deferred channels step 9 applied, by index, and empty where it
|
|
82
|
+
#: discarded them. The columns carry every channel either way: step 8
|
|
83
|
+
#: publishes the drawing and step 9 decides the marker and the alert, which
|
|
84
|
+
#: is the whole difference between what a script may do on a moving bar and
|
|
85
|
+
#: what it may not.
|
|
86
|
+
applied_channels: List[int] = field(default_factory=list)
|
|
87
|
+
applied: List[PendingEffect] = field(default_factory=list)
|
|
88
|
+
alerts: List[Alert] = field(default_factory=list)
|
|
89
|
+
diagnostic: Optional[Diagnostic] = None
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def ok(self) -> bool:
|
|
93
|
+
return self.diagnostic is None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Run:
|
|
97
|
+
"""One program, loaded, with the memory a run of it holds."""
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
program: LoadedProgram,
|
|
102
|
+
inputs: Sequence[ResolvedInput],
|
|
103
|
+
library: Library,
|
|
104
|
+
limits: EngineLimits = DEFAULT_LIMITS,
|
|
105
|
+
) -> None:
|
|
106
|
+
self.program = program
|
|
107
|
+
self.inputs = inputs
|
|
108
|
+
self.limits = limits
|
|
109
|
+
raw = program.raw
|
|
110
|
+
self.cells = Cells(len(raw["cells"]))
|
|
111
|
+
self.states = States(len(raw["states"]))
|
|
112
|
+
self.registers = [Register() for _ in raw["series"]]
|
|
113
|
+
self.channels = Channels(len(raw["channels"]))
|
|
114
|
+
self.budget = Budget(step_bound(raw), program.loop_budget, limits.string_length)
|
|
115
|
+
self.machine = Machine(
|
|
116
|
+
program, self.cells, self.states, self.registers, self.channels, self.budget, library
|
|
117
|
+
)
|
|
118
|
+
self._live = [at for at, one in enumerate(raw["cells"]) if one["kind"] == "live"]
|
|
119
|
+
self._columns: Dict[int, List[Any]] = {}
|
|
120
|
+
self._checkpoint: Optional[Checkpoint] = None
|
|
121
|
+
self._fired_ever: set = set()
|
|
122
|
+
self._fired_on: Dict[str, int] = {}
|
|
123
|
+
self._supplied = 0
|
|
124
|
+
|
|
125
|
+
# -- the declared shape, with every input reference resolved ------------
|
|
126
|
+
|
|
127
|
+
def declaration(self, path: Sequence[str]) -> Any:
|
|
128
|
+
"""A declaration field, with its input reference substituted, section 2.3."""
|
|
129
|
+
held: Any = self.program.raw
|
|
130
|
+
for step in path:
|
|
131
|
+
held = held[step]
|
|
132
|
+
return field_value(held, self.inputs)
|
|
133
|
+
|
|
134
|
+
# -- the checkpoint -----------------------------------------------------
|
|
135
|
+
|
|
136
|
+
def checkpoint(self, bar: int = -1) -> Checkpoint:
|
|
137
|
+
"""Every cell with its flag, every state region, and the heap under both.
|
|
138
|
+
|
|
139
|
+
One mechanical copy over the three together, so that two cells holding
|
|
140
|
+
one array still hold one array after a restore: a copy taken cell by cell
|
|
141
|
+
would turn one array into two and change what the script computes.
|
|
142
|
+
|
|
143
|
+
``bar`` is the bar this state is the start of, which is what makes a
|
|
144
|
+
re-execution recognisable; a caller keeping one for its own reasons, a
|
|
145
|
+
debugger stepping backwards, leaves it alone.
|
|
146
|
+
"""
|
|
147
|
+
kept = copy.deepcopy((self.cells.values, self.cells.ready, self.states.regions))
|
|
148
|
+
return Checkpoint(kept, [one.length for one in self.registers], bar)
|
|
149
|
+
|
|
150
|
+
def restore(self, mark: Checkpoint) -> None:
|
|
151
|
+
"""Section 6.3: everything goes back, except that live cells keep theirs."""
|
|
152
|
+
was = [self.cells.values[at] for at in self._live]
|
|
153
|
+
ready = [self.cells.ready[at] for at in self._live]
|
|
154
|
+
values, flags, regions = copy.deepcopy(mark.kept)
|
|
155
|
+
self.cells.values[:] = values
|
|
156
|
+
self.cells.ready[:] = flags
|
|
157
|
+
self.states.regions = regions
|
|
158
|
+
for at, value, flag in zip(self._live, was, ready):
|
|
159
|
+
self.cells.values[at] = value
|
|
160
|
+
self.cells.ready[at] = flag
|
|
161
|
+
for register, length in zip(self.registers, mark.lengths):
|
|
162
|
+
register.truncate(length)
|
|
163
|
+
|
|
164
|
+
# -- the bar ------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
def execute_bar(
|
|
167
|
+
self,
|
|
168
|
+
index: int,
|
|
169
|
+
bar: Bar,
|
|
170
|
+
state: BarState = BarState(),
|
|
171
|
+
supplied: Optional[int] = None,
|
|
172
|
+
instrument: Optional[Mapping[str, Any]] = None,
|
|
173
|
+
now: Any = ABSENT,
|
|
174
|
+
) -> BarResult:
|
|
175
|
+
"""The eleven steps, in order, for one execution of bar ``index``."""
|
|
176
|
+
held = index + 1 if supplied is None else supplied
|
|
177
|
+
self._supplied = held
|
|
178
|
+
|
|
179
|
+
# Step 1. On the first execution of this bar the state already is the
|
|
180
|
+
# checkpoint, so nothing is copied; it is recorded instead, because the
|
|
181
|
+
# next execution of the same bar is what needs it.
|
|
182
|
+
if self._checkpoint is not None and self._checkpoint.bar == index:
|
|
183
|
+
self.restore(self._checkpoint)
|
|
184
|
+
else:
|
|
185
|
+
self._checkpoint = self.checkpoint(index)
|
|
186
|
+
|
|
187
|
+
# Step 2.
|
|
188
|
+
for register in self.registers:
|
|
189
|
+
register.truncate(index)
|
|
190
|
+
|
|
191
|
+
# Step 3.
|
|
192
|
+
self.channels.clear()
|
|
193
|
+
for register in self.registers:
|
|
194
|
+
register.current = ABSENT
|
|
195
|
+
context = CallContext(
|
|
196
|
+
bar_index=index, instrument={} if instrument is None else instrument, now=now
|
|
197
|
+
)
|
|
198
|
+
self.machine.begin(index, context)
|
|
199
|
+
|
|
200
|
+
# Step 4.
|
|
201
|
+
facts = facts_for(index, held, state)
|
|
202
|
+
self._fill_registers(bar, facts)
|
|
203
|
+
|
|
204
|
+
# Step 5.
|
|
205
|
+
self._fill_inputs()
|
|
206
|
+
|
|
207
|
+
# Step 6.
|
|
208
|
+
try:
|
|
209
|
+
self.machine.execute()
|
|
210
|
+
except ScriptError as stopped:
|
|
211
|
+
return BarResult(index, self._columns.get(index, []), [], [], [], stopped.diagnostic)
|
|
212
|
+
|
|
213
|
+
# Step 7.
|
|
214
|
+
for register in self.registers:
|
|
215
|
+
register.close()
|
|
216
|
+
|
|
217
|
+
# Step 8.
|
|
218
|
+
columns = list(self.channels.values)
|
|
219
|
+
self._columns[index] = columns
|
|
220
|
+
|
|
221
|
+
# Step 9.
|
|
222
|
+
decided = state.is_confirmed or self.program.on_unconfirmed
|
|
223
|
+
applied: List[PendingEffect] = []
|
|
224
|
+
alerts: List[Alert] = []
|
|
225
|
+
channels: List[int] = []
|
|
226
|
+
if decided:
|
|
227
|
+
applied = list(self.machine.pending)
|
|
228
|
+
channels = [at for at, held in enumerate(self.program.defer) if held]
|
|
229
|
+
alerts = self._raise_alerts(index, bar, state)
|
|
230
|
+
|
|
231
|
+
# Step 10.
|
|
232
|
+
retained = self.program.retained
|
|
233
|
+
if retained is not None:
|
|
234
|
+
for register in self.registers:
|
|
235
|
+
register.trim(retained)
|
|
236
|
+
|
|
237
|
+
# Step 11 is the record taken at the top of the next bar: see the note
|
|
238
|
+
# at the head of this module.
|
|
239
|
+
return BarResult(index, columns, channels, applied, alerts, None)
|
|
240
|
+
|
|
241
|
+
def _fill_registers(self, bar: Bar, facts: BarFacts) -> None:
|
|
242
|
+
for at, declared in enumerate(self.program.raw["series"]):
|
|
243
|
+
if declared["kind"] == "bar":
|
|
244
|
+
self.registers[at].current = bar_field(declared["field"], bar, facts)
|
|
245
|
+
|
|
246
|
+
def _fill_inputs(self) -> None:
|
|
247
|
+
"""Step 5: the effective values, already resolved, written into slots.
|
|
248
|
+
|
|
249
|
+
A ``"source"`` input is the one that reads something here: its value is
|
|
250
|
+
the register it named, for the bar about to run, rather than a value
|
|
251
|
+
settled at load.
|
|
252
|
+
"""
|
|
253
|
+
for one in self.inputs:
|
|
254
|
+
if one.field is None:
|
|
255
|
+
self.machine.write_slot(one.slot, one.value)
|
|
256
|
+
continue
|
|
257
|
+
self.machine.write_slot(one.slot, self._register_named(one.field))
|
|
258
|
+
|
|
259
|
+
def _register_named(self, field: str) -> Any:
|
|
260
|
+
for at, declared in enumerate(self.program.raw["series"]):
|
|
261
|
+
if declared["kind"] == "bar" and declared["field"] == field:
|
|
262
|
+
return self.registers[at].current
|
|
263
|
+
return ABSENT
|
|
264
|
+
|
|
265
|
+
def _raise_alerts(self, index: int, bar: Bar, state: BarState) -> List[Alert]:
|
|
266
|
+
"""Section 5.4: on a bar this engine decided and the host is driving live.
|
|
267
|
+
|
|
268
|
+
Deferral covers half of when an alert fires. The other half is that
|
|
269
|
+
adding a study to a chart that already holds history fires nothing for
|
|
270
|
+
those bars, and the fact that separates a bar of history from the bar in
|
|
271
|
+
front of you is ``isRealtime``, which the host states and no engine can
|
|
272
|
+
derive. An engine that only backtests is handed it false throughout and
|
|
273
|
+
raises none, which is the same answer reached from the other side.
|
|
274
|
+
"""
|
|
275
|
+
if not state.is_realtime:
|
|
276
|
+
return []
|
|
277
|
+
raised: List[Alert] = []
|
|
278
|
+
for declared in self.program.raw["outputs"]["alerts"]:
|
|
279
|
+
if self.channels.read(declared["condChannel"]) is not True:
|
|
280
|
+
continue
|
|
281
|
+
key = declared["key"]
|
|
282
|
+
frequency = field_value(declared["frequency"], self.inputs)
|
|
283
|
+
if frequency == "once" and key in self._fired_ever:
|
|
284
|
+
continue
|
|
285
|
+
if frequency == "oncePerBar" and self._fired_on.get(key) == index:
|
|
286
|
+
continue
|
|
287
|
+
message = ABSENT
|
|
288
|
+
if declared.get("messageChannel") is not None:
|
|
289
|
+
message = self.channels.read(declared["messageChannel"])
|
|
290
|
+
raised.append(
|
|
291
|
+
Alert(
|
|
292
|
+
key=key,
|
|
293
|
+
title=field_value(declared["title"], self.inputs),
|
|
294
|
+
message=message,
|
|
295
|
+
bar=index,
|
|
296
|
+
time=bar.time,
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
self._fired_ever.add(key)
|
|
300
|
+
self._fired_on[key] = index
|
|
301
|
+
return raised
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@dataclass
|
|
305
|
+
class LoadResult:
|
|
306
|
+
run: Optional[Run] = None
|
|
307
|
+
diagnostic: Optional[Diagnostic] = None
|
|
308
|
+
|
|
309
|
+
@property
|
|
310
|
+
def ok(self) -> bool:
|
|
311
|
+
return self.diagnostic is None
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def load(
|
|
315
|
+
raw: Any,
|
|
316
|
+
settings: Optional[Mapping[str, Any]] = None,
|
|
317
|
+
library: Optional[Library] = None,
|
|
318
|
+
limits: EngineLimits = DEFAULT_LIMITS,
|
|
319
|
+
capabilities: Sequence[str] = MACHINE_CAPABILITIES,
|
|
320
|
+
read_time: Optional[TimeReader] = None,
|
|
321
|
+
) -> LoadResult:
|
|
322
|
+
"""Steps 2 to 8 of section 9.4, then check 10's value half, then a run.
|
|
323
|
+
|
|
324
|
+
An object built in the same process enters here: it was never text and has
|
|
325
|
+
nothing to be canonical about, which is section 9.4 step 1's other half and
|
|
326
|
+
the minute that settled it.
|
|
327
|
+
"""
|
|
328
|
+
served = library if library is not None else NoLibrary()
|
|
329
|
+
checked = verify(raw, VerifyOptions(capabilities=capabilities, limits=limits, library=served))
|
|
330
|
+
if not checked.ok:
|
|
331
|
+
return LoadResult(None, checked.diagnostic)
|
|
332
|
+
resolved = resolve_inputs(checked.program, {} if settings is None else settings, read_time)
|
|
333
|
+
if not resolved.ok:
|
|
334
|
+
return LoadResult(None, resolved.diagnostic)
|
|
335
|
+
entries = _entries_for(checked.program, served)
|
|
336
|
+
return LoadResult(
|
|
337
|
+
Run(loaded(checked.program, entries), resolved.inputs, served, limits), None
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def load_text(
|
|
342
|
+
text: str,
|
|
343
|
+
settings: Optional[Mapping[str, Any]] = None,
|
|
344
|
+
library: Optional[Library] = None,
|
|
345
|
+
limits: EngineLimits = DEFAULT_LIMITS,
|
|
346
|
+
capabilities: Sequence[str] = MACHINE_CAPABILITIES,
|
|
347
|
+
read_time: Optional[TimeReader] = None,
|
|
348
|
+
) -> LoadResult:
|
|
349
|
+
"""The text boundary: step 1 of section 9.4, then everything ``load`` does.
|
|
350
|
+
|
|
351
|
+
Text an engine reads from outside its process arrives as the canonical
|
|
352
|
+
encoding and its hash was taken over those bytes, so text that parses to a
|
|
353
|
+
program but is spelled some other way is text that hash does not name.
|
|
354
|
+
"""
|
|
355
|
+
parsed, refusal = parse(text)
|
|
356
|
+
if refusal is not None:
|
|
357
|
+
return LoadResult(None, refusal)
|
|
358
|
+
return load(parsed, settings, library, limits, capabilities, read_time)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _entries_for(raw: Any, library: Library) -> List[Any]:
|
|
362
|
+
"""The manifest entry for each function the program calls, in its own order.
|
|
363
|
+
|
|
364
|
+
Verification has already held every one of them to this engine's manifest, so
|
|
365
|
+
reading them here is reading facts that agree rather than trusting the
|
|
366
|
+
program's copy.
|
|
367
|
+
"""
|
|
368
|
+
return [library.entry(one["name"], one["arity"]) for one in raw["lib"]["functions"]]
|