python-library-ralf-model 0.1.1__tar.gz → 0.1.3__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 (18) hide show
  1. {python_library_ralf_model-0.1.1 → python_library_ralf_model-0.1.3}/PKG-INFO +3 -1
  2. {python_library_ralf_model-0.1.1 → python_library_ralf_model-0.1.3}/README.md +2 -0
  3. {python_library_ralf_model-0.1.1 → python_library_ralf_model-0.1.3}/pyproject.toml +1 -1
  4. {python_library_ralf_model-0.1.1 → python_library_ralf_model-0.1.3}/ralf_model/__init__.py +2 -1
  5. {python_library_ralf_model-0.1.1 → python_library_ralf_model-0.1.3}/ralf_model/emit.py +43 -3
  6. {python_library_ralf_model-0.1.1 → python_library_ralf_model-0.1.3}/ralf_model/nodes.py +37 -1
  7. {python_library_ralf_model-0.1.1 → python_library_ralf_model-0.1.3}/ralf_model/parse.py +155 -15
  8. {python_library_ralf_model-0.1.1 → python_library_ralf_model-0.1.3}/tests/test_ralf_model.py +115 -0
  9. {python_library_ralf_model-0.1.1 → python_library_ralf_model-0.1.3}/.gitignore +0 -0
  10. {python_library_ralf_model-0.1.1 → python_library_ralf_model-0.1.3}/example/__main__.py +0 -0
  11. {python_library_ralf_model-0.1.1 → python_library_ralf_model-0.1.3}/example.bat +0 -0
  12. {python_library_ralf_model-0.1.1 → python_library_ralf_model-0.1.3}/ralf_model/abc.py +0 -0
  13. {python_library_ralf_model-0.1.1 → python_library_ralf_model-0.1.3}/ralf_model/errors.py +0 -0
  14. {python_library_ralf_model-0.1.1 → python_library_ralf_model-0.1.3}/ralf_model/io.py +0 -0
  15. {python_library_ralf_model-0.1.1 → python_library_ralf_model-0.1.3}/ralf_model/source_expand.py +0 -0
  16. {python_library_ralf_model-0.1.1 → python_library_ralf_model-0.1.3}/test.bat +0 -0
  17. {python_library_ralf_model-0.1.1 → python_library_ralf_model-0.1.3}/tests/__init__.py +0 -0
  18. {python_library_ralf_model-0.1.1 → python_library_ralf_model-0.1.3}/tests/test_source_include.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-library-ralf-model
3
- Version: 0.1.1
3
+ Version: 0.1.3
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.1"
7
+ version = "0.1.3"
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -4,7 +4,7 @@ from ralf_model.abc import AbstractRalfBlock, AbstractRalfField, AbstractRalfReg
4
4
  from ralf_model.emit import dump_ralf
5
5
  from ralf_model.errors import RalfError, RalfParseError, RalfSourceError
6
6
  from ralf_model.io import dump_ralf_file, dumps_ralf, load_ralf_file, loads_ralf
7
- from ralf_model.nodes import BlockNode, FieldNode, RalfDocument, RegisterNode
7
+ from ralf_model.nodes import BlockNode, FieldNode, RalfDocument, RegisterNode, SystemNode
8
8
  from ralf_model.parse import normalize_ralf_whitespace, parse_ralf
9
9
  from ralf_model.source_expand import expand_ralf_sources, resolve_source_path
10
10
 
