Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions interscript-py/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,13 @@ License: BSD-3-Clause.
```

This package **executes the compiled JSON-IR maps** — the same artifact
the TypeScript runtime consumes. It does NOT parse `.isc` sources; the
compiler is the maps repo's CI (single source of truth, shared by every
runtime, which is what makes cross-runtime byte-identity checkable).
the TypeScript runtime consumes, which is what makes cross-runtime
byte-identity checkable. It also ships `interscript.isc`, a
recursive-descent parser for the `.isc` DSL (a port of the TypeScript
`src/isc/parser.ts`) that implements the ISC-WF-1 workflow extension:
named `ruleset`s and `use`/`override` expand at parse time, while
`languages`/`when` guards flow into the IR and resolve at run time
(`transliterate(map, text, language="kur")`).

### Map execution (now)

Expand Down Expand Up @@ -149,6 +153,8 @@ make lint # ruff if available
src/interscript/
__init__.py public API: add_load_path, map_list, transliterate, Runtime
_ir_types.py typed mirror of the JSON-IR wire format (strict decode)
isc.py .isc DSL parser (port of ts src/isc/parser.ts) + WF-1
parse-time ruleset/use/override expansion + IR lowering
loader.py artifact dirs, SHA256SUMS verify, tarball extraction
context.py execution context: alias + dependency resolution
items.py Item -> regex/literal compilation (port of compile-item.ts)
Expand Down
40 changes: 35 additions & 5 deletions interscript-py/src/interscript/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@
from .errors import (
ArtifactIntegrityError,
AsyncFunctionError,
IscParseError,
MapFormatError,
MapLogicError,
MapNotFoundError,
)
from .isc import IscDocument, isc_to_compiled_map, parse_isc
from .loader import (
MapLoader,
default_load_paths,
Expand Down Expand Up @@ -72,11 +74,26 @@ def map_exist(self, system_code: str) -> bool:
def load_map(self, system_code: str):
return self.loader.load(system_code)

def transliterate(self, system_code: str, text: str, stage: str | None = None) -> str:
def transliterate(
self,
system_code: str,
text: str,
stage: str | None = None,
language: str | None = None,
) -> str:
"""Apply a transliteration system to `text` (default stage: "main").

`language` is the ISC-WF-1 W3 caller selection: when it names one
of the map's declared `languages`, `when`-guarded rules for it
apply (and beat unguarded same-from rules); any other value makes
every guard inert (§5.1).
"""
from .executor import execute_stage

map_ = self.loader.load(system_code)
return execute_stage(map_, stage or self.default_stage, text, self.loader)
return execute_stage(
map_, stage or self.default_stage, text, self.loader, language
)


def _default_runtime() -> Runtime:
Expand Down Expand Up @@ -109,15 +126,26 @@ def load_map(system_code: str):
return _default_runtime().load_map(system_code)


def transliterate(system_code: str, text: str, stage: str | None = None) -> str:
"""Apply a transliteration system to `text` (default stage: "main")."""
return _default_runtime().transliterate(system_code, text, stage)
def transliterate(
system_code: str,
text: str,
stage: str | None = None,
language: str | None = None,
) -> str:
"""Apply a transliteration system to `text` (default stage: "main").

`language` selects one of the map's declared languages (ISC-WF-1 W3,
`--language kur` in the CLI contract); guards are inert otherwise.
"""
return _default_runtime().transliterate(system_code, text, stage, language)


__all__ = [
"DEFAULT_STAGE",
"ArtifactIntegrityError",
"AsyncFunctionError",
"IscDocument",
"IscParseError",
"MapFormatError",
"MapLoader",
"MapLogicError",
Expand All @@ -126,10 +154,12 @@ def transliterate(system_code: str, text: str, stage: str | None = None) -> str:
"add_load_path",
"default_load_paths",
"extract_release_tarball",
"isc_to_compiled_map",
"load_manifest",
"load_map",
"map_exist",
"map_list",
"parse_isc",
"transliterate",
"verify_artifact",
]
6 changes: 6 additions & 0 deletions interscript-py/src/interscript/_ir_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
systemCode: str
dependencies: [str]
metadata?: {str: ...} # omitted when empty
languages?: [str] # ISC-WF-1 W3; omitted when undeclared
stages: [Stage]
aliases: {name: Item}
functions: {name: {...}} # always {} today
}
Stage = {kind: "stage", name: str, rules: [Rule]}
Rule = SubRule | RunRule | FuncallRule | ParallelRule | SequentialRule
SubRule carries optional `when: [str]` (ISC-WF-1 W3 guard).
Item = StringItem | CaptureGroupItem | CaptureRefItem | AliasItem
| AnyItem | AnyCharClassItem | GroupItem | RepeatItem | StageItem
"""
Expand Down Expand Up @@ -131,6 +133,7 @@ class SubRule:
not_before: Item | None = None
not_after: Item | None = None
priority: int | None = None
when: tuple[str, ...] | None = None # ISC-WF-1 W3 guard languages
kind: str = "sub"


Expand Down Expand Up @@ -189,6 +192,7 @@ class CompiledMap:
aliases: dict[str, Item]
functions: dict[str, FunctionDef]
metadata: dict[str, Any] | None = None
languages: tuple[str, ...] | None = None # ISC-WF-1 W3 declared languages

def stage(self, name: str) -> Stage | None:
for s in self.stages:
Expand Down Expand Up @@ -254,6 +258,7 @@ def rule_from_json(obj: dict[str, Any]) -> Rule:
not_before=item_from_json(obj["notBefore"]) if obj.get("notBefore") else None,
not_after=item_from_json(obj["notAfter"]) if obj.get("notAfter") else None,
priority=int(obj["priority"]) if obj.get("priority") is not None else None,
when=tuple(str(w) for w in obj["when"]) if obj.get("when") else None,
)
if kind == "run":
return RunRule(stage=obj["stage"], doc_name=obj.get("docName"))
Expand Down Expand Up @@ -287,4 +292,5 @@ def map_from_json(obj: dict[str, Any]) -> CompiledMap:
for name, fd in obj.get("functions", {}).items()
},
metadata=dict(obj["metadata"]) if obj.get("metadata") else None,
languages=tuple(str(x) for x in obj["languages"]) if obj.get("languages") else None,
)
22 changes: 19 additions & 3 deletions interscript-py/src/interscript/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,35 @@ def load(self, system_code: str) -> CompiledMap: ...


class ExecutionContext:
"""State carried through one stage execution."""
"""State carried through one stage execution.

`language` is the ISC-WF-1 W3 caller-selected language (§5.1); it
flows across `run` boundaries and is validated against each map's
own `languages` declaration by `effective_language`.
"""

def __init__(
self,
map_: CompiledMap,
current: str,
loader: MapResolver | None = None,
language: str | None = None,
) -> None:
self.map = map_
self.current = current
self.loader = loader
self.language = language
self._alias_cache: dict[str, Item] = {}

@property
def effective_language(self) -> str | None:
"""The selected language when it is declared by the active map,
else None. Per ISC-WF-1 §5.1, selecting a language the map does
not declare makes every `when` guard INERT (as if unset)."""
if self.language is None or not self.map.languages:
return None
return self.language if self.language in self.map.languages else None

def resolve_alias(self, name: str) -> Item | None:
"""Resolve an alias against the current map, then (one level)
against the map's declared dependencies."""
Expand All @@ -56,8 +72,8 @@ def resolve_alias(self, name: str) -> Item | None:

def with_map(self, map_: CompiledMap) -> ExecutionContext:
"""Spawn a context for a different map (run rule with docName);
same loader, fresh alias cache."""
return ExecutionContext(map_, self.current, self.loader)
same loader and language selection, fresh alias cache."""
return ExecutionContext(map_, self.current, self.loader, self.language)

def load_dependency(self, system_code: str) -> CompiledMap:
if self.loader is None:
Expand Down
15 changes: 15 additions & 0 deletions interscript-py/src/interscript/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@ class MapFormatError(ValueError):
"""A compiled JSON-IR map file is not a valid `interscript.maps.ir.v1` document."""


class IscParseError(MapFormatError):
"""An .isc source could not be parsed (bad token, unclosed block,
malformed rule, `use` cycle...).

Carries the byte `position` plus 1-based `line`/`col` of the failure,
mirroring the TypeScript `IscParseError`.
"""

def __init__(self, message: str, position: int, line: int, col: int) -> None:
self.position = position
self.line = line
self.col = col
super().__init__(f"ISC parse error at line {line}:{col}: {message}")


class ArtifactIntegrityError(ValueError):
"""The compiled artifact failed its SHA256SUMS verification."""

Expand Down
65 changes: 60 additions & 5 deletions interscript-py/src/interscript/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,17 @@ def execute_stage(
stage_name: str,
text: str,
loader: MapResolver | None = None,
language: str | None = None,
) -> str:
"""Run one stage by name; returns the transformed string.

A missing stage is a no-op (returns the input unchanged), matching
the TS runtime.
the TS runtime. `language` is the ISC-WF-1 W3 caller selection (§5.1).
"""
stage = map_.stage(stage_name)
if stage is None:
return text
ctx = ExecutionContext(map_, text, loader)
ctx = ExecutionContext(map_, text, loader, language)
execute_stage_rules(stage, ctx)
return ctx.current

Expand All @@ -100,6 +101,8 @@ def execute_stage_rules(stage: Stage, ctx: ExecutionContext) -> None:
def execute_rule(rule: Rule, ctx: ExecutionContext) -> None:
kind = rule.kind
if kind == "sub":
if not _guard_active(rule, ctx): # type: ignore[arg-type]
return
_execute_sub_rule(rule, ctx) # type: ignore[arg-type]
elif kind == "run":
_execute_run_rule(rule, ctx) # type: ignore[arg-type]
Expand All @@ -114,6 +117,40 @@ def execute_rule(rule: Rule, ctx: ExecutionContext) -> None:
raise MapLogicError(f"unknown rule kind: {kind!r}")


# ---------------------------------------------------------------------------
# ISC-WF-1 W3 language guards
# ---------------------------------------------------------------------------


def _guard_active(rule: SubRule, ctx: ExecutionContext) -> bool:
"""A `when`-guarded sub rule applies only when the caller selected a
language the map declares AND the rule's `when` list contains it
(§5.1); unguarded rules always apply. Inert guards skip the rule
entirely, in every context (bare, sequence, parallel)."""
if rule.when is None:
return True
selected = ctx.effective_language
return selected is not None and selected in rule.when


def _drop_shadowed_unguarded(rules: list[SubRule]) -> list[SubRule]:
"""§5.2 precedence: when a guard-applicable rule and an unguarded
rule share the same from-string, the guard-applicable rule wins —
the unguarded one is dropped so no first-alternative/insertion-order
tie can resurrect the generic value. Structural equality of the
`from` items is the same-from test (byte-exact for string rows)."""
guarded_froms = {
r.from_ for r in rules if r.when is not None and r.from_ is not None
}
if not guarded_froms:
return rules
return [
r
for r in rules
if r.when is not None or r.from_ is None or r.from_ not in guarded_froms
]


# ---------------------------------------------------------------------------
# sub
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -227,16 +264,20 @@ def _execute_parallel(rule: ParallelRule, ctx: ExecutionContext) -> None:
trailing: list[Rule] = []
for inner in rule.rules:
if inner.kind == "sub" and inner.from_ is not None: # type: ignore[union-attr]
sub_rules.append(inner) # type: ignore[arg-type]
# Inert W3 guards (§5.1) leave the rule out of the pass; a
# guard-applicable rule beats an unguarded same-from rule (§5.2).
if _guard_active(inner, ctx): # type: ignore[arg-type]
sub_rules.append(inner) # type: ignore[arg-type]
else:
trailing.append(inner)

sub_rules = _drop_shadowed_unguarded(sub_rules)
any_constrained = any(not _tree_compatible(r, ctx) for r in sub_rules)

if not any_constrained:
# Tree mode: every `from` expands to literals; rules whose `to`
# is an inline funcall or non-literal are skipped (mirrors TS).
trie_pairs: list[tuple[str, str]] = []
flagged: list[tuple[str, str, bool]] = [] # (from_lit, to_lit, guarded)
for r in sub_rules:
if r.to is not None and r.to.kind == "funcall_inline":
continue
Expand All @@ -247,7 +288,13 @@ def _execute_parallel(rule: ParallelRule, ctx: ExecutionContext) -> None:
if from_alts is None:
continue
for from_lit in from_alts:
trie_pairs.append((from_lit, to_lit))
flagged.append((from_lit, to_lit, r.when is not None))
# §5.2 at string level: an expanded literal shared by a
# guard-applicable rule and an unguarded rule keeps the guard.
guarded_literals = {f for f, _t, g in flagged if g}
trie_pairs = [
(f, t) for f, t, g in flagged if g or f not in guarded_literals
]
if trie_pairs:
ctx.current = parallel_replace_tree(
ctx.current, compile_parallel_tree(trie_pairs)
Expand All @@ -268,12 +315,20 @@ def sort_key(pair: tuple[int, SubRule]):
return (-length, idx)

mega_rules: list[MegaregexpRule] = []
compiled: list[tuple[SubRule, str, str, str, str, str]] = []
for _idx, r in sorted(enumerate(sub_rules), key=sort_key):
src = compile_item(r.from_, ctx).re # type: ignore[arg-type]
before = compile_item(r.before, ctx).re if r.before else ""
after = compile_item(r.after, ctx).re if r.after else ""
not_before = compile_item(r.not_before, ctx).re if r.not_before else ""
not_after = compile_item(r.not_after, ctx).re if r.not_after else ""
compiled.append((r, src, before, after, not_before, not_after))
# §5.2 at pattern level: identical compiled `from` source under a
# guard-applicable rule shadows the unguarded one.
guarded_srcs = {c[1] for c in compiled if c[0].when is not None}
for r, src, before, after, not_before, not_after in compiled:
if r.when is None and src in guarded_srcs:
continue
pattern = (
(f"(?<={before})" if before else "")
+ (f"(?<!{not_before})" if not_before else "")
Expand Down
Loading
Loading