apc-model-parser 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: apc-model-parser
3
+ Version: 0.1.0
4
+ Summary: Convert process-model definitions to and from a canonical intermediate representation.
5
+ Project-URL: Repository, https://github.com/Advanced-Process-Control/model-parser
6
+ Author: Advanced Process Control
7
+ License: MIT
8
+ Keywords: codegen,exprtk,intermediate-representation,modelingtoolkit,process-model
9
+ Requires-Python: >=3.11
10
+ Requires-Dist: pydantic>=2.6
11
+ Requires-Dist: typer>=0.12
12
+ Description-Content-Type: text/markdown
13
+
14
+ # model-parser
15
+
16
+ **Convert process-model definitions to and from a canonical intermediate
17
+ representation (IR).**
18
+
19
+ `model-parser` is the Advanced Process Control toolbox component that owns the
20
+ *model scaffold* contract and the transformations around it. It parses an
21
+ authoring format (today: the ExprTk-style INI used by the MPC / simulation
22
+ toolchain) into a normalized, backend-independent **canonical IR**, and lowers
23
+ that IR into target views — starting with a generated **ModelingToolkit (Julia)**
24
+ model script.
25
+
26
+ ```text
27
+ authoring (ExprTk INI) --parse--> AST --normalize--> canonical IR (JSON)
28
+ |
29
+ emit julia --> ModelingToolkit .jl
30
+ emit cpp --> (planned) realtime C++
31
+ ```
32
+
33
+ The IR is the single semantic contract. Adding a backend means writing one
34
+ `lower` + one `export`, not an N×N mesh of view-to-view translators. See
35
+ [`docs/design/model-parser.md`](docs/design/model-parser.md) for the
36
+ authoritative product specification and
37
+ [`docs/decisions/`](docs/decisions/) for the design decision records.
38
+
39
+ ## Why two languages?
40
+
41
+ | Concern | Home | Why |
42
+ |---|---|---|
43
+ | CLI, AST, IR, JSON Schema, validators, **codegen** | **Python** (this package) | Parsing & orchestration strength; no symbolic runtime needed to emit code. |
44
+ | IR → MTK `System`, simulation, analysis, conformance | **Julia** ([`julia/ModelParserJL`](julia/ModelParserJL/)) | Natural fit for ModelingToolkit; reference for parity tests. |
45
+
46
+ The Python CLI builds and validates the IR and **generates** Julia code; the
47
+ Julia package can additionally load an IR directly into an MTK `System` for
48
+ in-memory, dynamic workflows. Both consume the *same* IR. See
49
+ [ADR 0001](docs/decisions/0001-python-cli-with-julia-backend.md).
50
+
51
+ ## Install
52
+
53
+ This project uses **[uv](https://docs.astral.sh/uv/)** for everything.
54
+
55
+ ```bash
56
+ uv sync --all-groups # include dev tools (ruff, pytest, mkdocs)
57
+ uv run model-parser --help
58
+ ```
59
+
60
+ ## CLI
61
+
62
+ ```bash
63
+ # 1. ExprTk INI -> canonical IR JSON
64
+ uv run model-parser parse examples/models/model_monod_simple.ini -o monod.ir.json
65
+
66
+ # 2. canonical IR -> ModelingToolkit (v11) Julia model
67
+ uv run model-parser emit julia monod.ir.json -o monod.jl
68
+
69
+ # Supporting commands
70
+ uv run model-parser validate monod.ir.json --profile julia-analysis
71
+ uv run model-parser inspect monod.ir.json
72
+ uv run model-parser ast examples/models/model_monod_simple.ini # debug tree
73
+ uv run model-parser schema -o schemas/canonical-ir.schema.json # export schema
74
+ ```
75
+
76
+ `parse` accepts `--from exprtk-ini` (default). `validate` accepts either an IR
77
+ `.json` file or an INI file (parsed on the fly). Exit codes: `0` success,
78
+ `1` validation errors, `2` usage / load failure.
79
+
80
+ ## How "stored MTK models" work
81
+
82
+ The persisted, version-controlled form of a model is the **IR JSON** plus the
83
+ **generated `.jl` script** — *not* a serialized `System` object. ModelingToolkit
84
+ v11 changed `System` internals significantly (precompilation, removal of
85
+ `defaults`, deprecation of `@mtkmodel`), so serializing the live object is
86
+ brittle across versions. Regenerating from the IR is the durable path. See
87
+ [ADR 0002](docs/decisions/0002-codegen-over-serialized-system.md) and
88
+ [`docs/design/storing-mtk-models.md`](docs/design/storing-mtk-models.md).
89
+
90
+ ## Development
91
+
92
+ ```bash
93
+ uv sync --all-groups
94
+ uv run ruff check .
95
+ uv run ruff format --check .
96
+ uv run pytest
97
+ uv run mkdocs build --strict # same as CI
98
+ ```
99
+
100
+ Documentation site (after [Pages](https://docs.github.com/en/pages/getting-started-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site#publishing-with-a-custom-github-actions-workflow) is set to GitHub Actions):
101
+ [https://advanced-process-control.github.io/model-parser/](https://advanced-process-control.github.io/model-parser/)
102
+
103
+ ## Scope
104
+
105
+ **In scope:** authoring-format parsing, the canonical IR data model + JSON
106
+ Schema, semantic & profile validation, IR↔backend lowering/codegen.
107
+
108
+ **Out of scope:** parameter identification, scenario execution, simulation
109
+ result storage, controller synthesis, deployment. Those are sibling tools that
110
+ *consume* the IR. The parser stays small (see
111
+ [`AGENTS.md`](AGENTS.md) scope guardrails).
@@ -0,0 +1,18 @@
1
+ model_parser/__init__.py,sha256=dACj1q8TSXCZLi8otXh2vmfUnCjfkrOdRYp9WKohWZU,641
2
+ model_parser/cli.py,sha256=aIuJC6bNVei7Gy2Kp7vu8KnqDTAuEXN9s1wK6XEWSUE,6654
3
+ model_parser/io.py,sha256=GkG2U4GILQVTq8fdb1c1tA9UOEsA69e9wdABQplDFnk,1662
4
+ model_parser/schema.py,sha256=xvyBmFjGGKrVLNzWM2ssC1CVBPtJtgsiXHNGie0oNZM,971
5
+ model_parser/backends/__init__.py,sha256=Y3YmRruw9whtvvtp2zmY-Ne4APNwFxNxhFSaxNxJhQA,172
6
+ model_parser/backends/julia_mtk.py,sha256=1P79lKVEgntYrQmR2VwAl1RaJKSS66cscAmDD_W-QzE,5363
7
+ model_parser/frontends/__init__.py,sha256=to_N4GEqlGX7-ipHuIOztOS-T8xCy8UjygMfKvOMztw,235
8
+ model_parser/frontends/expr_parser.py,sha256=Um49TQltfDhk0qx0xotF9nVbPS0QSquz2PjgqpfSYkw,5875
9
+ model_parser/frontends/exprtk_ini.py,sha256=JpgP_sJqYmk-huSXaReM4ZywSk6MTESAoLfAwSGYWTM,7138
10
+ model_parser/ir/__init__.py,sha256=CIzn0srFqsefwZwDkwu__lw-JpVSLM2j3rvoI73Q8ak,813
11
+ model_parser/ir/expr.py,sha256=890lOT1SqFPmNxzqT4c4ZiXUKz_DQKL3cW5rJDgNSlU,2837
12
+ model_parser/ir/model.py,sha256=dPYqQOWhQnAWmGTQJ0d0gz8_FZ2O324UD2GvK08-H6Q,3887
13
+ model_parser/validation/__init__.py,sha256=oe4BBC2AgumuIoA03tIVNtGoxlTgqfbckSjZVPU5-j4,235
14
+ model_parser/validation/validators.py,sha256=VwTUXyQhz6uYXoYkwWxGR4lUwO0SWJgmCj5rnQQsurQ,6207
15
+ apc_model_parser-0.1.0.dist-info/METADATA,sha256=xXObGpQrAwaDEjqRqZJJ0zk2GJUK6etYwdGl_fsJ_WE,4760
16
+ apc_model_parser-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
17
+ apc_model_parser-0.1.0.dist-info/entry_points.txt,sha256=7Lxr9OwAdGWx007g6CSk6-J_fx6l-V7BF2Umj8tj_Yc,54
18
+ apc_model_parser-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ model-parser = model_parser.cli:run
@@ -0,0 +1,18 @@
1
+ """model-parser: convert process-model definitions to and from a canonical IR.
2
+
3
+ The package owns the canonical intermediate representation (IR) for process
4
+ models and the transformations around it:
5
+
6
+ - **frontends** parse an authoring format (e.g. ExprTk INI) into the IR;
7
+ - **backends** lower the IR into a target view (e.g. a ModelingToolkit Julia
8
+ script);
9
+ - **validation** checks an IR against the schema and backend profiles.
10
+
11
+ See ``docs/design/model-parser.md`` for the authoritative product specification.
12
+ """
13
+
14
+ from model_parser.ir import IR_VERSION, IRModel
15
+
16
+ __all__ = ["IRModel", "IR_VERSION", "__version__"]
17
+
18
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ """Backends: lower the canonical IR into target views."""
2
+
3
+ from model_parser.backends.julia_mtk import emit_julia, expr_to_julia
4
+
5
+ __all__ = ["emit_julia", "expr_to_julia"]
@@ -0,0 +1,159 @@
1
+ """Julia / ModelingToolkit backend: lower the IR into a generated ``.jl`` script.
2
+
3
+ This is the ``lower`` transformation from IR to the Julia analysis view. It
4
+ *generates source code* rather than serializing a compiled ``System`` object —
5
+ see ADR 0002. The generated script is a re-runnable, diffable artifact whose
6
+ source of truth remains the IR.
7
+
8
+ The emitted code targets **ModelingToolkit v11** idioms (see ADR 0004):
9
+
10
+ - explicit ``System(eqs, t)`` construction (no deprecated ``@mtkmodel`` DSL);
11
+ - ``mtkcompile`` instead of ``structural_simplify``;
12
+ - ``t``/``D`` imported as ``t_nounits`` / ``D_nounits``;
13
+ - parameter defaults attached via ``@parameters name = value`` (translated by
14
+ MTK v11 into ``initial_conditions`` for constant values);
15
+ - inputs declared and passed to ``mtkcompile(sys; inputs = [...])``.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from model_parser.ir import Call, Expr, IRModel, Num, Sym
21
+
22
+ GENERATOR = "model-parser julia-mtk backend"
23
+
24
+ # IR op name -> Julia infix operator.
25
+ _INFIX: dict[str, str] = {
26
+ "+": "+",
27
+ "-": "-",
28
+ "*": "*",
29
+ "/": "/",
30
+ "^": "^",
31
+ "==": "==",
32
+ "!=": "!=",
33
+ "<": "<",
34
+ ">": ">",
35
+ "<=": "<=",
36
+ ">=": ">=",
37
+ }
38
+
39
+ # IR op name -> Julia function name.
40
+ _FUNCS: dict[str, str] = {
41
+ "max": "max",
42
+ "min": "min",
43
+ "sqrt": "sqrt",
44
+ "exp": "exp",
45
+ "log": "log",
46
+ "abs": "abs",
47
+ "ifelse": "ifelse",
48
+ }
49
+
50
+
51
+ class JuliaCodegenError(ValueError):
52
+ """Raised when an IR construct cannot be lowered to Julia."""
53
+
54
+
55
+ def expr_to_julia(expr: Expr) -> str:
56
+ """Render an IR expression as a parenthesized Julia expression string."""
57
+ if isinstance(expr, Num):
58
+ return _render_number(expr.value)
59
+ if isinstance(expr, Sym):
60
+ return expr.name
61
+ if isinstance(expr, Call):
62
+ return _render_call(expr)
63
+ raise JuliaCodegenError(f"unknown expression node: {expr!r}")
64
+
65
+
66
+ def _render_number(value: float) -> str:
67
+ if value.is_integer():
68
+ return f"{value:.1f}"
69
+ return repr(value)
70
+
71
+
72
+ def _render_call(call: Call) -> str:
73
+ op = call.op
74
+ args = call.args
75
+ if op == "neg":
76
+ if len(args) != 1:
77
+ raise JuliaCodegenError("'neg' expects exactly one argument")
78
+ return f"(-{expr_to_julia(args[0])})"
79
+ if op in _INFIX:
80
+ if len(args) != 2:
81
+ raise JuliaCodegenError(f"operator {op!r} expects two arguments")
82
+ left, right = (expr_to_julia(a) for a in args)
83
+ return f"({left} {_INFIX[op]} {right})"
84
+ if op in _FUNCS:
85
+ rendered = ", ".join(expr_to_julia(a) for a in args)
86
+ return f"{_FUNCS[op]}({rendered})"
87
+ raise JuliaCodegenError(f"unsupported operator/function {op!r}")
88
+
89
+
90
+ def _julia_name(ir: IRModel) -> str:
91
+ name = ir.model.name or "model"
92
+ cleaned = "".join(ch if ch.isalnum() or ch == "_" else "_" for ch in name)
93
+ if not cleaned or not (cleaned[0].isalpha() or cleaned[0] == "_"):
94
+ cleaned = f"m_{cleaned}"
95
+ return cleaned
96
+
97
+
98
+ def emit_julia(ir: IRModel) -> str:
99
+ """Generate a ModelingToolkit v11 Julia script that builds ``ir`` as a System."""
100
+ name = _julia_name(ir)
101
+ iv = ir.independent_variable
102
+ lines: list[str] = []
103
+
104
+ lines.append(f"# Generated by {GENERATOR}. Do not edit by hand.")
105
+ lines.append("# Source of truth: the canonical IR. Regenerate after IR changes.")
106
+ if ir.provenance is not None:
107
+ lines.append(f"# IR tool: {ir.provenance.tool}")
108
+ if ir.provenance.content_hash:
109
+ lines.append(f"# IR content hash: {ir.provenance.content_hash}")
110
+ lines.append(f"# IR version: {ir.ir_version}")
111
+ lines.append("")
112
+ lines.append("using ModelingToolkit")
113
+ lines.append(f"using ModelingToolkit: t_nounits as {iv}, D_nounits as D")
114
+ lines.append("")
115
+ lines.append(f'"""Build the `{ir.model.name}` model as a compiled MTK `System`."""')
116
+ lines.append(f"function build_{name}()")
117
+
118
+ # Parameters with defaults.
119
+ if ir.parameters:
120
+ decls = []
121
+ for param in ir.parameters:
122
+ if param.default is None:
123
+ decls.append(param.name)
124
+ else:
125
+ decls.append(f"{param.name} = {_render_number(param.default)}")
126
+ lines.append(f" @parameters {' '.join(decls)}")
127
+
128
+ # Time-dependent variables: states, inputs, outputs, locals.
129
+ tvars = (
130
+ [v.name for v in ir.states]
131
+ + [v.name for v in ir.inputs]
132
+ + [v.name for v in ir.outputs]
133
+ + [local.name for local in ir.locals]
134
+ )
135
+ if tvars:
136
+ decls = " ".join(f"{n}({iv})" for n in tvars)
137
+ lines.append(f" @variables {decls}")
138
+
139
+ lines.append("")
140
+ lines.append(" eqs = [")
141
+ for local in ir.locals:
142
+ lines.append(f" {local.name} ~ {expr_to_julia(local.expr)},")
143
+ for diff in ir.equations.differential:
144
+ lines.append(f" D({diff.state}) ~ {expr_to_julia(diff.rhs)},")
145
+ for out in ir.equations.outputs:
146
+ lines.append(f" {out.output} ~ {expr_to_julia(out.rhs)},")
147
+ lines.append(" ]")
148
+ lines.append("")
149
+ lines.append(f" @named {name} = System(eqs, {iv})")
150
+
151
+ if ir.inputs:
152
+ input_list = ", ".join(v.name for v in ir.inputs)
153
+ lines.append(f" return mtkcompile({name}; inputs = [{input_list}])")
154
+ else:
155
+ lines.append(f" return mtkcompile({name})")
156
+
157
+ lines.append("end")
158
+ lines.append("")
159
+ return "\n".join(lines)
model_parser/cli.py ADDED
@@ -0,0 +1,195 @@
1
+ """Command-line interface for model-parser.
2
+
3
+ The CLI exposes the IR transformations as composable subcommands. The two core
4
+ verbs follow the ecosystem's transformation vocabulary:
5
+
6
+ model-parser parse <model.ini> # authoring format -> canonical IR JSON
7
+ model-parser emit julia <model.ir.json> # canonical IR -> MTK Julia script
8
+
9
+ Plus supporting commands: ``validate``, ``inspect``, ``ast``, and ``schema``.
10
+
11
+ Exit codes:
12
+ 0 success, no ERROR-level diagnostics
13
+ 1 at least one ERROR diagnostic during execution
14
+ 2 invalid usage / load failure before meaningful work
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ import typer
23
+
24
+ from model_parser import __version__
25
+ from model_parser.backends import emit_julia
26
+ from model_parser.frontends import parse_ini_file
27
+ from model_parser.io import dumps_ir, load_ir, save_ir, with_content_hash
28
+ from model_parser.schema import dumps_schema
29
+ from model_parser.validation import validate_ir
30
+
31
+ app = typer.Typer(
32
+ add_completion=False,
33
+ no_args_is_help=True,
34
+ help="Convert process-model definitions to and from a canonical IR.",
35
+ )
36
+ emit_app = typer.Typer(no_args_is_help=True, help="Lower the canonical IR into a target view.")
37
+ app.add_typer(emit_app, name="emit")
38
+
39
+ _err = typer.style("error:", fg=typer.colors.RED, bold=True)
40
+ _warn = typer.style("warning:", fg=typer.colors.YELLOW, bold=True)
41
+
42
+
43
+ def _echo_err(message: str) -> None:
44
+ typer.echo(f"{_err} {message}", err=True)
45
+
46
+
47
+ def _echo_warn(message: str) -> None:
48
+ typer.echo(f"{_warn} {message}", err=True)
49
+
50
+
51
+ @app.callback(invoke_without_command=True)
52
+ def _main(
53
+ version: bool = typer.Option(
54
+ False, "--version", help="Show the version and exit.", is_eager=True
55
+ ),
56
+ ) -> None:
57
+ if version:
58
+ typer.echo(f"model-parser {__version__}")
59
+ raise typer.Exit()
60
+
61
+
62
+ @app.command()
63
+ def parse(
64
+ source: Path = typer.Argument(..., exists=True, readable=True, help="Authoring file."),
65
+ output: Path | None = typer.Option(
66
+ None, "-o", "--output", help="IR JSON output path (default: stdout)."
67
+ ),
68
+ from_format: str = typer.Option(
69
+ "exprtk-ini", "--from", help="Authoring format of the source file."
70
+ ),
71
+ ) -> None:
72
+ """Parse an authoring file (default: ExprTk INI) into canonical IR JSON."""
73
+ if from_format != "exprtk-ini":
74
+ _echo_err(f"unsupported source format {from_format!r} (only 'exprtk-ini' for now)")
75
+ raise typer.Exit(code=2)
76
+
77
+ result = parse_ini_file(str(source))
78
+ ir = with_content_hash(result.ir)
79
+ for warning in result.warnings:
80
+ _echo_warn(warning)
81
+
82
+ if output is None:
83
+ typer.echo(dumps_ir(ir), nl=False)
84
+ else:
85
+ save_ir(ir, output)
86
+ typer.echo(f"wrote IR to {output}", err=True)
87
+
88
+
89
+ @emit_app.command("julia")
90
+ def emit_julia_cmd(
91
+ ir_file: Path = typer.Argument(..., exists=True, readable=True, help="IR JSON file."),
92
+ output: Path | None = typer.Option(
93
+ None, "-o", "--output", help="Julia output path (default: stdout)."
94
+ ),
95
+ ) -> None:
96
+ """Generate a ModelingToolkit (v11) Julia script from an IR file."""
97
+ ir = load_ir(ir_file)
98
+ code = emit_julia(ir)
99
+ if output is None:
100
+ typer.echo(code, nl=False)
101
+ else:
102
+ Path(output).write_text(code, encoding="utf-8")
103
+ typer.echo(f"wrote Julia model to {output}", err=True)
104
+
105
+
106
+ @app.command()
107
+ def validate(
108
+ target: Path = typer.Argument(..., exists=True, readable=True, help="IR JSON or INI file."),
109
+ profile: str | None = typer.Option(
110
+ None, "--profile", help="Backend profile to check (e.g. julia-analysis, realtime-cpp)."
111
+ ),
112
+ ) -> None:
113
+ """Validate an IR (or an INI parsed on the fly) and report diagnostics."""
114
+ ir = _load_any(target)
115
+ report = validate_ir(ir, profile=profile)
116
+ for diag in report.diagnostics:
117
+ prefix = _err if diag.level == "ERROR" else _warn
118
+ typer.echo(f"{prefix} [{diag.code}] {diag.message}", err=True)
119
+ if report.ok:
120
+ typer.echo(f"OK: {ir.model.name} valid" + (f" for profile {profile!r}" if profile else ""))
121
+ else:
122
+ typer.echo(f"FAILED: {len(report.errors)} error(s)", err=True)
123
+ raise typer.Exit(code=1)
124
+
125
+
126
+ @app.command()
127
+ def inspect(
128
+ target: Path = typer.Argument(..., exists=True, readable=True, help="IR JSON or INI file."),
129
+ ) -> None:
130
+ """Print a human-readable summary of a model."""
131
+ ir = _load_any(target)
132
+ typer.echo(f"model: {ir.model.name}")
133
+ if ir.model.description:
134
+ typer.echo(f"description: {ir.model.description}")
135
+ typer.echo(f"ir version: {ir.ir_version}")
136
+ typer.echo(f"states: {len(ir.states)} ({', '.join(v.name for v in ir.states)})")
137
+ typer.echo(f"inputs: {len(ir.inputs)} ({', '.join(v.name for v in ir.inputs)})")
138
+ typer.echo(f"outputs: {len(ir.outputs)} ({', '.join(v.name for v in ir.outputs)})")
139
+ typer.echo(f"parameters: {len(ir.parameters)}")
140
+ typer.echo(f"locals: {len(ir.locals)}")
141
+ typer.echo(f"profiles: {', '.join(ir.profiles)}")
142
+ if ir.provenance and ir.provenance.content_hash:
143
+ typer.echo(f"hash: {ir.provenance.content_hash}")
144
+
145
+
146
+ @app.command()
147
+ def ast(
148
+ source: Path = typer.Argument(..., exists=True, readable=True, help="ExprTk INI file."),
149
+ output: Path | None = typer.Option(
150
+ None, "-o", "--output", help="AST/IR debug JSON output path (default: stdout)."
151
+ ),
152
+ ) -> None:
153
+ """Export the parsed IR as a debug JSON tree (parser inspection)."""
154
+ result = parse_ini_file(str(source))
155
+ text = dumps_ir(result.ir)
156
+ if output is None:
157
+ typer.echo(text, nl=False)
158
+ else:
159
+ Path(output).write_text(text, encoding="utf-8")
160
+ typer.echo(f"wrote debug tree to {output}", err=True)
161
+
162
+
163
+ @app.command()
164
+ def schema(
165
+ output: Path | None = typer.Option(
166
+ None, "-o", "--output", help="JSON Schema output path (default: stdout)."
167
+ ),
168
+ ) -> None:
169
+ """Print or write the canonical IR JSON Schema."""
170
+ text = dumps_schema()
171
+ if output is None:
172
+ typer.echo(text, nl=False)
173
+ else:
174
+ Path(output).write_text(text, encoding="utf-8")
175
+ typer.echo(f"wrote schema to {output}", err=True)
176
+
177
+
178
+ def _load_any(path: Path):
179
+ """Load an IR from a ``.json`` file, or parse an INI file on the fly."""
180
+ if path.suffix.lower() == ".json":
181
+ return load_ir(path)
182
+ return parse_ini_file(str(path)).ir
183
+
184
+
185
+ def run() -> None:
186
+ """Entry point wrapper that maps uncaught errors to exit code 2."""
187
+ try:
188
+ app()
189
+ except (ValueError, FileNotFoundError) as exc: # parse/codegen errors
190
+ _echo_err(str(exc))
191
+ sys.exit(2)
192
+
193
+
194
+ if __name__ == "__main__":
195
+ run()
@@ -0,0 +1,9 @@
1
+ """Frontends: parse authoring formats into the canonical IR."""
2
+
3
+ from model_parser.frontends.exprtk_ini import (
4
+ ParseResult,
5
+ parse_ini_file,
6
+ parse_ini_text,
7
+ )
8
+
9
+ __all__ = ["ParseResult", "parse_ini_file", "parse_ini_text"]