@@ -16,6 +16,7 @@ __all__ = [
16
16
  "FieldNode",
17
17
  "RalfDocument",
18
18
  "RegisterNode",
19
+ "SystemNode",
19
20
  "RalfError",
20
21
  "RalfParseError",
21
22
  "RalfSourceError",
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from ralf_model.nodes import BlockNode, FieldNode, RalfDocument, RegisterNode
3
+ from ralf_model.nodes import BlockNode, FieldNode, RalfDocument, RegisterNode, SystemNode
4
4
 
5
5
 
6
6
  def _fmt_at_int(v: int) -> str:
@@ -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:
@@ -54,6 +58,35 @@ def _emit_block_open_line(b: BlockNode, indent: str) -> str:
54
58
  return line
55
59
 
56
60
 
61
+ def _emit_system_open_line(s: SystemNode, indent: str) -> str:
62
+ line = f"{indent}system {s.name}"
63
+ if s.rhs_head is not None:
64
+ line += f" = {s.rhs_head}"
65
+ if s.rhs_paren_path is not None:
66
+ line += f" ({s.rhs_paren_path})"
67
+ if s.base_address is not None:
68
+ line += f" @{_fmt_at_int(s.base_address)}"
69
+ elif s.base_address is not None:
70
+ line += f" @{_fmt_at_int(s.base_address)}"
71
+ return line
72
+
73
+
74
+ def _emit_system(s: SystemNode, indent: str) -> list[str]:
75
+ head_line = _emit_system_open_line(s, indent)
76
+ if not s.has_body:
77
+ return [f"{head_line};"]
78
+ lines = [head_line + " {"]
79
+ inner = indent + " "
80
+ if s.bytes_width is not None:
81
+ lines.append(f"{inner}bytes {s.bytes_width};")
82
+ for sub in s.systems:
83
+ lines.extend(_emit_system(sub, inner))
84
+ for sub in s.blocks:
85
+ lines.extend(_emit_block(sub, inner))
86
+ lines.append(indent + "}")
87
+ return lines
88
+
89
+
57
90
  def _emit_block(b: BlockNode, indent: str) -> list[str]:
58
91
  head_line = _emit_block_open_line(b, indent)
59
92
  if not b.has_body:
@@ -73,8 +106,15 @@ def _emit_block(b: BlockNode, indent: str) -> list[str]:
73
106
  def dump_ralf(doc: RalfDocument) -> str:
74
107
  """将 `RalfDocument` 序列化为 RALF 源文本(规范化排版)。"""
75
108
  out: list[str] = []
76
- for i, b in enumerate(doc.blocks):
77
- if i:
109
+ first = True
110
+ for s in doc.systems:
111
+ if not first:
112
+ out.append("")
113
+ first = False
114
+ out.extend(_emit_system(s, ""))
115
+ for b in doc.blocks:
116
+ if not first:
78
117
  out.append("")
118
+ first = False
79
119
  out.extend(_emit_block(b, ""))
80
120
  return "\n".join(out) + ("\n" if out else "")
@@ -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)
@@ -61,9 +69,37 @@ class BlockNode(BaseModel):
61
69
  blocks: list[BlockNode] = Field(default_factory=list)
62
70
 
63
71
 
72
+ class SystemNode(BaseModel):
73
+ """层次化 system;体内可嵌套 system 与 block,不含 register。"""
74
+
75
+ model_config = ConfigDict(extra="forbid")
76
+
77
+ name: str
78
+ rhs_head: str | None = Field(
79
+ default=None,
80
+ description="`=` 右侧起始的层级名(含可能的 `[..]` 后缀)",
81
+ )
82
+ rhs_paren_path: str | None = Field(
83
+ default=None,
84
+ description="紧跟在 rhs_head 后的圆括号路径内容(不含括号)",
85
+ )
86
+ base_address: int | None = Field(
87
+ default=None,
88
+ description="`@` 后的地址;可出现于简单 `system 名 @addr` 或 `=` 形式末尾",
89
+ )
90
+ has_body: bool = Field(
91
+ default=True,
92
+ description="是否带有 `{ ... }`;仅分号结尾的声明为 False",
93
+ )
94
+ bytes_width: int | None = None
95
+ systems: list[SystemNode] = Field(default_factory=list)
96
+ blocks: list[BlockNode] = Field(default_factory=list)
97
+
98
+
64
99
  class RalfDocument(BaseModel):
65
- """顶层 RALF 文件内容(当前实现要求顶层为若干 `block`)。"""
100
+ """顶层 RALF 文件内容(顶层为若干 `system`;亦兼容顶层 `block`)。"""
66
101
 
67
102
  model_config = ConfigDict(extra="forbid")
68
103
 
104
+ systems: list[SystemNode] = Field(default_factory=list)
69
105
  blocks: list[BlockNode] = Field(default_factory=list)
@@ -5,7 +5,7 @@ from collections.abc import Sequence
5
5
  from pathlib import Path
6
6
 
7
7
  from ralf_model.errors import RalfParseError
