python-library-ralf-model 0.1.0__tar.gz → 0.1.2__tar.gz

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.
Files changed (19) hide show
  1. python_library_ralf_model-0.1.2/.gitignore +21 -0
  2. {python_library_ralf_model-0.1.0 → python_library_ralf_model-0.1.2}/PKG-INFO +3 -1
  3. {python_library_ralf_model-0.1.0 → python_library_ralf_model-0.1.2}/README.md +2 -0
  4. {python_library_ralf_model-0.1.0 → python_library_ralf_model-0.1.2}/pyproject.toml +1 -1
  5. {python_library_ralf_model-0.1.0 → python_library_ralf_model-0.1.2}/ralf_model/emit.py +4 -0
  6. {python_library_ralf_model-0.1.0 → python_library_ralf_model-0.1.2}/ralf_model/errors.py +14 -2
  7. {python_library_ralf_model-0.1.0 → python_library_ralf_model-0.1.2}/ralf_model/io.py +9 -4
  8. {python_library_ralf_model-0.1.0 → python_library_ralf_model-0.1.2}/ralf_model/nodes.py +8 -0
  9. {python_library_ralf_model-0.1.0 → python_library_ralf_model-0.1.2}/ralf_model/parse.py +35 -5
  10. {python_library_ralf_model-0.1.0 → python_library_ralf_model-0.1.2}/ralf_model/source_expand.py +9 -3
  11. {python_library_ralf_model-0.1.0 → python_library_ralf_model-0.1.2}/tests/test_ralf_model.py +90 -1
  12. {python_library_ralf_model-0.1.0 → python_library_ralf_model-0.1.2}/tests/test_source_include.py +1 -1
  13. python_library_ralf_model-0.1.0/.gitignore +0 -12
  14. {python_library_ralf_model-0.1.0 → python_library_ralf_model-0.1.2}/example/__main__.py +0 -0
  15. {python_library_ralf_model-0.1.0 → python_library_ralf_model-0.1.2}/example.bat +0 -0
  16. {python_library_ralf_model-0.1.0 → python_library_ralf_model-0.1.2}/ralf_model/__init__.py +0 -0
  17. {python_library_ralf_model-0.1.0 → python_library_ralf_model-0.1.2}/ralf_model/abc.py +0 -0
  18. {python_library_ralf_model-0.1.0 → python_library_ralf_model-0.1.2}/test.bat +0 -0
  19. {python_library_ralf_model-0.1.0 → python_library_ralf_model-0.1.2}/tests/__init__.py +0 -0
@@ -0,0 +1,21 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ .env
9
+ .env.*
10
+ !.env.example
11
+ # packages 示例本地密钥(*.example 为模板,可提交)
12
+ packages/**/examples/**/.env
13
+ !packages/**/examples/**/.env.example
14
+ packages/**/examples/**/mcp.json
15
+ !packages/**/examples/**/mcp.json.example
16
+ packages/**/examples/**/.sandbox/
17
+ .pytest_cache/
18
+ config.yaml
19
+ logs/
20
+ .cursor/
21
+ uv.lock
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-library-ralf-model
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Requires-Python: >=3.10
5
5
  Requires-Dist: pydantic<3,>=2.0
6
6
  Description-Content-Type: text/markdown
