infinity-data 2.2.0__tar.gz → 2.2.1__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.2.0/src/infinity_data.egg-info → infinity_data-2.2.1}/PKG-INFO +1 -1
  2. {infinity_data-2.2.0 → infinity_data-2.2.1}/pyproject.toml +1 -1
  3. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/parser/__init__.py +2 -0
  4. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/parser/models.py +66 -32
  5. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/parser/parser.py +23 -19
  6. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/semantic/builder/builder.py +122 -99
  7. {infinity_data-2.2.0 → infinity_data-2.2.1/src/infinity_data.egg-info}/PKG-INFO +1 -1
  8. {infinity_data-2.2.0 → infinity_data-2.2.1}/LICENSE +0 -0
  9. {infinity_data-2.2.0 → infinity_data-2.2.1}/README.md +0 -0
  10. {infinity_data-2.2.0 → infinity_data-2.2.1}/setup.cfg +0 -0
  11. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/__init__.py +0 -0
  12. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/emit/__init__.py +0 -0
  13. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/emit/converter.py +0 -0
  14. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/frontend.py +0 -0
  15. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/infra/__init__.py +0 -0
  16. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/infra/diagnostics.py +0 -0
  17. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/infra/file.py +0 -0
  18. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/infra/ll1_stream.py +0 -0
  19. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/infra/location.py +0 -0
  20. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/infra/path.py +0 -0
  21. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/parser/diagnostics.py +0 -0
  22. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/parser/token_stream.py +0 -0
  23. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/pipeline.py +0 -0
  24. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/py.typed +0 -0
  25. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/sandbox/__init__.py +0 -0
  26. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/sandbox/config.py +0 -0
  27. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/sandbox/errors.py +0 -0
  28. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/sandbox/mediator.py +0 -0
  29. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/sandbox/schema.py +0 -0
  30. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/semantic/__init__.py +0 -0
  31. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/semantic/builder/__init__.py +0 -0
  32. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/semantic/builder/models.py +0 -0
  33. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/semantic/constraints.py +0 -0
  34. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/semantic/diagnostics.py +0 -0
  35. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/semantic/executor/__init__.py +0 -0
  36. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/semantic/executor/executor.py +0 -0
  37. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/semantic/jsonpath.py +0 -0
  38. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/semantic/registry/__init__.py +0 -0
  39. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/semantic/registry/_core.py +0 -0
  40. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/semantic/registry/dict_constraints.py +0 -0
  41. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/semantic/registry/general.py +0 -0
  42. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/semantic/registry/logic.py +0 -0
  43. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/semantic/registry/types.py +0 -0
  44. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/semantic/resolver/__init__.py +0 -0
  45. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/semantic/resolver/identity.py +0 -0
  46. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/semantic/resolver/imports.py +0 -0
  47. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/semantic/resolver/models.py +0 -0
  48. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/semantic/resolver/resolver.py +0 -0
  49. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/semantic/std.py +0 -0
  50. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/tokenizer/__init__.py +0 -0
  51. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/tokenizer/char_stream.py +0 -0
  52. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/tokenizer/diagnostics.py +0 -0
  53. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/tokenizer/finalizer.py +0 -0
  54. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/tokenizer/models/__init__.py +0 -0
  55. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/tokenizer/models/raw_tokens.py +0 -0
  56. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/tokenizer/models/tokens.py +0 -0
  57. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data/tokenizer/tokenizer.py +0 -0
  58. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data.egg-info/SOURCES.txt +0 -0
  59. {infinity_data-2.2.0 → infinity_data-2.2.1}/src/infinity_data.egg-info/dependency_links.txt +0 -0
  60. {infinity_data-2.2.0 → infinity_data-2.2.1}/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.2.0
