infinity-data 1.0.0__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 (56) hide show
  1. infinity_data-1.0.0/LICENSE +21 -0
  2. infinity_data-1.0.0/PKG-INFO +60 -0
  3. infinity_data-1.0.0/README.md +38 -0
  4. infinity_data-1.0.0/pyproject.toml +91 -0
  5. infinity_data-1.0.0/setup.cfg +4 -0
  6. infinity_data-1.0.0/src/infinity_data/__init__.py +41 -0
  7. infinity_data-1.0.0/src/infinity_data/emit/__init__.py +9 -0
  8. infinity_data-1.0.0/src/infinity_data/emit/converter.py +55 -0
  9. infinity_data-1.0.0/src/infinity_data/frontend.py +37 -0
  10. infinity_data-1.0.0/src/infinity_data/infra/__init__.py +4 -0
  11. infinity_data-1.0.0/src/infinity_data/infra/diagnostics.py +212 -0
  12. infinity_data-1.0.0/src/infinity_data/infra/file.py +83 -0
  13. infinity_data-1.0.0/src/infinity_data/infra/ll1_stream.py +71 -0
  14. infinity_data-1.0.0/src/infinity_data/infra/location.py +80 -0
  15. infinity_data-1.0.0/src/infinity_data/infra/path.py +46 -0
  16. infinity_data-1.0.0/src/infinity_data/parser/__init__.py +79 -0
  17. infinity_data-1.0.0/src/infinity_data/parser/diagnostics.py +92 -0
  18. infinity_data-1.0.0/src/infinity_data/parser/models.py +295 -0
  19. infinity_data-1.0.0/src/infinity_data/parser/parser.py +988 -0
  20. infinity_data-1.0.0/src/infinity_data/parser/token_stream.py +128 -0
  21. infinity_data-1.0.0/src/infinity_data/pipeline.py +252 -0
  22. infinity_data-1.0.0/src/infinity_data/sandbox/__init__.py +32 -0
  23. infinity_data-1.0.0/src/infinity_data/sandbox/config.py +61 -0
  24. infinity_data-1.0.0/src/infinity_data/sandbox/errors.py +106 -0
  25. infinity_data-1.0.0/src/infinity_data/sandbox/mediator.py +214 -0
  26. infinity_data-1.0.0/src/infinity_data/sandbox/schema.py +21 -0
  27. infinity_data-1.0.0/src/infinity_data/semantic/__init__.py +59 -0
  28. infinity_data-1.0.0/src/infinity_data/semantic/builder/__init__.py +29 -0
  29. infinity_data-1.0.0/src/infinity_data/semantic/builder/builder.py +523 -0
  30. infinity_data-1.0.0/src/infinity_data/semantic/builder/models.py +163 -0
  31. infinity_data-1.0.0/src/infinity_data/semantic/constraints.py +163 -0
  32. infinity_data-1.0.0/src/infinity_data/semantic/diagnostics.py +172 -0
  33. infinity_data-1.0.0/src/infinity_data/semantic/executor/__init__.py +10 -0
  34. infinity_data-1.0.0/src/infinity_data/semantic/executor/executor.py +259 -0
  35. infinity_data-1.0.0/src/infinity_data/semantic/registry/__init__.py +168 -0
  36. infinity_data-1.0.0/src/infinity_data/semantic/registry/_core.py +212 -0
  37. infinity_data-1.0.0/src/infinity_data/semantic/registry/dict_constraints.py +56 -0
  38. infinity_data-1.0.0/src/infinity_data/semantic/registry/general.py +362 -0
  39. infinity_data-1.0.0/src/infinity_data/semantic/registry/logic.py +134 -0
  40. infinity_data-1.0.0/src/infinity_data/semantic/registry/types.py +136 -0
  41. infinity_data-1.0.0/src/infinity_data/semantic/resolver/__init__.py +20 -0
  42. infinity_data-1.0.0/src/infinity_data/semantic/resolver/imports.py +191 -0
  43. infinity_data-1.0.0/src/infinity_data/semantic/resolver/models.py +67 -0
  44. infinity_data-1.0.0/src/infinity_data/semantic/resolver/resolver.py +345 -0
  45. infinity_data-1.0.0/src/infinity_data/tokenizer/__init__.py +16 -0
  46. infinity_data-1.0.0/src/infinity_data/tokenizer/char_stream.py +72 -0
  47. infinity_data-1.0.0/src/infinity_data/tokenizer/diagnostics.py +62 -0
  48. infinity_data-1.0.0/src/infinity_data/tokenizer/finalizer.py +271 -0
  49. infinity_data-1.0.0/src/infinity_data/tokenizer/models/__init__.py +17 -0
  50. infinity_data-1.0.0/src/infinity_data/tokenizer/models/raw_tokens.py +61 -0
  51. infinity_data-1.0.0/src/infinity_data/tokenizer/models/tokens.py +245 -0
  52. infinity_data-1.0.0/src/infinity_data/tokenizer/tokenizer.py +531 -0
  53. infinity_data-1.0.0/src/infinity_data.egg-info/PKG-INFO +60 -0
  54. infinity_data-1.0.0/src/infinity_data.egg-info/SOURCES.txt +54 -0
  55. infinity_data-1.0.0/src/infinity_data.egg-info/dependency_links.txt +1 -0
  56. infinity_data-1.0.0/src/infinity_data.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 InfinitySystem (yin_bailiang)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.4
