Skip to content
Open
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
8 changes: 8 additions & 0 deletions src/jacobian/catalog/builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@
ADMISSIONS as BOOLEAN_ANALYSIS_ADMISSIONS,
)
from jacobian.math.boolean_analysis._tools import TOOLS as BOOLEAN_ANALYSIS_TOOLS
from jacobian.math.context_free_languages_ops._admission import (
ADMISSIONS as CONTEXT_FREE_LANGUAGES_OPS_ADMISSIONS,
)
from jacobian.math.context_free_languages_ops._tools import (
TOOLS as CONTEXT_FREE_LANGUAGES_OPS_TOOLS,
)
from jacobian.math.code_theory._admission import ADMISSIONS as CODE_THEORY_ADMISSIONS
from jacobian.math.code_theory._tools import TOOLS as CODE_THEORY_TOOLS
from jacobian.math.combinatorics._admission import (
Expand Down Expand Up @@ -284,6 +290,7 @@
*ROOT_ISOLATION_TOOLS,
*RECURRENCE_SOLVING_TOOLS,
*CODE_THEORY_TOOLS,
*CONTEXT_FREE_LANGUAGES_OPS_TOOLS,
*NUMBER_FIELD_TOOLS,
*MARKOV_CHAIN_TOOLS,
*ARITHMETIC_TOOLS,
Expand Down Expand Up @@ -359,6 +366,7 @@
*BOOLEAN_ADMISSIONS,
*BOOLEAN_ANALYSIS_ADMISSIONS,
*CODE_THEORY_ADMISSIONS,
*CONTEXT_FREE_LANGUAGES_OPS_ADMISSIONS,
*COMBINATORICS_ADMISSIONS,
*CONVEX_ANALYSIS_ADMISSIONS,
*DIOPHANTINE_APPROXIMATION_ADMISSIONS,
Expand Down
3 changes: 3 additions & 0 deletions src/jacobian/math/context_free_languages_ops/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Context-free language operations."""

__all__: list[str] = []
23 changes: 23 additions & 0 deletions src/jacobian/math/context_free_languages_ops/_admission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Owner-local admission decisions for built-in math operations."""

from __future__ import annotations

from jacobian.catalog.admission import AdmissionDecision, OperationAdmission

ADMISSIONS: tuple[OperationAdmission, ...] = (
OperationAdmission(
"grammar.symbol_profiles.compute",
AdmissionDecision.KEEP,
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
),
OperationAdmission(
"grammar.dependency_graph.compute",
AdmissionDecision.KEEP,
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
),
OperationAdmission(
"grammar.first_sets.compute",
AdmissionDecision.KEEP,
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
),
)
67 changes: 67 additions & 0 deletions src/jacobian/math/context_free_languages_ops/_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Typed wire contracts for context-free language operations."""

from __future__ import annotations

from typing import Self

from pydantic import Field, model_validator

from jacobian._models import StrictModel

MAX_NONTERMINALS = 32
MAX_RULES = 256


class GrammarRule(StrictModel):
"""One production rule A -> alpha."""

head: str = Field(min_length=1, max_length=64)
body: tuple[str, ...] = Field(min_length=0, max_length=32)


class FiniteCFGO(StrictModel):
"""A finite context-free grammar."""

nonterminals: tuple[str, ...] = Field(min_length=1, max_length=MAX_NONTERMINALS)
terminals: tuple[str, ...] = Field(min_length=0, max_length=MAX_NONTERMINALS)
rules: tuple[GrammarRule, ...] = Field(min_length=1, max_length=MAX_RULES)
start_symbol: str = Field(min_length=1, max_length=64)

@model_validator(mode="after")
def require_valid(self) -> Self:
if self.start_symbol not in self.nonterminals:
raise ValueError("start_symbol must be a nonterminal")
for rule in self.rules:
if rule.head not in self.nonterminals:
raise ValueError("rule heads must be nonterminals")
return self


class SymbolProfilesRequest(StrictModel):
grammar: FiniteCFGO


class DependencyGraphRequest(StrictModel):
grammar: FiniteCFGO


class FirstSetsRequest(StrictModel):
grammar: FiniteCFGO


# Results


class SymbolProfilesResult(StrictModel):
nullable: tuple[bool, ...]
method: str = "FIXED_POINT_ITERATION"


class DependencyGraphResult(StrictModel):
edges: tuple[tuple[str, str], ...]
method: str = "RULE_BODY_DEPENDENCY"


class FirstSetsResult(StrictModel):
first_sets: tuple[tuple[str, ...], ...]
method: str = "FIXED_POINT_ITERATION"
87 changes: 87 additions & 0 deletions src/jacobian/math/context_free_languages_ops/_operations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Domain functions for context-free language operations."""

from __future__ import annotations

from jacobian.math.context_free_languages_ops._models import (
DependencyGraphRequest,
DependencyGraphResult,
FirstSetsRequest,
FirstSetsResult,
SymbolProfilesRequest,
SymbolProfilesResult,
)


def compute_symbol_profiles(request: SymbolProfilesRequest) -> SymbolProfilesResult:
"""Compute nullable nonterminals via fixed-point iteration."""
grammar = request.grammar
nullable = dict.fromkeys(grammar.nonterminals, False)
terminal_set = set(grammar.terminals)
changed = True
while changed:
changed = False
for rule in grammar.rules:
if nullable[rule.head]:
continue
all_nullable = True
for symbol in rule.body:
if symbol in terminal_set:
all_nullable = False
break
if symbol in nullable and not nullable[symbol]:
all_nullable = False
break
if all_nullable:
nullable[rule.head] = True
changed = True
return SymbolProfilesResult(
nullable=tuple(nullable[nt] for nt in grammar.nonterminals)
)


def compute_dependency_graph(request: DependencyGraphRequest) -> DependencyGraphResult:
"""Compute the dependency graph: A -> B if A has a rule containing B."""
grammar = request.grammar
edges: set[tuple[str, str]] = set()
for rule in grammar.rules:
for symbol in rule.body:
if symbol in grammar.nonterminals:
edges.add((rule.head, symbol))
return DependencyGraphResult(edges=tuple(sorted(edges)))


def compute_first_sets(request: FirstSetsRequest) -> FirstSetsResult:
"""Compute FIRST sets via fixed-point iteration."""
grammar = request.grammar
terminals = set(grammar.terminals)
nonterminals = set(grammar.nonterminals)
first: dict[str, set[str]] = {nt: set() for nt in grammar.nonterminals}
for _ in range(256):
changed = False
for rule in grammar.rules:
head = rule.head
for symbol in rule.body:
if symbol in terminals:
if symbol not in first[head]:
first[head].add(symbol)
changed = True
break
elif symbol in nonterminals:
new = first[symbol] - first[head]
if new:
first[head] |= new
changed = True
if symbol not in first and not all(
first.get(s) for s in rule.body if s in nonterminals
):
pass
break
else:
break
if not changed:
break
return FirstSetsResult(
first_sets=tuple(
tuple(sorted(first[nt])) for nt in grammar.nonterminals
)
)
138 changes: 138 additions & 0 deletions src/jacobian/math/context_free_languages_ops/_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""Context-free language operation declarations."""

from collections.abc import Callable
from typing import Any

from jacobian._models import StrictModel
from jacobian.catalog._examples import example
from jacobian.catalog.models import MathTool, OperationExample
from jacobian.math.context_free_languages_ops._models import (
DependencyGraphRequest,
DependencyGraphResult,
FirstSetsRequest,
FirstSetsResult,
SymbolProfilesRequest,
SymbolProfilesResult,
)
from jacobian.math.context_free_languages_ops._operations import (
compute_dependency_graph,
compute_first_sets,
compute_symbol_profiles,
)


def _op[RequestT: StrictModel, ResultT: StrictModel](
operation_id: str,
title: str,
description: str,
request_model: type[RequestT],
result_model: type[ResultT],
operation: Callable[[RequestT], ResultT],
*tags: str,
examples: tuple[OperationExample, ...] = (),
version: str = "1",
) -> MathTool[RequestT, ResultT]:
return MathTool(
operation_id=operation_id,
version=version,
title=title,
description=description,
request_type=request_model,
result_type=result_model,
run=operation,
tags=tags,
examples=examples,
)


TOOLS: tuple[MathTool[Any, Any], ...] = (
_op(
"grammar.symbol_profiles.compute",
"Compute nullable nonterminals of a CFG",
"Compute which nonterminals are nullable (can derive epsilon) via "
"fixed-point iteration.",
SymbolProfilesRequest,
SymbolProfilesResult,
compute_symbol_profiles,
"grammar",
"nullable",
"exact",
examples=(
example(
"simple_grammar",
"Compute nullable symbols in S -> aS | epsilon.",
{
"grammar": {
"nonterminals": ["S"],
"terminals": ["a"],
"rules": [
{"head": "S", "body": ["a", "S"]},
{"head": "S", "body": []},
],
"start_symbol": "S",
},
},
),
),
),
_op(
"grammar.dependency_graph.compute",
"Compute the dependency graph of a CFG",
"Compute the dependency graph: A depends on B if A has a rule "
"containing B in its body.",
DependencyGraphRequest,
DependencyGraphResult,
compute_dependency_graph,
"grammar",
"dependency-graph",
"exact",
examples=(
example(
"simple_grammar",
"Dependency graph of S -> aS | epsilon.",
{
"grammar": {
"nonterminals": ["S"],
"terminals": ["a"],
"rules": [
{"head": "S", "body": ["a", "S"]},
{"head": "S", "body": []},
],
"start_symbol": "S",
},
},
),
),
),
_op(
"grammar.first_sets.compute",
"Compute FIRST sets of a CFG",
"Compute the FIRST set for each nonterminal via fixed-point "
"iteration.",
FirstSetsRequest,
FirstSetsResult,
compute_first_sets,
"grammar",
"first-sets",
"exact",
examples=(
example(
"simple_grammar",
"FIRST sets of S -> aS | epsilon.",
{
"grammar": {
"nonterminals": ["S"],
"terminals": ["a"],
"rules": [
{"head": "S", "body": ["a", "S"]},
{"head": "S", "body": []},
],
"start_symbol": "S",
},
},
),
),
),
)

__all__ = ["TOOLS"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"catalog_version": "1",
"domain": "context_free_languages_ops",
"operations": {
"grammar.dependency_graph.compute": {
"input_schema": "sha256:9272b38bc135a841533266cbb0c7a8ef896a8c2be9873fadd25a4d38cdfaaf62",
"output_schema": "sha256:50318dc606d6a1bbaf8cdb6447e3239fd737e40743fba9b7a291d99f165b5d98"
},
"grammar.first_sets.compute": {
"input_schema": "sha256:0946f7c5fb3d5490468dd8333b6ad8a05f48c4ecbc3d20b7a8efd0b7a1bd1ff4",
"output_schema": "sha256:6f42759884ed6d54e6736c09e11d7af445cc8a83c37da26faf0b74f1856ff9a4"
},
"grammar.symbol_profiles.compute": {
"input_schema": "sha256:1f355fbe3f661b15556c5ca8c8bfc5f5f6724a00277a880d67b4c3545e18b3c4",
"output_schema": "sha256:7f2e398b5add6b2d1dba269f7b7f6e22316b33c0b5656f8848492c3d9ebae5cd"
}
}
}
6 changes: 3 additions & 3 deletions tests/catalog/test_admission.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ def test_every_frozen_candidate_has_exactly_one_admission_decision() -> None:
reviewed_ids = [record.operation_id for record in OPERATION_ADMISSIONS]

assert REVIEWED_BASE_REVISION == "61589543bbbff546edbc51d34a07887982fa4ad6"
assert len(candidate_ids) == len(set(candidate_ids)) == 399
assert len(candidate_ids) == len(set(candidate_ids)) == 402
assert reviewed_ids == sorted(reviewed_ids)
assert set(reviewed_ids) == set(candidate_ids)
assert all(record.rationale.strip() for record in OPERATION_ADMISSIONS)
assert Counter(record.decision for record in OPERATION_ADMISSIONS) == {
AdmissionDecision.KEEP: 239,
AdmissionDecision.KEEP: 242,
AdmissionDecision.NATIVE_ONLY: 56,
AdmissionDecision.DROP: 104,
}
Expand All @@ -46,7 +46,7 @@ def test_public_catalog_contains_only_admitted_atomic_operations() -> None:
}

assert {tool.operation_id for tool in BUILTIN_TOOLS} == expected
assert len(BUILTIN_TOOLS) == 239
assert len(BUILTIN_TOOLS) == 242


def test_catalog_construction_fails_closed_on_duplicate_candidates() -> None:
Expand Down
Empty file.
Loading
Loading