feat(syntax): foundation types, error classes, ResolveContext Protocol

This commit is contained in:
DumiFlex
2026-04-28 03:10:27 +03:00
parent cd8d23a750
commit 45c2fa9bc4
4 changed files with 237 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
"""Engine syntax: tokenizer + resolver shared between runtime and Test Runner.
See `docs/superpowers/specs/2026-04-28-engine-syntax-design.md` for the
language contract and parity rules.
"""
from engine.syntax.types import (
CycleDetectedError,
MalformedSyntaxError,
RecursionLimitExceeded,
RefOutOfSurfaceError,
ResolveContext,
SurfaceKind,
SyntaxError,
Token,
TokenKind,
UnknownRefError,
)
__all__ = [
"CycleDetectedError",
"MalformedSyntaxError",
"RecursionLimitExceeded",
"RefOutOfSurfaceError",
"ResolveContext",
"SurfaceKind",
"SyntaxError",
"Token",
"TokenKind",
"UnknownRefError",
]
+127
View File
@@ -0,0 +1,127 @@
"""Foundational types for the engine syntax module.
Defines:
- Token + TokenKind (the tokenizer's output shape)
- TokenMeta (per-token metadata payload)
- ResolveContext Protocol (the interface resolvers depend on)
- SyntaxError hierarchy (UnknownRefError, RefOutOfSurfaceError,
RecursionLimitExceeded, CycleDetectedError, MalformedSyntaxError)
"""
from __future__ import annotations
import enum
import random
from dataclasses import dataclass, field
from typing import (
Any,
Literal,
Protocol,
runtime_checkable,
)
SurfaceKind = Literal["wildcard", "combine", "derivation", "assembler"]
class TokenKind(enum.Enum):
"""All token kinds emitted by tokenize_text. Single grammar, no surface variants."""
TEXT = "text"
VAR = "var"
REF = "ref"
DP_BRACE = "dp_brace"
DP_PIPE = "dp_pipe"
DP_MULTI = "dp_multi"
ESCAPE = "escape"
@dataclass
class Token:
"""A single tokenizer output. `meta` is kind-specific:
- VAR: {"name": str}
- REF: {"uuid": str}
- DP_MULTI: {"count": int, "sep": str, "branches": list[str]}
- DP_BRACE: {"branches": list[str]}
- ESCAPE: {"literal": str} (one of "$" or "@")
- TEXT, DP_PIPE: {} (empty)
"""
kind: TokenKind
raw: str
start: int
end: int
meta: dict[str, Any] = field(default_factory=dict)
@runtime_checkable
class ResolveContext(Protocol):
"""Interface the resolver depends on. Construct via build_resolve_ctx in
engine/modules/__init__.py at pipeline start; in tests use a duck-typed
fake (any class with these attrs and methods).
"""
rng: random.Random
max_ref_depth: int
strict: bool
surface: SurfaceKind
developer_mode: bool
warnings: list[dict[str, Any]]
def get_var(self, name: str) -> str | None:
"""Return the bound value of `name`, or None if unbound."""
...
def get_module(self, uuid: str) -> dict[str, Any] | None:
"""Return the module catalog row for `uuid`, or None if missing.
Row shape (only fields the resolver uses): `{"type": str,
"var_binding": str, "options": list[{"value": str, "weight": float}]}`.
"""
...
# ---------- Error hierarchy ---------------------------------------------------
class SyntaxError(Exception):
"""Base for all engine syntax errors. Distinct from Python's built-in
SyntaxError (which is for source-code parse errors); this one is for
wildcard syntax."""
class UnknownRefError(SyntaxError):
"""@{uuid} points to a module not in the catalog."""
def __init__(self, uuid: str) -> None:
self.uuid = uuid
super().__init__(f"unknown wildcard ref: @{{{uuid}}}")
class RefOutOfSurfaceError(SyntaxError):
"""@{uuid} appears in a non-wildcard surface (combine/derivation/assembler)."""
def __init__(self, uuid: str, surface: str) -> None:
self.uuid = uuid
self.surface = surface
super().__init__(
f"ref @{{{uuid}}} not allowed in {surface!r} surface; "
f"only wildcard option values"
)
class RecursionLimitExceeded(SyntaxError):
"""@{uuid} chain reached configured max_ref_depth."""
class CycleDetectedError(SyntaxError):
"""@{uuid} chain returned to an already-visiting UUID."""
def __init__(self, chain: list[str]) -> None:
self.chain = chain
super().__init__(
"cycle: " + " → ".join(f"@{{{u}}}" for u in chain)
)
class MalformedSyntaxError(SyntaxError):
"""Tokenizer / resolver received structurally invalid token meta."""
+6
View File
@@ -0,0 +1,6 @@
"""Tests for engine.syntax — tokenizer + resolver.
Phase 1 coverage targets (initial impl): 80% lines / 75% branches.
Phase 2 ratchet: 90% lines / 85% branches by 2026-07-28 (3 months post-merge).
Update gates in `pyproject.toml` and CI workflow when ratcheting.
"""
+74
View File
@@ -0,0 +1,74 @@
"""Test the foundational types and error class hierarchy."""
from __future__ import annotations
from engine.syntax import types as st
def test_token_kind_values():
"""All expected token kinds present, no extras."""
expected = {"text", "var", "ref", "dp_brace", "dp_pipe", "dp_multi", "escape"}
actual = {k.value for k in st.TokenKind}
assert actual == expected
def test_token_dataclass_shape():
tok = st.Token(kind=st.TokenKind.TEXT, raw="hello", start=0, end=5, meta={})
assert tok.kind == st.TokenKind.TEXT
assert tok.raw == "hello"
assert tok.start == 0
assert tok.end == 5
assert tok.meta == {}
def test_unknown_ref_error_carries_uuid():
err = st.UnknownRefError("a4f7b2e1")
assert err.uuid == "a4f7b2e1"
assert "a4f7b2e1" in str(err)
def test_ref_out_of_surface_error_carries_uuid_and_surface():
err = st.RefOutOfSurfaceError("a4f7b2e1", "combine")
assert err.uuid == "a4f7b2e1"
assert err.surface == "combine"
assert "combine" in str(err)
def test_cycle_detected_error_carries_chain():
err = st.CycleDetectedError(["a", "b", "a"])
assert err.chain == ["a", "b", "a"]
assert "@{a}" in str(err)
assert "@{b}" in str(err)
def test_recursion_limit_exceeded_is_distinct_class():
"""Distinct from CycleDetectedError so handlers can distinguish."""
assert issubclass(st.RecursionLimitExceeded, st.SyntaxError)
assert not issubclass(st.RecursionLimitExceeded, st.CycleDetectedError)
def test_malformed_syntax_error_is_distinct_class():
assert issubclass(st.MalformedSyntaxError, st.SyntaxError)
def test_resolve_context_protocol_runtime_check():
"""ResolveContext is a Protocol — duck-typeable."""
import random
class FakeCtx:
rng = random.Random(42)
max_ref_depth = 8
strict = False
surface = "wildcard"
developer_mode = False
warnings: list = []
def get_var(self, name: str) -> str | None:
return None
def get_module(self, uuid: str) -> dict | None:
return None
# Should accept any object with the right attribute / method shape.
ctx: st.ResolveContext = FakeCtx()
assert ctx.surface == "wildcard"
assert ctx.get_var("x") is None