infinity-data 1.0.0__py3-none-any.whl

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 (52) hide show
  1. infinity_data/__init__.py +41 -0
  2. infinity_data/emit/__init__.py +9 -0
  3. infinity_data/emit/converter.py +55 -0
  4. infinity_data/frontend.py +37 -0
  5. infinity_data/infra/__init__.py +4 -0
  6. infinity_data/infra/diagnostics.py +212 -0
  7. infinity_data/infra/file.py +83 -0
  8. infinity_data/infra/ll1_stream.py +71 -0
  9. infinity_data/infra/location.py +80 -0
  10. infinity_data/infra/path.py +46 -0
  11. infinity_data/parser/__init__.py +79 -0
  12. infinity_data/parser/diagnostics.py +92 -0
  13. infinity_data/parser/models.py +295 -0
  14. infinity_data/parser/parser.py +988 -0
  15. infinity_data/parser/token_stream.py +128 -0
  16. infinity_data/pipeline.py +252 -0
  17. infinity_data/sandbox/__init__.py +32 -0
  18. infinity_data/sandbox/config.py +61 -0
  19. infinity_data/sandbox/errors.py +106 -0
  20. infinity_data/sandbox/mediator.py +214 -0
  21. infinity_data/sandbox/schema.py +21 -0
  22. infinity_data/semantic/__init__.py +59 -0
  23. infinity_data/semantic/builder/__init__.py +29 -0
  24. infinity_data/semantic/builder/builder.py +523 -0
  25. infinity_data/semantic/builder/models.py +163 -0
  26. infinity_data/semantic/constraints.py +163 -0
  27. infinity_data/semantic/diagnostics.py +172 -0
  28. infinity_data/semantic/executor/__init__.py +10 -0
  29. infinity_data/semantic/executor/executor.py +259 -0
  30. infinity_data/semantic/registry/__init__.py +168 -0
  31. infinity_data/semantic/registry/_core.py +212 -0
  32. infinity_data/semantic/registry/dict_constraints.py +56 -0
  33. infinity_data/semantic/registry/general.py +362 -0
  34. infinity_data/semantic/registry/logic.py +134 -0
  35. infinity_data/semantic/registry/types.py +136 -0
  36. infinity_data/semantic/resolver/__init__.py +20 -0
  37. infinity_data/semantic/resolver/imports.py +191 -0
  38. infinity_data/semantic/resolver/models.py +67 -0
  39. infinity_data/semantic/resolver/resolver.py +345 -0
  40. infinity_data/tokenizer/__init__.py +16 -0
  41. infinity_data/tokenizer/char_stream.py +72 -0
  42. infinity_data/tokenizer/diagnostics.py +62 -0
  43. infinity_data/tokenizer/finalizer.py +271 -0
  44. infinity_data/tokenizer/models/__init__.py +17 -0
  45. infinity_data/tokenizer/models/raw_tokens.py +61 -0
  46. infinity_data/tokenizer/models/tokens.py +245 -0
  47. infinity_data/tokenizer/tokenizer.py +531 -0
  48. infinity_data-1.0.0.dist-info/METADATA +60 -0
  49. infinity_data-1.0.0.dist-info/RECORD +52 -0
  50. infinity_data-1.0.0.dist-info/WHEEL +5 -0
  51. infinity_data-1.0.0.dist-info/licenses/LICENSE +21 -0
  52. infinity_data-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,163 @@