2
+ Name: infinity-data
3
+ Version: 1.0.0
4
+ Summary: InfinityData:声明式配置语言(.infd/.inft)的 Python 编译器库
5
+ Author: infinity_system: yin_bailiang
6
+ License: MIT
7
+ Project-URL: Repository, https://github.com/yinbailiang/infinity_data
8
+ Project-URL: Documentation, https://github.com/yinbailiang/infinity_data#readme
9
+ Keywords: config,configuration,declarative,dsl,compiler,infinity-data
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Dynamic: license-file
22
+
23
+ # InfinityData
24
+
25
+ 声明式配置语言(`.infd` / `.inft`)的 Python 编译器库。
26
+
27
+ - **编译优先**:`.infd` 是源码,JSON/YAML/TOML 是构建产物
28
+ - **模板即约束**:模板定义自动注册为约束校验器
29
+ - **零信任默认**:库默认 `deny_all` 沙盒,显式开放而非事后限制
30
+ - **结构化错误**:稳定错误码 + 结构化参数 + 多语言渲染
31
+
32
+ ## 快速使用
33
+
34
+ ```python
35
+ from infinity_data import load, safe_load, SandboxConfig, Schema
36
+
37
+ result = load('app.infd') # 默认零信任
38
+ if result.has_errors:
39
+ for d in result.diagnostics:
40
+ print(d.location, d.code, d.message)
41
+ else:
42
+ print(result.value) # 降维后的 dict
43
+ ```
44
+
45
+ ## 文档导航
46
+
47
+ | 文档 | 内容 |
48
+ |---|---|
49
+ | [neo_desg.md](./neo_desg.md) | 语言基础设计(语法 / 类型 / 模板 / 约束) |
50
+ | [extra_desg.md](./extra_desg.md) | 库 API / CLI / 生态设计 |
51
+ | [impl_desg.md](./impl_desg.md) | 编译器实现层的取舍与假设 |
52
+
53
+ ## 开发
54
+
55
+ ```bash
56
+ uv run pytest -q # 测试
57
+ uv run ruff check src tests # lint
58
+ uv run pyright # 类型检查(strict)
59
+ uv run python test.py # 错误报告演示
60
+ ```
@@ -0,0 +1,38 @@
1
+ # InfinityData
2
+
3
+ 声明式配置语言(`.infd` / `.inft`)的 Python 编译器库。
4
+
5
+ - **编译优先**:`.infd` 是源码,JSON/YAML/TOML 是构建产物
6
+ - **模板即约束**:模板定义自动注册为约束校验器
7
+ - **零信任默认**:库默认 `deny_all` 沙盒,显式开放而非事后限制
8
+ - **结构化错误**:稳定错误码 + 结构化参数 + 多语言渲染
9
+
10
+ ## 快速使用
11
+
12
+ ```python
13
+ from infinity_data import load, safe_load, SandboxConfig, Schema
14
+
15
+ result = load('app.infd') # 默认零信任
16
+ if result.has_errors:
17
+ for d in result.diagnostics:
18
+ print(d.location, d.code, d.message)
19
+ else:
20
+ print(result.value) # 降维后的 dict
21
+ ```
22
+
23
+ ## 文档导航
24
+
25
+ | 文档 | 内容 |
26
+ |---|---|
27
+ | [neo_desg.md](./neo_desg.md) | 语言基础设计(语法 / 类型 / 模板 / 约束) |
28
+ | [extra_desg.md](./extra_desg.md) | 库 API / CLI / 生态设计 |
29
+ | [impl_desg.md](./impl_desg.md) | 编译器实现层的取舍与假设 |
30
+
31
+ ## 开发
32
+
33
+ ```bash
34
+ uv run pytest -q # 测试
35
+ uv run ruff check src tests # lint
36
+ uv run pyright # 类型检查(strict)
37
+ uv run python test.py # 错误报告演示
38
+ ```
@@ -0,0 +1,91 @@
1
+ [project]
2
+ name = "infinity-data"
3
+ version = "1.0.0"
4
+ description = "InfinityData:声明式配置语言(.infd/.inft)的 Python 编译器库"
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ authors = [
8
+ { name = "infinity_system: yin_bailiang" },
9
+ ]
10
+ keywords = [
11
+ "config",
12
+ "configuration",
13
+ "declarative",
14
+ "dsl",
15
+ "compiler",
16
+ "infinity-data",
17
+ ]
18
+ classifiers = [
19
+ "Development Status :: 5 - Production/Stable",
20
+ "Intended Audience :: Developers",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Software Development :: Libraries :: Python Modules",
27
+ ]
28
+ requires-python = ">=3.12"
29
+ dependencies = []
30
+
31
+ [project.urls]
32
+ Repository = "https://github.com/yinbailiang/infinity_data"
33
+ Documentation = "https://github.com/yinbailiang/infinity_data#readme"
34
+
35
+ [dependency-groups]
36
+ # 测试组:仅运行测试所需(CI test job 只装这组,更快)
37
+ test = [
38
+ "pytest>=8.0",
39
+ "pytest-cov>=5.0",
40
+ ]
41
+ # 开发组:lint/类型/钩子 + 测试组(本地开发与 CI pre-commit job 装这组)
42
+ dev = [
43
+ "ruff>=0.15.21",
44
+ "pyright",
45
+ "pre-commit>=4.0",
46
+ "interrogate>=1.7.0",
47
+ { include-group = "test" },
48
+ ]
49
+
50
+ [build-system]
51
+ requires = ["setuptools>=61.0"]
52
+ build-backend = "setuptools.build_meta"
53
+
54
+ [tool.setuptools]
55
+ package-dir = {"" = "src"}
56
+
57
+ [tool.setuptools.packages.find]
58
+ where = ["src"]
59
+
60
+ [tool.ruff]
61
+ line-length = 120
62
+ target-version = "py312"
63
+
64
+ [tool.ruff.lint]
65
+ select = [
66
+ "E", # pycodestyle errors
67
+ "F", # pyflakes
68
+ "I", # isort
69
+ "ASYNC", # async/await best practices
70
+ "PLE", # pylint errors
71
+ ]
72
+ ignore = [
73
+ "E501", # line too long (handled by formatter)
74
+ "ASYNC109", # async function with timeout parameter (intentional API design)
75
+ ]
76
+
77
+ [tool.ruff.lint.isort]
78
+ known-first-party = ["infinity_data"]
79
+
80
+ [tool.ruff.format]
81
+ indent-style = "space"
82
+ quote-style = "single"
83
+ line-ending = "lf"
84
+
85
+ [tool.pyright]
86
+ typeCheckingMode = "strict"
87
+ reportMissingTypeStubs = false
88
+ reportMissingImports = "error"
89
+ exclude = ["**/node_modules", "**/__pycache__", "**/.*", ".venv"]
90
+ venvPath = "."
91
+ venv = ".venv"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,41 @@
1
+ """InfinityData —— 声明式配置语言(.infd/.inft)的 Python 编译器库。
2
+
3
+ 编译流水线:RawTokenizer → FinalTokenizer → Parser → AstBuilder → Executor → Converter
4
+
5
+ 用法::
6
+
7
+ from infinity_data import load, safe_load, SandboxConfig, Schema
8
+
9
+ result = safe_load("app.infd")
10
+ if result.has_errors:
11
+ for d in result.diagnostics:
12
+ print(d.location, d.message)
13
+ else:
14
+ print(result.value)
15
+ """
16
+
17
+ from infinity_data.infra.diagnostics import Diagnostic, Severity
18
+ from infinity_data.pipeline import (
19
+ CompilationResult,
20
+ check,
21
+ compile_document,
22
+ compile_source,
23
+ load,
24
+ safe_load,
25
+ )
26
+ from infinity_data.sandbox import SandboxConfig, SandboxError, Schema, SchemaError
27
+
28
+ __all__ = [
29
+ 'compile_source',
30
+ 'load',
31
+ 'safe_load',
32
+ 'check',
33
+ 'compile_document',
34
+ 'CompilationResult',
35
+ 'SandboxConfig',
36
+ 'SandboxError',
37
+ 'Schema',
38
+ 'SchemaError',
39
+ 'Diagnostic',
40
+ 'Severity',
41
+ ]
@@ -0,0 +1,9 @@
1
+ """产物发射层:StdAst → 宿主表示(Python dict / list / 标量)。
2
+
3
+ 语义分析(semantic/)负责"值是否正确",本层负责"产物长什么样"。
4
+ M4 的 JSON/YAML/TOML 转换、M5 的 JSON Schema 生成均在此层扩展。
5
+ """
6
+
7
+ from infinity_data.emit.converter import reduce_array, reduce_object, reduce_value
8
+
9
+ __all__ = ['reduce_array', 'reduce_object', 'reduce_value']
@@ -0,0 +1,55 @@
1
+ """降维器:StdAst → 纯 Python 值(dict / list / 标量)。
2
+
3
+ - ``noexist`` 字段不出现在输出中
4
+ - ``null`` 字段保留键(值为 None),``keep_null=False`` 时跳过
5
+ - 浮点保持 :class:`decimal.Decimal`(无限精度);NaN / ±Infinity 以 Decimal 表示,
6
+ JSON/YAML 序列化时的特殊编码由 M4 转换层负责
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from infinity_data.semantic.builder.models import StdArray, StdLiteral, StdObject, StdValue
14
+
15
+
16
+ def reduce_object(obj: StdObject, *, keep_null: bool = True) -> dict[str, Any]:
17
+ """将 StdObject 降维为 Python dict。"""
18
+ result: dict[str, Any] = {}
19
+ for f in obj.fields:
20
+ if f.value is None or f.is_noexist:
21
+ continue
22
+ if f.is_null:
23
+ if keep_null:
24
+ result[f.name] = None
25
+ continue
26
+ result[f.name] = reduce_value(f.value, keep_null=keep_null)
27
+ return result
28
+
29
+
30
+ def reduce_array(arr: StdArray, *, keep_null: bool = True) -> list[Any]:
31
+ """将 StdArray 降维为 Python list。"""
32
+ return [reduce_value(v, keep_null=keep_null) for v in arr.elements]
33
+
34
+
35
+ def reduce_value(val: StdValue, *, keep_null: bool = True) -> Any:
36
+ """将任意 StdValue 降维为 Python 原生值。"""
37
+ match val:
38
+ case StdLiteral():
39
+ return _reduce_literal(val)
40
+ case StdArray():
41
+ return reduce_array(val, keep_null=keep_null)
42
+ case StdObject():
43
+ return reduce_object(val, keep_null=keep_null)
44
+ raise TypeError(f'未知 StdValue 类型: {type(val)}')
45
+
46
+
47
+ # ── 内部 ────────────────────────────────────────────────
48
+
49
+
50
+ def _reduce_literal(lit: StdLiteral) -> Any:
51
+ match lit.kind:
52
+ case 'null' | 'noexist':
53
+ return None
54
+ case _:
55
+ return lit.value
@@ -0,0 +1,37 @@
1
+ """前端流水线:源码 File → RawAst Document + 前端诊断(容错收集)。
2
+
3
+ 供 :mod:`pipeline`(主文件)与 :mod:`semantic.resolver`(外部模板文件)共用,
4
+ 消除两处重复的 RawTokenizer → FinalTokenizer → Parser 组装。
5
+ """
6
+
7
+ from infinity_data.infra.diagnostics import DiagnosticCollector
8
+ from infinity_data.infra.file import File
9
+ from infinity_data.parser import Document
10
+ from infinity_data.parser.parser import Parser
11
+ from infinity_data.tokenizer.finalizer import FinalTokenizer
12
+ from infinity_data.tokenizer.tokenizer import RawTokenizer
13
+
14
+ __all__ = ['parse_source']
15
+
16
+
17
+ def parse_source(
18
+ file: File,
19
+ collector: DiagnosticCollector | None = None,
20
+ ) -> tuple[Document, DiagnosticCollector]:
21
+ """词法 + 语法分析,返回 RawAst Document 与生效收集器。
22
+
23
+ 容错:词法/语法错误经 :class:`DiagnosticCollector` 收集而非抛出。
24
+ 传入 ``collector`` 时三阶段(RawTokenizer / FinalTokenizer / Parser)全程
25
+ 复用同一收集器并**原样返回**(非副本);缺省时内部新建并返回。
26
+ 返回值第二元素为生效收集器,可直接查询 errors / warnings / has_errors。
27
+ """
28
+ if collector is None:
29
+ collector = DiagnosticCollector()
30
+
31
+ raw_tokens = RawTokenizer(file=file, error_collector=collector)
32
+ tokens = FinalTokenizer(raw_tokens, error_collector=collector)
33
+ parser = Parser(tokens, error_collector=collector)
34
+ doc = parser.parse()
35
+
36
+ # 词法/语法错误统一为 Diagnostic(纯数据,直接聚合)
37
+ return doc, collector
@@ -0,0 +1,4 @@
1
+ from infinity_data.infra.diagnostics import Diagnostic, DiagnosticCollector, Severity
2
+ from infinity_data.infra.ll1_stream import LL1Stream
3
+
4
+ __all__ = ['Diagnostic', 'DiagnosticCollector', 'Severity', 'LL1Stream']
@@ -0,0 +1,212 @@
1
+ """统一诊断模型(所有阶段共用)与容错收集器。
2
+
3
+ - :class:`Severity` / :class:`Diagnostic`:稳定错误码 + 结构化参数 + 渲染消息。
4
+ 词法/语法/语义阶段统一使用;沙盒异常(见 :mod:`infinity_data.sandbox.errors`)
5
+ 经 ``check()`` 边界转换为 Diagnostic。
6
+ - :class:`DiagnosticCollector`:前端容错收集器(词法/语法错误作为 Diagnostic
7
+ 收集,从不抛异常)。
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Iterable, Iterator, Mapping
13
+ from dataclasses import dataclass, field
14
+ from enum import Enum
15
+ from typing import Any
16
+
17
+ from infinity_data.infra.location import SourceRange, format_location
18
+
19
+ __all__ = [
20
+ 'DEFAULT_LANG',
21
+ 'Severity',
22
+ 'Diagnostic',
23
+ 'DiagnosticDefine',
24
+ 'diagnostic_define',
25
+ 'register_diagnostic_define',
26
+ 'render_message',
27
+ 'DiagnosticCollector',
28
+ ]
29
+
30
+ DEFAULT_LANG = 'zh'
31
+ """默认渲染语言。"""
32
+
33
+
34
+ class Severity(Enum):
35
+ """诊断严重级别。"""
36
+
37
+ ERROR = 'error'
38
+ WARNING = 'warning'
39
+ INFO = 'info'
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class Diagnostic:
44
+ """统一诊断:稳定错误码 + 结构化参数 + 渲染消息。
45
+
46
+ - ``code``:稳定错误码(如 ``"template.undefined"``),测试/工具据此匹配
47
+ - ``params``:结构化参数;``message`` 为派生属性,由注册表按语言渲染
48
+ - ``lang``:渲染语言(None = 默认语言);模板缺失时回退默认语言
49
+ """
50
+
51
+ severity: Severity
52
+ code: str
53
+ params: Mapping[str, Any] = field(default_factory=dict[str, Any])
54
+ source: SourceRange | None = None
55
+ path: str = ''
56
+ lang: str | None = None
57
+
58
+ @property
59
+ def location(self) -> str:
60
+ return format_location(self.source)
61
+
62
+ @property
63
+ def message(self) -> str:
64
+ """按错误码 + 参数 + 语言渲染的人类可读消息。"""
65
+ return render_message(
66
+ self.code, self.params, location=self.location, path=self.path, lang=self.lang or DEFAULT_LANG
67
+ )
68
+
69
+ def sort_key(self) -> tuple[str, int, int]:
70
+ """按源码位置排序。"""
71
+ if self.source is None:
72
+ return ('\uffff', 0, 0)
73
+ s = self.source.start
74
+ return (self.source.file.name, s.line, s.col)
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class DiagnosticDefine:
79
+ """诊断定义:稳定错误码 + 语言模板。
80
+
81
+ - ``code``:稳定错误码(如 ``"template.undefined"``)
82
+ - ``template``:默认语言(``DEFAULT_LANG``)模板
83
+ - ``translations``:其他语言模板(语言码 → 模板),缺失时回退默认模板
84
+ """
85
+
86
+ code: str
87
+ template: str
88
+ translations: Mapping[str, str] = field(default_factory=dict[str, str])
89
+
90
+ def template_for(self, lang: str = DEFAULT_LANG) -> str:
91
+ """取指定语言的模板;缺失回退默认模板。"""
92
+ if lang == DEFAULT_LANG:
93
+ return self.template
94
+ return self.translations.get(lang, self.template)
95
+
96
+
97
+ _DIAGNOSTIC_DEFINE_REGISTRY: dict[str, DiagnosticDefine] = {}
98
+ """诊断定义注册表(code → 定义)。"""
99
+
100
+
101
+ def diagnostic_define(code: str, template: str, **translations: str) -> DiagnosticDefine:
102
+ """构造诊断定义(``translations`` 为其他语言模板,如 ``en=...``)。"""
103
+ return DiagnosticDefine(code=code, template=template, translations=translations)
104
+
105
+
106
+ def register_diagnostic_define(*defines: DiagnosticDefine) -> None:
107
+ """注册诊断定义(重复 code 后者覆盖)。"""
108
+ for d in defines:
109
+ _DIAGNOSTIC_DEFINE_REGISTRY[d.code] = d
110
+
111
+
112
+ def registered_diagnostic_defines() -> Mapping[str, DiagnosticDefine]:
113
+ """已注册定义表(只读视图)。"""
114
+ return _DIAGNOSTIC_DEFINE_REGISTRY
115
+
116
+
117
+ def render_message(
118
+ code: str,
119
+ params: Mapping[str, Any],
120
+ *,
121
+ location: str = '<unknown>',
122
+ path: str = '',
123
+ lang: str = DEFAULT_LANG,
124
+ ) -> str:
125
+ """按错误码 + 参数 + 语言渲染消息;未知错误码原样返回错误码本身。"""
126
+ d = _DIAGNOSTIC_DEFINE_REGISTRY.get(code)
127
+ if d is None:
128
+ return code
129
+ template = d.template_for(lang)
130
+ context: dict[str, Any] = {
131
+ 'location': location,
132
+ 'path': path,
133
+ 'path_prefix': f'{path}: ' if path else '',
134
+ **params,
135
+ }
136
+ try:
137
+ return template.format(**context)
138
+ except (KeyError, IndexError, ValueError, AttributeError):
139
+ return code
140
+
141
+
142
+ class DiagnosticCollector:
143
+ """诊断收集器:收集 :class:`Diagnostic`(词法/语法/语义阶段容错收集)。
144
+
145
+ 词法/语法/语义阶段不抛异常:错误以 Diagnostic 形式收集,边界处直接聚合。
146
+ 只读查询按 severity 分离:``errors`` / ``warnings`` / ``diagnostics``,
147
+ ``has_errors`` 仅当含 ERROR 级别时成立(warning 不算错误)。
148
+
149
+ 用法::
150
+
151
+ collector = DiagnosticCollector()
152
+ tokenizer = RawTokenizer(file, error_collector=collector)
153
+ ...
154
+ for err in collector:
155
+ print(err.code)
156
+ """
157
+
158
+ def __init__(self) -> None:
159
+ self._errors: list[Diagnostic] = []
160
+
161
+ # ── 写入 ──────────────────────────────────────────
162
+
163
+ def add(self, error: Diagnostic) -> None:
164
+ """添加一个诊断。"""
165
+ self._errors.append(error)
166
+
167
+ def extend(self, errors: Iterable[Diagnostic]) -> None:
168
+ """批量添加诊断。"""
169
+ self._errors.extend(errors)
170
+
171
+ # ── 只读查询(severity 感知:warning 与 error 语义分离) ──
172
+
173
+ @property
174
+ def diagnostics(self) -> list[Diagnostic]:
175
+ """全部诊断的副本(含 ERROR / WARNING)。"""
176
+ return self._errors.copy()
177
+
178
+ @property
179
+ def errors(self) -> list[Diagnostic]:
180
+ """仅 ERROR 级别诊断的副本(warning 不算错误)。"""
181
+ return [d for d in self._errors if d.severity is Severity.ERROR]
182
+
183
+ @property
184
+ def warnings(self) -> list[Diagnostic]:
185
+ """仅 WARNING 级别诊断的副本。"""
186
+ return [d for d in self._errors if d.severity is Severity.WARNING]
187
+
188
+ @property
189
+ def has_errors(self) -> bool:
190
+ """是否含 ERROR 级别诊断(与 :class:`CompilationResult` 的 has_errors 语义一致)。"""
191
+ return any(d.severity is Severity.ERROR for d in self._errors)
192
+
193
+ @property
194
+ def has_warnings(self) -> bool:
195
+ """是否含 WARNING 级别诊断。"""
196
+ return any(d.severity is Severity.WARNING for d in self._errors)
197
+
198
+ # ── 容器协议 ──────────────────────────────────────
199
+
200
+ def __iter__(self) -> Iterator[Diagnostic]:
201
+ """迭代所有已收集的诊断。"""
202
+ return iter(self._errors)
203
+
204
+ def __len__(self) -> int:
205
+ """已收集诊断数量(容器协议)。
206
+
207
+ 空收集器为 falsy(``bool(collector)`` = 是否收集到任何诊断,含 warning;
208
+ 区别于 ``has_errors`` 的"是否含 ERROR")——
209
+ 因此缺省构造**不可用 ``error_collector or DiagnosticCollector()``**:
210
+ 空收集器会被 `or` 判定为假而静默替换,丢弃调用方传入的收集器。
211
+ """
212
+ return len(self._errors)
@@ -0,0 +1,83 @@
1
+ """源码来源抽象:磁盘文件与内存源码统一为 :class:`File`。
2
+
3
+ 编译入口(``load`` / ``compile_source``)与模板导入链(``!from``)都消费 File:
4
+
5
+ - ``name``:诊断显示名(``file:line:col`` 中的 file)
6
+ - ``root_path``:相对导入解析基准(所在目录)
7
+ - ``read()``:源码内容
8
+ - ``chars()``:逐字符迭代流(词法分析输入)
9
+ - ``identity``:唯一身份(磁盘 = resolve 后绝对路径;内存 = ``路径:mem:内容hash``);模板身份(TemplateKey)基于它,含来源路径
10
+ - ``content_hash()``:内容 sha256 前缀(MemFile 身份的一部分 / 内容校验)
11
+ """
12
+
13
+ import hashlib
14
+ from collections.abc import Iterable
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+
18
+ __all__ = ['File', 'DiskFile', 'MemFile']
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class File:
23
+ """源码来源基类。"""
24
+
25
+ name: str
26
+ root_path: Path
27
+
28
+ def read(self) -> str:
29
+ """读取源码内容。"""
30
+ raise NotImplementedError
31
+
32
+ def chars(self) -> Iterable[str]:
33
+ """逐字符迭代流(词法分析输入)。"""
34
+ return iter(self.read())
35
+
36
+ @property
37
+ def identity(self) -> str:
38
+ """唯一身份(循环导入防护 / 模板身份(TemplateKey)基础,含来源路径)。"""
39
+ raise NotImplementedError
40
+
41
+ def content_hash(self) -> str:
42
+ """内容 sha256 前缀(MemFile 身份的一部分;内容校验,非模板身份本身)。"""
43
+ return hashlib.sha256(self.read().encode('utf-8')).hexdigest()[:12]
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class DiskFile(File):
48
+ """磁盘文件。path 为单一事实来源;name/root_path 由构造方从它派生。"""
49
+
50
+ @property
51
+ def path(self) -> Path:
52
+ return Path(self.name)
53
+
54
+ def read(self) -> str:
55
+ return self.path.read_text(encoding='utf-8')
56
+
57
+ @property
58
+ def identity(self) -> str:
59
+ return str(self.path.resolve())
60
+
61
+ @classmethod
62
+ def from_fullpath(cls, fullpath: str | Path) -> 'DiskFile':
63
+ """从完整路径构造(与调用点一致的规范化入口)。"""
64
+ p = Path(fullpath)
65
+ return cls(name=str(p), root_path=p.parent)
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class MemFile(File):
70
+ """内存源码(测试/嵌入式场景)。身份 = 根路径:mem:内容hash(含路径)。"""
71
+
72
+ content: str
73
+
74
+ def read(self) -> str:
75
+ return self.content
76
+
77
+ def chars(self) -> Iterable[str]:
78
+ """逐字符迭代流(O(1) 构造,直连内容)。"""
79
+ return iter(self.content)
80
+
81
+ @property
82
+ def identity(self) -> str:
83
+ return str(self.root_path.resolve()) + ':mem:' + self.content_hash()