3
+ Version: 2.2.1
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.2.0"
3
+ version = "2.2.1"
4
4
  description = "InfinityData:声明式配置语言(.infd/.inft)的 Python 编译器库"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -34,6 +34,7 @@ from infinity_data.parser.models import (
34
34
  JsonPathKey,
35
35
  JsonPathSegment,
36
36
  LiteralValue,
37
+ NamedArg,
37
38
  Statement,
38
39
  TemplateCallValue,
39
40
  TemplateConfig,
@@ -72,6 +73,7 @@ __all__ = [
72
73
  'JsonPathKey',
73
74
  'JsonPathSegment',
74
75
  'LiteralValue',
76
+ 'NamedArg',
75
77
  'Parser',
76
78
  'Statement',
77
79
  'TemplateCallValue',
@@ -37,6 +37,16 @@ def _canonical_constraint_list(constraints: Iterable['Constraint']) -> str:
37
37
  return '<' + ', '.join(c.canonical() for c in items) + '>'
38
38
 
39
39
 
40
+ def _canonical_error(message: str) -> str:
41
+ """错误节点容错序列化:``error("...")`` 占位符。
42
+
43
+ 错误节点是**解析失败恢复**的产物,对应的源码本就无效,无法还原为等价 AST——
44
+ canonical 仅作**容错**(模板真名哈希 / 调试 / LSP 提示不因错误节点崩溃),
45
+ 不承诺 round-trip(``error(...)`` 是显示占位符,不是可解析的模板调用语法)。
46
+ """
47
+ return f'error({_canonical_str(message)})'
48
+
49
+
40
50
  # ═══════════════════════════════════════════════════════════
41
51
  # 节点
42
52
  # ═══════════════════════════════════════════════════════════
@@ -457,24 +467,41 @@ class UnpackValue(AstNode):
457
467
  return ('**' if self.double else '*') + self.value.canonical()
458
468
 
459
469
 
470
+ @dataclass
471
+ class NamedArg(AstNode):
472
+ """模板调用命名参数槽: name = value。
473
+
474
+ 与 ``**expr`` 解包项(:class:`UnpackValue`)在 :attr:`TemplateCallValue.named_args`
475
+ 中按**源码顺序**混排——键冲突「保留先到者」(disjoint merge,§2.7)与书写顺序一致。
476
+ """
477
+
478
+ name: str
479
+ value: Value
480
+
481
+ def children(self) -> Iterable[AstNode]:
482
+ return [self.value]
483
+
484
+ def canonical(self) -> str:
485
+ return f'{self.name} = {self.value.canonical()}'
486
+
487
+
460
488
  @dataclass
461
489
  class DictValue(AstNode):
462
490
  """对象值: { ... }
463
491
 
464
492
  ``: <constraint, ...>`` 结构级约束作用于该字面量 dict 的整体。
465
- ``unpacks``:``**expr`` 解包项(展开为键值对后并入字段集)。
493
+ ``items``:字段(:class:`Field`)与 ``**expr`` 解包项(:class:`UnpackValue`)按
494
+ **源码顺序**混排——键冲突「保留先到者」(disjoint merge,§2.7)与书写顺序一致。
466
495
  """
467
496
 
468
- fields: list[Field]
497
+ items: list[Field | UnpackValue]
469
498
  constraints: list[Constraint] = field(default_factory=lambda: [])
470
- unpacks: list[UnpackValue] = field(default_factory=lambda: [])
471
499
 
472
500
  def children(self) -> Iterable[AstNode]:
473
- return [*self.fields, *self.constraints, *self.unpacks]
501
+ return [*self.items, *self.constraints]
474
502
 
475
503
  def canonical(self) -> str:
476
- inner: list[str] = [f.canonical() for f in self.fields]
477
- inner.extend(u.canonical() for u in self.unpacks)
504
+ inner: list[str] = [i.canonical() for i in self.items]
478
505
  cs = _canonical_constraint_list(self.constraints)
479
506
  if cs:
480
507
  inner.append(': ' + cs)
@@ -501,18 +528,21 @@ class TemplateCallValue(AstNode):
501
528
  展开(§2.8):参数值后缀 ``...`` = 展开轴;调用级(``)`` 后)``...`` = 展开传播
502
529
  (本调用的展开结果作为包围模板调用的轴,整体模式逐元素重复)、``^`` = 笛卡尔积
503
530
  (多轴全组合,首轴最慢变化;与 ``propagate`` 正交,``^...`` 可叠加)。
504
- - ``axis_positional``:位置参数中带 ``...`` 的索引
505
- - ``axis_named``:命名参数中带 ``...`` 的键
506
- - ``axis_unpack_kwargs``:``**expr`` 解包参数中带 ``...`` 的索引
531
+ - ``positional_args``:位置参数槽,**保持源码顺序**;``*expr`` 解包项
532
+ (:class:`UnpackValue`,double=False)也在其中,实例化时展开为多个位置参数(§2.7)
533
+ - ``named_args``:命名参数槽,**保持源码顺序**;显式 ``name = value``(:class:`NamedArg`)
534
+ 与 ``**expr`` 解包项(:class:`UnpackValue`,double=True)混排——键冲突「保留先到者」
535
+ 与书写顺序一致(§2.7 disjoint merge)
536
+ - ``axis_positional``:位置参数槽中带 ``...`` 的索引(``*expr`` 解包项不是轴)
537
+ - ``axis_named``:命名参数(:class:`NamedArg`)中带 ``...`` 的键
538
+ - ``axis_unpack_kwargs``:``**expr`` 解包项(named_args 槽索引)中带 ``...`` 的索引
507
539
  - ``propagate``:调用级 ``...``(展开传播;无 = 展开止于本调用)
508
540
  - ``cartesian``:调用级 ``^``(笛卡尔积展开;无 = zip 配对)
509
541
  """
510
542
 
511
543
  template_name: str
512
- positional_args: list[Value]
513
- named_args: dict[str, Value]
514
- unpack_args: list[UnpackValue] = field(default_factory=lambda: []) # *expr(list → 位置参数)
515
- unpack_kwargs: list[UnpackValue] = field(default_factory=lambda: []) # **expr(dict → 命名参数)
544
+ positional_args: list[Value | UnpackValue]
545
+ named_args: list[NamedArg | UnpackValue]
516
546
  axis_positional: frozenset[int] = field(default_factory=frozenset[int])
517
547
  axis_named: frozenset[str] = field(default_factory=frozenset[str])
518
548
  axis_unpack_kwargs: frozenset[int] = field(default_factory=frozenset[int])
@@ -522,27 +552,22 @@ class TemplateCallValue(AstNode):
522
552
  def children(self) -> Iterable[AstNode]:
523
553
  return [
524
554
  *self.positional_args,
525
- *self.named_args.values(),
526
- *self.unpack_args,
527
- *self.unpack_kwargs,
555
+ *self.named_args,
528
556
  ]
529
557
 
530
558
  def canonical(self) -> str:
531
559
  args: list[str] = []
532
560
  for i, a in enumerate(self.positional_args):
533
561
  s = a.canonical()
534
- if i in self.axis_positional:
535
- s += '...'
536
- args.append(s)
537
- args.extend(u.canonical() for u in self.unpack_args)
538
- for k, v in sorted(self.named_args.items()):
539
- s = f'{k} = {v.canonical()}'
540
- if k in self.axis_named:
562
+ if not isinstance(a, UnpackValue) and i in self.axis_positional:
541
563
  s += '...'
542
564
  args.append(s)
543
- for j, u in enumerate(self.unpack_kwargs):
544
- s = u.canonical()
545
- if j in self.axis_unpack_kwargs:
565
+ for i, n in enumerate(self.named_args):
566
+ s = n.canonical()
567
+ if isinstance(n, UnpackValue):
568
+ if i in self.axis_unpack_kwargs:
569
+ s += '...'
570
+ elif n.name in self.axis_named:
546
571
  s += '...'
547
572
  args.append(s)
548
573
  out = f'{self.template_name}({", ".join(args)})'
@@ -560,32 +585,41 @@ class TemplateCallValue(AstNode):
560
585
 
561
586
  @dataclass
562
587
  class ErrorStatement(AstNode):
563
- """解析失败的语句。用于错误恢复。"""
588
+ """解析失败的语句。用于错误恢复。
589
+
590
+ canonical 容错输出 ``error("message")``(不抛异常),供哈希 / LSP 提示。
591
+ """
564
592
 
565
593
  message: str
566
594
 
567
595
  def canonical(self) -> str:
568
- raise TypeError('错误节点不可序列化为 infd')
596
+ return _canonical_error(self.message)
569
597
 
570
598
 
571
599
  @dataclass
572
600
  class ErrorValue(AstNode):
573
- """解析失败的值。用于错误恢复。"""
601
+ """解析失败的值。用于错误恢复。
602
+
603
+ canonical 容错输出 ``error("message")``(不抛异常),供哈希 / LSP 提示。
604
+ """
574
605
 
575
606
  message: str
576
607
 
577
608
  def canonical(self) -> str:
578
- raise TypeError('错误节点不可序列化为 infd')
609
+ return _canonical_error(self.message)
579
610
 
580
611
 
581
612
  @dataclass
582
613
  class ErrorConstraint(AstNode):
583
- """解析失败的约束。用于错误恢复。"""
614
+ """解析失败的约束。用于错误恢复。
615
+
616
+ canonical 容错输出 ``error("message")``(不抛异常),供哈希 / LSP 提示。
617
+ """
584
618
 
585
619
  message: str
586
620
 
587
621
  def canonical(self) -> str:
588
- raise TypeError('错误节点不可序列化为 infd')
622
+ return _canonical_error(self.message)
589
623
 
590
624
 
591
625
  # ═══════════════════════════════════════════════════════════
@@ -29,6 +29,7 @@ from infinity_data.parser.models import (
29
29
  JsonPathKey,
30
30
  JsonPathSegment,
31
31
  LiteralValue,
32
+ NamedArg,
32
33
  Statement,
33
34
  TemplateCallValue,
34
35
  TemplateConfig,
@@ -1196,8 +1197,7 @@ class Parser:
1196
1197
  lbrace_tok = stream.expect(LbraceToken)
1197
1198
  stream.skip_separators()
1198
1199
 
1199
- fields: list[Field] = []
1200
- unpacks: list[UnpackValue] = []
1200
+ items: list[Field | UnpackValue] = []
1201
1201
  constraints: list[Constraint] = []
1202
1202
  missing_sep_reported = [False]
1203
1203
  while not stream.check(RawTokenType.RBRACE) and not stream.eof():
@@ -1207,12 +1207,13 @@ class Parser:
1207
1207
  parsed = Parser._parse_constraints(stream, collector)
1208
1208
  constraints.extend(parsed.constraints)
1209
1209
  elif isinstance(stream.peek(), DoubleStarToken):
1210
- # **expr:dict 解包(展开为键值对并入字段集)
1210
+ # **expr:dict 解包(展开为键值对并入字段集);
1211
+ # 与显式字段同槽存放,保持源码顺序(先到者胜出,§2.7)
1211
1212
  tok = stream.expect(DoubleStarToken)
1212
1213
  val = Parser._parse_value(stream, collector)
1213
- unpacks.append(UnpackValue(source=stream.span_from(tok), value=val, double=True))
1214
+ items.append(UnpackValue(source=stream.span_from(tok), value=val, double=True))
1214
1215
  else:
1215
- fields.append(Parser._parse_field(stream, collector))
1216
+ items.append(Parser._parse_field(stream, collector))
1216
1217
  had_sep = stream.skip_separators()
1217
1218
  Parser._missing_separator(
1218
1219
  stream,
@@ -1224,7 +1225,7 @@ class Parser:
1224
1225
  )
1225
1226
 
1226
1227
  stream.expect(RbraceToken)
1227
- return DictValue(source=stream.span_from(lbrace_tok), fields=fields, constraints=constraints, unpacks=unpacks)
1228
+ return DictValue(source=stream.span_from(lbrace_tok), items=items, constraints=constraints)
1228
1229
 
1229
1230
  @staticmethod
1230
1231
  def _parse_array(stream: TokenStream, collector: DiagnosticCollector) -> ArrayValue:
@@ -1263,13 +1264,11 @@ class Parser:
1263
1264
  stream.expect(LparenToken)
1264
1265
  stream.skip_newlines()
1265
1266
 
1266
- positional: list[Value] = []
1267
- named: dict[str, Value] = {}
1268
- unpack_args: list[UnpackValue] = [] # *expr(list → 位置参数)
1269
- unpack_kwargs: list[UnpackValue] = [] # **expr(dict → 命名参数)
1270
- axis_positional: set[int] = set() # 位置参数中带 ... 的索引(展开轴,§2.8)
1267
+ positional: list[Value | UnpackValue] = [] # 位置参数 + *expr 解包项(保持源码顺序,§2.7)
1268
+ named: list[NamedArg | UnpackValue] = [] # 命名参数 + **expr 解包项(保持源码顺序,§2.7)
1269
+ axis_positional: set[int] = set() # 位置参数槽中带 ... 的索引(展开轴,§2.8)
1271
1270
  axis_named: set[str] = set() # 命名参数中带 ... 的键
1272
- axis_unpack_kwargs: set[int] = set() # **expr 解包参数中带 ... 的索引
1271
+ axis_unpack_kwargs: set[int] = set() # named 槽中 **expr 解包项的索引
1273
1272
  saw_named = False
1274
1273
  missing_sep_reported = [False]
1275
1274
 
@@ -1284,15 +1283,16 @@ class Parser:
1284
1283
  if isinstance(tok, DoubleStarToken):
1285
1284
  stream.advance()
1286
1285
  val = Parser._parse_value(stream, collector)
1287
- idx = len(unpack_kwargs)
1288
- unpack_kwargs.append(UnpackValue(source=stream.single_span(tok), value=val, double=True))
1286
+ idx = len(named)
1287
+ named.append(UnpackValue(source=stream.single_span(tok), value=val, double=True))
1289
1288
  if Parser._consume_ellipsis(stream):
1290
1289
  axis_unpack_kwargs.add(idx) # **expr...:list[dict] 逐元素解包(§2.8)
1291
1290
  saw_named = True
1292
1291
  elif isinstance(tok, StarToken):
1293
1292
  stream.advance()
1294
1293
  val = Parser._parse_value(stream, collector)
1295
- unpack_args.append(UnpackValue(source=stream.single_span(tok), value=val, double=False))
1294
+ # *expr 解包项与显式位置参数同槽存放,保持源码顺序(§2.7)
1295
+ positional.append(UnpackValue(source=stream.single_span(tok), value=val, double=False))
1296
1296
  # 分支 1:标识符 → 可能是命名参数或模板调用(位置参数)
1297
1297
  elif isinstance(tok, IdentifierToken):
1298
1298
  ident: IdentifierToken = stream.expect(IdentifierToken) # 消费到缓冲区
@@ -1300,7 +1300,7 @@ class Parser:
1300
1300
 
1301
1301
  if isinstance(nxt, EqualsToken):
1302
1302
  stream.advance() # 消费 =
1303
- if ident.name in named:
1303
+ if ident.name in {s.name for s in named if isinstance(s, NamedArg)}:
1304
1304
  collector.add(
1305
1305
  diag(
1306
1306
  'template.dup_argument',
@@ -1308,7 +1308,13 @@ class Parser:
1308
1308
  stream.single_span(ident),
1309
1309
  )
1310
1310
  )
1311
- named[ident.name] = Parser._parse_value(stream, collector)
1311
+ named.append(
1312
+ NamedArg(
1313
+ source=stream.single_span(ident),
1314
+ name=ident.name,
1315
+ value=Parser._parse_value(stream, collector),
1316
+ )
1317
+ )
1312
1318
  if Parser._consume_ellipsis(stream):
1313
1319
  axis_named.add(ident.name) # 命名参数轴
1314
1320
  saw_named = True
@@ -1358,8 +1364,6 @@ class Parser:
1358
1364
  template_name=name_tok.name,
1359
1365
  positional_args=positional,
1360
1366
  named_args=named,
1361
- unpack_args=unpack_args,
1362
- unpack_kwargs=unpack_kwargs,
1363
1367
  axis_positional=frozenset(axis_positional),
1364
1368
  axis_named=frozenset(axis_named),
1365
1369
  axis_unpack_kwargs=frozenset(axis_unpack_kwargs),
@@ -16,7 +16,7 @@
16
16
  from __future__ import annotations
17
17
 
18
18
  import decimal
19
- from collections.abc import Iterator, Mapping, Sequence
19
+ from collections.abc import Iterator, Sequence
20
20
  from itertools import product
21
21
  from typing import Any, cast
22
22
 
@@ -32,6 +32,7 @@ from infinity_data.parser import (
32
32
  Field,
33
33
  FileImportStmt,
34
34
  LiteralValue,
35
+ NamedArg,
35
36
  TemplateCallValue,
36
37
  TemplateDef,
37
38
  TemplateImportStmt,
@@ -448,19 +449,21 @@ class AstBuilder:
448
449
  return self._convert_literal(tok)
449
450
  case DollarValue(name=n, type_cast=tc, source=src):
450
451
  return self._resolve_dollar(n, tc, path, src, scope)
451
- case DictValue(fields=fs, constraints=cs, unpacks=ups):
452
+ case DictValue(items=its, constraints=cs):
452
453
  std_fields: list[StdField] = []
453
- # **expr 解包:目标必须是 dict,展开为键值对(disjoint merge 查重在 _finalize_object)
454
- for up in ups:
455
- rv = self._resolve_value(up.value, path, scope)
456
- fields = self._unpack_dict(rv, up.source, path)
457
- if fields is not None:
458
- std_fields.extend(fields)
459
- for f in fs:
460
- child = f'{path}.{f.name}' if path else f.name
461
- sf = self._build_field(f, path=child, scope=scope)
462
- if sf is not None:
463
- std_fields.append(sf)
454
+ # **expr 解包与显式字段按源码顺序处理:解包展开为键值对,
455
+ # disjoint merge「保留先到者」查重在 _finalize_object(§2.7)
456
+ for it in its:
457
+ if isinstance(it, UnpackValue):
458
+ rv = self._resolve_value(it.value, path, scope)
459
+ fields = self._unpack_dict(rv, it.source, path)
460
+ if fields is not None:
461
+ std_fields.extend(fields)
462
+ else:
463
+ child = f'{path}.{it.name}' if path else it.name
464
+ sf = self._build_field(it, path=child, scope=scope)
465
+ if sf is not None:
466
+ std_fields.append(sf)
464
467
  # dict 结构级约束(作用于该字面量整体):解析后挂节点,不执行
465
468
  specs, diags = resolve_constraint_list(cs, scope)
466
469
  self._collector.extend(diags)
@@ -488,17 +491,13 @@ class AstBuilder:
488
491
  template_name=tn,
489
492
  positional_args=pa,
490
493
  named_args=na,
491
- unpack_args=upa,
492
- unpack_kwargs=upk,
493
494
  axis_positional=axp,
494
495
  axis_named=axn,
495
496
  axis_unpack_kwargs=axu,
496
497
  propagate=prop,
497
498
  cartesian=cart,
498
499
  ):
499
- return self._expand_template_call(
500
- tn, pa, na, upa, upk, axp, axn, axu, prop, cart, path, raw.source, scope
501
- )
500
+ return self._expand_template_call(tn, pa, na, axp, axn, axu, prop, cart, path, raw.source, scope)
502
501
  case ErrorValue():
503
502
  # 值解析失败已在语法层报告(parse.value_field / parse.unrecognized_value),不重复
504
503
  return None
@@ -645,10 +644,8 @@ class AstBuilder:
645
644
  def _expand_template_call(
646
645
  self,
647
646
  template_name: str,
648
- positional_args: list[Value],
649
- named_args: dict[str, Value],
650
- unpack_args: list[UnpackValue],
651
- unpack_kwargs: list[UnpackValue],
647
+ positional_args: list[Value | UnpackValue],
648
+ named_args: list[NamedArg | UnpackValue],
652
649
  axis_positional: frozenset[int],
653
650
  axis_named: frozenset[str],
654
651
  axis_unpack_kwargs: frozenset[int],
@@ -667,26 +664,36 @@ class AstBuilder:
667
664
 
668
665
  每个展开实例是完整模板实例(默认值注入 + 约束照常)。
669
666
  """
670
- # 收集本调用自身的轴值(参数级 ...,每个轴解析为 list)
667
+ # 收集本调用自身的轴值(参数级 ...,每个轴解析为 list)。
668
+ # 轴只可能标在显式位置/命名参数上(*expr / **expr 解包项不是轴,解析器保证)
671
669
  axes: list[tuple[tuple[str, str | int], StdArray]] = []
672
670
  for i in sorted(axis_positional):
673
- arr = self._resolve_axis(positional_args[i], scope, source, path, template_name)
671
+ arg = positional_args[i]
672
+ assert not isinstance(arg, UnpackValue), '展开轴不能是 *expr 解包项'
673
+ arr = self._resolve_axis(arg, scope, source, path, template_name)
674
674
  if arr is not None:
675
675
  axes.append((('pos', i), arr))
676
+ # 显式命名参数轴(name = value...,按名排序保持确定性)
676
677
  for k in sorted(axis_named):
677
- arr = self._resolve_axis(named_args[k], scope, source, path, template_name)
678
+ slot = next((s for s in named_args if isinstance(s, NamedArg) and s.name == k), None)
679
+ if slot is None:
680
+ continue
681
+ arr = self._resolve_axis(slot.value, scope, source, path, template_name)
678
682
  if arr is not None:
679
683
  axes.append((('named', k), arr))
684
+ # **expr 解包轴(**expr...:list[dict] 逐元素解包)
680
685
  for j in sorted(axis_unpack_kwargs):
681
- arr = self._resolve_axis(unpack_kwargs[j].value, scope, source, path, template_name)
686
+ slot = named_args[j]
687
+ assert isinstance(slot, UnpackValue), '** 解包轴槽必须是 UnpackValue'
688
+ arr = self._resolve_axis(slot.value, scope, source, path, template_name)
682
689
  if arr is not None:
683
690
  axes.append((('unpack', j), arr))
684
691
 
685
692
  # 传播轴:参数值是带调用级 ... 的模板调用 → 其展开结果(list)作为本调用的轴。
686
693
  # 内层无展开源(结果非 list,expand_no_source 已在内层报告)→ 用解析值替换参数,
687
694
  # 避免外层单次实例化时二次解析、重复报错。
688
- pos_args: list[Value | StdValue] = list(positional_args)
689
- named: dict[str, Value | StdValue] = dict(named_args)
695
+ pos_args: list[Value | UnpackValue | StdValue] = list(positional_args)
696
+ named: list[NamedArg | UnpackValue] = list(named_args)
690
697
  for i, arg in enumerate(pos_args):
691
698
  if i in axis_positional:
692
699
  continue
@@ -696,23 +703,24 @@ class AstBuilder:
696
703
  axes.append((('pos', i), rv))
697
704
  elif rv is not None:
698
705
  pos_args[i] = rv
699
- for k, arg in named.items():
700
- if k in axis_named:
706
+ for i, slot in enumerate(named):
707
+ if isinstance(slot, UnpackValue):
708
+ continue # ** 解包项不参与传播
709
+ if slot.name in axis_named:
701
710
  continue
702
- if isinstance(arg, TemplateCallValue) and arg.propagate:
703
- rv = self._resolve_value(arg, path, scope)
711
+ if isinstance(slot.value, TemplateCallValue) and slot.value.propagate:
712
+ rv = self._resolve_value(slot.value, path, scope)
704
713
  if isinstance(rv, StdArray):
705
- axes.append((('named', k), rv))
714
+ axes.append((('named', slot.name), rv))
706
715
  elif rv is not None:
707
- named[k] = rv
716
+ # 用解析值替换该槽(保持槽位顺序)
717
+ named[i] = NamedArg(name=slot.name, value=cast(Value, rv), source=slot.source)
708
718
 
709
719
  if not axes:
710
720
  # 调用级 ^ / ... 但无任何展开源(自身无轴、也无内层传播而来)→ 报错
711
721
  if propagate or cartesian:
712
722
  self._err('template.expand_no_source', {'template': template_name}, source, path)
713
- return self._instantiate_once(
714
- template_name, pos_args, named, unpack_args, unpack_kwargs, path, source, scope
715
- )
723
+ return self._instantiate_once(template_name, pos_args, named, path, source, scope)
716
724
 
717
725
  # 组合:zip(等长配对)或笛卡尔积(全组合,首轴最慢变化)
718
726
  if cartesian:
@@ -751,8 +759,8 @@ class AstBuilder:
751
759
 
752
760
  results: list[StdValue] = []
753
761
  for combo in combos:
754
- pa, na, upa, upk = self._build_expand_args(pos_args, named, unpack_args, unpack_kwargs, axes, combo)
755
- results.append(self._instantiate_once(template_name, pa, na, upa, upk, path, source, scope))
762
+ pa, na = self._build_expand_args(pos_args, named, axes, combo)
763
+ results.append(self._instantiate_once(template_name, pa, na, path, source, scope))
756
764
  return StdArray(elements=results, source=source)
757
765
 
758
766
  def _resolve_axis(
@@ -774,22 +782,20 @@ class AstBuilder:
774
782
 
775
783
  @staticmethod
776
784
  def _build_expand_args(
777
- positional_args: list[Value | StdValue],
778
- named_args: dict[str, Value | StdValue],
779
- unpack_args: list[UnpackValue],
780
- unpack_kwargs: list[UnpackValue],
785
+ positional_args: list[Value | UnpackValue | StdValue],
786
+ named_args: list[NamedArg | UnpackValue],
781
787
  axes: list[tuple[tuple[str, str | int], StdArray]],
782
788
  combo: tuple[StdValue, ...],
783
- ) -> tuple[list[Value | StdValue], dict[str, Value | StdValue], list[UnpackValue], list[UnpackValue]]:
789
+ ) -> tuple[list[Value | UnpackValue | StdValue], list[NamedArg | UnpackValue]]:
784
790
  """把组合元素替换进轴参数位,构造单次调用的参数视图。
785
791
 
786
- ``**`` 解包轴:元素(dict)字段并入命名参数,移除该项(§2.8 轴与解包叠加)。
792
+ ``**`` 解包轴:元素(dict)字段并入命名参数,移除该项;
793
+ 并入字段在**原槽位**展开(保持源码顺序,§2.8 轴与解包叠加)。
787
794
  """
788
- pa: list[Value | StdValue] = list(positional_args)
789
- na: dict[str, Value | StdValue] = dict(named_args)
790
- upa = list(unpack_args)
791
- upk = list(unpack_kwargs)
792
- unpack_removed: set[int] = set()
795
+ pa: list[Value | UnpackValue | StdValue] = list(positional_args)
796
+ na: list[NamedArg | UnpackValue] = list(named_args)
797
+ taken = {s.name for s in na if isinstance(s, NamedArg)}
798
+ slot_merges: dict[int, list[NamedArg]] = {}
793
799
  for (axis_spec, _arr), elem in zip(axes, combo):
794
800
  kind, idx = axis_spec
795
801
  if kind == 'pos':
@@ -797,25 +803,39 @@ class AstBuilder:
797
803
  pa[idx] = elem
798
804
  elif kind == 'named':
799
805
  assert isinstance(idx, str)
800
- na[idx] = elem
806
+ # 按名替换命名参数槽的值(保持槽位顺序)
807
+ for i, slot in enumerate(na):
808
+ if isinstance(slot, NamedArg) and slot.name == idx:
809
+ na[i] = NamedArg(name=slot.name, value=cast(Value, elem), source=slot.source)
810
+ break
801
811
  else: # unpack 轴:元素(dict)字段并入命名参数
802
812
  assert isinstance(idx, int)
813
+ slot = na[idx]
814
+ assert isinstance(slot, UnpackValue)
815
+ merged: list[NamedArg] = []
803
816
  if isinstance(elem, StdObject):
804
817
  for f in elem.fields:
805
- if f.value is not None and f.name not in na:
806
- na[f.name] = f.value
807
- unpack_removed.add(idx)
808
- if unpack_removed:
809
- upk = [u for i, u in enumerate(upk) if i not in unpack_removed]
810
- return pa, na, upa, upk
818
+ if f.value is not None and f.name not in taken:
819
+ taken.add(f.name)
820
+ # 并入字段的 source 指向 **expr... 槽位(诊断定位)
821
+ merged.append(NamedArg(name=f.name, value=cast(Value, f.value), source=slot.source))
822
+ slot_merges[idx] = merged
823
+ # 移除 ** 解包轴槽,在其原位置展开合并字段(保持源码顺序)
824
+ if slot_merges:
825
+ out: list[NamedArg | UnpackValue] = []
826
+ for i, s in enumerate(na):
827
+ if i in slot_merges:
828
+ out.extend(slot_merges[i])
829
+ else:
830
+ out.append(s)
831
+ na = out
832
+ return pa, na
811
833
 
812
834
  def _instantiate_once(
813
835
  self,
814
836
  template_name: str,
815
- positional_args: Sequence[Value | StdValue],
816
- named_args: Mapping[str, Value | StdValue],
817
- unpack_args: list[UnpackValue],
818
- unpack_kwargs: list[UnpackValue],
837
+ positional_args: Sequence[Value | UnpackValue | StdValue],
838
+ named_args: Sequence[NamedArg | UnpackValue],
819
839
  path: str,
820
840
  source: SourceRange | None,
821
841
  scope: Scope,
@@ -840,13 +860,19 @@ class AstBuilder:
840
860
 
841
861
  required = [tf for tf in template.fields if tf.default_value is None]
842
862
 
843
- # ── 位置参数:显式 + *expr 解包(list → 逐个位置参数)──
844
- expanded_positional: list[Value | StdValue] = list(positional_args)
845
- for up in unpack_args:
846
- rv = self._resolve_value(up.value, path, scope)
847
- items = self._unpack_list(rv, up.source, path)
848
- if items is not None:
849
- expanded_positional.extend(items)
863
+ # ── 位置参数:显式 + *expr 解包(list → 逐个位置参数),保持源码顺序(§2.7)──
864
+ # 解析器把 *expr 解包项与显式位置参数按源码顺序合并存进 positional_args:
865
+ # 解包项(UnpackValue)在此展开,显式值直接进入 → 顺序与书写一致
866
+ # (如 Test(*[1,2,3], 4, 5, 6) ⟹ [1,2,3,4,5,6],而非 [4,5,6,1,2,3])
867
+ expanded_positional: list[Value | StdValue] = []
868
+ for pa in positional_args:
869
+ if isinstance(pa, UnpackValue):
870
+ rv = self._resolve_value(pa.value, path, scope)
871
+ items = self._unpack_list(rv, pa.source, path)
872
+ if items is not None:
873
+ expanded_positional.extend(items)
874
+ else:
875
+ expanded_positional.append(pa)
850
876
 
851
877
  # 模板配置 positional=false:位置参数违规报错,但值仍绑定必填字段
852
878
  # (放宽必填绑定:只报 positional_disabled 一条,避免 missing_required 级联)
@@ -861,16 +887,25 @@ class AstBuilder:
861
887
  )
862
888
  )
