infinity-data 2.0.0__tar.gz → 2.2.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.
- {infinity_data-2.0.0/src/infinity_data.egg-info → infinity_data-2.2.0}/PKG-INFO +1 -1
- {infinity_data-2.0.0 → infinity_data-2.2.0}/pyproject.toml +3 -2
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/parser/__init__.py +2 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/parser/models.py +20 -6
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/parser/parser.py +43 -15
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/pipeline.py +1 -1
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/builder/__init__.py +4 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/builder/builder.py +102 -48
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/builder/models.py +10 -4
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/constraints.py +2 -2
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/diagnostics.py +4 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/executor/executor.py +15 -6
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/__init__.py +8 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/dict_constraints.py +3 -1
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/general.py +35 -2
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/resolver/identity.py +29 -7
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/resolver/imports.py +67 -9
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/resolver/models.py +28 -13
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/resolver/resolver.py +53 -16
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/std.py +40 -10
- {infinity_data-2.0.0 → infinity_data-2.2.0/src/infinity_data.egg-info}/PKG-INFO +1 -1
- {infinity_data-2.0.0 → infinity_data-2.2.0}/LICENSE +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/README.md +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/setup.cfg +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/__init__.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/emit/__init__.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/emit/converter.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/frontend.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/infra/__init__.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/infra/diagnostics.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/infra/file.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/infra/ll1_stream.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/infra/location.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/infra/path.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/parser/diagnostics.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/parser/token_stream.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/py.typed +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/sandbox/__init__.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/sandbox/config.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/sandbox/errors.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/sandbox/mediator.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/sandbox/schema.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/__init__.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/executor/__init__.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/jsonpath.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/_core.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/logic.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/types.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/resolver/__init__.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/__init__.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/char_stream.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/diagnostics.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/finalizer.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/models/__init__.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/models/raw_tokens.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/models/tokens.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/tokenizer.py +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data.egg-info/SOURCES.txt +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data.egg-info/dependency_links.txt +0 -0
- {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "infinity-data"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.2.0"
|
|
4
4
|
description = "InfinityData:声明式配置语言(.infd/.inft)的 Python 编译器库"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -35,6 +35,7 @@ Documentation = "https://github.com/yinbailiang/infinity_data#readme"
|
|
|
35
35
|
[dependency-groups]
|
|
36
36
|
# 测试组:仅运行测试所需(CI test job 只装这组,更快)
|
|
37
37
|
test = [
|
|
38
|
+
"pyyaml",
|
|
38
39
|
"pytest>=8.0",
|
|
39
40
|
"pytest-cov>=5.0",
|
|
40
41
|
]
|
|
@@ -89,7 +90,7 @@ line-ending = "lf"
|
|
|
89
90
|
typeCheckingMode = "strict"
|
|
90
91
|
reportMissingTypeStubs = false
|
|
91
92
|
reportMissingImports = "error"
|
|
92
|
-
exclude = ["**/node_modules", "**/__pycache__", "**/.*", ".venv"]
|
|
93
|
+
exclude = ["**/node_modules", "**/__pycache__", "**/.*", ".venv", "build"]
|
|
93
94
|
venvPath = "."
|
|
94
95
|
venv = ".venv"
|
|
95
96
|
|
|
@@ -22,6 +22,7 @@ from infinity_data.parser.models import (
|
|
|
22
22
|
DictValue,
|
|
23
23
|
Document,
|
|
24
24
|
DollarValue,
|
|
25
|
+
EnvImportItem,
|
|
25
26
|
EnvImportStmt,
|
|
26
27
|
ErrorConstraint,
|
|
27
28
|
ErrorStatement,
|
|
@@ -59,6 +60,7 @@ __all__ = [
|
|
|
59
60
|
'DictValue',
|
|
60
61
|
'Document',
|
|
61
62
|
'DollarValue',
|
|
63
|
+
'EnvImportItem',
|
|
62
64
|
'EnvImportStmt',
|
|
63
65
|
'ErrorConstraint',
|
|
64
66
|
'ErrorStatement',
|
|
@@ -120,16 +120,30 @@ class TemplateImportStmt(AstNode):
|
|
|
120
120
|
|
|
121
121
|
|
|
122
122
|
@dataclass
|
|
123
|
-
class
|
|
124
|
-
"""
|
|
123
|
+
class EnvImportItem(AstNode):
|
|
124
|
+
"""环境变量导入项: NAME [as NEW_NAME]。"""
|
|
125
125
|
|
|
126
126
|
name: str # 环境变量名
|
|
127
|
-
alias: str | None # 别名(可选)
|
|
127
|
+
alias: str | None = None # 别名(可选)
|
|
128
128
|
|
|
129
129
|
def canonical(self) -> str:
|
|
130
130
|
if self.alias is None:
|
|
131
|
-
return
|
|
132
|
-
return f'
|
|
131
|
+
return self.name
|
|
132
|
+
return f'{self.name} as {self.alias}'
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class EnvImportStmt(AstNode):
|
|
137
|
+
"""环境变量导入: !env import NAME1 [as NEW1], NAME2 [as NEW2], ..."""
|
|
138
|
+
|
|
139
|
+
items: list[EnvImportItem] # 导入项列表(项之间必须用逗号分隔)
|
|
140
|
+
|
|
141
|
+
def children(self) -> Iterable[AstNode]:
|
|
142
|
+
return self.items
|
|
143
|
+
|
|
144
|
+
def canonical(self) -> str:
|
|
145
|
+
items = ', '.join(i.canonical() for i in self.items)
|
|
146
|
+
return f'!env import {items}'
|
|
133
147
|
|
|
134
148
|
|
|
135
149
|
@dataclass
|
|
@@ -178,7 +192,7 @@ class FileImportStmt(AstNode):
|
|
|
178
192
|
"""配置文件导入: !file "path" as <format> import .path.to.key as alias, ..."""
|
|
179
193
|
|
|
180
194
|
file_path: str # 文件路径
|
|
181
|
-
format: str | None # 文件格式: "yaml", "json", "toml" 或 None(自动检测后缀)
|
|
195
|
+
format: str | None # 文件格式: "yaml", "json", "toml", "raw" 或 None(自动检测后缀)
|
|
182
196
|
imports: list[FileImportItem]
|
|
183
197
|
|
|
184
198
|
def children(self) -> Iterable[AstNode]:
|
|
@@ -17,6 +17,7 @@ from infinity_data.parser.models import (
|
|
|
17
17
|
DictValue,
|
|
18
18
|
Document,
|
|
19
19
|
DollarValue,
|
|
20
|
+
EnvImportItem,
|
|
20
21
|
EnvImportStmt,
|
|
21
22
|
ErrorConstraint,
|
|
22
23
|
ErrorStatement,
|
|
@@ -272,18 +273,29 @@ class Parser:
|
|
|
272
273
|
|
|
273
274
|
@staticmethod
|
|
274
275
|
def _parse_env_import(stream: TokenStream, collector: DiagnosticCollector, kw_tok: EnvImportToken) -> EnvImportStmt:
|
|
275
|
-
"""!env import
|
|
276
|
+
"""!env import NAME1 [as NEW1], NAME2 [as NEW2], ..."""
|
|
276
277
|
Parser._expect_keyword(stream, collector, 'import')
|
|
277
|
-
name_tok = stream.expect(IdentifierToken)
|
|
278
278
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
279
|
+
# 导入项列表(项之间必须用逗号分隔)
|
|
280
|
+
items: list[EnvImportItem] = []
|
|
281
|
+
items.append(Parser._parse_env_import_item(stream, collector))
|
|
282
|
+
|
|
283
|
+
while True:
|
|
284
|
+
tok = stream.peek()
|
|
285
|
+
if isinstance(tok, CommaToken):
|
|
286
|
+
stream.advance()
|
|
287
|
+
items.append(Parser._parse_env_import_item(stream, collector))
|
|
288
|
+
elif isinstance(tok, IdentifierToken):
|
|
289
|
+
# 同一行内空格分隔(无逗号/换行)→ 报缺失逗号,容错继续(漏逗号)
|
|
290
|
+
collector.add(diag('parse.import_missing_comma', {}, tok.raw.source))
|
|
291
|
+
items.append(Parser._parse_env_import_item(stream, collector))
|
|
292
|
+
else:
|
|
293
|
+
# 换行/EOF 等 → 导入列表结束(换行后是新的顶层语句,不是导入项)
|
|
294
|
+
break
|
|
283
295
|
|
|
284
|
-
# 导入语句必须换行/EOF
|
|
285
|
-
#
|
|
286
|
-
#
|
|
296
|
+
# 导入语句必须换行/EOF 结尾:若不检查,同一行的逗号会被顶层 skip_separators
|
|
297
|
+
# 吞掉(`!env import A as a, x = 1` 被误认为合法),与 !from / !file 的
|
|
298
|
+
# 「尾部必须换行」行为不一致。
|
|
287
299
|
tok = stream.peek()
|
|
288
300
|
# 显式排除 NoNextType 以收窄类型:stream.eof() 是方法调用,无法据此收窄 tok,
|
|
289
301
|
# 否则 tok 仍为 Token | NoNextType,访问 tok.raw 会触发类型错误。
|
|
@@ -300,6 +312,22 @@ class Parser:
|
|
|
300
312
|
# 否则语句 source 会把下一行行首吞入(与 Field 不一致)
|
|
301
313
|
return EnvImportStmt(
|
|
302
314
|
source=stream.span_from(kw_tok),
|
|
315
|
+
items=items,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
@staticmethod
|
|
319
|
+
def _parse_env_import_item(stream: TokenStream, collector: DiagnosticCollector) -> EnvImportItem:
|
|
320
|
+
"""解析单个导入项: NAME [as NEW_NAME]。"""
|
|
321
|
+
first = stream.peek()
|
|
322
|
+
name_tok = stream.expect(IdentifierToken)
|
|
323
|
+
|
|
324
|
+
alias = None
|
|
325
|
+
if Parser._peek_keyword(stream, 'as'):
|
|
326
|
+
stream.advance()
|
|
327
|
+
alias = stream.expect(IdentifierToken).name
|
|
328
|
+
|
|
329
|
+
return EnvImportItem(
|
|
330
|
+
source=stream.span_from(first),
|
|
303
331
|
name=name_tok.name,
|
|
304
332
|
alias=alias,
|
|
305
333
|
)
|
|
@@ -890,7 +918,7 @@ class Parser:
|
|
|
890
918
|
|
|
891
919
|
支持:
|
|
892
920
|
- int, str, bool, float, list, dict, ?, object
|
|
893
|
-
- type? →
|
|
921
|
+
- type? → any(type, ?)
|
|
894
922
|
- <constraint, constraint, ...> → all(constraint, ...)
|
|
895
923
|
- <any(...)>, <one(...)>, <not(...)>, <all(...)>
|
|
896
924
|
"""
|
|
@@ -929,7 +957,7 @@ class Parser:
|
|
|
929
957
|
|
|
930
958
|
if isinstance(stream.peek(), QuestionToken):
|
|
931
959
|
stream.advance()
|
|
932
|
-
# 直接展开: type? →
|
|
960
|
+
# 直接展开: type? → any(type, ?)
|
|
933
961
|
return Constraints(
|
|
934
962
|
source=stream.span_from(first),
|
|
935
963
|
constraints=[Parser._nullable(ident)],
|
|
@@ -938,7 +966,7 @@ class Parser:
|
|
|
938
966
|
# 单约束函数调用可省略尖括号: field: regex("re") = ...
|
|
939
967
|
if isinstance(stream.peek(), LparenToken):
|
|
940
968
|
call = Parser._parse_constraint_call(stream, collector, first)
|
|
941
|
-
# 调用后也可空: regex("re")? →
|
|
969
|
+
# 调用后也可空: regex("re")? → any(regex("re"), ?)
|
|
942
970
|
if isinstance(stream.peek(), QuestionToken):
|
|
943
971
|
stream.advance()
|
|
944
972
|
call = Parser._nullable(call)
|
|
@@ -971,10 +999,10 @@ class Parser:
|
|
|
971
999
|
|
|
972
1000
|
@staticmethod
|
|
973
1001
|
def _nullable(c: Constraint) -> ConstraintCall:
|
|
974
|
-
"""可空包装:constraint? →
|
|
1002
|
+
"""可空包装:constraint? → any(constraint, ?)。"""
|
|
975
1003
|
return ConstraintCall(
|
|
976
1004
|
source=c.source,
|
|
977
|
-
name='
|
|
1005
|
+
name='any',
|
|
978
1006
|
arguments=[c, ConstraintIdent(source=c.source, name='?')],
|
|
979
1007
|
)
|
|
980
1008
|
|
|
@@ -1025,7 +1053,7 @@ class Parser:
|
|
|
1025
1053
|
message=f'无法解析的约束: {bad_tok.raw.type.name}',
|
|
1026
1054
|
)
|
|
1027
1055
|
|
|
1028
|
-
# 可空后缀: constraint? →
|
|
1056
|
+
# 可空后缀: constraint? → any(constraint, ?)
|
|
1029
1057
|
if isinstance(stream.peek(), QuestionToken):
|
|
1030
1058
|
stream.advance()
|
|
1031
1059
|
return Parser._nullable(base)
|
|
@@ -137,7 +137,7 @@ def _compile(file: File, options: CompileOptions) -> CompilationResult:
|
|
|
137
137
|
if resolver.schema.from_file and context.schema_scope is not None
|
|
138
138
|
else context.root_scope
|
|
139
139
|
)
|
|
140
|
-
key = scope.get(resolver.schema.template)
|
|
140
|
+
key = scope.visible.get(resolver.schema.template)
|
|
141
141
|
if key is None:
|
|
142
142
|
raise SchemaError('schema.undefined_template', {'template': resolver.schema.template})
|
|
143
143
|
tpl = std.templates[key]
|
|
@@ -13,8 +13,10 @@ from infinity_data.semantic.builder.models import (
|
|
|
13
13
|
StdDocument,
|
|
14
14
|
StdField,
|
|
15
15
|
StdLiteral,
|
|
16
|
+
StdNode,
|
|
16
17
|
StdObject,
|
|
17
18
|
StdValue,
|
|
19
|
+
is_std_node,
|
|
18
20
|
)
|
|
19
21
|
|
|
20
22
|
__all__ = [
|
|
@@ -24,6 +26,8 @@ __all__ = [
|
|
|
24
26
|
'StdDocument',
|
|
25
27
|
'StdField',
|
|
26
28
|
'StdLiteral',
|
|
29
|
+
'StdNode',
|
|
27
30
|
'StdObject',
|
|
28
31
|
'StdValue',
|
|
32
|
+
'is_std_node',
|
|
29
33
|
]
|
|
@@ -82,9 +82,9 @@ class AstBuilder:
|
|
|
82
82
|
# 执行期状态(每次 build 重置)
|
|
83
83
|
self._templates: dict[TemplateKey, TemplateDef] = {}
|
|
84
84
|
self._template_scopes: dict[TemplateKey, Scope] = {}
|
|
85
|
-
self._root_scope: Scope =
|
|
85
|
+
self._root_scope: Scope = Scope()
|
|
86
86
|
self._schema_scope: Scope | None = None
|
|
87
|
-
|
|
87
|
+
# $ 命名空间按文件就地解析(§1.8):由各 Scope 对象自身携带(scope.namespaces)
|
|
88
88
|
self._collector: DiagnosticCollector = DiagnosticCollector() # 本次 build 的共享收集器
|
|
89
89
|
self._depth = 0
|
|
90
90
|
self._recursive_defaults: set[TemplateKey] = set()
|
|
@@ -107,9 +107,8 @@ class AstBuilder:
|
|
|
107
107
|
"""
|
|
108
108
|
self._templates = {}
|
|
109
109
|
self._template_scopes = {}
|
|
110
|
-
self._root_scope =
|
|
110
|
+
self._root_scope = Scope()
|
|
111
111
|
self._schema_scope = None
|
|
112
|
-
self._namespace = {}
|
|
113
112
|
self._collector = collector
|
|
114
113
|
self._depth = 0
|
|
115
114
|
self._recursive_defaults = set()
|
|
@@ -119,7 +118,6 @@ class AstBuilder:
|
|
|
119
118
|
self._template_scopes = context.template_scopes
|
|
120
119
|
self._root_scope = context.root_scope
|
|
121
120
|
self._schema_scope = context.schema_scope
|
|
122
|
-
self._namespace = context.namespace
|
|
123
121
|
|
|
124
122
|
# 静态防护:默认值引用环检测(默认值禁止自引用,见 neo_desg.md 2.6)
|
|
125
123
|
self._detect_recursive_defaults()
|
|
@@ -127,8 +125,8 @@ class AstBuilder:
|
|
|
127
125
|
# 模板配置校验:extra_*_vars 指向的字段必须在模板中声明(§2.9,定义即报错)
|
|
128
126
|
self._check_variadic_config()
|
|
129
127
|
|
|
130
|
-
# !var 求值(§2.10):root
|
|
131
|
-
self._resolve_var_statements(
|
|
128
|
+
# !var 求值(§2.10):root 构建前按文件填充 $ 命名空间(前向引用 + 环检测,就地解析)
|
|
129
|
+
self._resolve_var_statements(context)
|
|
132
130
|
|
|
133
131
|
# 构建 root(顶层结构约束挂在 root.constraints,不执行)
|
|
134
132
|
root_fields: list[StdField] = []
|
|
@@ -160,25 +158,44 @@ class AstBuilder:
|
|
|
160
158
|
return StdDocument(
|
|
161
159
|
root=StdObject(fields=self._finalize_object(root_fields, ''), constraints=root_constraints),
|
|
162
160
|
templates=dict(self._templates),
|
|
163
|
-
scope=dict(self._root_scope),
|
|
161
|
+
scope=dict(self._root_scope.visible),
|
|
164
162
|
)
|
|
165
163
|
|
|
166
164
|
# ═══════════════════════════════════════════════════════
|
|
167
165
|
# !var 求值(§2.10)
|
|
168
166
|
# ═══════════════════════════════════════════════════════
|
|
169
167
|
|
|
170
|
-
def _resolve_var_statements(self,
|
|
171
|
-
"""!var
|
|
168
|
+
def _resolve_var_statements(self, context: ResolvedContext) -> None:
|
|
169
|
+
"""!var 求值(按文件就地解析,§1.8):每文件独立依赖图 + 环检测 + 拓扑求值。
|
|
172
170
|
|
|
173
171
|
在 root 构建之前调用——前向引用经拓扑序天然支持;依赖环 → ``var.cycle``。
|
|
174
|
-
值构造(模板展开 / 解包 / $ 引用)全部复用 :meth:`_resolve_value
|
|
172
|
+
值构造(模板展开 / 解包 / $ 引用)全部复用 :meth:`_resolve_value`,
|
|
173
|
+
用**该文件**的 scope 就地解析、绑定**该文件**的 $ 命名空间(跨文件完全隔离)。
|
|
174
|
+
``!var`` 语句由 scope 对象携带(``scope.var_statements``),此处遍历全部文件 scope。
|
|
175
|
+
Scope 按身份相等且可哈希(dataclass eq=False),直接用 set 去重。
|
|
175
176
|
"""
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
seen: set[Scope] = set()
|
|
178
|
+
for scope in self._iter_scopes(context):
|
|
179
|
+
if scope in seen:
|
|
180
|
+
continue # 同一 scope 被多模板共享,只处理一次
|
|
181
|
+
seen.add(scope)
|
|
182
|
+
if scope.var_statements:
|
|
183
|
+
self._resolve_var_file(scope, scope.var_statements)
|
|
184
|
+
|
|
185
|
+
def _iter_scopes(self, context: ResolvedContext) -> list[Scope]:
|
|
186
|
+
"""context 中全部文件 scope(root + 各定义点 + schema 隐式导入)。"""
|
|
187
|
+
scopes: list[Scope] = [context.root_scope]
|
|
188
|
+
if context.schema_scope is not None:
|
|
189
|
+
scopes.append(context.schema_scope)
|
|
190
|
+
scopes.extend(context.template_scopes.values())
|
|
191
|
+
return scopes
|
|
192
|
+
|
|
193
|
+
def _resolve_var_file(self, scope: Scope, stmts: list[VarStmt]) -> None:
|
|
194
|
+
"""单个文件内 !var 求值(用该文件 scope 解析,绑定该文件命名空间)。"""
|
|
195
|
+
namespace = scope.namespaces
|
|
179
196
|
by_alias = {s.alias: i for i, s in enumerate(stmts)}
|
|
180
197
|
|
|
181
|
-
# 1) 依赖图:!var 的 $
|
|
198
|
+
# 1) 依赖图:!var 的 $ 引用指向同文件其他 !var 别名 → 边(walk 静态提取)
|
|
182
199
|
deps: dict[int, set[int]] = {}
|
|
183
200
|
for i, s in enumerate(stmts):
|
|
184
201
|
dep_indices: set[int] = set()
|
|
@@ -208,10 +225,10 @@ class AstBuilder:
|
|
|
208
225
|
for i in range(len(stmts)):
|
|
209
226
|
visit(i)
|
|
210
227
|
|
|
211
|
-
# 3) 拓扑序求值 → path 投影 →
|
|
228
|
+
# 3) 拓扑序求值 → path 投影 → 填本文件命名空间(duplicate 检测)
|
|
212
229
|
for i in order:
|
|
213
230
|
s = stmts[i]
|
|
214
|
-
rv = self._resolve_value(s.value, '',
|
|
231
|
+
rv = self._resolve_value(s.value, '', scope)
|
|
215
232
|
if rv is None:
|
|
216
233
|
continue # 值解析失败已报告
|
|
217
234
|
if s.json_path:
|
|
@@ -220,10 +237,10 @@ class AstBuilder:
|
|
|
220
237
|
except (KeyError, IndexError, TypeError):
|
|
221
238
|
self._err('var.path_failed', {'alias': s.alias}, s.source, '')
|
|
222
239
|
continue
|
|
223
|
-
if s.alias in
|
|
240
|
+
if s.alias in namespace:
|
|
224
241
|
self._err('namespace.duplicate', {'name': s.alias}, s.source, '')
|
|
225
242
|
continue
|
|
226
|
-
|
|
243
|
+
namespace[s.alias] = rv
|
|
227
244
|
|
|
228
245
|
# ═══════════════════════════════════════════════════════
|
|
229
246
|
# 递归默认值防护(方案 C:默认引用图 + 静态环检测)
|
|
@@ -299,7 +316,7 @@ class AstBuilder:
|
|
|
299
316
|
return
|
|
300
317
|
for node in walk(v):
|
|
301
318
|
if isinstance(node, TemplateCallValue):
|
|
302
|
-
k = scope.get(node.template_name)
|
|
319
|
+
k = scope.visible.get(node.template_name)
|
|
303
320
|
if k is not None:
|
|
304
321
|
yield k, node.source
|
|
305
322
|
|
|
@@ -401,7 +418,7 @@ class AstBuilder:
|
|
|
401
418
|
"""数组元素 noexist → 报错并按 null(保留位置)。"""
|
|
402
419
|
if isinstance(value, StdLiteral) and value.kind == 'noexist':
|
|
403
420
|
self._err('value.noexist_in_array', {}, source, path)
|
|
404
|
-
return StdLiteral(kind='null', value=None)
|
|
421
|
+
return StdLiteral(kind='null', value=None, source=source)
|
|
405
422
|
return value
|
|
406
423
|
|
|
407
424
|
def _bind_to_std(self, value: Value | StdValue, path: str, scope: Scope) -> StdValue:
|
|
@@ -430,7 +447,7 @@ class AstBuilder:
|
|
|
430
447
|
case LiteralValue(value=tok):
|
|
431
448
|
return self._convert_literal(tok)
|
|
432
449
|
case DollarValue(name=n, type_cast=tc, source=src):
|
|
433
|
-
return self._resolve_dollar(n, tc, path, src)
|
|
450
|
+
return self._resolve_dollar(n, tc, path, src, scope)
|
|
434
451
|
case DictValue(fields=fs, constraints=cs, unpacks=ups):
|
|
435
452
|
std_fields: list[StdField] = []
|
|
436
453
|
# **expr 解包:目标必须是 dict,展开为键值对(disjoint merge 查重在 _finalize_object)
|
|
@@ -447,7 +464,11 @@ class AstBuilder:
|
|
|
447
464
|
# dict 结构级约束(作用于该字面量整体):解析后挂节点,不执行
|
|
448
465
|
specs, diags = resolve_constraint_list(cs, scope)
|
|
449
466
|
self._collector.extend(diags)
|
|
450
|
-
return StdObject(
|
|
467
|
+
return StdObject(
|
|
468
|
+
fields=self._finalize_object(std_fields, path),
|
|
469
|
+
constraints=specs,
|
|
470
|
+
source=raw.source,
|
|
471
|
+
)
|
|
451
472
|
case ArrayValue(elements=els):
|
|
452
473
|
std_elements: list[StdValue] = []
|
|
453
474
|
for i, e in enumerate(els):
|
|
@@ -462,7 +483,7 @@ class AstBuilder:
|
|
|
462
483
|
if rv is None:
|
|
463
484
|
continue
|
|
464
485
|
std_elements.append(self._array_noexist(rv, e.source, f'{path}[{i}]'))
|
|
465
|
-
return StdArray(elements=std_elements)
|
|
486
|
+
return StdArray(elements=std_elements, source=raw.source)
|
|
466
487
|
case TemplateCallValue(
|
|
467
488
|
template_name=tn,
|
|
468
489
|
positional_args=pa,
|
|
@@ -489,39 +510,51 @@ class AstBuilder:
|
|
|
489
510
|
self,
|
|
490
511
|
tok: StringToken | IntegerToken | FloatToken | BoolToken | NullToken | NoexistToken,
|
|
491
512
|
) -> StdLiteral:
|
|
492
|
-
"""将字面量 Token 转为 StdLiteral
|
|
513
|
+
"""将字面量 Token 转为 StdLiteral(携带 token 在源文档中的位置)。"""
|
|
493
514
|
match tok:
|
|
494
515
|
case StringToken(value=v):
|
|
495
|
-
return StdLiteral(kind='str', value=v)
|
|
516
|
+
return StdLiteral(kind='str', value=v, source=tok.raw.source)
|
|
496
517
|
case IntegerToken(value=v):
|
|
497
|
-
return StdLiteral(kind='int', value=v)
|
|
518
|
+
return StdLiteral(kind='int', value=v, source=tok.raw.source)
|
|
498
519
|
case FloatToken(value=v):
|
|
499
|
-
return StdLiteral(kind='float', value=v) # Decimal,含 NaN/±Inf
|
|
520
|
+
return StdLiteral(kind='float', value=v, source=tok.raw.source) # Decimal,含 NaN/±Inf
|
|
500
521
|
case BoolToken(value=v):
|
|
501
|
-
return StdLiteral(kind='bool', value=v)
|
|
522
|
+
return StdLiteral(kind='bool', value=v, source=tok.raw.source)
|
|
502
523
|
case NullToken():
|
|
503
|
-
return StdLiteral(kind='null', value=None)
|
|
524
|
+
return StdLiteral(kind='null', value=None, source=tok.raw.source)
|
|
504
525
|
case NoexistToken():
|
|
505
|
-
return StdLiteral(kind='noexist', value=None)
|
|
526
|
+
return StdLiteral(kind='noexist', value=None, source=tok.raw.source)
|
|
506
527
|
raise TypeError(f'未知字面量 token 类型: {type(tok)}')
|
|
507
528
|
|
|
508
529
|
def _resolve_dollar(
|
|
509
|
-
self,
|
|
530
|
+
self,
|
|
531
|
+
name: str,
|
|
532
|
+
type_cast: str | None,
|
|
533
|
+
path: str,
|
|
534
|
+
source: SourceRange | None = None,
|
|
535
|
+
scope: Scope | None = None,
|
|
510
536
|
) -> StdValue:
|
|
511
|
-
"""解析 ``$name``
|
|
537
|
+
"""解析 ``$name`` 引用(按书写位置的 scope 就地查该文件 $ 命名空间,§1.8)。
|
|
512
538
|
|
|
513
|
-
source 为 ``$name``
|
|
539
|
+
type_cast 为显式 as bool/int/float/str 转换;source 为 ``$name`` 表达式在源码中的位置。
|
|
540
|
+
本文件空间查不到 → ``dollar.undefined`` 警告取 null(不中断编译)。
|
|
514
541
|
"""
|
|
515
|
-
if
|
|
542
|
+
namespace = scope.namespaces if scope is not None else {}
|
|
543
|
+
if name not in namespace:
|
|
516
544
|
self._warn('dollar.undefined', {'name': name}, source, path)
|
|
517
|
-
return StdLiteral(kind='null', value=None)
|
|
545
|
+
return StdLiteral(kind='null', value=None, source=source)
|
|
518
546
|
|
|
519
|
-
raw =
|
|
547
|
+
raw = namespace[name]
|
|
520
548
|
# namespace 统一存 StdValue(!env/!file/!var 均由 python_to_std / 求值产出)
|
|
521
549
|
if type_cast is None:
|
|
522
550
|
return raw
|
|
523
551
|
if isinstance(raw, (StdArray, StdObject)):
|
|
524
552
|
return raw # 结构化值 cast 不适用
|
|
553
|
+
# 空字面量:as 转换**保持传播**(null → null、noexist → noexist),
|
|
554
|
+
# 不做任何转换、不产生警告——三态可空语义不因显式 as 而丢失
|
|
555
|
+
# (此时 raw 已收窄为 StdLiteral)
|
|
556
|
+
if raw.kind in ('null', 'noexist'):
|
|
557
|
+
return raw
|
|
525
558
|
raw = raw.value # StdLiteral:提取 Python 值走转换
|
|
526
559
|
|
|
527
560
|
match type_cast:
|
|
@@ -558,10 +591,10 @@ class AstBuilder:
|
|
|
558
591
|
)
|
|
559
592
|
)
|
|
560
593
|
val = False
|
|
561
|
-
return StdLiteral(kind='bool', value=val)
|
|
594
|
+
return StdLiteral(kind='bool', value=val, source=source)
|
|
562
595
|
case 'int':
|
|
563
596
|
try:
|
|
564
|
-
return StdLiteral(kind='int', value=int(cast(Any, raw)))
|
|
597
|
+
return StdLiteral(kind='int', value=int(cast(Any, raw)), source=source)
|
|
565
598
|
except (ValueError, TypeError):
|
|
566
599
|
self._collector.add(
|
|
567
600
|
Diagnostic(
|
|
@@ -572,10 +605,10 @@ class AstBuilder:
|
|
|
572
605
|
path,
|
|
573
606
|
)
|
|
574
607
|
)
|
|
575
|
-
return StdLiteral(kind='int', value=0)
|
|
608
|
+
return StdLiteral(kind='int', value=0, source=source)
|
|
576
609
|
case 'float':
|
|
577
610
|
try:
|
|
578
|
-
return StdLiteral(kind='float', value=decimal.Decimal(str(raw)))
|
|
611
|
+
return StdLiteral(kind='float', value=decimal.Decimal(str(raw)), source=source)
|
|
579
612
|
except (ValueError, TypeError, decimal.InvalidOperation):
|
|
580
613
|
self._collector.add(
|
|
581
614
|
Diagnostic(
|
|
@@ -586,11 +619,22 @@ class AstBuilder:
|
|
|
586
619
|
path,
|
|
587
620
|
)
|
|
588
621
|
)
|
|
589
|
-
return StdLiteral(kind='float', value=decimal.Decimal(0))
|
|
622
|
+
return StdLiteral(kind='float', value=decimal.Decimal(0), source=source)
|
|
590
623
|
case 'str':
|
|
591
|
-
|
|
624
|
+
# as str = 字符串化:任意字面量按**语言字面量风格**转为字符串,
|
|
625
|
+
# 保证可 round-trip 还原回原值(bool → true/false、float 特殊值
|
|
626
|
+
# → nan/+inf/-inf),不泄漏 Python 内部表示;null/noexist 已在上方
|
|
627
|
+
# 保持传播,不进入本分支
|
|
628
|
+
if isinstance(raw, bool):
|
|
629
|
+
return StdLiteral(kind='str', value='true' if raw else 'false', source=source)
|
|
630
|
+
if isinstance(raw, decimal.Decimal):
|
|
631
|
+
if raw.is_nan():
|
|
632
|
+
return StdLiteral(kind='str', value='nan', source=source)
|
|
633
|
+
if raw.is_infinite():
|
|
634
|
+
return StdLiteral(kind='str', value='+inf' if raw > 0 else '-inf', source=source)
|
|
635
|
+
return StdLiteral(kind='str', value=str(raw), source=source)
|
|
592
636
|
case _:
|
|
593
|
-
return StdLiteral(kind='str', value=str(raw))
|
|
637
|
+
return StdLiteral(kind='str', value=str(raw), source=source)
|
|
594
638
|
|
|
595
639
|
# ═══════════════════════════════════════════════════════
|
|
596
640
|
# 模板展开
|
|
@@ -709,7 +753,7 @@ class AstBuilder:
|
|
|
709
753
|
for combo in combos:
|
|
710
754
|
pa, na, upa, upk = self._build_expand_args(pos_args, named, unpack_args, unpack_kwargs, axes, combo)
|
|
711
755
|
results.append(self._instantiate_once(template_name, pa, na, upa, upk, path, source, scope))
|
|
712
|
-
return StdArray(elements=results)
|
|
756
|
+
return StdArray(elements=results, source=source)
|
|
713
757
|
|
|
714
758
|
def _resolve_axis(
|
|
715
759
|
self,
|
|
@@ -782,7 +826,7 @@ class AstBuilder:
|
|
|
782
826
|
解包:``*expr``(list → 位置参数)/ ``**expr``(dict → 命名参数);
|
|
783
827
|
解包键与已有参数冲突 → ``dict.duplicate_key``(disjoint merge,§2.7)。
|
|
784
828
|
"""
|
|
785
|
-
key = scope.get(template_name)
|
|
829
|
+
key = scope.visible.get(template_name)
|
|
786
830
|
if key is None:
|
|
787
831
|
self._collector.add(
|
|
788
832
|
Diagnostic(Severity.ERROR, 'template.undefined', {'template': template_name}, source, path)
|
|
@@ -931,14 +975,19 @@ class AstBuilder:
|
|
|
931
975
|
if tf.name in param_values:
|
|
932
976
|
pv = param_values[tf.name]
|
|
933
977
|
v = self._bind_to_std(pv, child, scope)
|
|
978
|
+
# 字段位置 = 调用点参数表达式(值无来源时回退至此,而非模板定义);
|
|
979
|
+
# 展开轴元素等彻底无来源时最后回退模板定义(避免丢失位置)
|
|
980
|
+
field_src = getattr(pv, 'source', None) or v.source or tf.source
|
|
934
981
|
elif tf.default_value is not None:
|
|
935
982
|
v = self._resolve_value(tf.default_value, child, inner_scope)
|
|
983
|
+
# 字段位置 = 默认值表达式(模板定义内)
|
|
984
|
+
field_src = v.source if v is not None else tf.source
|
|
936
985
|
else:
|
|
937
986
|
continue # 必填且未提供 → 已在上面报错
|
|
938
987
|
|
|
939
988
|
specs, diags = resolve_constraints(tf.constraints, inner_scope)
|
|
940
989
|
self._collector.extend(diags)
|
|
941
|
-
std_fields.append(StdField(name=tf.name, value=v, source=
|
|
990
|
+
std_fields.append(StdField(name=tf.name, value=v, source=field_src, constraints=specs))
|
|
942
991
|
|
|
943
992
|
# allow_extra=true:额外字段作为扩展字段进入内容(按调用点 scope 解析)
|
|
944
993
|
for name, (arg_val, arg_src) in extra_args.items():
|
|
@@ -949,4 +998,9 @@ class AstBuilder:
|
|
|
949
998
|
# 模板级约束(: 起始,约束整个 dict):解析后挂到实例节点
|
|
950
999
|
tpl_specs, tpl_diags = resolve_constraint_list(template.constraints, inner_scope)
|
|
951
1000
|
self._collector.extend(tpl_diags)
|
|
952
|
-
return StdObject(
|
|
1001
|
+
return StdObject(
|
|
1002
|
+
fields=self._finalize_object(std_fields, path),
|
|
1003
|
+
template=key,
|
|
1004
|
+
constraints=tpl_specs,
|
|
1005
|
+
source=source,
|
|
1006
|
+
)
|
|
@@ -24,14 +24,16 @@ from infinity_data.semantic.std import (
|
|
|
24
24
|
StdArray,
|
|
25
25
|
StdField,
|
|
26
26
|
StdLiteral,
|
|
27
|
+
StdNode,
|
|
27
28
|
StdObject,
|
|
28
29
|
StdValue,
|
|
30
|
+
is_std_node,
|
|
29
31
|
is_std_value,
|
|
30
32
|
python_to_std,
|
|
31
33
|
)
|
|
32
34
|
|
|
33
35
|
if TYPE_CHECKING:
|
|
34
|
-
from infinity_data.semantic.resolver.models import
|
|
36
|
+
from infinity_data.semantic.resolver.models import TemplateKey
|
|
35
37
|
|
|
36
38
|
__all__ = [
|
|
37
39
|
'LiteralKind',
|
|
@@ -40,8 +42,10 @@ __all__ = [
|
|
|
40
42
|
'StdDocument',
|
|
41
43
|
'StdField',
|
|
42
44
|
'StdLiteral',
|
|
45
|
+
'StdNode',
|
|
43
46
|
'StdObject',
|
|
44
47
|
'StdValue',
|
|
48
|
+
'is_std_node',
|
|
45
49
|
'is_std_value',
|
|
46
50
|
'python_to_std',
|
|
47
51
|
]
|
|
@@ -58,8 +62,10 @@ class StdDocument:
|
|
|
58
62
|
|
|
59
63
|
- ``root``:编译产物(顶层对象)
|
|
60
64
|
- ``templates``:全部已加载模板(:class:`TemplateKey` → 定义,含 ``!from`` 导入的)
|
|
61
|
-
- ``scope
|
|
62
|
-
与 ``templates`` 配合可完整解析:可见名 → TemplateKey → TemplateDef
|
|
65
|
+
- ``scope``:**入口文件**的可见名表快照(可见名 → :class:`TemplateKey`),
|
|
66
|
+
与 ``templates`` 配合可完整解析:可见名 → TemplateKey → TemplateDef。
|
|
67
|
+
纯快照(普通 dict):各文件的 `$` 命名空间 / !var 关联在 Phase 1 的
|
|
68
|
+
:class:`Scope` 上(§1.8),不随产物下发。
|
|
63
69
|
|
|
64
70
|
不携带诊断:所有诊断经共享 :class:`DiagnosticCollector` 收集,
|
|
65
71
|
由流水线在 :class:`CompilationResult` 上承载——诊断不属于文档数据。
|
|
@@ -67,4 +73,4 @@ class StdDocument:
|
|
|
67
73
|
|
|
68
74
|
root: StdObject = field(default_factory=StdObject)
|
|
69
75
|
templates: dict[TemplateKey, TemplateDef] = field(default_factory=lambda: {})
|
|
70
|
-
scope:
|
|
76
|
+
scope: dict[str, TemplateKey] = field(default_factory=lambda: {})
|
|
@@ -38,7 +38,7 @@ _INVALID_CONSTRAINT = '@invalid'
|
|
|
38
38
|
def expand_annotation(annotation: Constraints) -> Constraints:
|
|
39
39
|
"""展开约束语法糖:多约束 ``<a, b, c>`` → ``all(a, b, c)``。
|
|
40
40
|
|
|
41
|
-
(``type?`` → ``
|
|
41
|
+
(``type?`` → ``any(type, ?)`` 已在 parser 阶段展开。)
|
|
42
42
|
"""
|
|
43
43
|
if len(annotation.constraints) > 1:
|
|
44
44
|
return Constraints(
|
|
@@ -141,7 +141,7 @@ def resolve_constraint_arg(c: Constraint, scope: Scope) -> tuple[Any, list[Diagn
|
|
|
141
141
|
|
|
142
142
|
def translate_name(name: str, scope: Scope) -> str:
|
|
143
143
|
"""可见名 → 真名字符串。未命中(如 has(field) 的裸字段名)保留原名。"""
|
|
144
|
-
key = scope.get(name)
|
|
144
|
+
key = scope.visible.get(name)
|
|
145
145
|
return str(key) if key is not None else name
|
|
146
146
|
|
|
147
147
|
|
|
@@ -171,6 +171,10 @@ register_diagnostic_define(
|
|
|
171
171
|
diagnostic_define('constraint.size_out', '{path_prefix}大小 {size} 不在范围 [{lo}, {hi}] 内'),
|
|
172
172
|
diagnostic_define('constraint.each_need', '{path_prefix}each() 需要一个约束参数'),
|
|
173
173
|
diagnostic_define('constraint.each_only', '{path_prefix}each 约束适用于 list/dict,实际 {actual}'),
|
|
174
|
+
diagnostic_define('constraint.position_only', '{path_prefix}position 约束只适用于 list,实际 {actual}'),
|
|
175
|
+
diagnostic_define('constraint.position_index', '{path_prefix}position 下标必须是整数: {value!r}'),
|
|
176
|
+
diagnostic_define('constraint.position_need', '{path_prefix}position() 需要一个约束参数'),
|
|
177
|
+
diagnostic_define('constraint.position_out', '{path_prefix}下标 {index} 越界(list 大小 {size})'),
|
|
174
178
|
diagnostic_define('constraint.in_not_in', '{path_prefix}值 {value} 不在允许的值 {choices!r} 中'),
|
|
175
179
|
diagnostic_define('constraint.string_only', '{path_prefix}{constraint} 约束只适用于字符串'),
|
|
176
180
|
diagnostic_define('constraint.invalid_value', '{path_prefix}无效的{what} {value!r}'),
|