@@ -41,6 +41,8 @@ doc = load_ralf_file(
41
41
  ## 能力范围
42
42
 
43
43
  - **block**:定义 ``block 名 { ... }``;简单映射 ``block 名 @地址;``;赋值与可选路径、地址 ``block 左名 = 右名``、``block 左名 = 右名 (hdl路径)``、``... @地址``,可与 ``{ ... }`` 组合。
44
+ - **register**:``register 名 [ (hdl路径) ] [ @字节偏移 ] { ... }`` 或 ``register 名;`` 前向引用。
45
+ - **field**:``field 名 [(hdl路径)] [ @位偏移 ] { ... }``(括号路径可紧贴字段名,如 ``field f(hdl.f)``)。
44
46
  - `field` 花括号内按**源顺序**保留各条语句(含 `enum { ... };` 等),便于往返。
45
47
  - `@` 后的偏移在写出时统一为 Verilog 风格十六进制字面量(如 `'h5`);`bytes` 等为十进制。
46
48
 
@@ -34,6 +34,8 @@ doc = load_ralf_file(
34
34
  ## 能力范围
35
35
 
36
36
  - **block**:定义 ``block 名 { ... }``;简单映射 ``block 名 @地址;``;赋值与可选路径、地址 ``block 左名 = 右名``、``block 左名 = 右名 (hdl路径)``、``... @地址``,可与 ``{ ... }`` 组合。
37
+ - **register**:``register 名 [ (hdl路径) ] [ @字节偏移 ] { ... }`` 或 ``register 名;`` 前向引用。
38
+ - **field**:``field 名 [(hdl路径)] [ @位偏移 ] { ... }``(括号路径可紧贴字段名,如 ``field f(hdl.f)``)。
37
39
  - `field` 花括号内按**源顺序**保留各条语句(含 `enum { ... };` 等),便于往返。
38
40
  - `@` 后的偏移在写出时统一为 Verilog 风格十六进制字面量(如 `'h5`);`bytes` 等为十进制。
39
41
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "python-library-ralf-model"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -12,6 +12,8 @@ def _fmt_at_int(v: int) -> str:
12
12
 
13
13
  def _emit_field(f: FieldNode, indent: str) -> list[str]:
14
14
  head = f"{indent}field {f.name}"
15
+ if f.paren_path is not None:
16
+ head += f"({f.paren_path})"
15
17
  if f.offset_bits is not None:
16
18
  head += f" @{_fmt_at_int(f.offset_bits)}"
17
19
  head += " {"
@@ -26,6 +28,8 @@ def _emit_field(f: FieldNode, indent: str) -> list[str]:
26
28
 
27
29
  def _emit_register(r: RegisterNode, indent: str) -> list[str]:
28
30
  head = f"{indent}register {r.name}"
31
+ if r.paren_path is not None:
32
+ head += f" ({r.paren_path})"
29
33
  if r.offset_bytes is not None:
30
34
  head += f" @{_fmt_at_int(r.offset_bytes)}"
31
35
  if r.declaration_only:
@@ -10,10 +10,22 @@ class RalfError(Exception):
10
10
  class RalfParseError(RalfError):
11
11
  """文本不符合预期语法或词法。"""
12
12
 
13
- def __init__(self, message: str, *, line: int, col: int) -> None:
14
- super().__init__(f"{message} (行 {line}, 列 {col})")
13
+ def __init__(
14
+ self,
15
+ message: str,
16
+ *,
17
+ line: int,
18
+ col: int,
19
+ path: Path | None = None,
20
+ ) -> None:
21
+ loc = f"行 {line}, 列 {col}"
22
+ if path is not None:
23
+ loc = f"{path}: {loc}"
24
+ super().__init__(f"{message} ({loc})")
25
+ self.message = message
15
26
  self.line = line
16
27
  self.col = col
28
+ self.path = path
17
29
 
18
30
 
19
31
  class RalfSourceError(RalfError):
@@ -20,9 +20,13 @@ def load_ralf_file(
20
20
  p = Path(path).resolve()
21
21
  text = p.read_text(encoding=encoding)
22
22
  inc = tuple(Path(x).resolve() for x in (include_paths or ()))
23
+ line_sources: list[Path] | None = None
23
24
  if expand_source:
24
- text = expand_ralf_sources(text, current_file=p, include_paths=inc, encoding=encoding)
25
- return parse_ralf(text)
25
+ text, line_sources = expand_ralf_sources(
26
+ text, current_file=p, include_paths=inc, encoding=encoding
27
+ )
28
+ return parse_ralf(text, line_sources=line_sources)
29
+ return parse_ralf(text, path=p)
26
30
 
27
31
 
28
32
  def loads_ralf(
@@ -38,13 +42,14 @@ def loads_ralf(
38
42
  virtual = bd / "__inline__.ralf"
39
43
  inc = tuple(Path(x).resolve() for x in (include_paths or ()))
40
44
  if expand_source:
41
- text = expand_ralf_sources(
45
+ text, line_sources = expand_ralf_sources(
42
46
  text,
43
47
  current_file=virtual,
44
48
  include_paths=inc,
45
49
  encoding=encoding,
46
50
  )
47
- return parse_ralf(text)
51
+ return parse_ralf(text, line_sources=line_sources)
52
+ return parse_ralf(text, path=virtual)
48
53
 
49
54
 
50
55
  def dump_ralf_file(doc: RalfDocument, path: str | Path, *, encoding: str = "utf-8") -> None:
@@ -9,6 +9,10 @@ class FieldNode(BaseModel):
9
9
  model_config = ConfigDict(extra="forbid")
10
10
 
11
11
  name: str
12
+ paren_path: str | None = Field(
13
+ default=None,
14
+ description="紧跟在 field 名后的圆括号路径内容(不含括号),如 ``field f(hdl.sig)``",
15
+ )
12
16
  offset_bits: int | None = Field(default=None, description="field 内 `@` 后的位偏移")
13
17
  inner_statements: list[str] = Field(
14
18
  default_factory=list,
@@ -22,6 +26,10 @@ class RegisterNode(BaseModel):
22
26
  model_config = ConfigDict(extra="forbid")
23
27
 
24
28
  name: str
29
+ paren_path: str | None = Field(
30
+ default=None,
31
+ description="紧跟在 register 名后的圆括号路径内容(不含括号),如 ``register r (dut.reg)``",
32
+ )
25
33
  offset_bytes: int | None = Field(default=None, description="register 后的 `@` 字节偏移")
26
34
  bytes_width: int | None = None
27
35
  fields: list[FieldNode] = Field(default_factory=list)
@@ -1,18 +1,28 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
+ from collections.abc import Sequence
5
+ from pathlib import Path
4
6
 
5
7
  from ralf_model.errors import RalfParseError
6
8
  from ralf_model.nodes import BlockNode, FieldNode, RalfDocument, RegisterNode
7
9
 
8
10
 
9
11
  class _Parser:
10
- def __init__(self, text: str) -> None:
12
+ def __init__(
13
+ self,
14
+ text: str,
15
+ *,
16
+ path: Path | None = None,
17
+ line_sources: Sequence[Path] | None = None,
18
+ ) -> None:
11
19
  self._s = text
12
20
  self._n = len(text)
13
21
  self._i = 0
14
22
  self.line = 1
15
23
  self.col = 1
24
+ self._path = path
25
+ self._line_sources = line_sources
16
26
 
17
27
  def _advance(self, n: int = 1) -> None:
18
28
  for _ in range(n):
@@ -30,7 +40,10 @@ class _Parser:
30
40
  return self._s[j] if j < self._n else None
31
41
 
32
42
  def _error(self, msg: str) -> None:
33
- raise RalfParseError(msg, line=self.line, col=self.col)
43
+ path = self._path
44
+ if self._line_sources and 0 < self.line <= len(self._line_sources):
45
+ path = self._line_sources[self.line - 1]
46
+ raise RalfParseError(msg, line=self.line, col=self.col, path=path)
34
47
 
35
48
  def skip_ws_and_comments(self) -> None:
36
49
  while self._i < self._n:
@@ -336,6 +349,10 @@ class _Parser:
336
349
  self.expect_keyword("register")
337
350
  name = self.read_ident()
338
351
  self.skip_ws_and_comments()
352
+ paren_path: str | None = None
353
+ if self._peek() == "(":
354
+ paren_path = self.read_round_paren_inner()
355
+ self.skip_ws_and_comments()
339
356
  offset: int | None = None
340
357
  if self._peek() == "@":
341
358
  self._advance()
@@ -345,6 +362,7 @@ class _Parser:
345
362
  self._advance()
346
363
  return RegisterNode(
347
364
  name=name,
365
+ paren_path=paren_path,
348
366
  offset_bytes=offset,
349
367
  declaration_only=True,
350
368
  )
@@ -354,6 +372,7 @@ class _Parser:
354
372
  self.expect_char("}")
355
373
  return RegisterNode(
356
374
  name=name,
375
+ paren_path=paren_path,
357
376
  offset_bytes=offset,
358
377
  bytes_width=rbw,
359
378
  fields=fields,
@@ -383,6 +402,10 @@ class _Parser:
383
402
  self.expect_keyword("field")
384
403
  name = self.read_ident()
385
404
  self.skip_ws_and_comments()
405
+ paren_path: str | None = None
406
+ if self._peek() == "(":
407
+ paren_path = self.read_round_paren_inner()
408
+ self.skip_ws_and_comments()
386
409
  off_bits: int | None = None
387
410
  if self._peek() == "@":
388
411
  self._advance()
@@ -390,7 +413,9 @@ class _Parser:
390
413
  self.expect_char("{")
391
414
  fn = self._parse_field_body()
392
415
  self.expect_char("}")
393
- return fn.model_copy(update={"name": name, "offset_bits": off_bits})
416
+ return fn.model_copy(
417
+ update={"name": name, "paren_path": paren_path, "offset_bits": off_bits}
418
+ )
394
419
 
395
420
  def _parse_field_body(self) -> FieldNode:
396
421
  inner: list[str] = []
@@ -519,9 +544,14 @@ def _parse_verilog_sized(s: str, i: int) -> tuple[int, int]:
519
544
  _VER_WS = re.compile(r"\s+")
520
545
 
521
546
 
522
- def parse_ralf(text: str) -> RalfDocument:
547
+ def parse_ralf(
548
+ text: str,
549
+ *,
550
+ path: Path | None = None,
551
+ line_sources: Sequence[Path] | None = None,
552
+ ) -> RalfDocument:
523
553
  """将 RALF 源文本解析为 `RalfDocument`。"""
524
- p = _Parser(text)
554
+ p = _Parser(text, path=path, line_sources=line_sources)
525
555
  doc = p.parse_document()
526
556
  return doc
527
557
 
@@ -109,11 +109,13 @@ def expand_ralf_sources(
109
109
  include_paths: Sequence[Path] = (),
110
110
  encoding: str = "utf-8",
111
111
  _chain: tuple[Path, ...] = (),
112
- ) -> str:
112
+ ) -> tuple[str, list[Path]]:
113
113
  """将 Tcl 风格 ``source path`` 递归展开为单段 RALF 文本后再交给 ``parse_ralf``。
114
114
 
115
115
  ``current_file`` 用于确定相对路径的基准目录(通常为 ``path.parent``),并参与循环检测。
116
116
  从内存加载字符串时可使用 ``base_dir / \"__inline__.ralf\"`` 这类占位路径。
117
+
118
+ 返回 ``(展开后文本, 行来源)``:``行来源[i]`` 对应展开结果第 ``i+1`` 行的源文件路径。
117
119
  """
118
120
  cf = current_file.resolve()
119
121
  if cf in _chain:
@@ -123,15 +125,17 @@ def expand_ralf_sources(
123
125
  inc = tuple(Path(p).resolve() for p in include_paths)
124
126
 
125
127
  out: list[str] = []
128
+ line_sources: list[Path] = []
126
129
  for line in text.splitlines(keepends=True):
127
130
  spec = _parse_source_argument(line)
128
131
  if spec is None:
129
132
  out.append(line)
133
+ line_sources.append(cf)
130
134
  continue
131
135
 
132
136
  inner_path = resolve_source_path(spec, base_dir=cf.parent, include_paths=inc)
133
137
  inner_text = inner_path.read_text(encoding=encoding)
134
- expanded_inner = expand_ralf_sources(
138
+ expanded_inner, inner_sources = expand_ralf_sources(
135
139
  inner_text,
136
140
  current_file=inner_path,
137
141
  include_paths=inc,
@@ -139,7 +143,9 @@ def expand_ralf_sources(
139
143
  _chain=chain,
140
144
  )
141
145
  out.append(expanded_inner)
146
+ line_sources.extend(inner_sources)
142
147
  if expanded_inner and not expanded_inner.endswith("\n"):
143
148
  out.append("\n")
149
+ line_sources.append(inner_sources[-1] if inner_sources else inner_path)
144
150
 
145
- return "".join(out)
151
+ return "".join(out), line_sources
@@ -1,8 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import tempfile
3
4
  import unittest
5
+ from pathlib import Path
4
6
 
5
- from ralf_model import dump_ralf, parse_ralf
7
+ from ralf_model import RalfParseError, dump_ralf, load_ralf_file, parse_ralf
6
8
  from ralf_model.parse import normalize_ralf_whitespace
7
9
 
8
10
 
@@ -146,6 +148,69 @@ block top {
146
148
  out = dump_ralf(doc)
147
149
  self.assertIn("register R0;", out)
148
150
 
151
+ def test_register_paren_path_at_offset(self) -> None:
152
+ src = """
153
+ block top {
154
+ bytes 4;
155
+ register CTRL (dut.ctrl_reg) @'h0 {
156
+ bytes 4;
157
+ field ena(dut.ctrl_reg.en) @1 {
158
+ bits 1;
159
+ access rw;
160
+ }
161
+ }
162
+ }
163
+ """
164
+ doc = parse_ralf(src)
165
+ reg = doc.blocks[0].registers[0]
166
+ self.assertEqual(reg.name, "CTRL")
167
+ self.assertEqual(reg.paren_path, "dut.ctrl_reg")
168
+ self.assertEqual(reg.offset_bytes, 0)
169
+ fld = reg.fields[0]
170
+ self.assertEqual(fld.name, "ena")
171
+ self.assertEqual(fld.paren_path, "dut.ctrl_reg.en")
172
+ self.assertEqual(fld.offset_bits, 1)
173
+ out = dump_ralf(doc)
174
+ self.assertIn("register CTRL (dut.ctrl_reg) @'h0", out)
175
+ self.assertIn("field ena(dut.ctrl_reg.en) @'h1", out)
176
+ doc2 = parse_ralf(out)
177
+ self.assertEqual(doc.model_dump(), doc2.model_dump())
178
+
179
+ def test_register_paren_path_forward_decl(self) -> None:
180
+ src = """
181
+ block top {
182
+ register R0 (hdl.r0) @'h10;
183
+ }
184
+ """
185
+ doc = parse_ralf(src)
186
+ r = doc.blocks[0].registers[0]
187
+ self.assertTrue(r.declaration_only)
188
+ self.assertEqual(r.paren_path, "hdl.r0")
189
+ self.assertEqual(r.offset_bytes, 0x10)
190
+ out = dump_ralf(doc)
191
+ self.assertIn("register R0 (hdl.r0) @'h10;", out)
192
+ doc2 = parse_ralf(out)
193
+ self.assertEqual(doc.model_dump(), doc2.model_dump())
194
+
195
+ def test_field_paren_path_no_space(self) -> None:
196
+ src = """
197
+ block top {
198
+ register r {
199
+ bytes 4;
200
+ field sig(hdl.sig) @0 {
201
+ bits 4;
202
+ }
203
+ }
204
+ }
205
+ """
206
+ doc = parse_ralf(src)
207
+ fld = doc.blocks[0].registers[0].fields[0]
208
+ self.assertEqual(fld.paren_path, "hdl.sig")
209
+ out = dump_ralf(doc)
210
+ self.assertIn("field sig(hdl.sig)", out)
211
+ doc2 = parse_ralf(out)
212
+ self.assertEqual(doc.model_dump(), doc2.model_dump())
213
+
149
214
  def test_block_ref_rhs_with_parentheses(self) -> None:
150
215
  src = """
151
216
  block top {
@@ -176,5 +241,29 @@ block a {
176
241
  self.assertNotIn("//", n)
177
242
 
178
243
 
244
+ class ParseErrorTests(unittest.TestCase):
245
+ def test_parse_error_includes_file_path(self) -> None:
246
+ with tempfile.TemporaryDirectory() as td:
247
+ bad = Path(td) / "bad.ralf"
248
+ bad.write_text("block x {\n ???\n}\n", encoding="utf-8")
249
+ with self.assertRaises(RalfParseError) as ctx:
250
+ load_ralf_file(bad, expand_source=False)
251
+ self.assertIn(str(bad.resolve()), str(ctx.exception))
252
+
253
+ def test_parse_error_in_included_file(self) -> None:
254
+ with tempfile.TemporaryDirectory() as td:
255
+ root = Path(td)
256
+ inc = root / "inc"
257
+ inc.mkdir()
258
+ (inc / "broken.ralf").write_text("block y {\n ???\n}\n", encoding="utf-8")
259
+ top = root / "top.ralf"
260
+ top.write_text('source "inc/broken.ralf"\n', encoding="utf-8")
261
+ broken = (inc / "broken.ralf").resolve()
262
+ with self.assertRaises(RalfParseError) as ctx:
263
+ load_ralf_file(top)
264
+ self.assertIn(str(broken), str(ctx.exception))
265
+ self.assertNotIn(str(top.resolve()), str(ctx.exception))
266
+
267
+
179
268
  if __name__ == "__main__":
180
269
  unittest.main()
@@ -51,7 +51,7 @@ class SourceIncludeTests(unittest.TestCase):
51
51
  top = root / "top.ralf"
52
52
  top.write_text('source "mid.ralf"\n', encoding="utf-8")
53
53
 
54
- out = expand_ralf_sources(
54
+ out, _line_sources = expand_ralf_sources(
55
55
  top.read_text(encoding="utf-8"),
56
56
  current_file=top.resolve(),
57
57
  include_paths=(),
@@ -1,12 +0,0 @@
1
- __pycache__/
2
- *.pyc
3
- *.pyo
4
- *.egg-info/
5
- dist/
6
- build/
7
- .venv/
8
- .env
9
- .pytest_cache/
10
- config.yaml
11
- logs/
12
- .cursor/