Skip to content

Commit a0cc45e

Browse files
committed
feat(math): add context-free language operations (#1894)
Add context_free_languages_ops domain with 3 operations: nullable symbol profiles, dependency graph, and FIRST sets. Uses fixed-point iteration over bounded context-free grammars. Closes #1894
1 parent 2e54684 commit a0cc45e

10 files changed

Lines changed: 413 additions & 3 deletions

File tree

src/jacobian/catalog/builtins.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@
4444
ADMISSIONS as BOOLEAN_ANALYSIS_ADMISSIONS,
4545
)
4646
from jacobian.math.boolean_analysis._tools import TOOLS as BOOLEAN_ANALYSIS_TOOLS
47+
from jacobian.math.context_free_languages_ops._admission import (
48+
ADMISSIONS as CONTEXT_FREE_LANGUAGES_OPS_ADMISSIONS,
49+
)
50+
from jacobian.math.context_free_languages_ops._tools import (
51+
TOOLS as CONTEXT_FREE_LANGUAGES_OPS_TOOLS,
52+
)
4753
from jacobian.math.code_theory._admission import ADMISSIONS as CODE_THEORY_ADMISSIONS
4854
from jacobian.math.code_theory._tools import TOOLS as CODE_THEORY_TOOLS
4955
from jacobian.math.combinatorics._admission import (
@@ -284,6 +290,7 @@
284290
*ROOT_ISOLATION_TOOLS,
285291
*RECURRENCE_SOLVING_TOOLS,
286292
*CODE_THEORY_TOOLS,
293+
*CONTEXT_FREE_LANGUAGES_OPS_TOOLS,
287294
*NUMBER_FIELD_TOOLS,
288295
*MARKOV_CHAIN_TOOLS,
289296
*ARITHMETIC_TOOLS,
@@ -359,6 +366,7 @@
359366
*BOOLEAN_ADMISSIONS,
360367
*BOOLEAN_ANALYSIS_ADMISSIONS,
361368
*CODE_THEORY_ADMISSIONS,
369+
*CONTEXT_FREE_LANGUAGES_OPS_ADMISSIONS,
362370
*COMBINATORICS_ADMISSIONS,
363371
*CONVEX_ANALYSIS_ADMISSIONS,
364372
*DIOPHANTINE_APPROXIMATION_ADMISSIONS,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Context-free language operations."""
2+
3+
__all__: list[str] = []
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""Owner-local admission decisions for built-in math operations."""
2+
3+
from __future__ import annotations
4+
5+
from jacobian.catalog.admission import AdmissionDecision, OperationAdmission
6+
7+
ADMISSIONS: tuple[OperationAdmission, ...] = (
8+
OperationAdmission(
9+
"grammar.symbol_profiles.compute",
10+
AdmissionDecision.KEEP,
11+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
12+
),
13+
OperationAdmission(
14+
"grammar.dependency_graph.compute",
15+
AdmissionDecision.KEEP,
16+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
17+
),
18+
OperationAdmission(
19+
"grammar.first_sets.compute",
20+
AdmissionDecision.KEEP,
21+
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
22+
),
23+
)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""Typed wire contracts for context-free language operations."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Self
6+
7+
from pydantic import Field, model_validator
8+
9+
from jacobian._models import StrictModel
10+
11+
MAX_NONTERMINALS = 32
12+
MAX_RULES = 256
13+
14+
15+
class GrammarRule(StrictModel):
16+
"""One production rule A -> alpha."""
17+
18+
head: str = Field(min_length=1, max_length=64)
19+
body: tuple[str, ...] = Field(min_length=0, max_length=32)
20+
21+
22+
class FiniteCFGO(StrictModel):
23+
"""A finite context-free grammar."""
24+
25+
nonterminals: tuple[str, ...] = Field(min_length=1, max_length=MAX_NONTERMINALS)
26+
terminals: tuple[str, ...] = Field(min_length=0, max_length=MAX_NONTERMINALS)
27+
rules: tuple[GrammarRule, ...] = Field(min_length=1, max_length=MAX_RULES)
28+
start_symbol: str = Field(min_length=1, max_length=64)
29+
30+
@model_validator(mode="after")
31+
def require_valid(self) -> Self:
32+
if self.start_symbol not in self.nonterminals:
33+
raise ValueError("start_symbol must be a nonterminal")
34+
for rule in self.rules:
35+
if rule.head not in self.nonterminals:
36+
raise ValueError("rule heads must be nonterminals")
37+
return self
38+
39+
40+
class SymbolProfilesRequest(StrictModel):
41+
grammar: FiniteCFGO
42+
43+
44+
class DependencyGraphRequest(StrictModel):
45+
grammar: FiniteCFGO
46+
47+
48+
class FirstSetsRequest(StrictModel):
49+
grammar: FiniteCFGO
50+
51+
52+
# Results
53+
54+
55+
class SymbolProfilesResult(StrictModel):
56+
nullable: tuple[bool, ...]
57+
method: str = "FIXED_POINT_ITERATION"
58+
59+
60+
class DependencyGraphResult(StrictModel):
61+
edges: tuple[tuple[str, str], ...]
62+
method: str = "RULE_BODY_DEPENDENCY"
63+
64+
65+
class FirstSetsResult(StrictModel):
66+
first_sets: tuple[tuple[str, ...], ...]
67+
method: str = "FIXED_POINT_ITERATION"
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""Domain functions for context-free language operations."""
2+
3+
from __future__ import annotations
4+
5+
from jacobian.math.context_free_languages_ops._models import (
6+
DependencyGraphRequest,
7+
DependencyGraphResult,
8+
FirstSetsRequest,
9+
FirstSetsResult,
10+
SymbolProfilesRequest,
11+
SymbolProfilesResult,
12+
)
13+
14+
15+
def compute_symbol_profiles(request: SymbolProfilesRequest) -> SymbolProfilesResult:
16+
"""Compute nullable nonterminals via fixed-point iteration."""
17+
grammar = request.grammar
18+
nullable = dict.fromkeys(grammar.nonterminals, False)
19+
terminal_set = set(grammar.terminals)
20+
changed = True
21+
while changed:
22+
changed = False
23+
for rule in grammar.rules:
24+
if nullable[rule.head]:
25+
continue
26+
all_nullable = True
27+
for symbol in rule.body:
28+
if symbol in terminal_set:
29+
all_nullable = False
30+
break
31+
if symbol in nullable and not nullable[symbol]:
32+
all_nullable = False
33+
break
34+
if all_nullable:
35+
nullable[rule.head] = True
36+
changed = True
37+
return SymbolProfilesResult(
38+
nullable=tuple(nullable[nt] for nt in grammar.nonterminals)
39+
)
40+
41+
42+
def compute_dependency_graph(request: DependencyGraphRequest) -> DependencyGraphResult:
43+
"""Compute the dependency graph: A -> B if A has a rule containing B."""
44+
grammar = request.grammar
45+
edges: set[tuple[str, str]] = set()
46+
for rule in grammar.rules:
47+
for symbol in rule.body:
48+
if symbol in grammar.nonterminals:
49+
edges.add((rule.head, symbol))
50+
return DependencyGraphResult(edges=tuple(sorted(edges)))
51+
52+
53+
def compute_first_sets(request: FirstSetsRequest) -> FirstSetsResult:
54+
"""Compute FIRST sets via fixed-point iteration."""
55+
grammar = request.grammar
56+
terminals = set(grammar.terminals)
57+
nonterminals = set(grammar.nonterminals)
58+
first: dict[str, set[str]] = {nt: set() for nt in grammar.nonterminals}
59+
for _ in range(256):
60+
changed = False
61+
for rule in grammar.rules:
62+
head = rule.head
63+
for symbol in rule.body:
64+
if symbol in terminals:
65+
if symbol not in first[head]:
66+
first[head].add(symbol)
67+
changed = True
68+
break
69+
elif symbol in nonterminals:
70+
new = first[symbol] - first[head]
71+
if new:
72+
first[head] |= new
73+
changed = True
74+
if symbol not in first and not all(
75+
first.get(s) for s in rule.body if s in nonterminals
76+
):
77+
pass
78+
break
79+
else:
80+
break
81+
if not changed:
82+
break
83+
return FirstSetsResult(
84+
first_sets=tuple(
85+
tuple(sorted(first[nt])) for nt in grammar.nonterminals
86+
)
87+
)
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"""Context-free language operation declarations."""
2+
3+
from collections.abc import Callable
4+
from typing import Any
5+
6+
from jacobian._models import StrictModel
7+
from jacobian.catalog._examples import example
8+
from jacobian.catalog.models import MathTool, OperationExample
9+
from jacobian.math.context_free_languages_ops._models import (
10+
DependencyGraphRequest,
11+
DependencyGraphResult,
12+
FirstSetsRequest,
13+
FirstSetsResult,
14+
SymbolProfilesRequest,
15+
SymbolProfilesResult,
16+
)
17+
from jacobian.math.context_free_languages_ops._operations import (
18+
compute_dependency_graph,
19+
compute_first_sets,
20+
compute_symbol_profiles,
21+
)
22+
23+
24+
def _op[RequestT: StrictModel, ResultT: StrictModel](
25+
operation_id: str,
26+
title: str,
27+
description: str,
28+
request_model: type[RequestT],
29+
result_model: type[ResultT],
30+
operation: Callable[[RequestT], ResultT],
31+
*tags: str,
32+
examples: tuple[OperationExample, ...] = (),
33+
version: str = "1",
34+
) -> MathTool[RequestT, ResultT]:
35+
return MathTool(
36+
operation_id=operation_id,
37+
version=version,
38+
title=title,
39+
description=description,
40+
request_type=request_model,
41+
result_type=result_model,
42+
run=operation,
43+
tags=tags,
44+
examples=examples,
45+
)
46+
47+
48+
TOOLS: tuple[MathTool[Any, Any], ...] = (
49+
_op(
50+
"grammar.symbol_profiles.compute",
51+
"Compute nullable nonterminals of a CFG",
52+
"Compute which nonterminals are nullable (can derive epsilon) via "
53+
"fixed-point iteration.",
54+
SymbolProfilesRequest,
55+
SymbolProfilesResult,
56+
compute_symbol_profiles,
57+
"grammar",
58+
"nullable",
59+
"exact",
60+
examples=(
61+
example(
62+
"simple_grammar",
63+
"Compute nullable symbols in S -> aS | epsilon.",
64+
{
65+
"grammar": {
66+
"nonterminals": ["S"],
67+
"terminals": ["a"],
68+
"rules": [
69+
{"head": "S", "body": ["a", "S"]},
70+
{"head": "S", "body": []},
71+
],
72+
"start_symbol": "S",
73+
},
74+
},
75+
),
76+
),
77+
),
78+
_op(
79+
"grammar.dependency_graph.compute",
80+
"Compute the dependency graph of a CFG",
81+
"Compute the dependency graph: A depends on B if A has a rule "
82+
"containing B in its body.",
83+
DependencyGraphRequest,
84+
DependencyGraphResult,
85+
compute_dependency_graph,
86+
"grammar",
87+
"dependency-graph",
88+
"exact",
89+
examples=(
90+
example(
91+
"simple_grammar",
92+
"Dependency graph of S -> aS | epsilon.",
93+
{
94+
"grammar": {
95+
"nonterminals": ["S"],
96+
"terminals": ["a"],
97+
"rules": [
98+
{"head": "S", "body": ["a", "S"]},
99+
{"head": "S", "body": []},
100+
],
101+
"start_symbol": "S",
102+
},
103+
},
104+
),
105+
),
106+
),
107+
_op(
108+
"grammar.first_sets.compute",
109+
"Compute FIRST sets of a CFG",
110+
"Compute the FIRST set for each nonterminal via fixed-point "
111+
"iteration.",
112+
FirstSetsRequest,
113+
FirstSetsResult,
114+
compute_first_sets,
115+
"grammar",
116+
"first-sets",
117+
"exact",
118+
examples=(
119+
example(
120+
"simple_grammar",
121+
"FIRST sets of S -> aS | epsilon.",
122+
{
123+
"grammar": {
124+
"nonterminals": ["S"],
125+
"terminals": ["a"],
126+
"rules": [
127+
{"head": "S", "body": ["a", "S"]},
128+
{"head": "S", "body": []},
129+
],
130+
"start_symbol": "S",
131+
},
132+
},
133+
),
134+
),
135+
),
136+
)
137+
138+
__all__ = ["TOOLS"]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"catalog_version": "1",
3+
"domain": "context_free_languages_ops",
4+
"operations": {
5+
"grammar.dependency_graph.compute": {
6+
"input_schema": "sha256:9272b38bc135a841533266cbb0c7a8ef896a8c2be9873fadd25a4d38cdfaaf62",
7+
"output_schema": "sha256:50318dc606d6a1bbaf8cdb6447e3239fd737e40743fba9b7a291d99f165b5d98"
8+
},
9+
"grammar.first_sets.compute": {
10+
"input_schema": "sha256:0946f7c5fb3d5490468dd8333b6ad8a05f48c4ecbc3d20b7a8efd0b7a1bd1ff4",
11+
"output_schema": "sha256:6f42759884ed6d54e6736c09e11d7af445cc8a83c37da26faf0b74f1856ff9a4"
12+
},
13+
"grammar.symbol_profiles.compute": {
14+
"input_schema": "sha256:1f355fbe3f661b15556c5ca8c8bfc5f5f6724a00277a880d67b4c3545e18b3c4",
15+
"output_schema": "sha256:7f2e398b5add6b2d1dba269f7b7f6e22316b33c0b5656f8848492c3d9ebae5cd"
16+
}
17+
}
18+
}

tests/catalog/test_admission.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ def test_every_frozen_candidate_has_exactly_one_admission_decision() -> None:
2727
reviewed_ids = [record.operation_id for record in OPERATION_ADMISSIONS]
2828

2929
assert REVIEWED_BASE_REVISION == "61589543bbbff546edbc51d34a07887982fa4ad6"
30-
assert len(candidate_ids) == len(set(candidate_ids)) == 399
30+
assert len(candidate_ids) == len(set(candidate_ids)) == 402
3131
assert reviewed_ids == sorted(reviewed_ids)
3232
assert set(reviewed_ids) == set(candidate_ids)
3333
assert all(record.rationale.strip() for record in OPERATION_ADMISSIONS)
3434
assert Counter(record.decision for record in OPERATION_ADMISSIONS) == {
35-
AdmissionDecision.KEEP: 239,
35+
AdmissionDecision.KEEP: 242,
3636
AdmissionDecision.NATIVE_ONLY: 56,
3737
AdmissionDecision.DROP: 104,
3838
}
@@ -46,7 +46,7 @@ def test_public_catalog_contains_only_admitted_atomic_operations() -> None:
4646
}
4747

4848
assert {tool.operation_id for tool in BUILTIN_TOOLS} == expected
49-
assert len(BUILTIN_TOOLS) == 239
49+
assert len(BUILTIN_TOOLS) == 242
5050

5151

5252
def test_catalog_construction_fails_closed_on_duplicate_candidates() -> None:

tests/math/context_free_languages_ops/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)