Utilities Component

Purpose And Boundaries

prik/utilities/ contains mechanisms shared by more than one architecture stage. A helper belongs here only while it remains independent of source grammar, semantic policy, backend behavior, and build orchestration.

Local Structure

prik/utilities/
├── declaration_expressions.py
├── stage_values.py
├── strings.py
└── visitor.py
  • declaration_expressions.py splits declaration text, translates Fortran extents, resolves references to wrapper roles, evaluates constant expressions, and renders completed C or Fortran expressions.
  • stage_values.py provides StageRecord, which a producer assembles before its consumer recursively freezes it. FrozenStageRecordError rejects later mutation.
  • strings.py provides create_incremented_string() for collision-free local names and random_string() for unconstrained temporary identifiers. Public and native name policy belongs in prik.naming.
  • visitor.py provides ClassVisitor. It selects the most specific configured class handler, then deliberately follows the model's method-resolution order.

Declaration-Expression Workflow

declaration_expressions.py keeps one expression in different forms at explicit boundaries:

Fortran declaration text
  -> parser-safe splitting
  -> public Python-style extent expression
  -> references bound to completed wrapper roles
  -> constant evaluation or backend rendering

The caller supplies array facts during translation, available roles during resolution, and backend substitutions during rendering. The utility reports unresolved blockers; it does not decide whether a wrapper can supply a value.

Run The Module Demonstrations

The declaration-expression example follows one extent through translation, role binding, and Fortran rendering:

python3 prik/utilities/declaration_expressions.py
Example source: prik/utilities/declaration_expressions.py
if __name__ == "__main__":
    # A declared lower bound of zero makes the source-to-public translation
    # visibly different from the original Fortran inquiry spelling.
    fortran_expression = "ubound(source, 1) - lbound(source, 1) + 1"
    source_arrays = {"source": ArrayExpressionSource(rank=2, lower_bounds=("0", "1"))}
    public_expression = canonicalize_declaration_extent(fortran_extent_to_python(fortran_expression, source_arrays))
    resolved = resolve_declaration_extent(
        public_expression,
        scalar_roles={},
        array_roles={"source": ("source", ("source_extent_0", "source_extent_1"))},
    )
    rendered = render_declaration_extent(
        resolved.expression,
        {"__prik_extent_source_0": "native_source_extent_0"},
        target="fortran",
    )

    print(f"Fortran extent: {fortran_expression}")
    print(f"Public expression: {public_expression}")
    print(f"Role-bound expression: {resolved.expression}")
    print(f"Fortran rendering: {rendered}")
    print(f"Compile-time product: {evaluate_integer_expression('product((/ 2, 3 /))')}")
Fortran extent: ubound(source, 1) - lbound(source, 1) + 1
Public expression: source.shape[0]
Role-bound expression: __prik_extent_source_0
Fortran rendering: native_source_extent_0
Compile-time product: 6

The stage-value example shows that a producer can edit nested values until the consumer freezes the record:

python3 prik/utilities/stage_values.py
Example source: prik/utilities/stage_values.py
if __name__ == "__main__":
    from dataclasses import dataclass, field

    @dataclass
    class ParserOutput(StageRecord):
        """Small parser-stage value used by the direct utilities example."""

        module: str
        procedures: list[str] = field(default_factory=list)

    parsed = ParserOutput(module="geometry", procedures=["scale", "norm"])
    print(f"Editable parser output: {parsed.module} -> {parsed.procedures}")
    consumed = parsed.freeze()
    print(f"Frozen consumer input: {consumed.module} -> {consumed.procedures}")
    try:
        consumed.module = "changed"
    except FrozenStageRecordError as exc:
        print(f"Mutation rejected: {exc}")
Editable parser output: geometry -> ['scale', 'norm']
Frozen consumer input: geometry -> ('scale', 'norm')
Mutation rejected: ParserOutput is frozen by its consuming stage

Wrapper generation freezes a completed plan, printers freeze generated nodes, and build integration freezes the generated wrapper before writing it.

The remaining examples show collision-free local naming and exact-class/MRO visitor dispatch:

python3 prik/utilities/strings.py
Example source: prik/utilities/strings.py
if __name__ == "__main__":
    name, next_counter = create_incremented_string(
        {"temporary_2", "temporary_3"},
        prefix="temporary",
    )

    print(f"First available name: {name}")
    print(f"Next counter: {next_counter}")
First available name: temporary_4
Next counter: 5
python3 prik/utilities/visitor.py
Example source: prik/utilities/visitor.py
if __name__ == "__main__":

    class Expression:
        """Example base model handled through MRO fallback."""

    class Literal(Expression):
        def __init__(self, value):
            self.value = value

    class ExpressionVisitor(ClassVisitor):
        def _visit_Literal(self, node):
            return f"literal:{node.value}"

        def _visit_Expression(self, node):
            return f"expression:{type(node).__name__}"

    visitor = ExpressionVisitor()
    print(f"Exact handler: {visitor._visit(Literal(42))}")
    print(f"MRO fallback: {visitor._visit(Expression())}")
Exact handler: literal:42
MRO fallback: expression:Expression

Change Routes And Evidence

  • Keep declaration parsing, public normalization, role resolution, constant evaluation, and backend rendering as separate operations.
  • Freeze a StageRecord at its consumer boundary, after its producing stage has completed local assembly.
  • Put public or target-language name rules in prik.naming, not strings.py.
  • Define semantic or backend visitor handlers in their owning stage; the shared visitor supplies dispatch only.
Evidence What it establishes
Utility tests Local-name allocation and generic visitor dispatch.
Declaration-expression tests Translation, validation, role resolution, evaluation, and rendering.
Wrapper freeze-boundary tests Plans and generated nodes reject mutation after consumption.

Move a helper out of utilities/ as soon as it starts selecting semantic policy, emitted mechanisms, or a pipeline action.