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/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"]]