1
+ """约束解析:RawAst 约束 → :class:`ResolvedConstraint`(挂在 StdAst 节点上)。
2
+
3
+ 纯函数、无副作用:解析约束 AST(名字经 scope 翻译为真名、参数求值),
4
+ 返回 ``(specs, diagnostics)``——``ErrorConstraint`` 的诊断在此收集返回,
5
+ 不直接写入任何收集器(调用方自行并入)。
6
+
7
+ 约束**只解析不执行**:执行由 :class:`ConstraintExecutor`(Phase 2b)完成。
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ from infinity_data.infra.diagnostics import Diagnostic, Severity
15
+ from infinity_data.parser import (
16
+ Constraint,
17
+ ConstraintCall,
18
+ ConstraintIdent,
19
+ ConstraintLiteral,
20
+ Constraints,
21
+ ErrorConstraint,
22
+ LiteralValue,
23
+ )
24
+ from infinity_data.semantic.builder.models import ResolvedConstraint
25
+ from infinity_data.semantic.resolver.models import Scope
26
+ from infinity_data.tokenizer.models.tokens import (
27
+ BoolToken,
28
+ FloatToken,
29
+ IntegerToken,
30
+ NoexistToken,
31
+ NullToken,
32
+ StringToken,
33
+ )
34
+
35
+ _INVALID_CONSTRAINT = '@invalid'
36
+
37
+
38
+ def expand_annotation(annotation: Constraints) -> Constraints:
39
+ """展开约束语法糖:多约束 ``<a, b, c>`` → ``all(a, b, c)``。
40
+
41
+ (``type?`` → ``one(type, ?)`` 已在 parser 阶段展开。)
42
+ """
43
+ if len(annotation.constraints) > 1:
44
+ return Constraints(
45
+ source=annotation.source,
46
+ constraints=[
47
+ ConstraintCall(
48
+ source=annotation.source,
49
+ name='all',
50
+ arguments=list(annotation.constraints),
51
+ ),
52
+ ],
53
+ )
54
+ return annotation
55
+
56
+
57
+ def resolve_constraints(
58
+ annotation: Constraints,
59
+ scope: Scope,
60
+ ) -> tuple[list[ResolvedConstraint], list[Diagnostic]]:
61
+ """解析字段注解约束链(语法糖展开 → 逐约束解析),返回 ``(specs, diagnostics)``。"""
62
+ expanded = expand_annotation(annotation)
63
+ specs: list[ResolvedConstraint] = []
64
+ diags: list[Diagnostic] = []
65
+ for c in expanded.constraints:
66
+ spec, ds = resolve_constraint(c, scope)
67
+ specs.append(spec)
68
+ diags.extend(ds)
69
+ return specs, diags
70
+
71
+
72
+ def resolve_constraint_list(
73
+ constraints: list[Constraint],
74
+ scope: Scope,
75
+ ) -> tuple[list[ResolvedConstraint], list[Diagnostic]]:
76
+ """解析结构级约束列表(dict 级 / 模板级 / 顶层 ``: <...>``),逐个解析。
77
+
78
+ 与字段注解(:class:`Constraints`)不同,结构约束已是展开后的列表:
79
+ 多约束是否合成 ``all`` 由 parser 决定,此处不再次展开。
80
+ """
81
+ specs: list[ResolvedConstraint] = []
82
+ diags: list[Diagnostic] = []
83
+ for c in constraints:
84
+ spec, ds = resolve_constraint(c, scope)
85
+ specs.append(spec)
86
+ diags.extend(ds)
87
+ return specs, diags
88
+
89
+
90
+ def resolve_constraint(c: Constraint, scope: Scope) -> tuple[ResolvedConstraint, list[Diagnostic]]:
91
+ """解析单个约束 AST(名字经 scope 翻译为真名)。"""
92
+ match c:
93
+ case ConstraintIdent(name=n):
94
+ return ResolvedConstraint(name=translate_name(n, scope), source=c.source), []
95
+ case ConstraintCall(name=n, arguments=args):
96
+ resolved_args: list[Any] = []
97
+ diags: list[Diagnostic] = []
98
+ for a in args:
99
+ arg, ds = resolve_constraint_arg(a, scope)
100
+ resolved_args.append(arg)
101
+ diags.extend(ds)
102
+ return (
103
+ ResolvedConstraint(name=translate_name(n, scope), args=resolved_args, source=c.source),
104
+ diags,
105
+ )
106
+ case ConstraintLiteral():
107
+ return ResolvedConstraint(name=_INVALID_CONSTRAINT, source=c.source), []
108
+ case ErrorConstraint(message=m):
109
+ return (
110
+ ResolvedConstraint(name=_INVALID_CONSTRAINT, source=c.source),
111
+ [Diagnostic(Severity.ERROR, 'error.generic', {'message': m}, c.source)],
112
+ )
113
+ return ResolvedConstraint(name=_INVALID_CONSTRAINT), []
114
+
115
+
116
+ def resolve_constraint_arg(c: Constraint, scope: Scope) -> tuple[Any, list[Diagnostic]]:
117
+ """解析约束参数:嵌套约束 → ResolvedConstraint;字面量 → Python 值。"""
118
+ match c:
119
+ case ConstraintIdent(name=n):
120
+ return ResolvedConstraint(name=translate_name(n, scope), source=c.source), []
121
+ case ConstraintCall(name=n, arguments=args):
122
+ resolved_args: list[Any] = []
123
+ diags: list[Diagnostic] = []
124
+ for a in args:
125
+ arg, ds = resolve_constraint_arg(a, scope)
126
+ resolved_args.append(arg)
127
+ diags.extend(ds)
128
+ return (
129
+ ResolvedConstraint(name=translate_name(n, scope), args=resolved_args, source=c.source),
130
+ diags,
131
+ )
132
+ case ConstraintLiteral(value=lit):
133
+ return literal_python_value(lit), []
134
+ case ErrorConstraint(message=m):
135
+ return (
136
+ ResolvedConstraint(name=_INVALID_CONSTRAINT, source=c.source),
137
+ [Diagnostic(Severity.ERROR, 'error.generic', {'message': m}, c.source)],
138
+ )
139
+ return ResolvedConstraint(name=_INVALID_CONSTRAINT), []
140
+
141
+
142
+ def translate_name(name: str, scope: Scope) -> str:
143
+ """可见名 → 真名字符串。未命中(如 has(field) 的裸字段名)保留原名。"""
144
+ key = scope.get(name)
145
+ return str(key) if key is not None else name
146
+
147
+
148
+ def literal_python_value(lit: LiteralValue) -> Any:
149
+ """约束参数字面量 → Python 值。"""
150
+ match lit.value:
151
+ case IntegerToken(value=v):
152
+ return v
153
+ case FloatToken(value=v):
154
+ return v
155
+ case BoolToken(value=v):
156
+ return v
157
+ case StringToken(value=v):
158
+ return v
159
+ case NullToken():
160
+ return None
161
+ case NoexistToken():
162
+ return None
163
+ return None
@@ -0,0 +1,172 @@
1
+ """语义分析阶段诊断定义(集中注册)。
2
+
3
+ 语义层的全部诊断定义(``template.*`` / ``constraint.*`` / ``field.*`` /
4
+ ``value.*`` / ``dollar.*`` / ``import.*`` / ``namespace.*`` 等)在此集中,
5
+ 导入时经 :func:`register_diagnostic_define` 合并进全局注册表。
6
+
7
+ 与词法(``tokenizer/diagnostics.py``)、语法(``parser/diagnostics.py``)一致:
8
+ **每层一个诊断词汇表文件**,便于审计与多语言维护。
9
+ """
10
+
11
+ from infinity_data.infra.diagnostics import diagnostic_define, register_diagnostic_define
12
+
13
+ register_diagnostic_define(
14
+ # ═══════════════════════════════════════════════════════════
15
+ # 模板
16
+ # ═══════════════════════════════════════════════════════════
17
+ diagnostic_define(
18
+ 'template.shadows_builtin',
19
+ '{path_prefix}模板 {template!r} 与内置约束同名,禁止定义(避免遮蔽 {template} 约束)',
20
+ ),
21
+ diagnostic_define(
22
+ 'template.duplicate', '{path_prefix}模板 {template!r} 重复定义(同一文件内不允许同名模板),后者被拒绝'
23
+ ),
24
+ diagnostic_define(
25
+ 'template.required_order', '{path_prefix}模板 {template!r} 中必填字段 {field!r} 出现在可选字段之后'
26
+ ),
27
+ diagnostic_define('template.import_depth', '{path_prefix}模板导入嵌套深度超过上限 {max}: {path_src}'),
28
+ diagnostic_define('template.read_failed', '{path_prefix}读取模板文件失败 {file}: {error}'),
29
+ diagnostic_define(
30
+ 'template.recursive_default',
31
+ '{path_prefix}模板 {template!r} 的默认值形成递归引用(默认值禁止自引用,展开会无限循环)',
32
+ en='{path_prefix}template {template!r} has a recursive default (self-reference in defaults is forbidden; expansion would loop forever)',
33
+ ),
34
+ diagnostic_define('template.import_not_found', '{path_prefix}导入文件中不存在模板 {template!r}'),
35
+ diagnostic_define('template.import_conflict_local', '{path_prefix}导入的模板 {visible!r} 与文件内定义冲突'),
36
+ diagnostic_define(
37
+ 'template.import_duplicate',
38
+ '{path_prefix}可见名 {visible!r} 重复导入(同一 scope 内不允许重复可见名),后者被拒绝',
39
+ ),
40
+ diagnostic_define(
41
+ 'template.undefined',
42
+ '{path_prefix}未定义的模板 {template!r}',
43
+ en='{path_prefix}undefined template: {template!r}',
44
+ ),
45
+ diagnostic_define(
46
+ 'template.missing_required',
47
+ '{path_prefix}模板 {template!r} 的必填字段 {field!r} 未提供',
48
+ en='{path_prefix}required field {field!r} of template {template!r} is missing',
49
+ ),
50
+ diagnostic_define(
51
+ 'template.arg_conflict', '{path_prefix}模板 {template!r} 字段 {field!r} 同时以位置和命名参数提供'
52
+ ),
53
+ diagnostic_define(
54
+ 'template.too_many_positional',
55
+ '{path_prefix}模板 {template!r} 只有 {count} 个必填字段,提供了 {given} 个位置参数',
56
+ ),
57
+ diagnostic_define(
58
+ 'template.positional_disabled',
59
+ '{path_prefix}模板 {template!r} 禁用位置参数,请使用命名参数',
60
+ en='{path_prefix}template {template!r} disables positional arguments; use named arguments',
61
+ ),
62
+ diagnostic_define(
63
+ 'template.unknown_argument',
64
+ '{path_prefix}模板 {template!r} 收到未知命名参数 {arg!r}',
65
+ en='{path_prefix}template {template!r} received unknown named argument {arg!r}',
66
+ ),
67
+ diagnostic_define('template.expect_value', '{path_prefix}期望 {template}(模板约束),实际没有值'),
68
+ diagnostic_define('template.expect_object', '{path_prefix}期望 {template}(对象),实际 {actual}'),
69
+ diagnostic_define(
70
+ 'template.null_use_nullable', '{path_prefix}期望 {template},实际 null(使用 {template}? 允许可空)'
71
+ ),
72
+ diagnostic_define('template.extra_field', '{path_prefix}模板 {template} 不允许额外字段 {field!r}'),
73
+ diagnostic_define('template.missing_field', '{path_prefix}模板 {template} 的必填字段 {field!r} 缺失'),
74
+ # ═══════════════════════════════════════════════════════════
75
+ # 字段 / 值 / $ 引用
76
+ # ═══════════════════════════════════════════════════════════
77
+ diagnostic_define(
78
+ 'field.missing_value',
79
+ '{path_prefix}字段缺少值(如需 noexist 请显式书写 = noexist)',
80
+ en='{path_prefix}field is missing a value (write = noexist explicitly if intended)',
81
+ ),
82
+ diagnostic_define('value.nesting_depth', '{path_prefix}嵌套层级超过上限 {max}'),
83
+ diagnostic_define(
84
+ 'value.noexist_in_array',
85
+ '{path_prefix}noexist 仅用于 dict 字段,数组元素中无意义(如需空位请用 null)',
86
+ en='{path_prefix}noexist is only for dict fields; meaningless in array elements (use null for a slot)',
87
+ ),
88
+ diagnostic_define(
89
+ 'dollar.undefined', '{path_prefix}未找到导入变量 ${name}', en='{path_prefix}undefined import variable ${name}'
90
+ ),
91
+ diagnostic_define(
92
+ 'dollar.convert_failed',
93
+ '{path_prefix}无法将 ${name}={raw!r} 转为 {type}',
94
+ en='{path_prefix}cannot convert ${name}={raw!r} to {type}',
95
+ ),
96
+ diagnostic_define('inft.not_allowed', '{path_prefix}.inft 文件只允许模板定义,发现其他语句'),
97
+ diagnostic_define('error.generic', '{path_prefix}{message}'),
98
+ # ═══════════════════════════════════════════════════════════
99
+ # 约束引擎
100
+ # ═══════════════════════════════════════════════════════════
101
+ diagnostic_define('constraint.invalid', '{path_prefix}无效的约束表达式'),
102
+ diagnostic_define('constraint.unknown', '{path_prefix}未知约束 {name!r}'),
103
+ diagnostic_define('constraint.arg_count', '{path_prefix}约束 {name}() 期望 {expected} 个参数,实际 {given} 个'),
104
+ diagnostic_define(
105
+ 'constraint.expect_value',
106
+ '{path_prefix}期望 {expected},实际没有值',
107
+ en='{path_prefix}expected {expected}, got no value',
108
+ ),
109
+ diagnostic_define(
110
+ 'constraint.type_mismatch',
111
+ '{path_prefix}期望 {expected},实际 {actual}',
112
+ en='{path_prefix}expected {expected}, got {actual}',
113
+ ),
114
+ diagnostic_define('constraint.numeric_only', '{path_prefix}{constraint} 约束只适用于数值,实际 {actual}'),
115
+ diagnostic_define('constraint.nan_not_allowed', '{path_prefix}{constraint} 约束不适用于 NaN'),
116
+ diagnostic_define('constraint.range_arg', '{path_prefix}range 参数必须是数值: {value!r}'),
117
+ diagnostic_define(
118
+ 'constraint.range_below',
119
+ '{path_prefix}值 {value} 小于下界 {lo}',
120
+ en='{path_prefix}value {value} is below lower bound {lo}',
121
+ ),
122
+ diagnostic_define(
123
+ 'constraint.range_above',
124
+ '{path_prefix}值 {value} 大于上界 {hi}',
125
+ en='{path_prefix}value {value} exceeds upper bound {hi}',
126
+ ),
127
+ diagnostic_define('constraint.size_only', '{path_prefix}size 约束适用于 str/list/dict,实际 {actual}'),
128
+ diagnostic_define('constraint.size_arg', '{path_prefix}size 参数必须是整数'),
129
+ diagnostic_define('constraint.size_out', '{path_prefix}大小 {size} 不在范围 [{lo}, {hi}] 内'),
130
+ diagnostic_define('constraint.each_need', '{path_prefix}each() 需要一个约束参数'),
131
+ diagnostic_define('constraint.each_only', '{path_prefix}each 约束适用于 list/dict,实际 {actual}'),
132
+ diagnostic_define('constraint.in_not_in', '{path_prefix}值 {value} 不在允许的值 {choices!r} 中'),
133
+ diagnostic_define('constraint.string_only', '{path_prefix}{constraint} 约束只适用于字符串'),
134
+ diagnostic_define('constraint.invalid_value', '{path_prefix}无效的{what} {value!r}'),
135
+ diagnostic_define('constraint.regex_no_match', '{path_prefix}值 {value!r} 不匹配正则 {pattern!r}'),
136
+ diagnostic_define('constraint.regex_invalid', '{path_prefix}无效的正则表达式 {pattern!r}: {error}'),
137
+ diagnostic_define('constraint.positive_fail', '{path_prefix}值 {value} 不是正数'),
138
+ diagnostic_define('constraint.negative_fail', '{path_prefix}值 {value} 不是负数'),
139
+ diagnostic_define('constraint.nonnegative_fail', '{path_prefix}值 {value} 是负数'),
140
+ diagnostic_define(
141
+ 'constraint.eq_mismatch',
142
+ '{path_prefix}值 {value} 不等于 {expected!r}',
143
+ en='{path_prefix}value {value} does not equal {expected!r}',
144
+ ),
145
+ diagnostic_define('constraint.unique_only', '{path_prefix}unique 约束只适用于 list'),
146
+ diagnostic_define('constraint.unique_dup', '{path_prefix}元素 {value} 重复'),
147
+ diagnostic_define('constraint.has_only', '{path_prefix}has 约束只适用于 dict'),
148
+ diagnostic_define('constraint.has_missing', '{path_prefix}缺少键 {key!r}'),
149
+ diagnostic_define('constraint.field_only', '{path_prefix}field 约束只适用于 dict'),
150
+ diagnostic_define('constraint.field_need', '{path_prefix}field() 的第二个参数必须是约束'),
151
+ diagnostic_define('constraint.field_missing', '{path_prefix}字段 {field!r} 不存在'),
152
+ diagnostic_define('constraint.not_need', '{path_prefix}not() 需要一个约束参数'),
153
+ diagnostic_define('constraint.not_fail', '{path_prefix}not 约束失败(内部约束意外满足)'),
154
+ diagnostic_define('constraint.any_fail', '{path_prefix}any 约束失败(所有子约束都不满足)'),
155
+ diagnostic_define('constraint.one_none', '{path_prefix}one 约束失败(没有子约束被满足)'),
156
+ diagnostic_define('constraint.one_many', '{path_prefix}one 约束失败({count} 个子约束被满足: {names})'),
157
+ diagnostic_define('constraint.all_fail', '{path_prefix}all 约束失败'),
158
+ diagnostic_define('constraint.when_need', '{path_prefix}when() 需要两个约束参数'),
159
+ # ═══════════════════════════════════════════════════════════
160
+ # 导入
161
+ # ═══════════════════════════════════════════════════════════
162
+ diagnostic_define('import.file_denied', '{path_prefix}文件导入超出沙盒授权,已忽略: {path_src}'),
163
+ diagnostic_define('import.file_missing', '{path_prefix}导入文件不存在: {name}'),
164
+ diagnostic_define('import.path_failed', '{path_prefix}无法解析导入路径: {name}'),
165
+ diagnostic_define('import.template_denied', '{path_prefix}模板导入超出沙盒授权,已忽略: {path_src}'),
166
+ diagnostic_define('import.yaml_missing', '{path_prefix}yaml 支持需要安装 PyYAML'),
167
+ diagnostic_define('import.unsupported_format', '{path_prefix}不支持的文件格式: {format}'),
168
+ diagnostic_define('import.parse_failed', '{path_prefix}解析数据失败: {error}'),
169
+ diagnostic_define(
170
+ 'namespace.duplicate', '{path_prefix}导入别名 {name!r} 重复绑定($ 命名空间内不允许重复),后者被拒绝'
171
+ ),
172
+ )
@@ -0,0 +1,10 @@
1
+ """Phase 2b(约束执行器):遍历 StdAst 执行约束 + 顶层 schema 校验。
2
+
3
+ - :class:`ConstraintExecutor`:消费 Phase 2a 产出的标准 AST(executor.py)
4
+
5
+ 子模块导出自身类,供流水线(pipeline / 测试)直接引用。
6
+ """
7
+
8
+ from infinity_data.semantic.executor.executor import ConstraintExecutor
9
+
10
+ __all__ = ['ConstraintExecutor']
@@ -0,0 +1,259 @@
1
+ """约束执行器(Phase 2b):遍历已完成 StdAst,执行节点携带的约束。
2
+
3
+ 约束器**工作在 AST 上**(不参与构建):
4
+
5
+ - 递归遍历 :class:`StdField` / :class:`StdObject` / :class:`StdArray`,
6
+ 执行节点携带的 :class:`ResolvedConstraint`
7
+ (字段注解 / dict 结构级 / 模板级 / 顶层结构约束)
8
+ - 「模板即约束」:约束名为模板真名时,查模板表做结构校验(手写 dict)
9
+ - **只校验、不转换**:约束失败仅产出诊断,不改变值(coercion 属错误设计)
10
+ - 顶层 schema 校验(strict/lenient/strip)也在此:
11
+ 对顶层对象执行模板约束 + 额外字段策略
12
+
13
+ 本层消费 Phase 2a 数据模型(:class:`StdDocument` 等),与构建器零耦合;
14
+ 诊断写入调用方注入的共享 :class:`DiagnosticCollector`(流水线单一收集器)。
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from infinity_data.infra.diagnostics import Diagnostic, DiagnosticCollector, Severity
20
+ from infinity_data.parser import TemplateDef
21
+ from infinity_data.sandbox import Schema, SchemaError
22
+ from infinity_data.semantic.builder.models import (
23
+ ResolvedConstraint,
24
+ StdArray,
25
+ StdField,
26
+ StdLiteral,
27
+ StdObject,
28
+ StdValue,
29
+ )
30
+ from infinity_data.semantic.constraints import resolve_constraint_list, resolve_constraints
31
+ from infinity_data.semantic.registry import (
32
+ ConstraintRegistry,
33
+ ConstraintResult,
34
+ describe,
35
+ fail_result,
36
+ ok_result,
37
+ )
38
+ from infinity_data.semantic.resolver.models import Scope, TemplateKey
39
+ from infinity_data.tokenizer.models.raw_tokens import SourceRange
40
+
41
+ _INVALID_CONSTRAINT = '@invalid'
42
+
43
+
44
+ class ConstraintExecutor:
45
+ """工作在已完成 StdAst 上的约束遍历器:节点约束 → 诊断(只校验,不转换)。"""
46
+
47
+ def __init__(
48
+ self,
49
+ *,
50
+ registry: ConstraintRegistry,
51
+ templates: dict[TemplateKey, TemplateDef],
52
+ template_scopes: dict[TemplateKey, Scope],
53
+ ) -> None:
54
+ self._registry = registry
55
+ self._templates = templates
56
+ self._template_scopes = template_scopes
57
+ self._templates_by_name: dict[str, TemplateKey] = {str(k): k for k in templates}
58
+
59
+ # ═══════════════════════════════════════════════════════
60
+ # 遍历入口
61
+ # ═══════════════════════════════════════════════════════
62
+
63
+ def validate(self, node: StdValue, collector: DiagnosticCollector, path: str = '') -> None:
64
+ """递归遍历节点,执行携带的约束,诊断写入 ``collector``(不修改值)。"""
65
+ if isinstance(node, StdObject):
66
+ for f in node.fields:
67
+ self._validate_field(f, collector, path)
68
+ # 结构级约束(dict 级 / 模板级 / 顶层):全部执行(不短路)
69
+ for spec in node.constraints:
70
+ result = self._exec(spec, node, spec.source, path)
71
+ if not result.ok:
72
+ collector.extend(result.diagnostics)
73
+ elif isinstance(node, StdArray):
74
+ for i, elem in enumerate(node.elements):
75
+ self.validate(elem, collector, f'{path}[{i}]')
76
+
77
+ def _validate_field(self, field: StdField, collector: DiagnosticCollector, path: str) -> None:
78
+ """字段:先递归值(内部结构约束),再执行字段注解约束(链式短路)。"""
79
+ child = f'{path}.{field.name}' if path else field.name
80
+ if field.value is not None:
81
+ self.validate(field.value, collector, child)
82
+ for spec in field.constraints:
83
+ result = self._exec(spec, field.value, spec.source or field.source, child)
84
+ if not result.ok:
85
+ collector.extend(result.diagnostics)
86
+ break # 约束链短路(与构建期语义一致)
87
+
88
+ # ═══════════════════════════════════════════════════════
89
+ # 约束执行
90
+ # ═══════════════════════════════════════════════════════
91
+
92
+ def _exec(
93
+ self,
94
+ constraint: ResolvedConstraint,
95
+ value: StdValue | None,
96
+ source: SourceRange | None,
97
+ path: str,
98
+ ) -> ConstraintResult:
99
+ """执行单个已解析约束;模板真名 → 模板结构校验;其余 → registry。
100
+
101
+ 签名与 :class:`Executor` 协议一致(嵌套约束回调复用本方法)。
102
+ """
103
+ if constraint.name == _INVALID_CONSTRAINT:
104
+ return fail_result('constraint.invalid', {}, source, path)
105
+ key = self._templates_by_name.get(constraint.name)
106
+ if key is not None:
107
+ return self._check_template(key, value, source, path)
108
+ return self._registry.apply(constraint, value, constraint.source or source, path, self._exec)
109
+
110
+ def _check_template(
111
+ self,
112
+ key: TemplateKey,
113
+ value: StdValue | None,
114
+ source: SourceRange | None,
115
+ path: str,
116
+ ) -> ConstraintResult:
117
+ """模板即约束:校验手写 dict 是否符合模板声明(含字段 / 模板级约束)。"""
118
+ tpl = self._templates[key]
119
+ display = key.name
120
+ scope = self._template_scopes[key]
121
+ allow_extra = tpl.config.allow_extra
122
+ declared = {tf.name for tf in tpl.fields}
123
+
124
+ if value is None:
125
+ return fail_result('template.expect_value', {'template': display}, source, path)
126
+ if isinstance(value, StdLiteral):
127
+ if value.kind == 'null':
128
+ return fail_result('template.null_use_nullable', {'template': display}, source, path)
129
+ return fail_result('template.expect_object', {'template': display, 'actual': describe(value)}, source, path)
130
+ if not isinstance(value, StdObject):
131
+ return fail_result('template.expect_object', {'template': display, 'actual': describe(value)}, source, path)
132
+
133
+ # 标记来源模板:这个手写 dict 被判定为模板 display 的结构(供下游引用)
134
+ if value.template is None:
135
+ value.template = key
136
+
137
+ diags: list[Diagnostic] = []
138
+ field_map = {f.name: f for f in value.fields}
139
+
140
+ # 逐字段校验(模板定义约束)
141
+ for tf in tpl.fields:
142
+ child = f'{path}.{tf.name}' if path else tf.name
143
+ f = field_map.get(tf.name)
144
+ if f is None:
145
+ if tf.default_value is None:
146
+ diags.append(
147
+ Diagnostic(
148
+ Severity.ERROR,
149
+ 'template.missing_field',
150
+ {'template': display, 'field': tf.name},
151
+ source,
152
+ child,
153
+ )
154
+ )
155
+ continue
156
+ if f.value is not None:
157
+ specs, rdiags = resolve_constraints(tf.constraints, scope)
158
+ diags.extend(rdiags)
159
+ for spec in specs:
160
+ result = self._exec(spec, f.value, tf.source, child)
161
+ if not result.ok:
162
+ diags.extend(result.diagnostics)
163
+ break
164
+
165
+ # 严格模式:不允许额外字段(allow_extra=true 时放行)
166
+ if not allow_extra:
167
+ for f in value.fields:
168
+ if f.name not in declared:
169
+ child = f'{path}.{f.name}' if path else f.name
170
+ diags.append(
171
+ Diagnostic(
172
+ Severity.ERROR,
173
+ 'template.extra_field',
174
+ {'template': display, 'field': f.name},
175
+ f.source or source,
176
+ child,
177
+ )
178
+ )
179
+
180
+ # 模板级约束
181
+ specs, rdiags = resolve_constraint_list(tpl.constraints, scope)
182
+ diags.extend(rdiags)
183
+ for spec in specs:
184
+ result = self._exec(spec, value, source, path)
185
+ if not result.ok:
186
+ diags.extend(result.diagnostics)
187
+
188
+ if diags:
189
+ return ConstraintResult(ok=False, diagnostics=diags)
190
+ return ok_result()
191
+
192
+ # ═══════════════════════════════════════════════════════
193
+ # 顶层 schema 校验
194
+ # ═══════════════════════════════════════════════════════
195
+
196
+ def apply_schema(
197
+ self,
198
+ root: StdObject,
199
+ schema: Schema,
200
+ tpl: TemplateDef,
201
+ scope: Scope,
202
+ collector: DiagnosticCollector,
203
+ ) -> StdObject:
204
+ """按模板校验顶层对象(strict/lenient/strip),返回校验后的 root。
205
+
206
+ - lenient 的额外字段 WARNING 写入 ``collector``(非致命)
207
+ - strict / 必填缺失 / 字段 / 模板级约束失败聚合为 :class:`SchemaError` 抛出
208
+ """
209
+ mode = schema.mode
210
+ declared = {tf.name for tf in tpl.fields}
211
+ diags: list[Diagnostic] = []
212
+
213
+ # 额外字段处理(模式差异)
214
+ extra_names = [f.name for f in root.fields if f.name not in declared]
215
+ if extra_names:
216
+ if mode == 'strict':
217
+ diags.append(Diagnostic(Severity.ERROR, 'schema.extra_fields', {'fields': extra_names}, None))
218
+ elif mode == 'lenient':
219
+ collector.add(
220
+ Diagnostic(Severity.WARNING, 'schema.extra_fields_lenient', {'fields': extra_names}, None)
221
+ )
222
+ else: # strip
223
+ root = StdObject(fields=[f for f in root.fields if f.name in declared], template=root.template)
224
+
225
+ # 必填字段缺失
226
+ for tf in tpl.fields:
227
+ if tf.default_value is None and root.get(tf.name) is None:
228
+ diags.append(
229
+ Diagnostic(
230
+ Severity.ERROR,
231
+ 'schema.missing_required',
232
+ {'field': tf.name, 'template': tpl.name},
233
+ None,
234
+ tf.name,
235
+ )
236
+ )
237
+
238
+ # 字段约束校验
239
+ for tf in tpl.fields:
240
+ f = root.get(tf.name)
241
+ if f is not None and f.value is not None:
242
+ specs, rdiags = resolve_constraints(tf.constraints, scope)
243
+ diags.extend(rdiags)
244
+ for spec in specs:
245
+ result = self._exec(spec, f.value, tf.source, tf.name)
246
+ if not result.ok:
247
+ diags.extend(result.diagnostics)
248
+
249
+ # 模板级约束
250
+ specs, rdiags = resolve_constraint_list(tpl.constraints, scope)
251
+ diags.extend(rdiags)
252
+ for spec in specs:
253
+ result = self._exec(spec, root, None, '')
254
+ if not result.ok:
255
+ diags.extend(result.diagnostics)
256
+
257
+ if diags:
258
+ raise SchemaError('schema.failed', {'detail': ';'.join(d.message for d in diags)})
259
+ return root