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.
Files changed (60) hide show
  1. {infinity_data-2.1.0/src/infinity_data.egg-info → infinity_data-2.2.0}/PKG-INFO +1 -1
  2. {infinity_data-2.1.0 → infinity_data-2.2.0}/pyproject.toml +3 -2
  3. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/pipeline.py +1 -1
  4. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/builder/__init__.py +4 -0
  5. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/builder/builder.py +89 -51
  6. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/builder/models.py +10 -4
  7. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/constraints.py +1 -1
  8. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/executor/executor.py +15 -6
  9. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/dict_constraints.py +3 -1
  10. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/general.py +8 -3
  11. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/resolver/identity.py +29 -7
  12. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/resolver/imports.py +55 -5
  13. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/resolver/models.py +28 -13
  14. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/resolver/resolver.py +53 -16
  15. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/std.py +40 -10
  16. {infinity_data-2.1.0 → infinity_data-2.2.0/src/infinity_data.egg-info}/PKG-INFO +1 -1
  17. {infinity_data-2.1.0 → infinity_data-2.2.0}/LICENSE +0 -0
  18. {infinity_data-2.1.0 → infinity_data-2.2.0}/README.md +0 -0
  19. {infinity_data-2.1.0 → infinity_data-2.2.0}/setup.cfg +0 -0
  20. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/__init__.py +0 -0
  21. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/emit/__init__.py +0 -0
  22. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/emit/converter.py +0 -0
  23. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/frontend.py +0 -0
  24. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/infra/__init__.py +0 -0
  25. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/infra/diagnostics.py +0 -0
  26. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/infra/file.py +0 -0
  27. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/infra/ll1_stream.py +0 -0
  28. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/infra/location.py +0 -0
  29. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/infra/path.py +0 -0
  30. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/parser/__init__.py +0 -0
  31. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/parser/diagnostics.py +0 -0
  32. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/parser/models.py +0 -0
  33. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/parser/parser.py +0 -0
  34. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/parser/token_stream.py +0 -0
  35. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/py.typed +0 -0
  36. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/sandbox/__init__.py +0 -0
  37. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/sandbox/config.py +0 -0
  38. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/sandbox/errors.py +0 -0
  39. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/sandbox/mediator.py +0 -0
  40. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/sandbox/schema.py +0 -0
  41. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/__init__.py +0 -0
  42. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/diagnostics.py +0 -0
  43. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/executor/__init__.py +0 -0
  44. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/jsonpath.py +0 -0
  45. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/__init__.py +0 -0
  46. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/_core.py +0 -0
  47. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/logic.py +0 -0
  48. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/types.py +0 -0
  49. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/semantic/resolver/__init__.py +0 -0
  50. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/__init__.py +0 -0
  51. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/char_stream.py +0 -0
  52. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/diagnostics.py +0 -0
  53. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/finalizer.py +0 -0
  54. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/models/__init__.py +0 -0
  55. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/models/raw_tokens.py +0 -0
  56. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/models/tokens.py +0 -0
  57. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/tokenizer.py +0 -0
  58. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data.egg-info/SOURCES.txt +0 -0
  59. {infinity_data-2.1.0 → infinity_data-2.2.0}/src/infinity_data.egg-info/dependency_links.txt +0 -0
  60. {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
  Metadata-Version: 2.4
2
2
  Name: infinity-data
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: InfinityData:声明式配置语言(.infd/.inft)的 Python 编译器库
5
5
  Author: infinity_system: yin_bailiang
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "infinity-data"
3
- version = "2.1.0"
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
- self._namespace: dict[str, StdValue] = {} # $ 引用解析目标(!env/!file/!var 统一为 StdValue)
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(doc)
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, doc: Document) -> None:
171
- """!var 求值:静态依赖图 + 环检测 + 拓扑求值 + path 投影,填充 $ 命名空间。
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
- stmts = [s for s in doc.statements if isinstance(s, VarStmt)]
177
- if not stmts:
178
- return
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 的 $ 引用指向其他 !var 别名 → 边(walk 静态提取)
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 投影 → 填 namespace(duplicate 检测)
228
+ # 3) 拓扑序求值 → path 投影 → 填本文件命名空间(duplicate 检测)
212
229
  for i in order:
213
230
  s = stmts[i]
214
- rv = self._resolve_value(s.value, '', self._root_scope)
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 self._namespace:
240
+ if s.alias in namespace:
224
241
  self._err('namespace.duplicate', {'name': s.alias}, s.source, '')
225
242
  continue
226
- self._namespace[s.alias] = rv
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(fields=self._finalize_object(std_fields, path), constraints=specs)
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, name: str, type_cast: str | None, path: str, source: SourceRange | None = None
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`` 引用,type_cast 为显式 as bool/int/float/str 转换。
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 name not in self._namespace:
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 = self._namespace[name]
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=tf.source, constraints=specs))
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(fields=self._finalize_object(std_fields, path), template=key, constraints=tpl_specs)
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 Scope, TemplateKey
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``:**入口文件**的可见名表(可见名 → :class:`TemplateKey`),
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: Scope = field(default_factory=lambda: {})
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
- result = self._exec(spec, node, spec.source, path)
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, spec.source or field.source, child)
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
- return self._registry.apply(constraint, value, constraint.source or source, path, self._exec)
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
- result = self._exec(spec, f.value, tf.source, child)
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
- result = self._exec(spec, f.value, tf.source, tf.name)
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
- result = self._exec(spec, root, None, '')
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
 
@@ -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
- return executor(spec, f.value, source, f'{path}.{name}')
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
- r = executor(spec, elem, source, f'{path}[{i}]')
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
- r = executor(spec, f.value, source, f'{path}.{f.name}')
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
- return executor(spec, val.elements[idx], source, f'{path}[{idx}]')
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
- dependencies[key] = extract_dependencies(tpl, template_scopes.get(key, {}), builtin_names)
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
- combined = content_hashes[key] + '|' + ','.join(dep_ids)
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)