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/budget.py ADDED
@@ -0,0 +1,342 @@
1
+ """The counters that make a runaway script stop, and the ceilings a host sets.
2
+
3
+ The engine owns the loop, so every one of these is a number it increments rather
4
+ than something it hopes about. A platform running many customers' scripts in one
5
+ process needs exactly that: exceeding a budget is a diagnostic on the script that
6
+ did it, never a hang and never a frozen tab.
7
+
8
+ **Two kinds of budget, and the difference matters.**
9
+
10
+ The *loop budget* is part of the language. Section 8.5 requires two engines to
11
+ fail at the same iteration of the same loop on the same bar, so it counts
12
+ ``TICK`` executions and nothing else, it comes from the program's own ``limits()``
13
+ line, and it is identical on every engine.
14
+
15
+ The *host ceilings* are not part of the language and cannot be. They exist so
16
+ that one script cannot take a process down, and a host that sets one accepts
17
+ that a program refused here may run elsewhere. None of them silently caps
18
+ anything: section 2.4 is explicit that a host refuses at load, naming the limit
19
+ and the value it allows, because a program that quietly gets a smaller budget
20
+ than it asked for produces a wrong number instead of a message and nothing on
21
+ the chart would say which number.
22
+
23
+ **There is no wall clock here.** A clock measures the machine rather than the
24
+ program, so a default one would make the same script pass on a fast machine and
25
+ fail on a slow one with nobody having asked for that trade. The step ceiling
26
+ below does the work a clock would otherwise do, and it is derived from the
27
+ program rather than chosen.
28
+ """
29
+
30
+ import math
31
+ from dataclasses import dataclass
32
+ from typing import Any, Dict, List, Optional, Sequence, Tuple
33
+
34
+ from .diagnostics import Diagnostic, Position, failure, raise_at
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class EngineLimits:
39
+ """What this engine allows, with ``None`` meaning no ceiling of its own."""
40
+
41
+ #: Call frames, OS5005 at load.
42
+ frames: int = 64
43
+ #: Code points one string may hold, OS5008.
44
+ #:
45
+ #: No document fixes this number: ``errors.md`` OS5008 states that there is a
46
+ #: ceiling and carries it in the message, and a host that sets one accepts
47
+ #: that a script refused here runs elsewhere. What a number left open does
48
+ #: not excuse is two engines choosing differently, because a case that
49
+ #: reaches the ceiling would then be refused by one of them and run by the
50
+ #: other, and section 10 calls a difference on a channel both answer a
51
+ #: release blocker. So this is the first engine's number, and the day either
52
+ #: moves the other moves with it.
53
+ string_length: int = 100_000
54
+ #: The most a program's own ``limits(loops = ...)`` may ask for, OS5003.
55
+ loops: Optional[int] = None
56
+ #: The most a program's own ``limits(history = ...)`` may ask for, OS5003.
57
+ history: Optional[int] = None
58
+ #: Instructions in the whole program, OS5009 at load.
59
+ instructions: Optional[int] = None
60
+ #: State regions, OS5004 at load.
61
+ states: Optional[int] = None
62
+ #: Reads one file may make, OS5006 at load.
63
+ requests: Optional[int] = None
64
+
65
+
66
+ DEFAULT_LIMITS = EngineLimits()
67
+
68
+
69
+ def all_code(program: Any) -> List[Sequence[Any]]:
70
+ """Every instruction list in the program, the bodies of its reads included.
71
+
72
+ A read's expression is part of the program: an engine that cannot run an
73
+ ``ELEM`` cannot run one inside a read either, and a budget or a tag worked
74
+ out from the bar's list alone would say otherwise.
75
+ """
76
+ lists: List[Sequence[Any]] = [program["code"]]
77
+ lists.extend(one["code"] for one in program["functions"])
78
+
79
+ def walk(requests: Sequence[Any]) -> None:
80
+ for request in requests:
81
+ body = request["body"]
82
+ lists.append(body["code"])
83
+ lists.extend(one["code"] for one in body["functions"])
84
+ walk(body["requests"])
85
+
86
+ walk(program["requests"])
87
+ return lists
88
+
89
+
90
+ def _opcode_of(instruction: Any) -> Optional[str]:
91
+ """The opcode of one element of a list, or nothing when it is not an instruction.
92
+
93
+ The budget refusals are step 7 of section 9.4 and verification is step 8, so
94
+ the walks here read lists that nothing has yet proved are instruction lists.
95
+ Reading them defensively is what keeps the order from turning a malformed
96
+ program into a thrown error rather than the OS6018 step 8 is about to report.
97
+ """
98
+ return instruction[0] if isinstance(instruction, list) and instruction else None
99
+
100
+
101
+ def _count_requests(requests: Sequence[Any]) -> int:
102
+ found = 0
103
+ for one in requests:
104
+ found += 1 + _count_requests(one["body"]["requests"])
105
+ return found
106
+
107
+
108
+ def _sites_in(code: Sequence[Any]) -> List[int]:
109
+ return [
110
+ one[1]
111
+ for one in code
112
+ if _opcode_of(one) == "CALL_FN" and len(one) > 1 and isinstance(one[1], int)
113
+ ]
114
+
115
+
116
+ def call_depth(program: Any) -> int:
117
+ """The deepest chain of call sites, which is the deepest the frame stack goes.
118
+
119
+ Recursion is an error in the language, so the call graph is acyclic and the
120
+ walk terminates; the guard below is against a program written by something
121
+ other than a conforming compiler, and it stops rather than looping forever.
122
+ """
123
+ bodies = [one["code"] for one in program["functions"]]
124
+ known: Dict[int, int] = {}
125
+ walking: set = set()
126
+
127
+ def depth_of(site: int) -> int:
128
+ if site in known:
129
+ return known[site]
130
+ if site in walking:
131
+ return 1
132
+ walking.add(site)
133
+ entry = program["callSites"][site] if site < len(program["callSites"]) else None
134
+ body = bodies[entry["fn"]] if entry is not None and entry["fn"] < len(bodies) else []
135
+ deepest = 0
136
+ for inner in _sites_in(body):
137
+ deepest = max(deepest, depth_of(inner))
138
+ walking.discard(site)
139
+ known[site] = deepest + 1
140
+ return known[site]
141
+
142
+ deepest = 0
143
+ for site in _sites_in(program["code"]):
144
+ deepest = max(deepest, depth_of(site))
145
+ return deepest + 1
146
+
147
+
148
+ def _busiest_pair(program: Any) -> Tuple[str, str]:
149
+ """The two functions whose nesting produced the most call paths.
150
+
151
+ OS5004's message names them because the number of paths grows
152
+ multiplicatively where several functions each call the next more than once,
153
+ and a reader told only the total has nowhere to start.
154
+ """
155
+ counts: Dict[str, int] = {}
156
+ listings = [("the top level", program["code"])]
157
+ listings.extend((one["name"], one["code"]) for one in program["functions"])
158
+ for caller, code in listings:
159
+ for instruction in code:
160
+ if _opcode_of(instruction) != "CALL_FN":
161
+ continue
162
+ site = instruction[1]
163
+ if not isinstance(site, int) or site >= len(program["callSites"]):
164
+ continue
165
+ which = program["callSites"][site]["fn"]
166
+ if which >= len(program["functions"]):
167
+ continue
168
+ callee = program["functions"][which]["name"]
169
+ key = f"{caller}\x00{callee}"
170
+ counts[key] = counts.get(key, 0) + 1
171
+ best = ("the top level", "the functions it calls")
172
+ most = 0
173
+ # Sorted before the walk, so the answer does not depend on a table's order.
174
+ for key in sorted(counts):
175
+ if counts[key] <= most:
176
+ continue
177
+ caller, callee = key.split("\x00")
178
+ best, most = (caller, callee), counts[key]
179
+ return best
180
+
181
+
182
+ def check_budgets(program: Any, limits: EngineLimits) -> Optional[Diagnostic]:
183
+ """Step 7 of section 9.4: each ceiling in turn, stopping at the first one over."""
184
+ asked = program["limits"]
185
+ if limits.loops is not None and asked["loops"] > limits.loops:
186
+ return failure("OS5003", option="loops", max=limits.loops, found=asked["loops"])
187
+ history = asked["history"]
188
+ if limits.history is not None and history is not None and history > limits.history:
189
+ return failure("OS5003", option="history", max=limits.history, found=history)
190
+ if limits.instructions is not None:
191
+ found = sum(len(code) for code in all_code(program))
192
+ if found > limits.instructions:
193
+ return failure("OS5009", found=found, max=limits.instructions)
194
+ if limits.states is not None and len(program["states"]) > limits.states:
195
+ first, second = _busiest_pair(program)
196
+ return failure(
197
+ "OS5004",
198
+ found=len(program["states"]),
199
+ max=limits.states,
200
+ first=first,
201
+ second=second,
202
+ )
203
+ if limits.requests is not None:
204
+ found = _count_requests(program["requests"])
205
+ if found > limits.requests:
206
+ return failure("OS5006", found=found, max=limits.requests)
207
+ depth = call_depth(program)
208
+ if depth > limits.frames:
209
+ return failure("OS5005", construct="a call", found=depth, max=limits.frames)
210
+ return None
211
+
212
+
213
+ def step_bound(program: Any) -> int:
214
+ """The most instructions one bar of this program can possibly execute.
215
+
216
+ Verification proves three things that together bound a bar: the target of
217
+ every backward jump is a ``TICK``, so no cycle runs without charging the loop
218
+ budget; recursion is an error, so the call graph is acyclic and each call
219
+ site's body runs at most once per acyclic segment; and the lists are finite.
220
+ So a segment between two ``TICK`` executions costs at most the program's
221
+ whole instruction count once, and there are at most ``limits.loops`` such
222
+ segments plus the one that ends at ``HALT``.
223
+
224
+ The number is generous by design. It is not a performance budget: it is the
225
+ proof that a bar terminates, turned into a counter so that a program which
226
+ somehow exceeds its own static bound stops instead of running forever.
227
+ """
228
+ acyclic = len(program["code"])
229
+ for site in program["callSites"]:
230
+ acyclic += len(program["functions"][site["fn"]]["code"])
231
+ segments = max(program["limits"]["loops"], 0) + 1
232
+ return max(acyclic, 1) * segments + acyclic
233
+
234
+
235
+ def suggest_budget(budget: int) -> int:
236
+ """A budget with room to spare, for OS5001's fix line.
237
+
238
+ The catalogue asks for a budget that would have completed the bar, and the
239
+ engine cannot know one: it stopped the loop rather than finishing it. Twice
240
+ the budget, rounded to one significant figure so the number reads like
241
+ something a person would type, is the nearest honest thing, and the message
242
+ beside it says the first fix is the exit condition.
243
+ """
244
+ doubled = max(budget * 2, 1000)
245
+ magnitude = 10 ** math.floor(math.log10(doubled))
246
+ return int(math.ceil(doubled / magnitude) * magnitude)
247
+
248
+
249
+ class Budget:
250
+ """The per-bar counters, reset at step 3 of every execution of a bar.
251
+
252
+ Per bar rather than per loop so that ten sequential loops and one nested
253
+ loop are treated alike, and reset each bar so that a long dataset is never
254
+ itself a reason to fail.
255
+ """
256
+
257
+ def __init__(self, step_ceiling: int, loop_ceiling: int, string_ceiling: int) -> None:
258
+ self._step_ceiling = step_ceiling
259
+ self._loop_ceiling = loop_ceiling
260
+ self._string_ceiling = string_ceiling
261
+ self._loops = 0
262
+ self._steps = 0
263
+
264
+ def begin(self) -> None:
265
+ self._loops = 0
266
+ self._steps = 0
267
+
268
+ def step(self) -> bool:
269
+ """One instruction executed, and whether the ceiling is now past.
270
+
271
+ Answers rather than raising, so the dispatch loop does the check inline
272
+ and the cost is one comparison on the path every instruction takes.
273
+ """
274
+ self._steps += 1
275
+ return self._steps > self._step_ceiling
276
+
277
+ @property
278
+ def loops(self) -> int:
279
+ return self._loops
280
+
281
+ def tick(self, position: Position, line: int) -> None:
282
+ """Section 5.5: one iteration of a loop charged to the per-bar budget."""
283
+ self._loops += 1
284
+ if self._loops > self._loop_ceiling:
285
+ self.spent(position, line)
286
+
287
+ def text(self, position: Position, built: str) -> str:
288
+ """A string the bar built, against the ceiling, counted in code points.
289
+
290
+ `stdlib.md` section 10 counts a string in code points and not in the
291
+ storage unit of the language an engine happens to be written in, which on
292
+ this host is what a string already is, so the length is the count.
293
+
294
+ Answers the string so that a caller applies the ceiling in the expression
295
+ that produces the value, rather than in a line above it that is easy to
296
+ move away from the thing it guards.
297
+ """
298
+ self.measured(position, len(built))
299
+ return built
300
+
301
+ def measured(self, position: Position, length: Optional[int]) -> None:
302
+ """The same ceiling against a length nothing has built yet.
303
+
304
+ ``None`` is a call the library cannot measure in advance, which is every
305
+ call but the two that can be asked for a string no engine could hold:
306
+ a repeat is a length times a count, and a fixed decimal conversion is one
307
+ character per decimal place asked for. Measuring those first is the
308
+ difference between reporting that a string is too long and running out of
309
+ memory finding out (`stdlib.md` section 10, on ``text(x, decimals)``).
310
+
311
+ The refusal is the run's and not the library's: nothing in that package
312
+ raises, because a wrong argument there is absence and a ceiling is the
313
+ engine's to spend.
314
+ """
315
+ if length is not None and length > self._string_ceiling:
316
+ raise_at("OS5008", position, max=self._string_ceiling, found=length)
317
+
318
+ def spent(self, position: Position, line: Optional[int]) -> None:
319
+ """The refusal, whichever of the two counters ran out.
320
+
321
+ The step ceiling is the static bound of ``step_bound`` and a loop is the
322
+ only construct that can approach it, so the loop that was running is the
323
+ one to name. A bar that passes it with no loop running contradicts what
324
+ verification proved about the program, which is OS6018's case.
325
+ """
326
+ if line is None:
327
+ raise_at(
328
+ "OS6018",
329
+ position,
330
+ location=f"step {self._steps}",
331
+ reason=(
332
+ "the bar executed more instructions than the program can reach without a "
333
+ "loop, so the instruction list is not the one verification walked"
334
+ ),
335
+ )
336
+ raise_at(
337
+ "OS5001",
338
+ position,
339
+ budget=self._loop_ceiling,
340
+ line=line,
341
+ suggested=suggest_budget(self._loop_ceiling),
342
+ )
@@ -0,0 +1,192 @@
1
+ """The canonical encoding, ``compiled-program.md`` section 2.14, and the door text comes in by.
2
+
3
+ A hash is taken over canonical bytes and a host records it against a chart, a
4
+ backtest run and a live process. So text that parses to a program but is spelled
5
+ some other way is text that hash does not name, and an engine that accepted it
6
+ would report a run under a name identifying different bytes. That is why
7
+ ``load_text`` below parses, writes the result out again through the writer here,
8
+ and refuses any difference, naming the character the two part at.
9
+
10
+ **Canonicity is required of text and not of an object.** ``spec/decisions.md``
11
+ minute 57 settled it and section 9.4 step 1 carries the sentence: an object
12
+ built in the same process was never text, has nothing to be canonical about, and
13
+ enters the refusal order at step 2. The check lives at the text boundary and
14
+ nowhere else, so the in-process case is free of a rule it could not break.
15
+
16
+ The rules, none of them cosmetic and none of them this file's invention:
17
+
18
+ - UTF-8, no byte order mark, no whitespace between tokens.
19
+ - Object keys sorted ascending by Unicode code point, because a sort is a rule
20
+ an emitter in any language can follow without a table.
21
+ - A number written by the one rule ``language.md`` section 5.5 gives, which is
22
+ the same rule for every number that becomes text anywhere in the language.
23
+ - A string escaping only the quote, the backslash and the code points below
24
+ 0x20, the last as ``\\u00XX`` except for the three that have a letter.
25
+
26
+ **The digits are the interpreter's and the layout is not.** The shortest decimal
27
+ digit string that reads back as the same binary64 is what every host worth
28
+ shipping on produces, so that half is taken from this one. Where two hosts part
29
+ company is the layout: when a value is written positionally and when with an
30
+ exponent, and how the exponent is spelled. That half is written out here from
31
+ the rule, once: the library's ``number_text`` calls this writer rather than
32
+ restating it, and ``spec/vectors/number-text.json`` holds the boundary cases
33
+ every engine checks itself against.
34
+ """
35
+
36
+ import json
37
+ import math
38
+ from typing import Any, Tuple
39
+
40
+ from .diagnostics import Diagnostic, malformed
41
+
42
+ #: The two thresholds section 5.5 puts the positional form between.
43
+ _HIGHEST_POSITIONAL = 21
44
+ _LOWEST_POSITIONAL = -6
45
+
46
+
47
+ def spread(shown: str, places: int = 0) -> Tuple[str, int]:
48
+ """A written magnitude as digits, and where the point falls.
49
+
50
+ The value is ``0.d1d2...dk`` times ten to the returned power, which is the
51
+ shape 5.5 states the rule in. Whatever layout the writing arrived in is
52
+ undone here: the digits and the point are all that is kept, so the layout
53
+ below is free to be the language's rather than the host's. ``places`` moves
54
+ the point right, which is how the fixed decimal conversion scales without
55
+ multiplying a second time.
56
+
57
+ It takes the writing rather than the number because the two callers need two
58
+ different ones: which digits a magnitude has is the host's question, and
59
+ where they sit is this one's.
60
+ """
61
+ marker = shown.find("e")
62
+ mantissa = shown if marker < 0 else shown[:marker]
63
+ exponent = 0 if marker < 0 else int(shown[marker + 1 :])
64
+ dot = mantissa.find(".")
65
+ whole = mantissa if dot < 0 else mantissa[:dot]
66
+ fraction = "" if dot < 0 else mantissa[dot + 1 :]
67
+ digits = whole + fraction
68
+ point = len(whole) + exponent + places
69
+ # A positional form below one carries leading zeros that are not digits of
70
+ # the value, and a whole number carries trailing zeros that are its layout.
71
+ while digits.startswith("0"):
72
+ digits = digits[1:]
73
+ point -= 1
74
+ while digits.endswith("0"):
75
+ digits = digits[:-1]
76
+ return digits, point
77
+
78
+
79
+ def _layout(digits: str, point: int) -> str:
80
+ """Positional between the two thresholds, an exponent outside them."""
81
+ count = len(digits)
82
+ if count <= point <= _HIGHEST_POSITIONAL:
83
+ return digits + "0" * (point - count)
84
+ if 0 < point <= _HIGHEST_POSITIONAL:
85
+ return f"{digits[:point]}.{digits[point:]}"
86
+ if _LOWEST_POSITIONAL < point <= 0:
87
+ return f"0.{'0' * -point}{digits}"
88
+ exponent = point - 1
89
+ lead = digits if count == 1 else f"{digits[0]}.{digits[1:]}"
90
+ return f"{lead}e{'-' if exponent < 0 else ''}{abs(exponent)}"
91
+
92
+
93
+ def canonical_number(value: float) -> str:
94
+ """A number as text: the one writer.
95
+
96
+ Two engines compare numbers as bits, and they compare text in a case file,
97
+ an expected column, a table cell and ``text(x)``, so how a number becomes
98
+ text has to be one rule both implement and, here, one function both call.
99
+
100
+ The two callers are ``canonicalise`` below, which writes a constant into the
101
+ text a recorded hash is taken over, and ``library/number_text.py``, which is
102
+ ``text(x)`` and the digits of ``text(x, decimals)``. The sentence above was
103
+ a claim rather than a fact for one stage: the rule was implemented twice,
104
+ with the two layout thresholds typed out in each, and the vectors held both
105
+ to the same answers, so nothing failed. What would have failed is the day
106
+ one of the two moved and a label stopped agreeing with a recorded hash.
107
+ """
108
+ number = float(value)
109
+ if not math.isfinite(number):
110
+ raise ValueError("a compiled program holds finite numbers only")
111
+ # Zero and negative zero are one value to the language and one spelling here.
112
+ if number == 0:
113
+ return "0"
114
+ digits, point = spread(repr(abs(number)))
115
+ return ("-" if number < 0 else "") + _layout(digits, point)
116
+
117
+
118
+ _LETTERED = {"\n": "\\n", "\r": "\\r", "\t": "\\t", '"': '\\"', "\\": "\\\\"}
119
+
120
+
121
+ def canonical_string(value: str) -> str:
122
+ out = ['"']
123
+ for character in value:
124
+ lettered = _LETTERED.get(character)
125
+ if lettered is not None:
126
+ out.append(lettered)
127
+ elif ord(character) < 0x20:
128
+ out.append(f"\\u{ord(character):04x}")
129
+ else:
130
+ out.append(character)
131
+ out.append('"')
132
+ return "".join(out)
133
+
134
+
135
+ def canonicalise(value: Any) -> str:
136
+ """One value of a parsed program, written the one way it may be written."""
137
+ if value is None:
138
+ return "null"
139
+ if isinstance(value, bool):
140
+ return "true" if value else "false"
141
+ if isinstance(value, (int, float)):
142
+ return canonical_number(value)
143
+ if isinstance(value, str):
144
+ return canonical_string(value)
145
+ if isinstance(value, (list, tuple)):
146
+ return "[" + ",".join(canonicalise(one) for one in value) + "]"
147
+ if isinstance(value, dict):
148
+ pairs = sorted(value.items(), key=lambda pair: pair[0])
149
+ body = ",".join(f"{canonical_string(key)}:{canonicalise(one)}" for key, one in pairs)
150
+ return "{" + body + "}"
151
+ raise ValueError(f"a compiled program holds no value of this kind: {type(value).__name__}")
152
+
153
+
154
+ def _refuse_constant(text: str) -> Any:
155
+ """What a reader must not build: the three spellings outside binary64's finite range."""
156
+ raise ValueError(f"{text} is not a value a compiled program holds")
157
+
158
+
159
+ def _parted_at(text: str, written: str) -> int:
160
+ """The index the two spellings part at, which is where the message points."""
161
+ shortest = min(len(text), len(written))
162
+ for index in range(shortest):
163
+ if text[index] != written[index]:
164
+ return index
165
+ return shortest
166
+
167
+
168
+ def parse(text: str) -> Tuple[Any, Diagnostic]:
169
+ """Step 1 of section 9.4: parse the text, and require it to be canonical.
170
+
171
+ Returns the parsed program and no diagnostic, or nothing and the refusal.
172
+ Both halves are OS6018 and each names where the text stopped being readable
173
+ or stopped being canonical, because neither is repairable by hand: a program
174
+ is written by a compiler and a malformed one is that compiler's defect.
175
+ """
176
+ if text.startswith(""):
177
+ return None, malformed("character 0", "the text carries a byte order mark")
178
+ try:
179
+ parsed = json.loads(text, parse_constant=_refuse_constant)
180
+ except ValueError as reason:
181
+ return None, malformed("the encoding", f"the text is not readable: {reason}")
182
+ try:
183
+ written = canonicalise(parsed)
184
+ except ValueError as reason:
185
+ return None, malformed("the encoding", str(reason))
186
+ if written != text:
187
+ return None, malformed(
188
+ f"character {_parted_at(text, written)}",
189
+ "the text is not the canonical encoding of what it parses to, and the hash a host "
190
+ "records against a run is taken over canonical bytes",
191
+ )
192
+ return parsed, None