openscript 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openscript/__init__.py +40 -0
- openscript/__main__.py +62 -0
- openscript/accounting/__init__.py +74 -0
- openscript/accounting/analysis.py +174 -0
- openscript/accounting/charges.py +397 -0
- openscript/accounting/equity.py +234 -0
- openscript/accounting/report.py +82 -0
- openscript/accounting/shapes.py +74 -0
- openscript/accounting/statistics.py +300 -0
- openscript/accounting/trades.py +294 -0
- openscript/adapter/__init__.py +32 -0
- openscript/adapter/answers.py +215 -0
- openscript/adapter/channels.py +137 -0
- openscript/adapter/expectations.py +67 -0
- openscript/adapter/facts.py +127 -0
- openscript/adapter/matching.py +257 -0
- openscript/adapter/ordering.py +187 -0
- openscript/adapter/page.py +130 -0
- openscript/adapter/reading.py +357 -0
- openscript/adapter/reporting.py +244 -0
- openscript/adapter/running.py +449 -0
- openscript/adapter/serving.py +229 -0
- openscript/adapter/sessions.py +168 -0
- openscript/adapter/spellings.py +184 -0
- openscript/bars.py +157 -0
- openscript/budget.py +342 -0
- openscript/canonical.py +192 -0
- openscript/civil.py +196 -0
- openscript/contracts.py +165 -0
- openscript/dates.py +302 -0
- openscript/diagnostics.py +104 -0
- openscript/hours.py +165 -0
- openscript/inputs.py +239 -0
- openscript/intervals.py +60 -0
- openscript/library/__init__.py +76 -0
- openscript/library/arithmetic.py +128 -0
- openscript/library/averages.py +133 -0
- openscript/library/bars.py +60 -0
- openscript/library/bookkeeping.py +166 -0
- openscript/library/code_points.py +85 -0
- openscript/library/colour.py +202 -0
- openscript/library/composites.py +208 -0
- openscript/library/counting.py +218 -0
- openscript/library/deviation.py +155 -0
- openscript/library/elementary.py +206 -0
- openscript/library/extremes.py +122 -0
- openscript/library/flows.py +220 -0
- openscript/library/momentum.py +203 -0
- openscript/library/number_text.py +223 -0
- openscript/library/prices.py +36 -0
- openscript/library/ranges.py +105 -0
- openscript/library/rounding.py +123 -0
- openscript/library/series.py +213 -0
- openscript/library/stateful.py +442 -0
- openscript/library/stateless.py +261 -0
- openscript/library/strength.py +180 -0
- openscript/library/strings.py +228 -0
- openscript/library/trend.py +260 -0
- openscript/library/values.py +91 -0
- openscript/logbook.py +119 -0
- openscript/machine.py +499 -0
- openscript/memory.py +204 -0
- openscript/opcodes.py +166 -0
- openscript/program.py +146 -0
- openscript/run.py +368 -0
- openscript/strategy/__init__.py +78 -0
- openscript/strategy/calls.py +201 -0
- openscript/strategy/closable.py +182 -0
- openscript/strategy/fills.py +131 -0
- openscript/strategy/holdings.py +277 -0
- openscript/strategy/intents.py +162 -0
- openscript/strategy/ledger.py +270 -0
- openscript/strategy/placing.py +206 -0
- openscript/strategy/positions.py +124 -0
- openscript/strategy/refusals.py +293 -0
- openscript/strategy/rows.py +219 -0
- openscript/strategy/sizing.py +229 -0
- openscript/strategy/statuses.py +65 -0
- openscript/surface/__init__.py +115 -0
- openscript/surface/bands.py +103 -0
- openscript/surface/levels.py +44 -0
- openscript/surface/marks.py +52 -0
- openscript/surface/paints.py +58 -0
- openscript/surface/plots.py +44 -0
- openscript/surface/published.py +119 -0
- openscript/values.py +210 -0
- openscript/verify.py +301 -0
- openscript/verify_code.py +290 -0
- openscript/verify_requests.py +271 -0
- openscript/verify_shape.py +162 -0
- openscript/verify_tables.py +256 -0
- openscript/version.py +39 -0
- openscript/zones.py +118 -0
- openscript-0.4.0.dist-info/METADATA +82 -0
- openscript-0.4.0.dist-info/RECORD +97 -0
- openscript-0.4.0.dist-info/WHEEL +5 -0
- openscript-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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
|