8
- from ralf_model.nodes import BlockNode, FieldNode, RalfDocument, RegisterNode
8
+ from ralf_model.nodes import BlockNode, FieldNode, RalfDocument, RegisterNode, SystemNode
9
9
 
10
10
 
11
11
  class _Parser:
@@ -39,11 +39,22 @@ class _Parser:
39
39
  j = self._i + offset
40
40
  return self._s[j] if j < self._n else None
41
41
 
42
- def _error(self, msg: str) -> None:
42
+ def _error_location(self) -> tuple[Path | None, int]:
43
43
  path = self._path
44
+ line = self.line
44
45
  if self._line_sources and 0 < self.line <= len(self._line_sources):
45
46
  path = self._line_sources[self.line - 1]
46
- raise RalfParseError(msg, line=self.line, col=self.col, path=path)
47
+ local = 1
48
+ idx = self.line - 2
49
+ while idx >= 0 and self._line_sources[idx] == path:
50
+ local += 1
51
+ idx -= 1
52
+ line = local
53
+ return path, line
54
+
55
+ def _error(self, msg: str) -> None:
56
+ path, line = self._error_location()
57
+ raise RalfParseError(msg, line=line, col=self.col, path=path)
47
58
 
48
59
  def skip_ws_and_comments(self) -> None:
49
60
  while self._i < self._n:
@@ -168,16 +179,19 @@ class _Parser:
168
179
  self._error("期望 ;")
169
180
 
170
181
  def parse_document(self) -> RalfDocument:
182
+ systems: list[SystemNode] = []
171
183
  blocks: list[BlockNode] = []
172
184
  self.skip_ws_and_comments()
173
185
  while self._i < self._n:
174
186
  kw = self._peek_keyword()
175
- if kw == "block":
187
+ if kw == "system":
188
+ systems.append(self.parse_system())
189
+ elif kw == "block":
176
190
  blocks.append(self.parse_block())
177
191
  else:
178
- self._error(f"顶层期望 block,得到 {kw!r}")
192
+ self._error(f"顶层期望 system 或 block,得到 {kw!r}")
179
193
  self.skip_ws_and_comments()
180
- return RalfDocument(blocks=blocks)
194
+ return RalfDocument(systems=systems, blocks=blocks)
181
195
 
182
196
  def _peek_keyword(self) -> str:
183
197
  self.skip_ws_and_comments()
@@ -261,8 +275,8 @@ class _Parser:
261
275
  self.skip_ws_and_comments()
262
276
  return head, paren_inner, addr
263
277
 
264
- def parse_block(self) -> BlockNode:
265
- self.expect_keyword("block")
278
+ def parse_system(self) -> SystemNode:
279
+ self.expect_keyword("system")
266
280
  name = self.read_hierarchical_block_name()
267
281
  self.skip_ws_and_comments()
268
282
 
@@ -270,14 +284,128 @@ class _Parser:
270
284
  self._advance()
271
285
  addr = self.parse_integer_value()
272
286
  self.skip_ws_and_comments()
