tenncell 0.7.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.
- nnc/__init__.py +3 -0
- nnc/__main__.py +6 -0
- nnc/_version.py +1 -0
- nnc/cli.py +152 -0
- nnc/cli_transform.py +268 -0
- nnc/inputs/__init__.py +1 -0
- nnc/inputs/yaml/__init__.py +1 -0
- nnc/inputs/yaml/errors.py +61 -0
- nnc/inputs/yaml/lexical.py +60 -0
- nnc/inputs/yaml/locations.py +89 -0
- nnc/inputs/yaml/lowering/__init__.py +24 -0
- nnc/inputs/yaml/lowering/common.py +85 -0
- nnc/inputs/yaml/lowering/fsm.py +203 -0
- nnc/inputs/yaml/lowering/rules.py +60 -0
- nnc/inputs/yaml/module_config.py +57 -0
- nnc/inputs/yaml/parsing.py +41 -0
- nnc/inputs/yaml/raw_model.py +52 -0
- nnc/inputs/yaml/resolution.py +51 -0
- nnc/inputs/yaml/sections.py +30 -0
- nnc/inputs/yaml/yaml_loader.py +217 -0
- nnc/model/__init__.py +16 -0
- nnc/model/cell.py +25 -0
- nnc/model/rule.py +45 -0
- nnc/model/system.py +456 -0
- nnc/parser/__init__.py +13 -0
- nnc/parser/ast/__init__.py +61 -0
- nnc/parser/ast/boolean_expression.py +152 -0
- nnc/parser/ast/expression.py +268 -0
- nnc/parser/ast/value/__init__.py +12 -0
- nnc/parser/ast/value/array_value.py +64 -0
- nnc/parser/ast/value/base_value.py +34 -0
- nnc/parser/ast/value/float_value.py +35 -0
- nnc/parser/ast/value/math_functions.py +156 -0
- nnc/parser/ast/value/numerical_value.py +313 -0
- nnc/parser/ast/variable.py +16 -0
- nnc/parser/parser.py +357 -0
- nnc/transformers/__init__.py +22 -0
- nnc/transformers/base_transformer.py +50 -0
- nnc/transformers/python/__init__.py +11 -0
- nnc/transformers/python/core_emitter.py +320 -0
- nnc/transformers/python/csv_script_emitter.py +188 -0
- nnc/transformers/python/emission_context.py +17 -0
- nnc/transformers/python/expression_emitter.py +192 -0
- nnc/transformers/python_transformer.py +182 -0
- nnc/transformers/verilog/__init__.py +33 -0
- nnc/transformers/verilog/generation/__init__.py +1 -0
- nnc/transformers/verilog/generation/context.py +33 -0
- nnc/transformers/verilog/generation/conversions.py +343 -0
- nnc/transformers/verilog/generation/encodings.py +76 -0
- nnc/transformers/verilog/generation/expressions.py +517 -0
- nnc/transformers/verilog/generation/literals.py +159 -0
- nnc/transformers/verilog/generation/references.py +505 -0
- nnc/transformers/verilog/generation/state.py +165 -0
- nnc/transformers/verilog/generation/structure.py +486 -0
- nnc/transformers/verilog/hardware_config.py +209 -0
- nnc/transformers/verilog/verilog_yaml_section_parser.py +395 -0
- nnc/transformers/verilog_transformer.py +253 -0
- nnc/transformers/webots/__init__.py +22 -0
- nnc/transformers/webots/emission_context.py +14 -0
- nnc/transformers/webots/webots_config.py +50 -0
- nnc/transformers/webots/webots_yaml_section_parser.py +147 -0
- nnc/transformers/webots_transformer.py +217 -0
- tenncell-0.7.0.dist-info/METADATA +502 -0
- tenncell-0.7.0.dist-info/RECORD +67 -0
- tenncell-0.7.0.dist-info/WHEEL +4 -0
- tenncell-0.7.0.dist-info/entry_points.txt +6 -0
- tenncell-0.7.0.dist-info/licenses/LICENSE +21 -0
nnc/__init__.py
ADDED
nnc/__main__.py
ADDED
nnc/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.7.0'
|
nnc/cli.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import csv
|
|
3
|
+
import sys
|
|
4
|
+
from contextlib import ExitStack
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from nnc.model.system import NncSystem
|
|
8
|
+
|
|
9
|
+
from nnc._version import __version__
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> int:
|
|
13
|
+
"""Run the TENNCell simulator CLI and return a process-style exit code."""
|
|
14
|
+
try:
|
|
15
|
+
return _run()
|
|
16
|
+
except Exception as e:
|
|
17
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
18
|
+
return 1
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _run() -> int:
|
|
22
|
+
"""Parse arguments, run the simulator, and return ``0`` on success."""
|
|
23
|
+
parser = argparse.ArgumentParser(
|
|
24
|
+
description=f"TENNCell simulator v{__version__}", prog="nnc-sim"
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"system_file",
|
|
28
|
+
type=Path,
|
|
29
|
+
help="The file containing the description of the TENNCell system",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"input",
|
|
33
|
+
type=Path,
|
|
34
|
+
nargs="?",
|
|
35
|
+
default=None,
|
|
36
|
+
help="Input (CSV) file giving stimulus for each step (default: stdin)",
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"output",
|
|
40
|
+
type=Path,
|
|
41
|
+
nargs="?",
|
|
42
|
+
default=None,
|
|
43
|
+
help="Output (CSV) file giving the results of each step (default: stdout)",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"-c",
|
|
47
|
+
"--compute_mode",
|
|
48
|
+
action="store_true",
|
|
49
|
+
help="Run in continuous compute mode (default: False)",
|
|
50
|
+
)
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"-s", "--steps", type=int, default=1, help="Number of steps to run (default: 1)"
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--csv",
|
|
56
|
+
action="store_true",
|
|
57
|
+
help="Output in CSV format when in continuous compute mode",
|
|
58
|
+
)
|
|
59
|
+
args = parser.parse_args()
|
|
60
|
+
|
|
61
|
+
with ExitStack() as stack:
|
|
62
|
+
input_handle = (
|
|
63
|
+
sys.stdin
|
|
64
|
+
if args.input is None
|
|
65
|
+
else stack.enter_context(args.input.open("r", encoding="utf-8", newline=""))
|
|
66
|
+
)
|
|
67
|
+
output_handle = (
|
|
68
|
+
sys.stdout
|
|
69
|
+
if args.output is None
|
|
70
|
+
else stack.enter_context(
|
|
71
|
+
args.output.open("w", encoding="utf-8", newline="")
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
nnc = NncSystem.from_yaml(str(args.system_file))
|
|
75
|
+
|
|
76
|
+
if args.compute_mode:
|
|
77
|
+
if len(nnc.input_variables) > 0:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
"Cannot run in continuous compute mode with input variables"
|
|
80
|
+
)
|
|
81
|
+
if args.csv:
|
|
82
|
+
# CSV output mode
|
|
83
|
+
result = {
|
|
84
|
+
name: str(variable.value)
|
|
85
|
+
for name, variable in nnc.output_variables.items()
|
|
86
|
+
}
|
|
87
|
+
fieldnames = ["step"] + list(result.keys())
|
|
88
|
+
writer = csv.DictWriter(
|
|
89
|
+
output_handle, fieldnames=fieldnames, lineterminator="\n"
|
|
90
|
+
)
|
|
91
|
+
writer.writeheader()
|
|
92
|
+
# Write initial state (step 0) with only output variables
|
|
93
|
+
row = {"step": 0}
|
|
94
|
+
row.update(result)
|
|
95
|
+
writer.writerow(row)
|
|
96
|
+
for i in range(args.steps):
|
|
97
|
+
nnc.step()
|
|
98
|
+
result = {
|
|
99
|
+
name: str(variable.value)
|
|
100
|
+
for name, variable in nnc.output_variables.items()
|
|
101
|
+
}
|
|
102
|
+
row = {"step": i + 1}
|
|
103
|
+
row.update(result)
|
|
104
|
+
writer.writerow(row)
|
|
105
|
+
else:
|
|
106
|
+
print(
|
|
107
|
+
f"Running in continuous compute mode for {args.steps} steps",
|
|
108
|
+
file=output_handle,
|
|
109
|
+
)
|
|
110
|
+
result = {
|
|
111
|
+
name: str(variable.value)
|
|
112
|
+
for name, variable in nnc.output_variables.items()
|
|
113
|
+
}
|
|
114
|
+
print(f"Step: 0: {result}", file=output_handle)
|
|
115
|
+
for i in range(args.steps):
|
|
116
|
+
nnc.step()
|
|
117
|
+
result = {
|
|
118
|
+
name: str(variable.value)
|
|
119
|
+
for name, variable in nnc.output_variables.items()
|
|
120
|
+
}
|
|
121
|
+
print(f"Step: {i + 1}: {result}", file=output_handle)
|
|
122
|
+
else: # IO mode
|
|
123
|
+
reader = csv.DictReader(input_handle)
|
|
124
|
+
writer = None
|
|
125
|
+
|
|
126
|
+
for csv_row in reader:
|
|
127
|
+
inputs = {}
|
|
128
|
+
for input_var_name, input_var_value in csv_row.items():
|
|
129
|
+
if input_var_name not in nnc.input_variables.keys():
|
|
130
|
+
raise ValueError(f"Unknown input variable {input_var_name}")
|
|
131
|
+
inputs[input_var_name] = float(input_var_value)
|
|
132
|
+
|
|
133
|
+
nnc.step(inputs)
|
|
134
|
+
|
|
135
|
+
result = {
|
|
136
|
+
name: variable.value
|
|
137
|
+
for name, variable in nnc.output_variables.items()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if writer is None:
|
|
141
|
+
writer = csv.DictWriter(
|
|
142
|
+
output_handle,
|
|
143
|
+
fieldnames=result.keys(),
|
|
144
|
+
lineterminator="\n",
|
|
145
|
+
)
|
|
146
|
+
writer.writeheader()
|
|
147
|
+
writer.writerow(result)
|
|
148
|
+
return 0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
sys.exit(main())
|
nnc/cli_transform.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""CLI tool for transforming TENNCell systems to various output formats."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, Type
|
|
8
|
+
|
|
9
|
+
from nnc.inputs.yaml.sections import YamlSectionContext, parse_registered_sections
|
|
10
|
+
from nnc.inputs.yaml.locations import YamlLocationIndex
|
|
11
|
+
from nnc.model.system import NncSystem
|
|
12
|
+
from nnc.transformers import (
|
|
13
|
+
BaseTransformer,
|
|
14
|
+
PythonTransformer,
|
|
15
|
+
VerilogTransformer,
|
|
16
|
+
WebotsTransformer,
|
|
17
|
+
)
|
|
18
|
+
from nnc.transformers.webots.webots_yaml_section_parser import parse_webots_section
|
|
19
|
+
from nnc.transformers.verilog.verilog_yaml_section_parser import parse_verilog_section
|
|
20
|
+
from nnc._version import __version__
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Registry of available transformers
|
|
24
|
+
TRANSFORMERS: Dict[str, Type[BaseTransformer]] = {
|
|
25
|
+
"python": PythonTransformer,
|
|
26
|
+
"verilog": VerilogTransformer,
|
|
27
|
+
"webots": WebotsTransformer,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_transformer(transform_type: str) -> BaseTransformer:
|
|
32
|
+
"""Get a transformer instance by type name.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
transform_type: The type of transformer to create
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Transformer instance
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ValueError: If transform_type is not supported
|
|
42
|
+
"""
|
|
43
|
+
if transform_type not in TRANSFORMERS:
|
|
44
|
+
available = ", ".join(TRANSFORMERS.keys())
|
|
45
|
+
raise ValueError(
|
|
46
|
+
f"Unsupported transformation type '{transform_type}'. Available types: {available}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return TRANSFORMERS[transform_type]()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def main():
|
|
53
|
+
"""Main entry point for the ``nnc-gen`` CLI."""
|
|
54
|
+
parser = argparse.ArgumentParser(
|
|
55
|
+
description=f"TENNCell Transformer v{__version__} - Convert TENNCell systems to various output formats",
|
|
56
|
+
prog="nnc-gen",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"system_files", nargs="+", help="TENNCell system files (.yaml) to transform"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"-t",
|
|
65
|
+
"--type",
|
|
66
|
+
dest="transform_type",
|
|
67
|
+
required=True,
|
|
68
|
+
choices=list(TRANSFORMERS.keys()),
|
|
69
|
+
help="Transformation type",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"-o",
|
|
74
|
+
"--output-dir",
|
|
75
|
+
type=Path,
|
|
76
|
+
default=Path("."),
|
|
77
|
+
help="Output directory for transformed files (default: current directory)",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"--output-suffix",
|
|
82
|
+
default="",
|
|
83
|
+
help="Suffix to add to output filenames (before extension)",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"-v", "--verbose", action="store_true", help="Enable verbose output"
|
|
88
|
+
)
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
"--import-path",
|
|
91
|
+
action="append",
|
|
92
|
+
default=[],
|
|
93
|
+
help="Additional directory to search for imported TENNCell files and external headers",
|
|
94
|
+
)
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"--import-paths",
|
|
97
|
+
default="",
|
|
98
|
+
help="Path-separated list of additional import directories",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
args = parser.parse_args()
|
|
102
|
+
|
|
103
|
+
# Ensure output directory exists
|
|
104
|
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
|
|
106
|
+
# Get transformer
|
|
107
|
+
try:
|
|
108
|
+
transformer = get_transformer(args.transform_type)
|
|
109
|
+
except ValueError as e:
|
|
110
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
111
|
+
sys.exit(1)
|
|
112
|
+
|
|
113
|
+
extra_import_paths = list(args.import_path)
|
|
114
|
+
if args.import_paths:
|
|
115
|
+
extra_import_paths.extend(
|
|
116
|
+
[path for path in args.import_paths.split(os.pathsep) if path]
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Process each file
|
|
120
|
+
had_error = False
|
|
121
|
+
for nnc_file_path in args.system_files:
|
|
122
|
+
nnc_file = Path(nnc_file_path)
|
|
123
|
+
|
|
124
|
+
if not nnc_file.exists():
|
|
125
|
+
print(f"Error: File '{nnc_file}' not found", file=sys.stderr)
|
|
126
|
+
had_error = True
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
if args.verbose:
|
|
130
|
+
print(f"Processing: {nnc_file}")
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
# Load TENNCell system
|
|
134
|
+
raw_data_cache = {}
|
|
135
|
+
nnc_system = NncSystem.from_yaml(
|
|
136
|
+
str(nnc_file),
|
|
137
|
+
import_paths=extra_import_paths,
|
|
138
|
+
_raw_data_cache=raw_data_cache,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Verilog emits one file per module in the import closure.
|
|
142
|
+
# Python and Webots emit one composed file for the root system only.
|
|
143
|
+
if args.transform_type == "verilog":
|
|
144
|
+
systems_to_emit = _collect_import_closure(nnc_system)
|
|
145
|
+
assert isinstance(transformer, VerilogTransformer)
|
|
146
|
+
transformer.set_verilog_configs(
|
|
147
|
+
_parse_verilog_configs(
|
|
148
|
+
systems_to_emit, raw_data_cache, extra_import_paths
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
elif args.transform_type == "webots":
|
|
152
|
+
assert isinstance(transformer, WebotsTransformer)
|
|
153
|
+
transformer.set_webots_configs(
|
|
154
|
+
_parse_webots_configs([nnc_system], raw_data_cache)
|
|
155
|
+
)
|
|
156
|
+
systems_to_emit = [nnc_system]
|
|
157
|
+
else:
|
|
158
|
+
systems_to_emit = [nnc_system]
|
|
159
|
+
for system in systems_to_emit:
|
|
160
|
+
transformed_code = transformer.transform(system)
|
|
161
|
+
source_path = (
|
|
162
|
+
getattr(system, "__dict__", {}).get("source_path") or nnc_file
|
|
163
|
+
)
|
|
164
|
+
base_name = source_path.stem
|
|
165
|
+
if args.output_suffix:
|
|
166
|
+
base_name += args.output_suffix
|
|
167
|
+
|
|
168
|
+
output_file = args.output_dir / (
|
|
169
|
+
base_name + transformer.get_file_extension()
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
173
|
+
f.write(transformed_code)
|
|
174
|
+
|
|
175
|
+
if args.verbose:
|
|
176
|
+
print(f" -> {output_file}")
|
|
177
|
+
|
|
178
|
+
except Exception as e:
|
|
179
|
+
print(f"Error processing '{nnc_file}': {e}", file=sys.stderr)
|
|
180
|
+
had_error = True
|
|
181
|
+
if args.verbose:
|
|
182
|
+
import traceback
|
|
183
|
+
|
|
184
|
+
traceback.print_exc()
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
if args.verbose:
|
|
188
|
+
print("Transformation complete.")
|
|
189
|
+
if had_error:
|
|
190
|
+
return 1
|
|
191
|
+
return 0
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _collect_import_closure(root: NncSystem) -> list[NncSystem]:
|
|
195
|
+
seen: set[str] = set()
|
|
196
|
+
ordered: list[NncSystem] = []
|
|
197
|
+
|
|
198
|
+
def visit(system: NncSystem):
|
|
199
|
+
source_key = str(getattr(system, "source_path", None) or id(system))
|
|
200
|
+
if source_key in seen:
|
|
201
|
+
return
|
|
202
|
+
seen.add(source_key)
|
|
203
|
+
imports = getattr(system, "__dict__", {}).get("imports", [])
|
|
204
|
+
for item in imports or []:
|
|
205
|
+
if getattr(item, "system", None) is not None:
|
|
206
|
+
visit(item.system)
|
|
207
|
+
ordered.append(system)
|
|
208
|
+
|
|
209
|
+
visit(root)
|
|
210
|
+
return ordered
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _parse_verilog_configs(
|
|
214
|
+
systems: list[NncSystem],
|
|
215
|
+
raw_data_cache: dict[Path, dict],
|
|
216
|
+
import_paths: list[str],
|
|
217
|
+
):
|
|
218
|
+
configs = {}
|
|
219
|
+
header_cache: dict[Path, object] = {}
|
|
220
|
+
import_paths_resolved = [Path(path).resolve() for path in import_paths]
|
|
221
|
+
for system in systems:
|
|
222
|
+
if system.source_path is None:
|
|
223
|
+
raise ValueError("Verilog export requires systems loaded from YAML files")
|
|
224
|
+
data = raw_data_cache[system.source_path]
|
|
225
|
+
context = YamlSectionContext(
|
|
226
|
+
source_path=system.source_path,
|
|
227
|
+
import_paths=import_paths_resolved,
|
|
228
|
+
header_cache=header_cache,
|
|
229
|
+
raw_data=data,
|
|
230
|
+
locations=system.source_locations
|
|
231
|
+
or YamlLocationIndex(system.source_path, {}),
|
|
232
|
+
)
|
|
233
|
+
parsed = parse_registered_sections(
|
|
234
|
+
data, context, {"verilog": parse_verilog_section}
|
|
235
|
+
)
|
|
236
|
+
configs[system.source_path] = parsed["verilog"]
|
|
237
|
+
return configs
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _parse_webots_configs(
|
|
241
|
+
systems: list[NncSystem],
|
|
242
|
+
raw_data_cache: dict[Path, dict],
|
|
243
|
+
):
|
|
244
|
+
configs = {}
|
|
245
|
+
for system in systems:
|
|
246
|
+
if system.source_path is None:
|
|
247
|
+
raise ValueError("Webots export requires systems loaded from YAML files")
|
|
248
|
+
data = raw_data_cache[system.source_path]
|
|
249
|
+
context = YamlSectionContext(
|
|
250
|
+
source_path=system.source_path,
|
|
251
|
+
import_paths=[],
|
|
252
|
+
header_cache={},
|
|
253
|
+
raw_data=data,
|
|
254
|
+
locations=system.source_locations
|
|
255
|
+
or YamlLocationIndex(system.source_path, {}),
|
|
256
|
+
)
|
|
257
|
+
parsed = parse_registered_sections(
|
|
258
|
+
data, context, {"webots": parse_webots_section}
|
|
259
|
+
)
|
|
260
|
+
configs[system.source_path] = parsed["webots"]
|
|
261
|
+
return configs
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
if __name__ == "__main__":
|
|
265
|
+
try:
|
|
266
|
+
sys.exit(main())
|
|
267
|
+
except Exception as e:
|
|
268
|
+
print(f"Error: {e}")
|
nnc/inputs/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""TENNCell input format loaders."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""TENNCell YAML input loader package."""
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""YAML loader error helpers for TENNCell."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .locations import format_yaml_location
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class YamlLocatedError(ValueError):
|
|
11
|
+
"""ValueError carrying YAML source location metadata."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
message: str,
|
|
16
|
+
source_path: Path | None = None,
|
|
17
|
+
line: int | None = None,
|
|
18
|
+
):
|
|
19
|
+
super().__init__(message)
|
|
20
|
+
self.message = message
|
|
21
|
+
self.source_path = source_path
|
|
22
|
+
self.line = line
|
|
23
|
+
|
|
24
|
+
def attach_location(
|
|
25
|
+
self, source_path: Path | None = None, line: int | None = None
|
|
26
|
+
) -> "YamlLocatedError":
|
|
27
|
+
"""Update the stored location and return the error itself."""
|
|
28
|
+
if source_path is not None:
|
|
29
|
+
self.source_path = source_path
|
|
30
|
+
if line is not None:
|
|
31
|
+
self.line = line
|
|
32
|
+
return self
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def location(self) -> str | None:
|
|
36
|
+
"""Return the formatted location prefix, if available."""
|
|
37
|
+
if self.source_path is None:
|
|
38
|
+
return None
|
|
39
|
+
return format_yaml_location(self.source_path, self.line)
|
|
40
|
+
|
|
41
|
+
def __str__(self) -> str:
|
|
42
|
+
if self.location is None:
|
|
43
|
+
return self.message
|
|
44
|
+
return f"{self.location}: {self.message}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def as_yaml_located_error(
|
|
48
|
+
error: Exception,
|
|
49
|
+
source_path: Path | None = None,
|
|
50
|
+
line: int | None = None,
|
|
51
|
+
) -> YamlLocatedError:
|
|
52
|
+
"""Convert or annotate an exception with YAML location metadata."""
|
|
53
|
+
if isinstance(error, YamlLocatedError):
|
|
54
|
+
if error.source_path is None:
|
|
55
|
+
error.attach_location(source_path, line)
|
|
56
|
+
elif error.line is None and line is not None:
|
|
57
|
+
error.attach_location(line=line)
|
|
58
|
+
return error
|
|
59
|
+
located_error = YamlLocatedError(str(error), source_path, line)
|
|
60
|
+
located_error.__cause__ = error
|
|
61
|
+
return located_error
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Lexical preprocessing helpers for TENNCell YAML loading."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _split_comment(line: str) -> tuple[str, str]:
|
|
10
|
+
"""Split a YAML line into content and an inline comment."""
|
|
11
|
+
line = line.rstrip("\r\n")
|
|
12
|
+
in_single = False
|
|
13
|
+
in_double = False
|
|
14
|
+
for index, char in enumerate(line):
|
|
15
|
+
if char == "'" and not in_double:
|
|
16
|
+
in_single = not in_single
|
|
17
|
+
elif char == '"' and not in_single:
|
|
18
|
+
in_double = not in_double
|
|
19
|
+
elif char == "#" and not in_single and not in_double:
|
|
20
|
+
if index == 0 or line[index - 1].isspace():
|
|
21
|
+
return line[:index], line[index:]
|
|
22
|
+
return line, ""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _quote_bang_scalar(content: str) -> str:
|
|
26
|
+
"""Quote an unquoted scalar that starts with a bang."""
|
|
27
|
+
if not content:
|
|
28
|
+
return content
|
|
29
|
+
prefix = ""
|
|
30
|
+
value = content
|
|
31
|
+
if ":" in content:
|
|
32
|
+
prefix, value = content.split(":", 1)
|
|
33
|
+
if not value.startswith((" ", "\t")):
|
|
34
|
+
return content
|
|
35
|
+
prefix = f"{prefix}: "
|
|
36
|
+
value = value.lstrip()
|
|
37
|
+
elif content.lstrip().startswith("- "):
|
|
38
|
+
leading = len(content) - len(content.lstrip())
|
|
39
|
+
prefix = content[:leading] + "- "
|
|
40
|
+
value = content[leading + 2 :].lstrip()
|
|
41
|
+
else:
|
|
42
|
+
return content
|
|
43
|
+
|
|
44
|
+
if not value.startswith("!"):
|
|
45
|
+
return content
|
|
46
|
+
return f"{prefix}{json.dumps(value)}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def sanitize_bang_prefixed_scalars(lines: Iterable[str]) -> str:
|
|
50
|
+
"""Quote plain YAML scalars that start with ``!``.
|
|
51
|
+
|
|
52
|
+
PyYAML interprets bare ``!name`` values as tags. TENNCell expressions use
|
|
53
|
+
``!`` as a boolean operator, so plain scalars that start with ``!`` must be
|
|
54
|
+
converted to quoted strings before the YAML parser sees them.
|
|
55
|
+
"""
|
|
56
|
+
sanitized_lines: list[str] = []
|
|
57
|
+
for line in lines:
|
|
58
|
+
content, comment = _split_comment(line)
|
|
59
|
+
sanitized_lines.append(_quote_bang_scalar(content) + comment)
|
|
60
|
+
return "\n".join(sanitized_lines)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Source-location helpers for TENNCell YAML documents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
from yaml.nodes import MappingNode, Node, ScalarNode, SequenceNode
|
|
11
|
+
|
|
12
|
+
from .lexical import sanitize_bang_prefixed_scalars
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True)
|
|
16
|
+
class YamlLocationIndex:
|
|
17
|
+
"""Path-to-line index for one YAML source file."""
|
|
18
|
+
|
|
19
|
+
source_path: Path
|
|
20
|
+
paths: dict[tuple[object, ...], int]
|
|
21
|
+
|
|
22
|
+
def line_for(self, *path: object) -> int | None:
|
|
23
|
+
"""Return the 1-based line number for a YAML path, if known."""
|
|
24
|
+
return self.paths.get(path)
|
|
25
|
+
|
|
26
|
+
def first_line_under(self, *prefix: object) -> int | None:
|
|
27
|
+
"""Return the first known line number under a YAML path prefix."""
|
|
28
|
+
for path, line in self.paths.items():
|
|
29
|
+
if len(path) >= len(prefix) and path[: len(prefix)] == prefix:
|
|
30
|
+
return line
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
def error(self, message: str, *path: object) -> YamlLocatedError:
|
|
34
|
+
"""Build a located YAML error for this file and path."""
|
|
35
|
+
from .errors import YamlLocatedError
|
|
36
|
+
|
|
37
|
+
return YamlLocatedError(message, self.source_path, self.line_for(*path))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _node_key(node: Node) -> object:
|
|
41
|
+
"""Convert a YAML node used as a mapping key into a hashable path element."""
|
|
42
|
+
if isinstance(node, ScalarNode):
|
|
43
|
+
return node.value
|
|
44
|
+
return yaml.safe_load(yaml.serialize(node))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def collect_yaml_locations(node: Node | None) -> dict[tuple[object, ...], int]:
|
|
48
|
+
"""Collect 1-based line numbers for YAML values reachable from a node tree."""
|
|
49
|
+
locations: dict[tuple[object, ...], int] = {}
|
|
50
|
+
|
|
51
|
+
def visit(current: Node, path: tuple[object, ...]) -> None:
|
|
52
|
+
if isinstance(current, MappingNode):
|
|
53
|
+
for key_node, value_node in current.value:
|
|
54
|
+
key = _node_key(key_node)
|
|
55
|
+
child_path = path + (key,)
|
|
56
|
+
locations[child_path] = value_node.start_mark.line + 1
|
|
57
|
+
if isinstance(value_node, (MappingNode, SequenceNode)):
|
|
58
|
+
visit(value_node, child_path)
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
if isinstance(current, SequenceNode):
|
|
62
|
+
for index, item in enumerate(current.value):
|
|
63
|
+
child_path = path + (index,)
|
|
64
|
+
locations[child_path] = item.start_mark.line + 1
|
|
65
|
+
if isinstance(item, (MappingNode, SequenceNode)):
|
|
66
|
+
visit(item, child_path)
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
if node is not None:
|
|
70
|
+
visit(node, ())
|
|
71
|
+
return locations
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def load_yaml_data_and_locations(
|
|
75
|
+
source_path: Path,
|
|
76
|
+
) -> tuple[dict[str, Any], YamlLocationIndex]:
|
|
77
|
+
"""Load YAML content and collect path-to-line metadata for the document."""
|
|
78
|
+
with source_path.open("r", encoding="utf-8") as file:
|
|
79
|
+
sanitized = sanitize_bang_prefixed_scalars(file)
|
|
80
|
+
node = yaml.compose(sanitized)
|
|
81
|
+
data = yaml.safe_load(sanitized) or {}
|
|
82
|
+
return data, YamlLocationIndex(source_path, collect_yaml_locations(node))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def format_yaml_location(source_path: Path, line: int | None) -> str:
|
|
86
|
+
"""Format a source location prefix for a YAML error message."""
|
|
87
|
+
if line is None:
|
|
88
|
+
return str(source_path)
|
|
89
|
+
return f"{source_path}:{line}"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""TENNCell YAML lowering helpers."""
|
|
2
|
+
|
|
3
|
+
from .common import (
|
|
4
|
+
combine_guards,
|
|
5
|
+
guard_negation,
|
|
6
|
+
make_transition_rule,
|
|
7
|
+
normalize_rule_items,
|
|
8
|
+
parse_structured_rule,
|
|
9
|
+
with_guard,
|
|
10
|
+
)
|
|
11
|
+
from .fsm import build_state_constants, lower_fsm_rules
|
|
12
|
+
from .rules import lower_rule_items
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"build_state_constants",
|
|
16
|
+
"combine_guards",
|
|
17
|
+
"guard_negation",
|
|
18
|
+
"lower_fsm_rules",
|
|
19
|
+
"lower_rule_items",
|
|
20
|
+
"make_transition_rule",
|
|
21
|
+
"normalize_rule_items",
|
|
22
|
+
"parse_structured_rule",
|
|
23
|
+
"with_guard",
|
|
24
|
+
]
|