863
889
 
864
- # ── 命名参数:显式 + **expr 解包(dict → 键值对)──
890
+ # ── 命名参数:显式 + **expr 解包(dict → 键值对),保持源码顺序(§2.7)──
891
+ # 解析器把 **expr 解包项与显式命名参数按源码顺序合并存进 named_args:
892
+ # 解包项(UnpackValue)在此展开,显式命名参数(NamedArg)直接进入 →
893
+ # 键冲突「保留先到者」(disjoint merge)与书写顺序一致
865
894
  # 未知命名参数:extra_named_vars 收集(与 allow_extra 互斥);否则 allow_extra/报错
866
895
  declared = {tf.name for tf in template.fields}
867
896
  named_vars = template.config.extra_named_vars
868
897
  if named_vars is not None and named_vars not in declared:
869
898
  named_vars = None # 定义时已报 variadic_target_missing(_check_variadic_config)
870
- param_values: dict[str, Value | StdValue] = dict(named_args)
899
+ param_values: dict[str, Value | StdValue] = {}
871
900
  extra_args: dict[str, tuple[Value | StdValue, SourceRange | None]] = {}
872
901
  extra_named: dict[str, Value | StdValue] = {}
873
- for name, arg_val in named_args.items():
902
+
903
+ def _bind_named(name: str, arg_val: Value | StdValue, arg_source: SourceRange | None) -> None:
904
+ """按名绑定命名参数:已绑定 / 已收集 → 重复键(先到者保留);否则走声明/收集/放行。"""
905
+ child = f'{path}.{name}' if path else name
906
+ if name in param_values or name in extra_args or name in extra_named:
907
+ self._err('dict.duplicate_key', {'name': name}, arg_source, child)
908
+ return
874
909
  if name not in declared:
875
910
  if named_vars is not None:
876
911
  extra_named[name] = arg_val
@@ -881,36 +916,24 @@ class AstBuilder:
881
916
  self._err(
882
917
  'template.unknown_argument',
883
918
  {'template': template_name, 'arg': name},
884
- source,
885
- path,
919
+ arg_source,
920
+ child,
886
921
  )
887
- for up in unpack_kwargs:
888
- rv = self._resolve_value(up.value, path, scope)
889
- fields = self._unpack_dict(rv, up.source, path)
890
- if fields is None:
891
- continue
892
- for f in fields:
893
- if f.value is None:
894
- continue # 防御:解包字段值缺失(理论不可达)
895
- child = f'{path}.{f.name}' if path else f.name
896
- if f.name in param_values or f.name in extra_args:
897
- # 解包键与已有参数冲突 → 重复键(disjoint merge)
898
- self._err('dict.duplicate_key', {'name': f.name}, up.source, child)
922
+ else:
923
+ param_values[name] = arg_val
924
+
925
+ for slot in named_args:
926
+ if isinstance(slot, UnpackValue):
927
+ rv = self._resolve_value(slot.value, path, scope)
928
+ fields = self._unpack_dict(rv, slot.source, path)
929
+ if fields is None:
899
930
  continue
900
- if f.name not in declared:
901
- if named_vars is not None:
902
- extra_named[f.name] = f.value
903
- elif template.config.allow_extra:
904
- extra_args[f.name] = (f.value, f.source)
905
- else:
906
- self._err(
907
- 'template.unknown_argument',
908
- {'template': template_name, 'arg': f.name},
909
- up.source,
910
- child,
911
- )
912
- else:
913
- param_values[f.name] = f.value
931
+ for f in fields:
932
+ if f.value is None:
933
+ continue # 防御:解包字段值缺失(理论不可达)
934
+ _bind_named(f.name, f.value, slot.source)
935
+ else: # 显式命名参数(值可能是展开轴替换来的 StdValue)
936
+ _bind_named(slot.name, slot.value, slot.source)
914
937
  if named_vars is not None and extra_named:
915
938
  # 收集未声明命名参数为 dict 字段(约束由字段声明承担,§2.9)
916
939
  param_values[named_vars] = StdObject(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: infinity-data
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: InfinityData:声明式配置语言(.infd/.inft)的 Python 编译器库
5
5
  Author: infinity_system: yin_bailiang
6
6
  License: MIT
File without changes
File without changes
File without changes