273
- self.expect_char(";")
274
- return BlockNode(
287
+ if self._peek() == ";":
288
+ self._advance()
289
+ return SystemNode(
290
+ name=name,
291
+ base_address=addr,
292
+ has_body=False,
293
+ systems=[],
294
+ blocks=[],
295
+ )
296
+ if self._peek() == "{":
297
+ self._advance()
298
+ bw, subs_sys, subs_blk = self._parse_system_body()
299
+ self.expect_char("}")
300
+ return SystemNode(
301
+ name=name,
302
+ base_address=addr,
303
+ has_body=True,
304
+ bytes_width=bw,
305
+ systems=subs_sys,
306
+ blocks=subs_blk,
307
+ )
308
+ self._error("system @地址 之后应为 ; 或 {")
309
+
310
+ if self._peek() == "=":
311
+ self._advance()
312
+ self.skip_ws_and_comments()
313
+ rhs_head, rhs_path, addr_rhs = self.parse_block_rhs_after_equals()
314
+ self.skip_ws_and_comments()
315
+ if self._peek() == ";":
316
+ self._advance()
317
+ return SystemNode(
318
+ name=name,
319
+ rhs_head=rhs_head,
320
+ rhs_paren_path=rhs_path,
321
+ base_address=addr_rhs,
322
+ has_body=False,
323
+ systems=[],
324
+ blocks=[],
325
+ )
326
+ if self._peek() == "{":
327
+ self._advance()
328
+ bw, subs_sys, subs_blk = self._parse_system_body()
329
+ self.expect_char("}")
330
+ return SystemNode(
331
+ name=name,
332
+ rhs_head=rhs_head,
333
+ rhs_paren_path=rhs_path,
334
+ base_address=addr_rhs,
335
+ has_body=True,
336
+ bytes_width=bw,
337
+ systems=subs_sys,
338
+ blocks=subs_blk,
339
+ )
340
+ self._error("system ... = ... 之后应为 ; 或 {")
341
+
342
+ if self._peek() == "{":
343
+ self._advance()
344
+ bw, subs_sys, subs_blk = self._parse_system_body()
345
+ self.expect_char("}")
346
+ return SystemNode(
275
347
  name=name,
276
- base_address=addr,
277
- has_body=False,
278
- registers=[],
279
- blocks=[],
348
+ has_body=True,
349
+ bytes_width=bw,
350
+ systems=subs_sys,
351
+ blocks=subs_blk,
280
352
  )
353
+ self._error("system 名称之后应为 @、= 或 {")
354
+
355
+ def _parse_system_body(
356
+ self,
357
+ ) -> tuple[int | None, list[SystemNode], list[BlockNode]]:
358
+ bw: int | None = None
359
+ subs_sys: list[SystemNode] = []
360
+ subs_blk: list[BlockNode] = []
361
+ while True:
362
+ self.skip_ws_and_comments()
363
+ if self._peek() == "}":
364
+ break
365
+ kw = self._peek_keyword()
366
+ if kw == "bytes":
367
+ self.expect_keyword("bytes")
368
+ bw = self.parse_integer_value()
369
+ self.expect_char(";")
370
+ elif kw == "system":
371
+ subs_sys.append(self.parse_system())
372
+ elif kw == "block":
373
+ subs_blk.append(self.parse_block())
374
+ else:
375
+ self._error(f"system 内出现未识别的内容 {kw!r}")
376
+ return bw, subs_sys, subs_blk
377
+
378
+ def parse_block(self) -> BlockNode:
379
+ self.expect_keyword("block")
380
+ name = self.read_hierarchical_block_name()
381
+ self.skip_ws_and_comments()
382
+
383
+ if self._peek() == "@":
384
+ self._advance()
385
+ addr = self.parse_integer_value()
386
+ self.skip_ws_and_comments()
387
+ if self._peek() == ";":
388
+ self._advance()
389
+ return BlockNode(
390
+ name=name,
391
+ base_address=addr,
392
+ has_body=False,
393
+ registers=[],
394
+ blocks=[],
395
+ )
396
+ if self._peek() == "{":
397
+ self._advance()
398
+ bw, regs, subs = self._parse_block_body()
399
+ self.expect_char("}")
400
+ return BlockNode(
401
+ name=name,
402
+ base_address=addr,
403
+ has_body=True,
404
+ bytes_width=bw,
405
+ registers=regs,
406
+ blocks=subs,
407
+ )
408
+ self._error("block @地址 之后应为 ; 或 {")
281
409
 
282
410
  if self._peek() == "=":
283
411
  self._advance()
@@ -349,6 +477,10 @@ class _Parser:
349
477
  self.expect_keyword("register")
350
478
  name = self.read_ident()
351
479
  self.skip_ws_and_comments()
480
+ paren_path: str | None = None
481
+ if self._peek() == "(":
482
+ paren_path = self.read_round_paren_inner()
483
+ self.skip_ws_and_comments()
352
484
  offset: int | None = None
353
485
  if self._peek() == "@":
354
486
  self._advance()
@@ -358,6 +490,7 @@ class _Parser:
358
490
  self._advance()
359
491
  return RegisterNode(
360
492
  name=name,
493
+ paren_path=paren_path,
361
494
  offset_bytes=offset,
362
495
  declaration_only=True,
363
496
  )
@@ -367,6 +500,7 @@ class _Parser:
367
500
  self.expect_char("}")
368
501
  return RegisterNode(
369
502
  name=name,
503
+ paren_path=paren_path,
370
504
  offset_bytes=offset,
371
505
  bytes_width=rbw,
372
506
  fields=fields,
@@ -396,6 +530,10 @@ class _Parser:
396
530
  self.expect_keyword("field")
397
531
  name = self.read_ident()
398
532
  self.skip_ws_and_comments()
533
+ paren_path: str | None = None
534
+ if self._peek() == "(":
535
+ paren_path = self.read_round_paren_inner()
536
+ self.skip_ws_and_comments()
399
537
  off_bits: int | None = None
400
538
  if self._peek() == "@":
401
539
  self._advance()
@@ -403,7 +541,9 @@ class _Parser:
403
541
  self.expect_char("{")
404
542
  fn = self._parse_field_body()
405
543
  self.expect_char("}")
406
- return fn.model_copy(update={"name": name, "offset_bits": off_bits})
544
+ return fn.model_copy(
545
+ update={"name": name, "paren_path": paren_path, "offset_bits": off_bits}
546
+ )
407
547
 
408
548
  def _parse_field_body(self) -> FieldNode:
409
549
  inner: list[str] = []
@@ -148,6 +148,69 @@ block top {
148
148
  out = dump_ralf(doc)
149
149
  self.assertIn("register R0;", out)
150
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
+
151
214
  def test_block_ref_rhs_with_parentheses(self) -> None:
152
215
  src = """
153
216
  block top {
@@ -178,6 +241,41 @@ block a {
178
241
  self.assertNotIn("//", n)
179
242
 
180
243
 
244
+ class SystemParseTests(unittest.TestCase):
245
+ def test_top_system_nests_system_and_block(self) -> None:
246
+ src = """
247
+ system chip {
248
+ system cpu @0 {
249
+ block core {
250
+ register r @0 {
251
+ bytes 4;
252
+ field f {}
253
+ }
254
+ }
255
+ }
256
+ block peri @'h1000 {
257
+ register s @0 {
258
+ bytes 1;
259
+ field g {}
260
+ }
261
+ }
262
+ }
263
+ """
264
+ doc = parse_ralf(src)
265
+ self.assertEqual(len(doc.systems), 1)
266
+ chip = doc.systems[0]
267
+ self.assertEqual(chip.name, "chip")
268
+ self.assertEqual(len(chip.systems), 1)
269
+ self.assertEqual(chip.systems[0].name, "cpu")
270
+ self.assertEqual(chip.systems[0].base_address, 0)
271
+ self.assertEqual(chip.blocks[0].name, "peri")
272
+ self.assertEqual(chip.blocks[0].base_address, 0x1000)
273
+ core = chip.systems[0].blocks[0]
274
+ self.assertEqual(core.registers[0].name, "r")
275
+ doc2 = parse_ralf(dump_ralf(doc))
276
+ self.assertEqual(doc.model_dump(), doc2.model_dump())
277
+
278
+
181
279
  class ParseErrorTests(unittest.TestCase):
182
280
  def test_parse_error_includes_file_path(self) -> None:
183
281
  with tempfile.TemporaryDirectory() as td:
@@ -200,6 +298,23 @@ class ParseErrorTests(unittest.TestCase):
200
298
  load_ralf_file(top)
201
299
  self.assertIn(str(broken), str(ctx.exception))
202
300
  self.assertNotIn(str(top.resolve()), str(ctx.exception))
301
+ self.assertEqual(ctx.exception.line, 2)
302
+
303
+ def test_parse_error_line_in_included_file_after_filler(self) -> None:
304
+ with tempfile.TemporaryDirectory() as td:
305
+ root = Path(td)
306
+ inc = root / "inc"
307
+ inc.mkdir()
308
+ (inc / "broken.ralf").write_text(
309
+ "// line1\n// line2\nblock y {\n ???\n}\n",
310
+ encoding="utf-8",
311
+ )
312
+ top = root / "top.ralf"
313
+ filler = "\n".join(["// filler"] * 50)
314
+ top.write_text(filler + '\nsource "inc/broken.ralf"\n', encoding="utf-8")
315
+ with self.assertRaises(RalfParseError) as ctx:
316
+ load_ralf_file(top)
317
+ self.assertEqual(ctx.exception.line, 4)
203
318
 
204
319
 
205
320
  if __name__ == "__main__":