python-library-ralf-model 0.1.2__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.2 → python_library_ralf_model-0.1.3}/PKG-INFO +1 -1
  2. {python_library_ralf_model-0.1.2 → python_library_ralf_model-0.1.3}/pyproject.toml +1 -1
  3. {python_library_ralf_model-0.1.2 → python_library_ralf_model-0.1.3}/ralf_model/__init__.py +2 -1
  4. {python_library_ralf_model-0.1.2 → python_library_ralf_model-0.1.3}/ralf_model/emit.py +39 -3
  5. {python_library_ralf_model-0.1.2 → python_library_ralf_model-0.1.3}/ralf_model/nodes.py +29 -1
  6. {python_library_ralf_model-0.1.2 → python_library_ralf_model-0.1.3}/ralf_model/parse.py +142 -14
  7. {python_library_ralf_model-0.1.2 → python_library_ralf_model-0.1.3}/tests/test_ralf_model.py +52 -0
  8. {python_library_ralf_model-0.1.2 → python_library_ralf_model-0.1.3}/.gitignore +0 -0
  9. {python_library_ralf_model-0.1.2 → python_library_ralf_model-0.1.3}/README.md +0 -0
  10. {python_library_ralf_model-0.1.2 → python_library_ralf_model-0.1.3}/example/__main__.py +0 -0
  11. {python_library_ralf_model-0.1.2 → python_library_ralf_model-0.1.3}/example.bat +0 -0
  12. {python_library_ralf_model-0.1.2 → python_library_ralf_model-0.1.3}/ralf_model/abc.py +0 -0
  13. {python_library_ralf_model-0.1.2 → python_library_ralf_model-0.1.3}/ralf_model/errors.py +0 -0
  14. {python_library_ralf_model-0.1.2 → python_library_ralf_model-0.1.3}/ralf_model/io.py +0 -0
  15. {python_library_ralf_model-0.1.2 → python_library_ralf_model-0.1.3}/ralf_model/source_expand.py +0 -0
  16. {python_library_ralf_model-0.1.2 → python_library_ralf_model-0.1.3}/test.bat +0 -0
  17. {python_library_ralf_model-0.1.2 → python_library_ralf_model-0.1.3}/tests/__init__.py +0 -0
  18. {python_library_ralf_model-0.1.2 → 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.2
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "python-library-ralf-model"
7
- version = "0.1.2"
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:
@@ -58,6 +58,35 @@ def _emit_block_open_line(b: BlockNode, indent: str) -> str:
58
58
  return line
59
59
 
60
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
+
61
90
  def _emit_block(b: BlockNode, indent: str) -> list[str]:
62
91
  head_line = _emit_block_open_line(b, indent)
63
92
  if not b.has_body:
@@ -77,8 +106,15 @@ def _emit_block(b: BlockNode, indent: str) -> list[str]:
77
106
  def dump_ralf(doc: RalfDocument) -> str:
78
107
  """将 `RalfDocument` 序列化为 RALF 源文本(规范化排版)。"""
79
108
  out: list[str] = []
80
- for i, b in enumerate(doc.blocks):
81
- 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:
82
117
  out.append("")
118
+ first = False
83
119
  out.extend(_emit_block(b, ""))
84
120
  return "\n".join(out) + ("\n" if out else "")
@@ -69,9 +69,37 @@ class BlockNode(BaseModel):
69
69
  blocks: list[BlockNode] = Field(default_factory=list)
70
70
 
71
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
+
72
99
  class RalfDocument(BaseModel):
73
- """顶层 RALF 文件内容(当前实现要求顶层为若干 `block`)。"""
100
+ """顶层 RALF 文件内容(顶层为若干 `system`;亦兼容顶层 `block`)。"""
74
101
 
75
102
  model_config = ConfigDict(extra="forbid")
76
103
 
104
+ systems: list[SystemNode] = Field(default_factory=list)
77
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()
@@ -241,6 +241,41 @@ block a {
241
241
  self.assertNotIn("//", n)
242
242
 
243
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
+
244
279
  class ParseErrorTests(unittest.TestCase):
245
280
  def test_parse_error_includes_file_path(self) -> None:
246
281
  with tempfile.TemporaryDirectory() as td:
@@ -263,6 +298,23 @@ class ParseErrorTests(unittest.TestCase):
263
298
  load_ralf_file(top)
264
299
  self.assertIn(str(broken), str(ctx.exception))
265
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)
266
318
 
267
319
 
268
320
  if __name__ == "__main__":