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 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", ""