infinity-data 2.1.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.1.0/src/infinity_data.egg-info → infinity_data-2.2.0}/PKG-INFO +1 -1
- {infinity_data-2.1.0 → infinity_data-2.2.0}/pyproject.toml +3 -2
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/pipeline.py +1 -1
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/builder/__init__.py +4 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/builder/builder.py +89 -51
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/builder/models.py +10 -4
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/constraints.py +1 -1
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/executor/executor.py +15 -6
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/dict_constraints.py +3 -1
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/general.py +8 -3
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/resolver/identity.py +29 -7
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/resolver/imports.py +55 -5
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/resolver/models.py +28 -13
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/resolver/resolver.py +53 -16
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/std.py +40 -10
- {infinity_data-2.1.0 → infinity_data-2.2.0/src/infinity_data.egg-info}/PKG-INFO +1 -1
- {infinity_data-2.1.0 → infinity_data-2.2.0}/LICENSE +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/README.md +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/setup.cfg +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/__init__.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/emit/__init__.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/emit/converter.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/frontend.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/infra/__init__.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/infra/diagnostics.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/infra/file.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/infra/ll1_stream.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/infra/location.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/infra/path.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/parser/__init__.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/parser/diagnostics.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/parser/models.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/parser/parser.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/parser/token_stream.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/py.typed +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/sandbox/__init__.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/sandbox/config.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/sandbox/errors.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/sandbox/mediator.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/sandbox/schema.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/__init__.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/diagnostics.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/executor/__init__.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/jsonpath.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/__init__.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/_core.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/logic.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/types.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/resolver/__init__.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/__init__.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/char_stream.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/diagnostics.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/finalizer.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/models/__init__.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/models/raw_tokens.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/models/tokens.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/tokenizer.py +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data.egg-info/SOURCES.txt +0 -0
- {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data.egg-info/dependency_links.txt +0 -0
- {infinity_data-2.1.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
|
|
|
@@ -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,34 +510,41 @@ 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
|
|
@@ -563,10 +591,10 @@ class AstBuilder:
|
|
|
563
591
|
)
|
|
564
592
|
)
|
|
565
593
|
val = False
|
|
566
|
-
return StdLiteral(kind='bool', value=val)
|
|
594
|
+
return StdLiteral(kind='bool', value=val, source=source)
|
|
567
595
|
case 'int':
|
|
568
596
|
try:
|
|
569
|
-
return StdLiteral(kind='int', value=int(cast(Any, raw)))
|
|
597
|
+
return StdLiteral(kind='int', value=int(cast(Any, raw)), source=source)
|
|
570
598
|
except (ValueError, TypeError):
|
|
571
599
|
self._collector.add(
|
|
572
600
|
Diagnostic(
|
|
@@ -577,10 +605,10 @@ class AstBuilder:
|
|
|
577
605
|
path,
|
|
578
606
|
)
|
|
579
607
|
)
|
|
580
|
-
return StdLiteral(kind='int', value=0)
|
|
608
|
+
return StdLiteral(kind='int', value=0, source=source)
|
|
581
609
|
case 'float':
|
|
582
610
|
try:
|
|
583
|
-
return StdLiteral(kind='float', value=decimal.Decimal(str(raw)))
|
|
611
|
+
return StdLiteral(kind='float', value=decimal.Decimal(str(raw)), source=source)
|
|
584
612
|
except (ValueError, TypeError, decimal.InvalidOperation):
|
|
585
613
|
self._collector.add(
|
|
586
614
|
Diagnostic(
|
|
@@ -591,22 +619,22 @@ class AstBuilder:
|
|
|
591
619
|
path,
|
|
592
620
|
)
|
|
593
621
|
)
|
|
594
|
-
return StdLiteral(kind='float', value=decimal.Decimal(0))
|
|
622
|
+
return StdLiteral(kind='float', value=decimal.Decimal(0), source=source)
|
|
595
623
|
case 'str':
|
|
596
624
|
# as str = 字符串化:任意字面量按**语言字面量风格**转为字符串,
|
|
597
625
|
# 保证可 round-trip 还原回原值(bool → true/false、float 特殊值
|
|
598
626
|
# → nan/+inf/-inf),不泄漏 Python 内部表示;null/noexist 已在上方
|
|
599
627
|
# 保持传播,不进入本分支
|
|
600
628
|
if isinstance(raw, bool):
|
|
601
|
-
return StdLiteral(kind='str', value='true' if raw else 'false')
|
|
629
|
+
return StdLiteral(kind='str', value='true' if raw else 'false', source=source)
|
|
602
630
|
if isinstance(raw, decimal.Decimal):
|
|
603
631
|
if raw.is_nan():
|
|
604
|
-
return StdLiteral(kind='str', value='nan')
|
|
632
|
+
return StdLiteral(kind='str', value='nan', source=source)
|
|
605
633
|
if raw.is_infinite():
|
|
606
|
-
return StdLiteral(kind='str', value='+inf' if raw > 0 else '-inf')
|
|
607
|
-
return StdLiteral(kind='str', value=str(raw))
|
|
634
|
+
return StdLiteral(kind='str', value='+inf' if raw > 0 else '-inf', source=source)
|
|
635
|
+
return StdLiteral(kind='str', value=str(raw), source=source)
|
|
608
636
|
case _:
|
|
609
|
-
return StdLiteral(kind='str', value=str(raw))
|
|
637
|
+
return StdLiteral(kind='str', value=str(raw), source=source)
|
|
610
638
|
|
|
611
639
|
# ═══════════════════════════════════════════════════════
|
|
612
640
|
# 模板展开
|
|
@@ -725,7 +753,7 @@ class AstBuilder:
|
|
|
725
753
|
for combo in combos:
|
|
726
754
|
pa, na, upa, upk = self._build_expand_args(pos_args, named, unpack_args, unpack_kwargs, axes, combo)
|
|
727
755
|
results.append(self._instantiate_once(template_name, pa, na, upa, upk, path, source, scope))
|
|
728
|
-
return StdArray(elements=results)
|
|
756
|
+
return StdArray(elements=results, source=source)
|
|
729
757
|
|
|
730
758
|
def _resolve_axis(
|
|
731
759
|
self,
|
|
@@ -798,7 +826,7 @@ class AstBuilder:
|
|
|
798
826
|
解包:``*expr``(list → 位置参数)/ ``**expr``(dict → 命名参数);
|
|
799
827
|
解包键与已有参数冲突 → ``dict.duplicate_key``(disjoint merge,§2.7)。
|
|
800
828
|
"""
|
|
801
|
-
key = scope.get(template_name)
|
|
829
|
+
key = scope.visible.get(template_name)
|
|
802
830
|
if key is None:
|
|
803
831
|
self._collector.add(
|
|
804
832
|
Diagnostic(Severity.ERROR, 'template.undefined', {'template': template_name}, source, path)
|
|
@@ -947,14 +975,19 @@ class AstBuilder:
|
|
|
947
975
|
if tf.name in param_values:
|
|
948
976
|
pv = param_values[tf.name]
|
|
949
977
|
v = self._bind_to_std(pv, child, scope)
|
|
978
|
+
# 字段位置 = 调用点参数表达式(值无来源时回退至此,而非模板定义);
|
|
979
|
+
# 展开轴元素等彻底无来源时最后回退模板定义(避免丢失位置)
|
|
980
|
+
field_src = getattr(pv, 'source', None) or v.source or tf.source
|
|
950
981
|
elif tf.default_value is not None:
|
|
951
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
|
|
952
985
|
else:
|
|
953
986
|
continue # 必填且未提供 → 已在上面报错
|
|
954
987
|
|
|
955
988
|
specs, diags = resolve_constraints(tf.constraints, inner_scope)
|
|
956
989
|
self._collector.extend(diags)
|
|
957
|
-
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))
|
|
958
991
|
|
|
959
992
|
# allow_extra=true:额外字段作为扩展字段进入内容(按调用点 scope 解析)
|
|
960
993
|
for name, (arg_val, arg_src) in extra_args.items():
|
|
@@ -965,4 +998,9 @@ class AstBuilder:
|
|
|
965
998
|
# 模板级约束(: 起始,约束整个 dict):解析后挂到实例节点
|
|
966
999
|
tpl_specs, tpl_diags = resolve_constraint_list(template.constraints, inner_scope)
|
|
967
1000
|
self._collector.extend(tpl_diags)
|
|
968
|
-
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: {})
|
|
@@ -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
|
|
|
@@ -67,7 +67,8 @@ class ConstraintExecutor:
|
|
|
67
67
|
self._validate_field(f, collector, path)
|
|
68
68
|
# 结构级约束(dict 级 / 模板级 / 顶层):全部执行(不短路)
|
|
69
69
|
for spec in node.constraints:
|
|
70
|
-
|
|
70
|
+
# 诊断指向被检查的对象本身(无来源时回退约束表达式位置)
|
|
71
|
+
result = self._exec(spec, node, node.source or spec.source, path)
|
|
71
72
|
if not result.ok:
|
|
72
73
|
collector.extend(result.diagnostics)
|
|
73
74
|
elif isinstance(node, StdArray):
|
|
@@ -79,8 +80,10 @@ class ConstraintExecutor:
|
|
|
79
80
|
child = f'{path}.{field.name}' if path else field.name
|
|
80
81
|
if field.value is not None:
|
|
81
82
|
self.validate(field.value, collector, child)
|
|
83
|
+
# 被检查对象 = 字段值:诊断指向值本身(无来源时回退字段位置)
|
|
84
|
+
value_src = field.value.source if field.value is not None else None
|
|
82
85
|
for spec in field.constraints:
|
|
83
|
-
result = self._exec(spec, field.value,
|
|
86
|
+
result = self._exec(spec, field.value, value_src or field.source, child)
|
|
84
87
|
if not result.ok:
|
|
85
88
|
collector.extend(result.diagnostics)
|
|
86
89
|
break # 约束链短路(与构建期语义一致)
|
|
@@ -105,7 +108,9 @@ class ConstraintExecutor:
|
|
|
105
108
|
key = self._templates_by_name.get(constraint.name)
|
|
106
109
|
if key is not None:
|
|
107
110
|
return self._check_template(key, value, source, path)
|
|
108
|
-
|
|
111
|
+
# 诊断位置 = 被检查对象的位置(调用方已按值/字段/对象解析),
|
|
112
|
+
# 不再取约束表达式自身(spec.source)——错误指向值而非约束声明
|
|
113
|
+
return self._registry.apply(constraint, value, source, path, self._exec)
|
|
109
114
|
|
|
110
115
|
def _check_template(
|
|
111
116
|
self,
|
|
@@ -157,7 +162,9 @@ class ConstraintExecutor:
|
|
|
157
162
|
specs, rdiags = resolve_constraints(tf.constraints, scope)
|
|
158
163
|
diags.extend(rdiags)
|
|
159
164
|
for spec in specs:
|
|
160
|
-
|
|
165
|
+
# 诊断指向手写 dict 中被检查的字段值(而非模板定义里的约束);
|
|
166
|
+
# 值/字段均无来源(如外部导入数据)时回退外层被检查对象位置
|
|
167
|
+
result = self._exec(spec, f.value, f.value.source or f.source or source, child)
|
|
161
168
|
if not result.ok:
|
|
162
169
|
diags.extend(result.diagnostics)
|
|
163
170
|
break
|
|
@@ -242,7 +249,8 @@ class ConstraintExecutor:
|
|
|
242
249
|
specs, rdiags = resolve_constraints(tf.constraints, scope)
|
|
243
250
|
diags.extend(rdiags)
|
|
244
251
|
for spec in specs:
|
|
245
|
-
|
|
252
|
+
# 诊断指向顶层对象中被检查的字段值(而非模板定义里的约束)
|
|
253
|
+
result = self._exec(spec, f.value, f.value.source or f.source, tf.name)
|
|
246
254
|
if not result.ok:
|
|
247
255
|
diags.extend(result.diagnostics)
|
|
248
256
|
|
|
@@ -250,7 +258,8 @@ class ConstraintExecutor:
|
|
|
250
258
|
specs, rdiags = resolve_constraint_list(tpl.constraints, scope)
|
|
251
259
|
diags.extend(rdiags)
|
|
252
260
|
for spec in specs:
|
|
253
|
-
|
|
261
|
+
# 顶层对象无来源时回退约束表达式位置(避免丢位置)
|
|
262
|
+
result = self._exec(spec, root, root.source or spec.source, '')
|
|
254
263
|
if not result.ok:
|
|
255
264
|
diags.extend(result.diagnostics)
|
|
256
265
|
|
{infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/dict_constraints.py
RENAMED
|
@@ -62,4 +62,6 @@ def _check_field(
|
|
|
62
62
|
# noexist = 键不出现 → 视为字段缺失(与 has 同理)
|
|
63
63
|
if f is None or f.is_noexist:
|
|
64
64
|
return fail_result('constraint.field_missing', {'field': name}, source, path)
|
|
65
|
-
|
|
65
|
+
# 诊断指向被检查的字段值本身(无来源时回退字段位置)
|
|
66
|
+
f_src = f.value.source if f.value is not None else f.source
|
|
67
|
+
return executor(spec, f.value, f_src or source, f'{path}.{name}')
|
|
@@ -123,12 +123,15 @@ def _check_each(
|
|
|
123
123
|
diags: list[Diagnostic] = []
|
|
124
124
|
if isinstance(val, StdArray):
|
|
125
125
|
for i, elem in enumerate(val.elements):
|
|
126
|
-
|
|
126
|
+
# 诊断指向被检查的元素本身(无来源时回退容器位置)
|
|
127
|
+
r = executor(spec, elem, elem.source or source, f'{path}[{i}]')
|
|
127
128
|
if not r.ok:
|
|
128
129
|
diags.extend(r.diagnostics)
|
|
129
130
|
elif isinstance(val, StdObject):
|
|
130
131
|
for f in val.fields:
|
|
131
|
-
|
|
132
|
+
# 诊断指向被检查的字段值本身(无来源时回退字段位置)
|
|
133
|
+
f_src = f.value.source if f.value is not None else f.source
|
|
134
|
+
r = executor(spec, f.value, f_src or source, f'{path}.{f.name}')
|
|
132
135
|
if not r.ok:
|
|
133
136
|
diags.extend(r.diagnostics)
|
|
134
137
|
else:
|
|
@@ -162,7 +165,9 @@ def _check_position(
|
|
|
162
165
|
return fail_result('constraint.position_need', {}, source, path)
|
|
163
166
|
if idx < 0 or idx >= len(val.elements):
|
|
164
167
|
return fail_result('constraint.position_out', {'index': idx, 'size': len(val.elements)}, source, path)
|
|
165
|
-
|
|
168
|
+
# 诊断指向被检查的元素本身(无来源时回退容器位置)
|
|
169
|
+
elem = val.elements[idx]
|
|
170
|
+
return executor(spec, elem, elem.source or source, f'{path}[{idx}]')
|
|
166
171
|
|
|
167
172
|
|
|
168
173
|
def _check_in(
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
"""模板真名计算:直接依赖组合哈希(§2.5
|
|
1
|
+
"""模板真名计算:直接依赖组合哈希(§2.5,含 `$` 数据依赖)。
|
|
2
2
|
|
|
3
|
-
identity(T) = SHA256(canon(T) || sorted(identity(直接依赖模板)))
|
|
3
|
+
identity(T) = SHA256(canon(T) || sorted(identity(直接依赖模板)) || sorted(import_identity(T 的 $ 引用)))
|
|
4
4
|
|
|
5
5
|
- ``canon(T)``::meth:`TemplateDef.canonical` —— AST 规范化序列化,输出**标准
|
|
6
6
|
infd 源码**(可被 parser 还原,round-trip;排除 source/位置,注释不影响)
|
|
7
7
|
- 直接依赖:T 定义文件 scope 中、T 实际引用的模板(值位置模板调用 + 约束中的
|
|
8
8
|
模板名,排除注册约束名——模板名不与已注册约束同名,见 ``template.shadows_builtin``)
|
|
9
|
+
- ``import_identity``:T 定义文件命名空间中其 `$` 引用对应绑定的**导入真名**(§1.8)
|
|
10
|
+
——模板身份覆盖数据依赖(!file/!env/!var 来源变化 → 身份变化,可审计)
|
|
9
11
|
- 闭包无需显式计算(Merkle 式):直接依赖的 identity 已含其自身依赖子树
|
|
10
12
|
- 环处理:DFS 栈上的依赖退化为「该模板内容 hash」(不递归),保证终止、确定、路径无关
|
|
11
13
|
|
|
@@ -22,6 +24,7 @@ from collections.abc import Collection
|
|
|
22
24
|
from infinity_data.parser import (
|
|
23
25
|
ConstraintCall,
|
|
24
26
|
ConstraintIdent,
|
|
27
|
+
DollarValue,
|
|
25
28
|
TemplateCallValue,
|
|
26
29
|
TemplateDef,
|
|
27
30
|
walk,
|
|
@@ -53,12 +56,26 @@ def extract_dependencies(tpl: TemplateDef, scope: Scope, builtin_names: Collecti
|
|
|
53
56
|
for n in names:
|
|
54
57
|
if n in builtin_names:
|
|
55
58
|
continue # 注册约束(内置/自定义)名,非模板依赖
|
|
56
|
-
key = scope.get(n)
|
|
59
|
+
key = scope.visible.get(n)
|
|
57
60
|
if key is not None:
|
|
58
61
|
deps.add(key)
|
|
59
62
|
return deps
|
|
60
63
|
|
|
61
64
|
|
|
65
|
+
def extract_import_dependencies(tpl: TemplateDef, scope: Scope) -> set[str]:
|
|
66
|
+
"""模板 T 的 `$` 数据依赖:walk 收集 ``$`` 引用名,映射为定义文件命名空间的**导入真名**(§1.8)。
|
|
67
|
+
|
|
68
|
+
导入真名由 scope 对象自身携带(``scope.import_identities``);真名是叶子哈希
|
|
69
|
+
(无递归),直接进入模板身份组合;找不到映射(如 `$` 未定义或该文件无对应
|
|
70
|
+
导入绑定)→ 跳过。数据来源变化 → 导入真名变化 → 模板身份变化。
|
|
71
|
+
"""
|
|
72
|
+
idents = scope.import_identities
|
|
73
|
+
if not idents:
|
|
74
|
+
return set()
|
|
75
|
+
names = {node.name for node in walk(tpl) if isinstance(node, DollarValue)}
|
|
76
|
+
return {idents[n] for n in names if n in idents}
|
|
77
|
+
|
|
78
|
+
|
|
62
79
|
# ═══════════════════════════════════════════════════════════
|
|
63
80
|
# 依赖闭包组合哈希
|
|
64
81
|
# ═══════════════════════════════════════════════════════════
|
|
@@ -73,16 +90,20 @@ def compute_identity_map(
|
|
|
73
90
|
template_scopes: dict[TemplateKey, Scope],
|
|
74
91
|
builtin_names: Collection[str],
|
|
75
92
|
) -> dict[TemplateKey, TemplateKey]:
|
|
76
|
-
"""计算 old → new 的 TemplateKey 映射(新 identity =
|
|
93
|
+
"""计算 old → new 的 TemplateKey 映射(新 identity = 依赖闭包组合哈希 + $ 数据依赖)。
|
|
77
94
|
|
|
78
|
-
- 无环:``identity = hash(content_hash || sorted(依赖 identity))``
|
|
95
|
+
- 无环:``identity = hash(content_hash || sorted(依赖 identity) || sorted(导入真名))``
|
|
79
96
|
- 环:DFS 栈上依赖退化为内容 hash(不递归)——终止、确定、路径无关
|
|
97
|
+
- `$` 导入真名由各定义点 scope 自身携带(§1.8,``scope.import_identities``)
|
|
80
98
|
"""
|
|
81
99
|
content_hashes: dict[TemplateKey, str] = {}
|
|
82
100
|
dependencies: dict[TemplateKey, set[TemplateKey]] = {}
|
|
101
|
+
import_deps: dict[TemplateKey, set[str]] = {}
|
|
83
102
|
for key, tpl in templates.items():
|
|
84
103
|
content_hashes[key] = _content_hash(tpl)
|
|
85
|
-
|
|
104
|
+
scope = template_scopes.get(key, Scope())
|
|
105
|
+
dependencies[key] = extract_dependencies(tpl, scope, builtin_names)
|
|
106
|
+
import_deps[key] = extract_import_dependencies(tpl, scope)
|
|
86
107
|
|
|
87
108
|
memo: dict[TemplateKey, str] = {}
|
|
88
109
|
stack: set[TemplateKey] = set()
|
|
@@ -94,7 +115,8 @@ def compute_identity_map(
|
|
|
94
115
|
return content_hashes[key] # 环:依赖退化为内容 hash(不递归)
|
|
95
116
|
stack.add(key)
|
|
96
117
|
dep_ids = sorted(identity_of(d) for d in dependencies[key])
|
|
97
|
-
|
|
118
|
+
import_ids = sorted(import_deps[key])
|
|
119
|
+
combined = content_hashes[key] + '|' + ','.join(dep_ids) + '|' + ','.join(import_ids)
|
|
98
120
|
ident = IDENTITY_PREFIX + hashlib.sha256(combined.encode('utf-8')).hexdigest()
|
|
99
121
|
memo[key] = ident
|
|
100
122
|
stack.remove(key)
|