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.
Files changed (60) hide show
  1. {infinity_data-2.0.0/src/infinity_data.egg-info → infinity_data-2.2.0}/PKG-INFO +1 -1
  2. {infinity_data-2.0.0 → infinity_data-2.2.0}/pyproject.toml +3 -2
  3. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/parser/__init__.py +2 -0
  4. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/parser/models.py +20 -6
  5. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/parser/parser.py +43 -15
  6. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/pipeline.py +1 -1
  7. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/builder/__init__.py +4 -0
  8. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/builder/builder.py +102 -48
  9. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/builder/models.py +10 -4
  10. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/constraints.py +2 -2
  11. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/diagnostics.py +4 -0
  12. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/executor/executor.py +15 -6
  13. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/__init__.py +8 -0
  14. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/dict_constraints.py +3 -1
  15. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/general.py +35 -2
  16. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/resolver/identity.py +29 -7
  17. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/resolver/imports.py +67 -9
  18. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/resolver/models.py +28 -13
  19. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/resolver/resolver.py +53 -16
  20. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/std.py +40 -10
  21. {infinity_data-2.0.0 → infinity_data-2.2.0/src/infinity_data.egg-info}/PKG-INFO +1 -1
  22. {infinity_data-2.0.0 → infinity_data-2.2.0}/LICENSE +0 -0
  23. {infinity_data-2.0.0 → infinity_data-2.2.0}/README.md +0 -0
  24. {infinity_data-2.0.0 → infinity_data-2.2.0}/setup.cfg +0 -0
  25. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/__init__.py +0 -0
  26. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/emit/__init__.py +0 -0
  27. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/emit/converter.py +0 -0
  28. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/frontend.py +0 -0
  29. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/infra/__init__.py +0 -0
  30. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/infra/diagnostics.py +0 -0
  31. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/infra/file.py +0 -0
  32. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/infra/ll1_stream.py +0 -0
  33. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/infra/location.py +0 -0
  34. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/infra/path.py +0 -0
  35. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/parser/diagnostics.py +0 -0
  36. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/parser/token_stream.py +0 -0
  37. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/py.typed +0 -0
  38. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/sandbox/__init__.py +0 -0
  39. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/sandbox/config.py +0 -0
  40. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/sandbox/errors.py +0 -0
  41. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/sandbox/mediator.py +0 -0
  42. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/sandbox/schema.py +0 -0
  43. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/__init__.py +0 -0
  44. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/executor/__init__.py +0 -0
  45. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/jsonpath.py +0 -0
  46. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/_core.py +0 -0
  47. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/logic.py +0 -0
  48. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/registry/types.py +0 -0
  49. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/semantic/resolver/__init__.py +0 -0
  50. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/__init__.py +0 -0
  51. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/char_stream.py +0 -0
  52. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/diagnostics.py +0 -0
  53. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/finalizer.py +0 -0
  54. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/models/__init__.py +0 -0
  55. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/models/raw_tokens.py +0 -0
  56. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/models/tokens.py +0 -0
  57. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data/tokenizer/tokenizer.py +0 -0
  58. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data.egg-info/SOURCES.txt +0 -0
  59. {infinity_data-2.0.0 → infinity_data-2.2.0}/src/infinity_data.egg-info/dependency_links.txt +0 -0
  60. {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
  Metadata-Version: 2.4
2
2
  Name: infinity-data
3
- Version: 2.0.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.0.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
 
@@ -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 EnvImportStmt(AstNode):
124
- """环境变量导入: !env import NAME [as NEW_NAME]"""
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 f'!env import {self.name}'
132
- return f'!env import {self.name} as {self.alias}'
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 NAME [as NEW_NAME]"""
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
- alias = None
280
- if Parser._peek_keyword(stream, 'as'):
281
- stream.advance()
282
- alias = stream.expect(IdentifierToken).name
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 结尾:!env 无导入项列表,若不加检查,同一行的逗号会被
285
- # 顶层 skip_separators 吞掉(`!env import A as a, x = 1` 被误认为合法),与
286
- # !from / !file 的「尾部必须换行」行为不一致。
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? → one(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? → one(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")? → one(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? → one(constraint, ?)。"""
1002
+ """可空包装:constraint? → any(constraint, ?)。"""
975
1003
  return ConstraintCall(
976
1004
  source=c.source,
977
- name='one',
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? → one(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
- 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,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, 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
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
- return StdLiteral(kind='str', value=str(raw))
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=tf.source, constraints=specs))
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(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: {})
@@ -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?`` → ``one(type, ?)`` 已在 parser 阶段展开。)
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}'),