imlx-gate 0.1.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.
- imlx/__init__.py +30 -0
- imlx/charset.py +165 -0
- imlx/cli.py +109 -0
- imlx/declarations.py +167 -0
- imlx/executor.py +213 -0
- imlx/gate.py +101 -0
- imlx/layer1.py +563 -0
- imlx/layer2.py +434 -0
- imlx/trace.py +94 -0
- imlx_gate-0.1.0.dist-info/METADATA +71 -0
- imlx_gate-0.1.0.dist-info/RECORD +15 -0
- imlx_gate-0.1.0.dist-info/WHEEL +5 -0
- imlx_gate-0.1.0.dist-info/entry_points.txt +2 -0
- imlx_gate-0.1.0.dist-info/licenses/LICENSE +21 -0
- imlx_gate-0.1.0.dist-info/top_level.txt +1 -0
imlx/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
imlx
|
|
3
|
+
====
|
|
4
|
+
Reference implementation of IMLX: Invariant Markup Language (eXtended).
|
|
5
|
+
|
|
6
|
+
Binary determinism: a deterministic pass/fail gate at input, a
|
|
7
|
+
deterministic, auditable pass/fail at output. The model produces; the
|
|
8
|
+
gate decides.
|
|
9
|
+
|
|
10
|
+
Spec: SPEC.md v0.1.0-rc.1 (github.com/passert-ai/imlx). Foundation:
|
|
11
|
+
the recovered iml-cli v1.0.0 validator core's result-object and CLI
|
|
12
|
+
patterns, rebuilt spec-facing against SPEC v0.1. Zero runtime
|
|
13
|
+
dependencies.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
|
|
18
|
+
from .gate import GateResult, gate_bytes, gate_file, run_file
|
|
19
|
+
from .layer1 import Verdict, Reason, gate_layer1_bytes
|
|
20
|
+
from .layer2 import gate_layer2, OPCODES
|
|
21
|
+
from .declarations import Registries, parse_declarations
|
|
22
|
+
from .executor import RunResult, FailureRecord, run
|
|
23
|
+
from .trace import Trace, TraceEvent
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"GateResult", "gate_bytes", "gate_file", "run_file",
|
|
27
|
+
"Verdict", "Reason", "gate_layer1_bytes", "gate_layer2",
|
|
28
|
+
"Registries", "parse_declarations", "RunResult", "FailureRecord",
|
|
29
|
+
"run", "Trace", "TraceEvent", "OPCODES", "__version__",
|
|
30
|
+
]
|
imlx/charset.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
imlx.charset
|
|
3
|
+
============
|
|
4
|
+
The closed alphabet (SPEC Section 6) and Layer 1 character law.
|
|
5
|
+
|
|
6
|
+
Binary determinism: every check is a membership test over an enumerated set.
|
|
7
|
+
Absence from the allowlist is what forbids (Law 1); the forbidden list in
|
|
8
|
+
SPEC 6.3 exists only so this module can emit useful reason codes.
|
|
9
|
+
|
|
10
|
+
Spec basis (SPEC.md v0.1.0-rc.1):
|
|
11
|
+
- 5.1 encoding and line discipline
|
|
12
|
+
- 6.1 allowed characters (content space)
|
|
13
|
+
- 6.2 allowed constructs
|
|
14
|
+
- 6.4 straight quote codification
|
|
15
|
+
- 9 the @ reference namespace (program/declaration space only)
|
|
16
|
+
|
|
17
|
+
IMPLEMENTATION NOTES (ratified in SPEC v0.1.0-rc.2):
|
|
18
|
+
- PROGRAM_SPACE_EXTRA: characters permitted inside program-space and
|
|
19
|
+
declaration-space pipe-table cells beyond the content alphabet. `@` and
|
|
20
|
+
`=` are attested by SPEC Appendix B/D examples; `< > +` are included to
|
|
21
|
+
make SELECT predicates and COMPUTE operators expressible per 11.2
|
|
22
|
+
("closed operator set"). Interpretation, not verbatim spec text.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
|
|
27
|
+
import re
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Character sets (SPEC 6.1, 6.4)
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
_LETTERS = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
|
|
34
|
+
_DIGITS = set("0123456789")
|
|
35
|
+
|
|
36
|
+
# Punctuation allowed in content space, subject to positional rules
|
|
37
|
+
# (SPEC 6.1). Straight double quote and apostrophe per 6.4.
|
|
38
|
+
_CONTENT_PUNCT = set(". , : ; - ' \" ( ) [ ] | * # $ %".split()) | {"/", "?"}
|
|
39
|
+
|
|
40
|
+
#: Every character legal anywhere in content space (positional rules are
|
|
41
|
+
#: enforced in layer1, not here). LF is handled at the line level.
|
|
42
|
+
CONTENT_CHARS = _LETTERS | _DIGITS | {" "} | _CONTENT_PUNCT
|
|
43
|
+
|
|
44
|
+
#: Characters that MUST appear only inside $-fences in content space
|
|
45
|
+
#: (SPEC 6.2 math fencing). `%` bare is legal only within %%PAGEBREAK%%.
|
|
46
|
+
MATH_ONLY_CHARS = set("<>=+")
|
|
47
|
+
|
|
48
|
+
#: Extra characters legal inside program-space / declaration-space /
|
|
49
|
+
#: trace-space pipe-table cells (see module docstring).
|
|
50
|
+
PROGRAM_SPACE_EXTRA = {"@", "=", "<", ">", "+", "_"}
|
|
51
|
+
|
|
52
|
+
#: Full alphabet for program/declaration/trace table cells.
|
|
53
|
+
PROGRAM_CELL_CHARS = CONTENT_CHARS | PROGRAM_SPACE_EXTRA
|
|
54
|
+
|
|
55
|
+
#: Characters legal on an envelope opening line, beyond content letters and
|
|
56
|
+
#: digits (SPEC 7.1): ::: { } = " - and space are all consumed by the exact
|
|
57
|
+
#: line regex in layer1; no free-form check applies to envelope lines.
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# Exact tokens and line-form regexes
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
PAGEBREAK_TOKEN = "%%PAGEBREAK%%"
|
|
64
|
+
|
|
65
|
+
#: SPEC 5.2 header line 1.
|
|
66
|
+
HEADER_IMLX_RE = re.compile(r"^IMLX: (\d+\.\d+)$")
|
|
67
|
+
|
|
68
|
+
#: SPEC 5.2 header line 2: external pairing or the literal INLINE.
|
|
69
|
+
HEADER_DECL_RE = re.compile(
|
|
70
|
+
r"^DECLARATIONS: (?:(INLINE)|([A-Za-z0-9._-]+\.imlx); (\d+\.\d+))$"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
#: SPEC 10.3 declaration-file third header line.
|
|
74
|
+
HEADER_DECLVER_RE = re.compile(r"^DECL_VERSION: (\d+\.\d+)$")
|
|
75
|
+
|
|
76
|
+
#: SPEC 7.1 envelope opening line, exact form.
|
|
77
|
+
ENVELOPE_OPEN_RE = re.compile(r'^::: \{custom-style="([A-Za-z][A-Za-z0-9_]*)"\}$')
|
|
78
|
+
|
|
79
|
+
#: SPEC 7.1 envelope closing line.
|
|
80
|
+
ENVELOPE_CLOSE = ":::"
|
|
81
|
+
|
|
82
|
+
#: Headings: #, ##, ### + one space (SPEC 6.2). Deeper levels are illegal.
|
|
83
|
+
HEADING_RE = re.compile(r"^(#{1,3}) (\S.*)$")
|
|
84
|
+
HEADING_TOO_DEEP_RE = re.compile(r"^#{4,}")
|
|
85
|
+
|
|
86
|
+
#: Bullets: `* ` and one indented sublevel `* * ` (SPEC 6.2).
|
|
87
|
+
BULLET_RE = re.compile(r"^\* (?:\* )?(\S.*)$")
|
|
88
|
+
|
|
89
|
+
#: Legal-numbered list line (SPEC 6.2): 1.1, 1.2 ... at line start.
|
|
90
|
+
LEGAL_LIST_RE = re.compile(r"^\d+\.\d+ \S")
|
|
91
|
+
|
|
92
|
+
#: Step numbering (SPEC 6.2): Step 1:, Step 2:, ... at line start.
|
|
93
|
+
STEP_LINE_RE = re.compile(r"^Step \d+: \S")
|
|
94
|
+
|
|
95
|
+
#: Pipe-table row: starts and ends with |, at least two pipes.
|
|
96
|
+
TABLE_ROW_RE = re.compile(r"^\|.*\|$")
|
|
97
|
+
|
|
98
|
+
#: Pipe-table separator row (SPEC 6.2): | :--- | :--- |
|
|
99
|
+
TABLE_SEP_CELL = ":---"
|
|
100
|
+
|
|
101
|
+
#: Reference name (SPEC 9): letters, digits, _, beginning with a letter.
|
|
102
|
+
REFERENCE_RE = re.compile(r"^@([A-Za-z][A-Za-z0-9_]*)$")
|
|
103
|
+
REFERENCE_SCAN_RE = re.compile(r"@([A-Za-z][A-Za-z0-9_]*)")
|
|
104
|
+
|
|
105
|
+
#: Program-table header cells, exact (SPEC 11.1).
|
|
106
|
+
PROGRAM_HEADER = ["step", "opcode", "operand", "bind", "style"]
|
|
107
|
+
|
|
108
|
+
#: Trace-table header cells, exact (SPEC 13.6).
|
|
109
|
+
TRACE_HEADER = ["seq", "event", "step", "opcode", "subject", "outcome", "digest"]
|
|
110
|
+
|
|
111
|
+
#: DECLARE section heading (SPEC 10.4): `# DECLARE <kind>`.
|
|
112
|
+
DECLARE_HEADING_RE = re.compile(r"^# DECLARE ([A-Z]+)$")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
# Character-law checks
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
def illegal_content_chars(text: str) -> list[str]:
|
|
120
|
+
"""Characters in ``text`` outside the content-space alphabet, fence-aware.
|
|
121
|
+
|
|
122
|
+
Outside $-fences the alphabet is CONTENT_CHARS; inside a fence the
|
|
123
|
+
mathematical symbols (SPEC 6.2) are additionally legal. Positional
|
|
124
|
+
rules remain layer1's job; this is membership.
|
|
125
|
+
"""
|
|
126
|
+
outside, inside, _balanced = split_math_fences(text)
|
|
127
|
+
bad = {c for c in outside if c not in CONTENT_CHARS}
|
|
128
|
+
bad |= {c for c in inside if c not in CONTENT_CHARS | MATH_ONLY_CHARS}
|
|
129
|
+
return sorted(bad)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def illegal_program_cell_chars(text: str) -> list[str]:
|
|
133
|
+
"""Characters in a program/declaration/trace cell outside its alphabet."""
|
|
134
|
+
return sorted({c for c in text if c not in PROGRAM_CELL_CHARS})
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def split_math_fences(line: str):
|
|
138
|
+
"""Split a line into (outside, inside) character streams by $-fences.
|
|
139
|
+
|
|
140
|
+
Returns (outside_text, inside_text, balanced) where ``balanced`` is
|
|
141
|
+
False if the line ends inside an unclosed fence (fences MUST close
|
|
142
|
+
within the line; multi-line blocks are hard-broken lines, SPEC 6.2).
|
|
143
|
+
"""
|
|
144
|
+
outside: list[str] = []
|
|
145
|
+
inside: list[str] = []
|
|
146
|
+
in_fence = False
|
|
147
|
+
for ch in line:
|
|
148
|
+
if ch == "$":
|
|
149
|
+
in_fence = not in_fence
|
|
150
|
+
continue
|
|
151
|
+
(inside if in_fence else outside).append(ch)
|
|
152
|
+
return "".join(outside), "".join(inside), not in_fence
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def bare_math_chars_outside_fences(line: str) -> list[str]:
|
|
156
|
+
"""Math-only characters appearing outside $-fences on a content line.
|
|
157
|
+
|
|
158
|
+
``%`` is excluded when the line is exactly %%PAGEBREAK%% (caller
|
|
159
|
+
handles that line form before reaching here).
|
|
160
|
+
"""
|
|
161
|
+
outside, _inside, _balanced = split_math_fences(line)
|
|
162
|
+
hits = [c for c in outside if c in MATH_ONLY_CHARS]
|
|
163
|
+
if "%" in outside:
|
|
164
|
+
hits.append("%")
|
|
165
|
+
return sorted(set(hits))
|
imlx/cli.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
imlx.cli
|
|
3
|
+
========
|
|
4
|
+
Command-line interface. Zero dependencies (argparse), mirroring the
|
|
5
|
+
recovered iml-cli's verdict-first ergonomics.
|
|
6
|
+
|
|
7
|
+
imlx gate ARTIFACT [--decls FILE] [--json]
|
|
8
|
+
imlx run ARTIFACT [--decls FILE] [--trace OUT] [--trace-json OUT] [--json]
|
|
9
|
+
imlx version
|
|
10
|
+
|
|
11
|
+
Exit code IS the verdict: 0 = PASS, 1 = FAIL, 2 = usage error.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from . import __version__ as pkg_version
|
|
22
|
+
from .gate import gate_file, run_file
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _print_gate(result, as_json: bool) -> None:
|
|
26
|
+
if as_json:
|
|
27
|
+
print(json.dumps(result.to_dict(), indent=2))
|
|
28
|
+
return
|
|
29
|
+
print(f"Layer 1: {result.layer1.bit}")
|
|
30
|
+
for r in result.layer1.reasons:
|
|
31
|
+
print(f" {r.code} line {r.line}: {r.message}")
|
|
32
|
+
if result.layer2 is None:
|
|
33
|
+
if not result.layer1.passed:
|
|
34
|
+
print("Layer 2: not attempted (Layer 1 failed; nothing runs, SPEC 13.5)")
|
|
35
|
+
else:
|
|
36
|
+
print("Layer 2: not attempted (no declaration source; artifact remains "
|
|
37
|
+
"fully Layer 1-validatable alone, SPEC 5.2)")
|
|
38
|
+
else:
|
|
39
|
+
print(f"Layer 2: {result.layer2.bit}")
|
|
40
|
+
for r in result.layer2.reasons:
|
|
41
|
+
print(f" {r.code} line {r.line}: {r.message}")
|
|
42
|
+
print(f"VERDICT: {result.verdict}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def main(argv=None) -> int:
|
|
46
|
+
parser = argparse.ArgumentParser(prog="imlx",
|
|
47
|
+
description="IMLX gate and executor. One bit.")
|
|
48
|
+
sub = parser.add_subparsers(dest="cmd")
|
|
49
|
+
|
|
50
|
+
p_gate = sub.add_parser("gate", help="render the conformance verdict")
|
|
51
|
+
p_gate.add_argument("artifact")
|
|
52
|
+
p_gate.add_argument("--decls", help="external declaration file")
|
|
53
|
+
p_gate.add_argument("--json", action="store_true")
|
|
54
|
+
|
|
55
|
+
p_run = sub.add_parser("run", help="gate, then execute (skeleton mode)")
|
|
56
|
+
p_run.add_argument("artifact")
|
|
57
|
+
p_run.add_argument("--decls")
|
|
58
|
+
p_run.add_argument("--trace", help="write canonical IMLX trace to this path")
|
|
59
|
+
p_run.add_argument("--trace-json", help="write JSON trace projection to this path")
|
|
60
|
+
p_run.add_argument("--json", action="store_true")
|
|
61
|
+
|
|
62
|
+
sub.add_parser("version", help="print version")
|
|
63
|
+
|
|
64
|
+
args = parser.parse_args(argv)
|
|
65
|
+
if args.cmd == "version":
|
|
66
|
+
print(f"imlx {pkg_version} (SPEC v0.1)")
|
|
67
|
+
return 0
|
|
68
|
+
if args.cmd is None:
|
|
69
|
+
parser.print_help()
|
|
70
|
+
return 2
|
|
71
|
+
|
|
72
|
+
if not Path(args.artifact).is_file():
|
|
73
|
+
print(f"error: no such file: {args.artifact}", file=sys.stderr)
|
|
74
|
+
return 2
|
|
75
|
+
|
|
76
|
+
if args.cmd == "gate":
|
|
77
|
+
result = gate_file(args.artifact, args.decls)
|
|
78
|
+
_print_gate(result, args.json)
|
|
79
|
+
return 0 if result.verdict in ("PASS", "L1-PASS") else 1
|
|
80
|
+
|
|
81
|
+
if args.cmd == "run":
|
|
82
|
+
result, run_result = run_file(args.artifact, args.decls)
|
|
83
|
+
if run_result is None:
|
|
84
|
+
_print_gate(result, args.json)
|
|
85
|
+
return 1
|
|
86
|
+
if args.trace:
|
|
87
|
+
Path(args.trace).write_text(run_result.trace.render_imlx(), encoding="utf-8")
|
|
88
|
+
if args.trace_json:
|
|
89
|
+
Path(args.trace_json).write_text(run_result.trace.render_json(), encoding="utf-8")
|
|
90
|
+
if args.json:
|
|
91
|
+
out = {"verdict": run_result.verdict,
|
|
92
|
+
"events": [e.to_dict() for e in run_result.trace.events]}
|
|
93
|
+
if run_result.failure:
|
|
94
|
+
out["failure"] = run_result.failure.to_dict()
|
|
95
|
+
print(json.dumps(out, indent=2))
|
|
96
|
+
else:
|
|
97
|
+
print(run_result.trace.render_imlx(), end="")
|
|
98
|
+
if run_result.failure:
|
|
99
|
+
f = run_result.failure
|
|
100
|
+
print(f"FAILURE RECORD: step {f.step} {f.opcode} {f.reason_code} "
|
|
101
|
+
f"[{f.contract}]")
|
|
102
|
+
print(f"VERDICT: {run_result.verdict}")
|
|
103
|
+
return 0 if run_result.verdict == "PASS" else 1
|
|
104
|
+
|
|
105
|
+
return 2
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
sys.exit(main())
|
imlx/declarations.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
imlx.declarations
|
|
3
|
+
=================
|
|
4
|
+
The declaration resolver (SPEC Section 10). Populates the registries Layer 2
|
|
5
|
+
validates against: block types (with capabilities), procedures, converters,
|
|
6
|
+
registers, symbols, policy data.
|
|
7
|
+
|
|
8
|
+
- Exactly one declaration source per artifact, named by the header pairing
|
|
9
|
+
line (10.2). External is canonical; INLINE is permitted.
|
|
10
|
+
- A declaration file is itself an IMLX artifact (10.3): same parser, no
|
|
11
|
+
second format. It MUST pass Layer 1.
|
|
12
|
+
- Duplicates fail the gate: L2-DUP01. No precedence, no overlay, no merge.
|
|
13
|
+
|
|
14
|
+
L2-DEC01 ("malformed declaration table": unknown DECLARE kind or wrong
|
|
15
|
+
column set) is ratified in SPEC Appendix C as of v0.1.0-rc.2 and ships
|
|
16
|
+
here with its fixtures.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
|
|
23
|
+
from . import charset as cs
|
|
24
|
+
from .layer1 import Document, Reason, Table, Verdict, gate_layer1_bytes
|
|
25
|
+
|
|
26
|
+
#: SPEC 10.4: registry kinds and their REQUIRED columns.
|
|
27
|
+
REQUIRED_COLUMNS: dict[str, list[str]] = {
|
|
28
|
+
"TYPE": ["name", "capabilities"],
|
|
29
|
+
"PROCEDURE": ["name", "arity", "operand_schema"],
|
|
30
|
+
"CONVERTER": ["name", "from_type", "to_type"],
|
|
31
|
+
"REGISTER": ["name", "type"],
|
|
32
|
+
"SYMBOL": ["sigil_name", "target"],
|
|
33
|
+
"POLICY": ["name", "kind", "payload_ref"],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#: Capabilities defined by SPEC 8. The only sanctioned relaxation mechanism.
|
|
37
|
+
KNOWN_CAPABILITIES = {"legal_lists", "-"}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class Registries:
|
|
42
|
+
"""The declaration space in force for one artifact."""
|
|
43
|
+
types: dict[str, dict] = field(default_factory=dict) # name -> {capabilities}
|
|
44
|
+
procedures: dict[str, dict] = field(default_factory=dict) # name -> {arity, operand_schema}
|
|
45
|
+
converters: dict[str, dict] = field(default_factory=dict) # name -> {from_type, to_type}
|
|
46
|
+
registers: dict[str, dict] = field(default_factory=dict) # name -> {type}
|
|
47
|
+
symbols: dict[str, str] = field(default_factory=dict) # sigil_name -> target
|
|
48
|
+
policies: dict[str, dict] = field(default_factory=dict) # name -> {kind, payload_ref}
|
|
49
|
+
source_name: str = ""
|
|
50
|
+
source_version: str = ""
|
|
51
|
+
|
|
52
|
+
_KIND_MAP = {
|
|
53
|
+
"TYPE": ("types", "name"),
|
|
54
|
+
"PROCEDURE": ("procedures", "name"),
|
|
55
|
+
"CONVERTER": ("converters", "name"),
|
|
56
|
+
"REGISTER": ("registers", "name"),
|
|
57
|
+
"SYMBOL": ("symbols", "sigil_name"),
|
|
58
|
+
"POLICY": ("policies", "name"),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
def type_capabilities(self, type_name: str) -> set[str]:
|
|
62
|
+
entry = self.types.get(type_name)
|
|
63
|
+
if not entry:
|
|
64
|
+
return set()
|
|
65
|
+
caps = entry.get("capabilities", "-")
|
|
66
|
+
return set() if caps == "-" else {c.strip() for c in caps.split(";")}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def parse_declarations(doc: Document, reasons: list[Reason]) -> Registries:
|
|
70
|
+
"""Build registries from a document's # DECLARE sections (SPEC 10.4).
|
|
71
|
+
|
|
72
|
+
Works identically for a declaration file and for an INLINE artifact's
|
|
73
|
+
own declaration section: one parser, no second format (SPEC 10.3).
|
|
74
|
+
"""
|
|
75
|
+
regs = Registries()
|
|
76
|
+
for tbl in doc.declaration_tables:
|
|
77
|
+
_ingest_table(tbl, regs, reasons)
|
|
78
|
+
return regs
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _ingest_table(tbl: Table, regs: Registries, reasons: list[Reason]) -> None:
|
|
82
|
+
kind = tbl.declare_kind or ""
|
|
83
|
+
required = REQUIRED_COLUMNS.get(kind)
|
|
84
|
+
if required is None:
|
|
85
|
+
reasons.append(Reason("L2-DEC01", tbl.start_line,
|
|
86
|
+
f"unknown DECLARE kind '{kind}' (SPEC 10.4 enumerates "
|
|
87
|
+
f"{sorted(REQUIRED_COLUMNS)})"))
|
|
88
|
+
return
|
|
89
|
+
if tbl.header_cells != required:
|
|
90
|
+
reasons.append(Reason("L2-DEC01", tbl.start_line,
|
|
91
|
+
f"DECLARE {kind} requires columns {required}, "
|
|
92
|
+
f"found {tbl.header_cells} (SPEC 10.4)"))
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
attr, key_col = Registries._KIND_MAP[kind]
|
|
96
|
+
store = getattr(regs, attr)
|
|
97
|
+
key_idx = required.index(key_col)
|
|
98
|
+
|
|
99
|
+
for cells, ln in zip(tbl.rows, tbl.row_lines):
|
|
100
|
+
name = cells[key_idx]
|
|
101
|
+
if name in store:
|
|
102
|
+
reasons.append(Reason("L2-DUP01", ln,
|
|
103
|
+
f"duplicate {kind} declaration '{name}' "
|
|
104
|
+
f"(SPEC 10.2: no precedence, no overlay, no merge)"))
|
|
105
|
+
continue
|
|
106
|
+
if kind == "SYMBOL":
|
|
107
|
+
store[name] = cells[required.index("target")]
|
|
108
|
+
else:
|
|
109
|
+
store[name] = {col: cells[idx] for idx, col in enumerate(required) if idx != key_idx}
|
|
110
|
+
# entry-shape checks decidable without other registries
|
|
111
|
+
if kind == "TYPE":
|
|
112
|
+
caps = store[name]["capabilities"]
|
|
113
|
+
unknown = {c.strip() for c in caps.split(";")} - KNOWN_CAPABILITIES
|
|
114
|
+
if caps != "-" and unknown:
|
|
115
|
+
reasons.append(Reason("L2-DEC01", ln,
|
|
116
|
+
f"unknown capability {sorted(unknown)} "
|
|
117
|
+
f"(SPEC 8 defines: legal_lists)"))
|
|
118
|
+
if kind == "PROCEDURE" and not store[name]["arity"].isdigit():
|
|
119
|
+
reasons.append(Reason("L2-DEC01", ln, "PROCEDURE arity must be a non-negative integer"))
|
|
120
|
+
if kind == "SYMBOL" and not cs.REFERENCE_RE.match("@" + name):
|
|
121
|
+
reasons.append(Reason("L2-DEC01", ln,
|
|
122
|
+
"sigil_name must be letters, digits, _, beginning "
|
|
123
|
+
"with a letter (SPEC 9)"))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def load_external_source(path_bytes: bytes, file_name: str,
|
|
127
|
+
expected_name: str, expected_version: str,
|
|
128
|
+
reasons: list[Reason]) -> Registries | None:
|
|
129
|
+
"""Load and verify an external declaration file (SPEC 5.2, 10.3).
|
|
130
|
+
|
|
131
|
+
The Layer 2 gate MUST verify that the declaration source it was given
|
|
132
|
+
matches the name and version the artifact demands (L2-PAIR01), and the
|
|
133
|
+
file itself MUST pass Layer 1.
|
|
134
|
+
"""
|
|
135
|
+
verdict, decl_doc = gate_layer1_bytes(path_bytes, file_name)
|
|
136
|
+
if not verdict.passed:
|
|
137
|
+
reasons.append(Reason("L2-PAIR01", 0,
|
|
138
|
+
f"declaration file '{file_name}' fails Layer 1 "
|
|
139
|
+
f"({verdict.reasons[0].code} at its line "
|
|
140
|
+
f"{verdict.reasons[0].line}); a declaration file MUST "
|
|
141
|
+
f"pass Layer 1 like any artifact (SPEC 10.3)"))
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
if file_name != expected_name:
|
|
145
|
+
reasons.append(Reason("L2-PAIR01", 2,
|
|
146
|
+
f"artifact header pairs '{expected_name}' but was given "
|
|
147
|
+
f"'{file_name}' (SPEC 5.2)"))
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
if decl_doc.header.decl_mode != "INLINE" or decl_doc.header.file_decl_version is None:
|
|
151
|
+
reasons.append(Reason("L2-PAIR01", 0,
|
|
152
|
+
f"'{file_name}' is not a declaration file: its DECLARATIONS "
|
|
153
|
+
f"line must read INLINE and it must carry DECL_VERSION "
|
|
154
|
+
f"(SPEC 10.3)"))
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
if decl_doc.header.file_decl_version != expected_version:
|
|
158
|
+
reasons.append(Reason("L2-PAIR01", 2,
|
|
159
|
+
f"artifact header demands version {expected_version}; "
|
|
160
|
+
f"'{file_name}' declares DECL_VERSION "
|
|
161
|
+
f"{decl_doc.header.file_decl_version} (SPEC 5.2)"))
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
regs = parse_declarations(decl_doc, reasons)
|
|
165
|
+
regs.source_name = file_name
|
|
166
|
+
regs.source_version = decl_doc.header.file_decl_version
|
|
167
|
+
return regs
|
imlx/executor.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
imlx.executor
|
|
3
|
+
=============
|
|
4
|
+
The execution model (SPEC 13). State is the register file; steps execute in
|
|
5
|
+
step-number order; SLOT fills are sequential, never parallel.
|
|
6
|
+
|
|
7
|
+
- Skeleton mode (13.3): runs every program end-to-end with no engine; each
|
|
8
|
+
SLOT renders a placeholder displaying its full contract. Fully
|
|
9
|
+
deterministic; no network, no credentials; the mode conformance tests
|
|
10
|
+
run in.
|
|
11
|
+
- Engine mode (13.4): an engine adapter fills SLOTs; the paired GATE is
|
|
12
|
+
applied immediately to what returns. The engine is outside the guarantee
|
|
13
|
+
boundary.
|
|
14
|
+
- Failure semantics (13.5): a runtime GATE FAIL halts at that step with
|
|
15
|
+
verdict FAIL and a failure record (step, opcode, reason code, violated
|
|
16
|
+
contract). A conforming executor MUST NOT retry internally.
|
|
17
|
+
|
|
18
|
+
SKELETON DIGEST CANON (SPEC 13.6, ratified v0.1.0-rc.2): the
|
|
19
|
+
content of a skeleton-mode binding is defined as the canonical string
|
|
20
|
+
``SKELETON|<step>|<opcode>|<operand>|<bind>`` (UTF-8). SPEC 13.6 requires
|
|
21
|
+
byte-identical skeleton traces across implementations, which requires the
|
|
22
|
+
digested content to be canonically defined; this string is that definition,
|
|
23
|
+
ratified, and the trace fixtures pin it.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
27
|
+
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from typing import Callable
|
|
30
|
+
|
|
31
|
+
from . import charset as cs
|
|
32
|
+
from .declarations import Registries
|
|
33
|
+
from .layer1 import Document
|
|
34
|
+
from .layer2 import parse_steps, Step, SLOT_CONTRACT_KEYS
|
|
35
|
+
from .trace import Trace, sha256_hex
|
|
36
|
+
|
|
37
|
+
#: Engine adapter: contract dict -> payload string.
|
|
38
|
+
EngineAdapter = Callable[[dict], str]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class FailureRecord:
|
|
43
|
+
"""SPEC 13.5: step number, opcode, reason code, violated contract."""
|
|
44
|
+
step: int
|
|
45
|
+
opcode: str
|
|
46
|
+
reason_code: str
|
|
47
|
+
contract: str
|
|
48
|
+
|
|
49
|
+
def to_dict(self) -> dict:
|
|
50
|
+
return {"step": self.step, "opcode": self.opcode,
|
|
51
|
+
"reason_code": self.reason_code, "contract": self.contract}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class RunResult:
|
|
56
|
+
verdict: str # PASS | FAIL
|
|
57
|
+
trace: Trace
|
|
58
|
+
registers: dict[str, str] = field(default_factory=dict)
|
|
59
|
+
failure: FailureRecord | None = None
|
|
60
|
+
outputs: list[tuple[str, str]] = field(default_factory=list) # (type, content)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def skeleton_content(st: Step) -> str:
|
|
64
|
+
"""Canonical skeleton-mode binding content (module docstring)."""
|
|
65
|
+
return f"SKELETON|{st.number}|{st.opcode}|{st.operand}|{st.bind}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def slot_placeholder(contract: dict) -> str:
|
|
69
|
+
"""SPEC 13.3: a placeholder displaying the SLOT's full contract."""
|
|
70
|
+
body = "; ".join(f"{k}={contract[k]}" for k in sorted(contract))
|
|
71
|
+
return f"[SLOT UNFILLED: {body}]"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def parse_contract(operand: str) -> dict:
|
|
75
|
+
kv = {}
|
|
76
|
+
for p in (x.strip() for x in operand.split(";")):
|
|
77
|
+
if "=" in p:
|
|
78
|
+
k, v = p.split("=", 1)
|
|
79
|
+
kv[k] = v
|
|
80
|
+
return kv
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def run(doc: Document, registries: Registries,
|
|
84
|
+
engine: EngineAdapter | None = None,
|
|
85
|
+
policy_payloads: dict[str, list[str]] | None = None) -> RunResult:
|
|
86
|
+
"""Execute a Layer 1- and Layer 2-passed document (SPEC 13).
|
|
87
|
+
|
|
88
|
+
``policy_payloads`` supplies blocklist policy data (Law 6): a mapping
|
|
89
|
+
from POLICY name to its list of forbidden substrings. Policy is data
|
|
90
|
+
given to the gate, never spec or code content.
|
|
91
|
+
"""
|
|
92
|
+
trace = Trace(doc.name)
|
|
93
|
+
trace.emit("GATE_L1", "-", "-", doc.name, "PASS")
|
|
94
|
+
l2_subject = (f"{registries.source_name}; {registries.source_version}"
|
|
95
|
+
if registries.source_name else "INLINE")
|
|
96
|
+
trace.emit("GATE_L2", "-", "-", l2_subject, "PASS")
|
|
97
|
+
|
|
98
|
+
steps = parse_steps(doc)
|
|
99
|
+
registers: dict[str, str] = {}
|
|
100
|
+
slot_contracts: dict[str, dict] = {}
|
|
101
|
+
slot_payloads: dict[str, str] = {}
|
|
102
|
+
policy_payloads = policy_payloads or {}
|
|
103
|
+
|
|
104
|
+
for st in steps:
|
|
105
|
+
s = str(st.number)
|
|
106
|
+
|
|
107
|
+
if st.opcode == "SLOT":
|
|
108
|
+
contract = parse_contract(st.operand)
|
|
109
|
+
trace.emit("SLOT_OPEN", s, "SLOT", f"type={contract.get('type', '-')}", "DONE")
|
|
110
|
+
if engine is None:
|
|
111
|
+
registers[st.bind] = slot_placeholder(contract)
|
|
112
|
+
slot_contracts[st.bind] = contract
|
|
113
|
+
trace.emit("SLOT_FILL", s, "SLOT", st.bind, "SKELETON")
|
|
114
|
+
else:
|
|
115
|
+
payload = engine(contract) # sequential, program order (13.2)
|
|
116
|
+
registers[st.bind] = payload
|
|
117
|
+
slot_contracts[st.bind] = contract
|
|
118
|
+
slot_payloads[st.bind] = payload
|
|
119
|
+
trace.emit("SLOT_FILL", s, "SLOT", st.bind, "FILLED",
|
|
120
|
+
sha256_hex(payload))
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
if st.opcode == "GATE":
|
|
124
|
+
target = st.operand.strip()
|
|
125
|
+
contract = slot_contracts.get(target, {})
|
|
126
|
+
if target in slot_payloads: # engine mode: gate what returned
|
|
127
|
+
verdict, code = _gate_slot_payload(
|
|
128
|
+
slot_payloads[target], contract, registries, policy_payloads)
|
|
129
|
+
else: # skeleton fill: the placeholder is the deterministic pass
|
|
130
|
+
verdict, code = "PASS", ""
|
|
131
|
+
trace.emit("GATE_VERDICT", s, "GATE", target, verdict)
|
|
132
|
+
if st.bind != "-":
|
|
133
|
+
registers[st.bind] = verdict
|
|
134
|
+
if verdict == "FAIL":
|
|
135
|
+
trace.emit("HALT", s, "-", doc.name, "FAIL")
|
|
136
|
+
return RunResult(
|
|
137
|
+
verdict="FAIL", trace=trace, registers=registers,
|
|
138
|
+
failure=FailureRecord(step=st.number, opcode="GATE",
|
|
139
|
+
reason_code=code,
|
|
140
|
+
contract="; ".join(
|
|
141
|
+
f"{k}={contract[k]}"
|
|
142
|
+
for k in sorted(contract))))
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
if st.opcode == "OUTPUT_PRINT":
|
|
146
|
+
target = st.operand.strip()
|
|
147
|
+
trace.emit("STEP", s, st.opcode, target, "DONE")
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
# all other opcodes: deterministic step; binding opcodes emit BIND
|
|
151
|
+
subject = st.bind if st.bind != "-" else st.operand
|
|
152
|
+
trace.emit("STEP", s, st.opcode, subject, "DONE")
|
|
153
|
+
bind_name = st.bind if st.bind != "-" else None
|
|
154
|
+
if bind_name:
|
|
155
|
+
content = skeleton_content(st) if engine is None else \
|
|
156
|
+
_engine_mode_binding_content(st, registers)
|
|
157
|
+
registers[bind_name] = content
|
|
158
|
+
trace.emit("BIND", s, st.opcode, bind_name, "BOUND", sha256_hex(content))
|
|
159
|
+
|
|
160
|
+
# collect outputs after a completed run
|
|
161
|
+
outputs = [(st.style, registers.get(st.operand.strip(), ""))
|
|
162
|
+
for st in steps if st.opcode == "OUTPUT_PRINT"]
|
|
163
|
+
|
|
164
|
+
last = str(steps[-1].number) if steps else "-"
|
|
165
|
+
trace.emit("HALT", last, "-", doc.name, "PASS")
|
|
166
|
+
return RunResult(verdict="PASS", trace=trace, registers=registers,
|
|
167
|
+
outputs=outputs)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _engine_mode_binding_content(st: Step, registers: dict[str, str]) -> str:
|
|
171
|
+
"""Deterministic data-opcode semantics shared by both modes.
|
|
172
|
+
|
|
173
|
+
Phase 1 executes data opcodes symbolically: the binding's content is the
|
|
174
|
+
canonical step description. Real corpus-backed LOAD/QUERY semantics are
|
|
175
|
+
toolchain integrations layered on later; the trace interface does not
|
|
176
|
+
change. In engine mode the SLOT is the only moving part (SPEC 13.6).
|
|
177
|
+
"""
|
|
178
|
+
return skeleton_content(st)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
# The runtime GATE on a SLOT payload (SPEC 13.4, 11.2)
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
def _gate_slot_payload(payload: str, contract: dict, regs: Registries,
|
|
186
|
+
policy_payloads: dict[str, list[str]]) -> tuple[str, str]:
|
|
187
|
+
"""One-bit verdict on an engine payload: charset law + blocklist policy.
|
|
188
|
+
|
|
189
|
+
The charset law is always in force on what returns; the blocklist is
|
|
190
|
+
the policy data the contract names (Law 6). RT-GATE01 on FAIL.
|
|
191
|
+
"""
|
|
192
|
+
# charset law: every payload line must satisfy content-space rules
|
|
193
|
+
for line in payload.split("\n"):
|
|
194
|
+
if line == "":
|
|
195
|
+
continue
|
|
196
|
+
if cs.illegal_content_chars(line):
|
|
197
|
+
return "FAIL", "RT-GATE01"
|
|
198
|
+
bare = cs.bare_math_chars_outside_fences(line)
|
|
199
|
+
if bare:
|
|
200
|
+
return "FAIL", "RT-GATE01"
|
|
201
|
+
if "@" in line:
|
|
202
|
+
return "FAIL", "RT-GATE01"
|
|
203
|
+
|
|
204
|
+
# blocklist policy
|
|
205
|
+
ref = contract.get("blocklist", "")
|
|
206
|
+
policy_name = ref[1:] if ref.startswith("@") else ref
|
|
207
|
+
target = regs.symbols.get(policy_name, policy_name)
|
|
208
|
+
blocked = policy_payloads.get(policy_name) or policy_payloads.get(target) or []
|
|
209
|
+
for term in blocked:
|
|
210
|
+
if term and term in payload:
|
|
211
|
+
return "FAIL", "RT-GATE01"
|
|
212
|
+
|
|
213
|
+
return "PASS", ""
|