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
@@ -0,0 +1,44 @@
1
+ """What the other surfaces need of a plot: the column a declared key names.
2
+
3
+ A plot's own column is not a surface channel. ``conformance.md`` section 4 puts
4
+ one value per bar in ``expected.csv``, one column per plot, and the adapter
5
+ reads a column's name against this program's plots and takes the channel behind
6
+ it. That is where a plot's values are compared and this module does not repeat
7
+ it. What is here is the one question the band next door asks: which channel does
8
+ a plot key name, since ``compiled-program.md`` 2.8 carries a band's two sides as
9
+ the plot keys of ``fills[].between`` rather than as channels.
10
+
11
+ **A plot's per-bar colour has no channel to be asserted through, and that is a
12
+ gap in the page rather than in this engine.** 2.8 gives a plot a
13
+ ``colorChannel`` for the bars where the colour is computed rather than declared,
14
+ and 4.11 lists "a plot's per-bar colour" among the values an ``EMIT`` writes, so
15
+ it is a per-bar output like any other. Section 2's list of channels a case may
16
+ assert has no name for it: ``values`` names a plot by its title or its key and
17
+ reads the channel carrying its value, and there is no spelling that would reach
18
+ the colour beside it. So a study that paints a histogram by sign produces output
19
+ no case can compare, on either engine. The same is true of the four colour
20
+ channels of a candle. The stage's report carries it.
21
+ """
22
+
23
+ from typing import Any, Dict
24
+
25
+ from ..diagnostics import ScriptError, malformed
26
+
27
+
28
+ def channel_of_key(raw: Dict[str, Any], key: Any, path: str) -> int:
29
+ """The channel of the plot a declaration names by key, 2.8's ``between``.
30
+
31
+ A key naming no declared plot is OS6018 at the field that named it, which is
32
+ the code 3.5 check 1 refuses an index out of range with. This engine's
33
+ verification reads the indexes and not this pair of names, so a band drawn
34
+ between a column the program never declared would otherwise be a band drawn
35
+ nowhere and nothing said. The stage's report carries the change that moves
36
+ the refusal into verification, where the rest of check 1 is.
37
+ """
38
+ for one in raw["outputs"]["plots"]:
39
+ if one.get("key") == key:
40
+ return int(one["channel"])
41
+ raise ScriptError(
42
+ malformed(path, f"names the plot {key!r} and this program declares no plot with that key")
43
+ )
44
+
@@ -0,0 +1,119 @@
1
+ """What a bar left the machine, after however many times that bar was executed.
2
+
3
+ Every surface here is read from the same two things: the columns step 8
4
+ published for a bar, and the deferred channels step 9 applied on it
5
+ (``compiled-program.md`` 5.1). This module turns a run's executions into that
6
+ pair, once, so that no surface module counts a bar twice or reads a channel the
7
+ engine held back.
8
+
9
+ **The last execution of a bar is the bar.** Section 6.4: a bar that
10
+ re-executes has rolled its state back first, so the set of drawings after five
11
+ updates to a moving bar is the set after one. An engine that appended a marker
12
+ per execution would draw five where a host redrawing what it is handed draws
13
+ one, and the difference would only ever show up on a live chart. So an
14
+ execution of a bar replaces the one before it rather than adding to it.
15
+
16
+ **A deferred channel reaches a host only on a bar the engine decided.** Section
17
+ 5.4 holds back the channels declared ``defer``, which are the markers and the
18
+ alert conditions, until step 9, and step 9 runs only on a bar that was confirmed
19
+ or under ``meta.onUnconfirmed``. ``reached`` is that rule, and it is asked of
20
+ every channel rather than of the markers alone: a channel that is not deferred
21
+ is published at step 8 on every execution, and a surface that assumed which of
22
+ its channels carried the flag would be reading the compiler's habits rather than
23
+ the program in front of it.
24
+
25
+ **A field the verifier does not reach is checked here.** ``compiled-program.md``
26
+ 3.5 check 1 holds every index into the channel table in range, and this engine's
27
+ verification reads the required ones: a plot's ``channel``, a level's, a
28
+ marker's, an alert's ``condChannel`` and the two paint channels. The nullable
29
+ ones beside them, ``plots[].colorChannel``, ``fills[].colorUpChannel``,
30
+ ``fills[].colorDownChannel``, the four of ``plots[].ohlc`` and an alert's
31
+ ``messageChannel``, are not read there, so a program naming a channel past the
32
+ end of the table would be read past the end of a list here. ``channel_named``
33
+ refuses it with OS6018 naming the field, which is the code check 1 refuses with,
34
+ rather than answering a colour nobody wrote. The stage's report carries the
35
+ verification change that would make this unreachable.
36
+ """
37
+
38
+ from dataclasses import dataclass
39
+ from typing import Any, Dict, List, Optional, Sequence
40
+
41
+ from ..diagnostics import ScriptError, malformed
42
+ from ..program import LoadedProgram
43
+ from ..run import BarResult
44
+ from ..values import ABSENT
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class Published:
49
+ """One bar, as the last execution of it left the machine.
50
+
51
+ ``columns`` is what step 8 published and ``applied`` is the deferred
52
+ channels step 9 applied, which is empty on a bar the engine did not decide.
53
+ """
54
+
55
+ index: int
56
+ columns: Sequence[Any]
57
+ applied: Sequence[int]
58
+
59
+
60
+ def published(executions: Sequence[BarResult]) -> List[Published]:
61
+ """Every bar the run published, in bar order, each as its last execution.
62
+
63
+ An execution that failed published nothing: ``run.py`` returns the columns
64
+ the bar began with and a diagnostic, and the bar keeps whatever a previous
65
+ execution published, which is section 5.1's own sentence about an error
66
+ during step 6. So a failed execution is not a bar here, and a bar that
67
+ failed after an earlier execution succeeded keeps that earlier one.
68
+ """
69
+ found: Dict[int, Published] = {}
70
+ for one in executions:
71
+ if one.diagnostic is not None:
72
+ continue
73
+ found[one.index] = Published(one.index, list(one.columns), list(one.applied_channels))
74
+ return [found[at] for at in sorted(found)]
75
+
76
+
77
+ def reached(program: LoadedProgram, bar: Published, channel: int) -> bool:
78
+ """Whether what a channel held for this bar reached the host, section 5.4."""
79
+ if channel < 0 or channel >= len(program.defer):
80
+ return False
81
+ if program.defer[channel]:
82
+ return channel in bar.applied
83
+ return True
84
+
85
+
86
+ def value_of(program: LoadedProgram, bar: Published, channel: int) -> Any:
87
+ """What a channel held for this bar, or absence where nothing reached the host.
88
+
89
+ Absence is the whole of the rule 2.7 states and 18 repeats: a channel
90
+ nothing wrote is absent, and absence reaching a surface is a gap in a plot,
91
+ no marker, a bar left its own colour. Never a zero and never a default.
92
+ """
93
+ if not reached(program, bar, channel):
94
+ return ABSENT
95
+ if channel < 0 or channel >= len(bar.columns):
96
+ return ABSENT
97
+ return bar.columns[channel]
98
+
99
+
100
+ def channel_named(raw: Dict[str, Any], holder: Any, field: str, path: str) -> Optional[int]:
101
+ """A channel a nullable declaration field names, held to the program's table.
102
+
103
+ ``None`` where the field is null, which is what every one of these fields
104
+ holds in the common case: a plot with a constant colour, a band with two,
105
+ an alert with no message.
106
+ """
107
+ held = holder.get(field)
108
+ if held is None:
109
+ return None
110
+ count = len(raw["channels"])
111
+ whole = isinstance(held, int) and not isinstance(held, bool)
112
+ if not whole or held < 0 or held >= count:
113
+ raise ScriptError(
114
+ malformed(
115
+ f"{path}.{field}",
116
+ f"names channel {held} and the program declares {count} of them",
117
+ )
118
+ )
119
+ return held
openscript/values.py ADDED
@@ -0,0 +1,210 @@
1
+ """The six things a value on this machine can be, and what absence does to each.
2
+
3
+ ``compiled-program.md`` section 3.1 fixes the set and the rules that hold
4
+ everywhere, and section 7 says the whole of warmup is the absent value. So this
5
+ module is the one place absence is decided, and every instruction in
6
+ ``machine.py`` reaches it through the operations below rather than testing for
7
+ absence itself. An engine that special cases absence anywhere else has a bug.
8
+
9
+ **Absence is a tag and never a number.** A sentinel such as not-a-number would
10
+ make absence propagate through arithmetic by accident, which is the right answer
11
+ for four operators and the wrong answer for six, and it would turn equality into
12
+ a floating point comparison. Here it is this interpreter's own null: the pool
13
+ entry ``["z", null]`` on the way in, and the ``null`` section 3.4 requires on the
14
+ way out to a host, with nothing to convert at either boundary.
15
+
16
+ **A number is a float and always finite.** Every arithmetic result goes through
17
+ ``finite``, which answers absence for anything that is not, checked after each
18
+ individual operation rather than at the end of an expression: ``(1e308 * 10) /
19
+ 10`` is absent and not ``1e307``. The same function normalises a negative zero
20
+ away, at every result and every store, because nothing in the language can
21
+ observe the sign of a zero and a difference nobody can act on is still a
22
+ difference two engines could reach text with.
23
+
24
+ **A boolean is not a number here.** This interpreter's own ``True`` is equal to
25
+ ``1`` and orders against it, so every comparison below reads the tag first. That
26
+ single habit is what keeps ``true == 1`` false, as ``language.md`` section 9.3
27
+ requires, and it is the mistake this module exists to make unavailable.
28
+ """
29
+
30
+ import math
31
+ from dataclasses import dataclass, field
32
+ from typing import Any, List
33
+
34
+ #: The absent value, written ``none`` in a script.
35
+ #:
36
+ #: The library package spells it the same way, and the two have to agree: a
37
+ #: value crosses that boundary on every ``CALL_LIB``, in both directions.
38
+ ABSENT = None
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class Colour:
43
+ """Red, green and blue as whole numbers from 0 to 255, alpha from 0 to 1."""
44
+
45
+ red: float
46
+ green: float
47
+ blue: float
48
+ alpha: float
49
+
50
+
51
+ class Reference:
52
+ """Something in the object heap. Equality on two of them is identity."""
53
+
54
+ __slots__ = ()
55
+
56
+
57
+ @dataclass(eq=False)
58
+ class ArrayValue(Reference):
59
+ """An array. ``ARRAY`` allocates a new one every time it executes."""
60
+
61
+ elements: List[Any] = field(default_factory=list)
62
+
63
+
64
+ def is_absent(value: Any) -> bool:
65
+ return value is ABSENT
66
+
67
+
68
+ def tag(value: Any) -> str:
69
+ """The value's tag, in the spelling section 3.1's table uses."""
70
+ if value is ABSENT:
71
+ return "absent"
72
+ if isinstance(value, bool):
73
+ return "bool"
74
+ if isinstance(value, (int, float)):
75
+ return "number"
76
+ if isinstance(value, str):
77
+ return "string"
78
+ if isinstance(value, Colour):
79
+ return "color"
80
+ if isinstance(value, Reference):
81
+ return "reference"
82
+ return "unknown"
83
+
84
+
85
+ def finite(result: float) -> Any:
86
+ """An arithmetic result: absent when it is not finite, and never a minus zero."""
87
+ if not math.isfinite(result):
88
+ return ABSENT
89
+ return 0.0 if result == 0.0 else float(result)
90
+
91
+
92
+ def stored(value: Any) -> Any:
93
+ """A value on its way into a slot, a cell, a register or a channel.
94
+
95
+ Section 8.1 normalises a negative zero at every store as well as at every
96
+ result, because a value can reach a store without an arithmetic instruction
97
+ having touched it: a library function's answer, or a constant.
98
+ """
99
+ if is_number(value) and value == 0.0:
100
+ return 0.0
101
+ return value
102
+
103
+
104
+ def is_number(value: Any) -> bool:
105
+ """Whether a value is a number, which a bool is not."""
106
+ return isinstance(value, (int, float)) and not isinstance(value, bool)
107
+
108
+
109
+ def is_whole(value: Any) -> bool:
110
+ """A number with nothing after the point, which is what a history index must be."""
111
+ return is_number(value) and math.isfinite(value) and float(value) == math.floor(value)
112
+
113
+
114
+ def equal(left: Any, right: Any) -> bool:
115
+ """``EQ``: total, and always a boolean.
116
+
117
+ Absent equals absent and equals nothing else. Two colours are equal when all
118
+ four channels match. Two references are equal when they are the same object,
119
+ which is why the library has a call for comparing an array's contents.
120
+ """
121
+ left_tag, right_tag = tag(left), tag(right)
122
+ if left_tag != right_tag:
123
+ return False
124
+ if left_tag == "absent":
125
+ return True
126
+ if left_tag == "reference":
127
+ return left is right
128
+ return bool(left == right)
129
+
130
+
131
+ def ordered(left: Any, right: Any) -> Any:
132
+ """The comparison the four ordering instructions share: -1, 0, 1, or absent.
133
+
134
+ Absence propagates rather than answering false, which is ``language.md``
135
+ section 6.4 and the one thing that keeps a warming up study from drawing a
136
+ confident line. Two numbers compare numerically and two strings by code
137
+ point; anything else is a program the checker should have rejected, and an
138
+ engine answers it with absence rather than inventing a conversion.
139
+ """
140
+ if left is ABSENT or right is ABSENT:
141
+ return ABSENT
142
+ both_numbers = is_number(left) and is_number(right)
143
+ both_strings = isinstance(left, str) and isinstance(right, str)
144
+ if not both_numbers and not both_strings:
145
+ return ABSENT
146
+ if left < right:
147
+ return -1.0
148
+ return 1.0 if left > right else 0.0
149
+
150
+
151
+ #: The three valued conjunction of ``language.md`` section 6.6, as a table.
152
+ #:
153
+ #: Written out rather than computed so that the page and the code are read side
154
+ #: by side. Each table holds only the cases its short circuit leaves to it:
155
+ #: ``AND_SHORT`` has already decided a false left operand, ``OR_SHORT`` a true
156
+ #: one, and absence on the left short circuits neither because the other side
157
+ #: can still decide the answer by itself. A pair neither table holds cannot
158
+ #: arise once the short circuit has run, and a program that produced one is
159
+ #: corrupt rather than unusual, so it is answered with absence rather than with
160
+ #: a row this page never wrote.
161
+ _ABSENT_KEY = "absent"
162
+
163
+ _AND = {
164
+ (True, True): True,
165
+ (True, False): False,
166
+ (True, _ABSENT_KEY): ABSENT,
167
+ (_ABSENT_KEY, True): ABSENT,
168
+ (_ABSENT_KEY, False): False,
169
+ (_ABSENT_KEY, _ABSENT_KEY): ABSENT,
170
+ }
171
+
172
+ _OR = {
173
+ (False, True): True,
174
+ (False, False): False,
175
+ (False, _ABSENT_KEY): ABSENT,
176
+ (_ABSENT_KEY, True): True,
177
+ (_ABSENT_KEY, False): ABSENT,
178
+ (_ABSENT_KEY, _ABSENT_KEY): ABSENT,
179
+ }
180
+
181
+
182
+ def _key(value: Any) -> Any:
183
+ """A boolean, or the stand-in the two tables are keyed by for anything else."""
184
+ return value if isinstance(value, bool) else _ABSENT_KEY
185
+
186
+
187
+ def conjunction(left: Any, right: Any) -> Any:
188
+ return _AND.get((_key(left), _key(right)), ABSENT)
189
+
190
+
191
+ def disjunction(left: Any, right: Any) -> Any:
192
+ return _OR.get((_key(left), _key(right)), ABSENT)
193
+
194
+
195
+ def negation(value: Any) -> Any:
196
+ """``NOT``: true becomes false, false becomes true, absent stays absent."""
197
+ if isinstance(value, bool):
198
+ return not value
199
+ return ABSENT
200
+
201
+
202
+ def truthy(value: Any) -> bool:
203
+ """What ``JUMP_FALSE`` asks.
204
+
205
+ The one place absence is absorbed rather than propagated, and it is
206
+ unavoidable: execution has to go somewhere. It serves ``if``, ``else if``,
207
+ ``while``, the ternary and a ``switch`` arm, so the rule is written once
208
+ here and once in the instruction that reads it.
209
+ """
210
+ return value is True
openscript/verify.py ADDED
@@ -0,0 +1,301 @@
1
+ """Load-time verification, section 3.5, and the refusals a host makes beside it.
2
+
3
+ Before executing a single bar an engine must verify the program. What is bought
4
+ by doing it here rather than discovering a problem halfway through a bar is
5
+ stated in the specification and is the reason the interpreter next door is as
6
+ plain as it is: a verified program cannot underflow the stack, cannot jump out
7
+ of bounds, cannot address a slot that does not exist and cannot loop without
8
+ charging the budget. Every remaining failure is a script error with a source
9
+ position, which is the only kind of failure a user should ever see.
10
+
11
+ Three refusals that are not verification sit here too, because they happen at
12
+ the same moment and for the same reason: a version this engine does not
13
+ implement (OS6016, OS6017), a capability it does not have (OS6006) and a library
14
+ entry that disagrees with its manifest (OS6004). The fourth, the budgets a host
15
+ is willing to spend, is ``budget.py``: that is a question about this host rather
16
+ than about this program, and two hosts may honestly answer it differently.
17
+
18
+ **The order of those refusals is the specification's, 9.4, and not an accident
19
+ of where the code grew.** It is an ordered list that stops at the first failure,
20
+ so a program that is wrong in two ways reports the one the list reaches first,
21
+ and two engines hand the same program the same message. Each step below says
22
+ which number it is.
23
+ """
24
+
25
+ from dataclasses import dataclass
26
+ from typing import Any, Dict, Optional, Sequence, Tuple
27
+
28
+ from .budget import EngineLimits, all_code, check_budgets
29
+ from .contracts import Library, NoLibrary
30
+ from .diagnostics import Diagnostic, failure, malformed
31
+ from .verify_code import ListLimits, check_list, check_once, missing_tag
32
+ from .verify_shape import ShapeCheck
33
+ from .verify_tables import check_tables
34
+ from .version import FORMAT, LANGUAGE_VERSIONS, format_major
35
+
36
+ #: What this engine can do on its own, as the tags of section 2.2.
37
+ #:
38
+ #: Five of the ten, and the five that are the machine's. The others are not
39
+ #: refusals of this module's making: ``orders`` needs an order route and a
40
+ #: ledger, ``objects`` and ``tables`` need the library calls that build one, and
41
+ #: the two read tags need bars this engine is not given. Each is added by the
42
+ #: stage that serves it, through ``capabilities`` below, so a program that needs
43
+ #: one is refused at load naming the feature rather than drawing a study with a
44
+ #: silently empty line through it.
45
+ MACHINE_CAPABILITIES: Tuple[str, ...] = ("core.1", "arrays", "functions", "loops", "alerts")
46
+
47
+
48
+ def capabilities(*served: str) -> Tuple[str, ...]:
49
+ """The machine's tags, plus whatever the stages wired in this run serve."""
50
+ return MACHINE_CAPABILITIES + tuple(tag for tag in served if tag not in MACHINE_CAPABILITIES)
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class VerifyOptions:
55
+ capabilities: Sequence[str] = MACHINE_CAPABILITIES
56
+ limits: EngineLimits = EngineLimits()
57
+ library: Library = NoLibrary()
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class VerifyResult:
62
+ program: Optional[Dict[str, Any]]
63
+ diagnostic: Optional[Diagnostic]
64
+
65
+ @property
66
+ def ok(self) -> bool:
67
+ return self.diagnostic is None
68
+
69
+
70
+ def _major_of(version: str) -> Optional[int]:
71
+ parts = version.split(".")
72
+ if not parts or not all(part.isdigit() for part in parts):
73
+ return None
74
+ return int(parts[0])
75
+
76
+
77
+ def verify(raw: Any, options: VerifyOptions = VerifyOptions()) -> VerifyResult:
78
+ """Steps 2 to 8 of section 9.4, stopping at the first failure."""
79
+ shape = ShapeCheck()
80
+
81
+ def broken() -> VerifyResult:
82
+ found = shape.problem()
83
+ if found is None:
84
+ found = malformed("the program", "it is not a compiled program")
85
+ return VerifyResult(None, found)
86
+
87
+ def refused(diagnostic: Diagnostic) -> VerifyResult:
88
+ return VerifyResult(None, diagnostic)
89
+
90
+ if not shape.object(raw, "the program"):
91
+ return broken()
92
+ version = raw.get("openscript")
93
+ if not shape.object(version, "openscript"):
94
+ return broken()
95
+ if not shape.string(version.get("format"), "openscript.format"):
96
+ return broken()
97
+ if not shape.whole(version.get("language"), "openscript.language"):
98
+ return broken()
99
+
100
+ # Steps 2 and 3: the major decides, and a minor in either direction loads.
101
+ major = _major_of(version["format"])
102
+ if major is None:
103
+ shape.fail("openscript.format", "a version of the form major.minor was required")
104
+ return broken()
105
+ if major != format_major():
106
+ return refused(failure("OS6016", found=version["format"], max=FORMAT))
107
+
108
+ # Step 1's other half: the tables every later step indexes into.
109
+ if not check_tables(shape, raw):
110
+ return broken()
111
+
112
+ # Step 4, and it comes before the language version on purpose: a program
113
+ # that is both compiled from a language this engine lacks and dependent on a
114
+ # capability it lacks reports the capability, which names the feature that
115
+ # was refused rather than a number the reader has to look up.
116
+ for tag in raw["requires"]:
117
+ if not isinstance(tag, str):
118
+ return VerifyResult(None, malformed("requires", "a capability tag is a string"))
119
+ if tag not in options.capabilities:
120
+ return refused(failure("OS6006", tag=tag))
121
+
122
+ # Step 5.
123
+ language = raw["openscript"]["language"]
124
+ if language not in LANGUAGE_VERSIONS:
125
+ return refused(
126
+ failure(
127
+ "OS6017",
128
+ found=language,
129
+ versions=", ".join(str(one) for one in LANGUAGE_VERSIONS),
130
+ )
131
+ )
132
+
133
+ # Step 6.
134
+ disagreement = _check_library(raw, options.library)
135
+ if disagreement is not None:
136
+ return refused(disagreement)
137
+
138
+ # Step 7.
139
+ over = check_budgets(raw, options.limits)
140
+ if over is not None:
141
+ return refused(over)
142
+
143
+ # Step 8, which is section 3.5 itself.
144
+ sizes = _table_sizes(raw)
145
+ if not check_list(shape, "code", raw["code"], sizes, "HALT"):
146
+ return broken()
147
+ for at in range(len(raw["functions"])):
148
+ body = raw["functions"][at]["code"]
149
+ if not check_list(shape, f"functions[{at}]", body, _body_sizes(sizes, raw, at), "RET"):
150
+ return broken()
151
+ if not check_once(shape, raw["code"], [one["once"] for one in raw["channels"]]):
152
+ return broken()
153
+ # 2.16.1: a read's body is an instruction list the same machine walks, so it
154
+ # gets the same walk. Its terminator is RET rather than HALT, because a body
155
+ # produces a value and HALT does not.
156
+ if not _check_bodies(shape, "requests", raw["requests"], sizes):
157
+ return broken()
158
+
159
+ # Check 8, the program's half: an instruction whose tag it never declared.
160
+ missing = missing_tag(all_code(raw), raw["requires"])
161
+ if missing is not None:
162
+ return refused(
163
+ failure(
164
+ "OS6018",
165
+ location="requires",
166
+ reason=f"the program uses {missing['opcode']} and does not declare {missing['tag']}",
167
+ )
168
+ )
169
+
170
+ return VerifyResult(raw, None)
171
+
172
+
173
+ def _check_library(raw: Any, library: Library) -> Optional[Diagnostic]:
174
+ """Check 9: every ``lib.functions`` entry agrees with this engine's manifest.
175
+
176
+ This catches a program compiled against a newer library before it computes a
177
+ single wrong number, which is why the entries carry facts the engine already
178
+ knows: they are there to be disagreed with.
179
+ """
180
+ for at, entry in enumerate(raw["lib"]["functions"]):
181
+ mine = library.entry(entry["name"], entry["arity"])
182
+ if mine is None:
183
+ return failure(
184
+ "OS6004",
185
+ index=at,
186
+ name=entry["name"],
187
+ arity=entry["arity"],
188
+ manifest=library.describe(entry["name"]),
189
+ )
190
+ if mine.state != entry["state"] or mine.effect != entry["effect"]:
191
+ held = f"{entry['name']} holding state {mine.state} with effect {mine.effect}"
192
+ return failure(
193
+ "OS6004", index=at, name=entry["name"], arity=entry["arity"], manifest=held
194
+ )
195
+ return None
196
+
197
+
198
+ def _argc_reader(sites: Sequence[Any]):
199
+ def argc_of(site: int) -> int:
200
+ return sites[site]["argc"] if 0 <= site < len(sites) else 0
201
+
202
+ return argc_of
203
+
204
+
205
+ def _table_sizes(raw: Any) -> ListLimits:
206
+ return ListLimits(
207
+ slots=raw["frame"]["slots"],
208
+ cells=len(raw["cells"]),
209
+ states=len(raw["states"]),
210
+ registers=len(raw["series"]),
211
+ channels=len(raw["channels"]),
212
+ libFunctions=len(raw["lib"]["functions"]),
213
+ callSites=len(raw["callSites"]),
214
+ loops=len(raw["loops"]),
215
+ consts=len(raw["consts"]),
216
+ bindings=0,
217
+ argc_of=_argc_reader(raw["callSites"]),
218
+ )
219
+
220
+
221
+ def _bindings_for(sites: Sequence[Any], fn: int) -> int:
222
+ """How many series bindings a ``HISTP`` inside this function body may index.
223
+
224
+ Every site that calls the function has to bind the same number, because the
225
+ operand is fixed in the body, so the smallest binding list is the one that
226
+ decides what is in range.
227
+ """
228
+ found = 0
229
+ for site in sites:
230
+ if site["fn"] != fn:
231
+ continue
232
+ found = len(site["series"]) if found == 0 else min(found, len(site["series"]))
233
+ return found
234
+
235
+
236
+ def _with(sizes: ListLimits, **changed: Any) -> ListLimits:
237
+ fields = dict(
238
+ slots=sizes.slots,
239
+ cells=sizes.cells,
240
+ states=sizes.states,
241
+ registers=sizes.registers,
242
+ channels=sizes.channels,
243
+ libFunctions=sizes.libFunctions,
244
+ callSites=sizes.callSites,
245
+ loops=sizes.loops,
246
+ consts=sizes.consts,
247
+ bindings=sizes.bindings,
248
+ argc_of=sizes.argc_of,
249
+ )
250
+ fields.update(changed)
251
+ return ListLimits(**fields)
252
+
253
+
254
+ def _body_sizes(base: ListLimits, raw: Any, fn: int) -> ListLimits:
255
+ return _with(
256
+ base,
257
+ slots=raw["functions"][fn]["slots"],
258
+ bindings=_bindings_for(raw["callSites"], fn),
259
+ )
260
+
261
+
262
+ def _check_bodies(
263
+ shape: ShapeCheck, prefix: str, requests: Sequence[Any], outer: ListLimits
264
+ ) -> bool:
265
+ """Check 8 over every read's body, and every read written inside one.
266
+
267
+ The body's tables are its own and counted from zero, so the sizes are rebuilt
268
+ for each of them; ``consts`` and ``lib.functions`` stay the program's,
269
+ because those are the two a body shares. ``channels`` is zero, which is what
270
+ refuses an ``EMIT`` inside a body without a rule of its own: a read carries
271
+ no channel, no plot and no declaration.
272
+ """
273
+ for at, request in enumerate(requests):
274
+ body = request["body"]
275
+ where = f"{prefix}[{at}].body"
276
+ sizes = _with(
277
+ outer,
278
+ slots=body["frame"]["slots"],
279
+ cells=len(body["cells"]),
280
+ states=len(body["states"]),
281
+ registers=len(body["series"]),
282
+ channels=0,
283
+ callSites=len(body["callSites"]),
284
+ loops=len(body["loops"]),
285
+ bindings=0,
286
+ argc_of=_argc_reader(body["callSites"]),
287
+ )
288
+ if not check_list(shape, f"{where}.code", body["code"], sizes, "RET"):
289
+ return False
290
+ for which in range(len(body["functions"])):
291
+ one = body["functions"][which]
292
+ inner = _with(
293
+ sizes,
294
+ slots=one["slots"],
295
+ bindings=_bindings_for(body["callSites"], which),
296
+ )
297
+ if not check_list(shape, f"{where}.functions[{which}]", one["code"], inner, "RET"):
298
+ return False
299
+ if not _check_bodies(shape, f"{where}.requests", body["requests"], sizes):
300
+ return False
301
+ return True