openpuppet-language 2.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 (40) hide show
  1. openpuppet_language-2.0.data/data/share/puppet/conformance/README.md +274 -0
  2. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/binding.json +238 -0
  3. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/capabilities.json +155 -0
  4. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/grammar.json +248 -0
  5. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/interaction.json +220 -0
  6. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/listen.json +133 -0
  7. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/platform.json +274 -0
  8. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/probes.json +117 -0
  9. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/render.json +180 -0
  10. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/tabs.json +39 -0
  11. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/templates.json +143 -0
  12. openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_bad_result.py +11 -0
  13. openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_basic.py +24 -0
  14. openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_deps_mismatch.py +11 -0
  15. openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_no_doc.py +10 -0
  16. openpuppet_language-2.0.data/data/share/puppet/conformance/runner.py +721 -0
  17. openpuppet_language-2.0.data/data/share/puppet/spec/01-grammar.md +283 -0
  18. openpuppet_language-2.0.data/data/share/puppet/spec/02-ir.md +77 -0
  19. openpuppet_language-2.0.data/data/share/puppet/spec/03-semantics.md +167 -0
  20. openpuppet_language-2.0.data/data/share/puppet/spec/04-vocabulary.md +227 -0
  21. openpuppet_language-2.0.data/data/share/puppet/spec/05-render-contract.md +126 -0
  22. openpuppet_language-2.0.data/data/share/puppet/spec/06-diagnostics.md +151 -0
  23. openpuppet_language-2.0.data/data/share/puppet/spec/README.md +73 -0
  24. openpuppet_language-2.0.dist-info/METADATA +50 -0
  25. openpuppet_language-2.0.dist-info/RECORD +40 -0
  26. openpuppet_language-2.0.dist-info/WHEEL +5 -0
  27. openpuppet_language-2.0.dist-info/entry_points.txt +2 -0
  28. openpuppet_language-2.0.dist-info/top_level.txt +1 -0
  29. puppet/__init__.py +19 -0
  30. puppet/adapter.py +112 -0
  31. puppet/capabilities.py +172 -0
  32. puppet/cli.py +132 -0
  33. puppet/diag.py +42 -0
  34. puppet/engine.py +1038 -0
  35. puppet/eval.py +243 -0
  36. puppet/ir.py +538 -0
  37. puppet/lang.py +755 -0
  38. puppet/raster_adapter.py +348 -0
  39. puppet/tk_adapter.py +374 -0
  40. puppet/vocab.py +147 -0
puppet/ir.py ADDED
@@ -0,0 +1,538 @@
1
+ """程序 IR 与校验(`spec/02-ir.md`)。
2
+
3
+ 程序 IR 只由命令批次改变;运行期状态另存(见 `engine.py`)。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import Dict, List, Optional
10
+
11
+ from . import vocab
12
+ from .diag import Diagnostic, ERROR, INFO, WARNING
13
+ from .lang import (ActionStmt, Add, CallStmt, Data, Del, DictLit, Expr, ListLit,
14
+ Listen, Lit, Move, On, Probe, Ref, SetStmt, Stmt, collect_refs,
15
+ is_static)
16
+
17
+ ROOT = "root"
18
+
19
+
20
+ @dataclass
21
+ class Node:
22
+ id: str
23
+ type: str
24
+ parent: Optional[str] = None
25
+ children: List[str] = field(default_factory=list)
26
+ attrs: Dict[str, Expr] = field(default_factory=dict)
27
+ as_name: str = ""
28
+ line: int = 0
29
+
30
+
31
+ @dataclass
32
+ class DataSource:
33
+ id: str
34
+ persist: bool = False
35
+ schema: Dict[str, tuple] = field(default_factory=dict)
36
+ initial: Optional[Expr] = None
37
+ line: int = 0
38
+
39
+
40
+ @dataclass
41
+ class Handler:
42
+ target: str
43
+ event: str
44
+ bind: str = ""
45
+ when: Optional[Expr] = None
46
+ actions: List = field(default_factory=list)
47
+ line: int = 0
48
+
49
+
50
+ @dataclass
51
+ class ListenDecl:
52
+ target: str
53
+ event: str
54
+ line: int = 0
55
+
56
+
57
+ @dataclass
58
+ class Program:
59
+ nodes: Dict[str, Node] = field(default_factory=dict)
60
+ data: Dict[str, DataSource] = field(default_factory=dict)
61
+ handlers: List[Handler] = field(default_factory=list)
62
+ listens: List[ListenDecl] = field(default_factory=list)
63
+ calls: List[CallStmt] = field(default_factory=list)
64
+ probes: List[Probe] = field(default_factory=list)
65
+
66
+
67
+ def new_program() -> Program:
68
+ program = Program()
69
+ program.nodes[ROOT] = Node(id=ROOT, type="root")
70
+ return program
71
+
72
+
73
+ # ------------------------------------------------------------------ 结构应用
74
+
75
+ def apply_stmt(program: Program, stmt: Stmt, diags: List[Diagnostic]) -> List[str]:
76
+ """把一条语句应用到程序 IR。返回被改动的数据源地址(供级联使用)。"""
77
+ changed: List[str] = []
78
+ line = getattr(stmt, "line", 0)
79
+
80
+ if isinstance(stmt, Add):
81
+ return _apply_add(program, stmt, diags)
82
+
83
+ if isinstance(stmt, SetStmt):
84
+ node = program.nodes.get(stmt.target)
85
+ if node is None:
86
+ src = program.data.get(stmt.target)
87
+ if src is None:
88
+ diags.append(Diagnostic("TARGET_MISSING", ERROR,
89
+ "目标 %s 不存在" % _hash(stmt.target), line=line))
90
+ return changed
91
+ return changed
92
+ for key, expr in stmt.attrs.items():
93
+ old = node.attrs.get(key)
94
+ if old is not None and not is_static(old):
95
+ diags.append(Diagnostic("BIND_OVERRIDDEN", INFO,
96
+ "属性 %s 的绑定已被覆盖" % key, line=line))
97
+ node.attrs[key] = expr
98
+ return changed
99
+
100
+ if isinstance(stmt, Del):
101
+ node = program.nodes.get(stmt.target)
102
+ if node is None:
103
+ diags.append(Diagnostic("DEL_MISSING", WARNING,
104
+ "要删除的 %s 不存在" % _hash(stmt.target), line=line))
105
+ return changed
106
+ _detach(program, node)
107
+ return changed
108
+
109
+ if isinstance(stmt, Move):
110
+ node = program.nodes.get(stmt.target)
111
+ new_parent = program.nodes.get(stmt.new_parent)
112
+ if node is None or new_parent is None:
113
+ diags.append(Diagnostic("TARGET_MISSING", ERROR, "移动的目标或新父不存在", line=line))
114
+ return changed
115
+ if stmt.anchor is not None:
116
+ anchor = program.nodes.get(stmt.anchor[1])
117
+ if anchor is None or anchor.parent != new_parent.id:
118
+ diags.append(Diagnostic("ANCHOR_MISSING", ERROR,
119
+ "锚点 %s 不存在于新父之下" % _hash(stmt.anchor[1]), line=line))
120
+ return changed
121
+ if new_parent.id == node.id or node.id in _ancestors(program, new_parent.id):
122
+ diags.append(Diagnostic("MOVE_CYCLE", ERROR, "移动会造成成环", line=line))
123
+ return changed
124
+ _detach(program, node)
125
+ node.parent = new_parent.id
126
+ _insert(program, new_parent, node.id, stmt.anchor)
127
+ return changed
128
+
129
+ if isinstance(stmt, Data):
130
+ if stmt.source in program.data or stmt.source in program.nodes:
131
+ diags.append(Diagnostic("ID_DUP", ERROR,
132
+ "地址 %s 已被占用" % _hash(stmt.source), line=line))
133
+ return changed
134
+ program.data[stmt.source] = DataSource(
135
+ id=stmt.source, persist=stmt.persist, schema=stmt.schema,
136
+ initial=stmt.initial, line=line)
137
+ # 声明不是"变化":装载期不得触发 `on … change` 级联。
138
+ return changed
139
+
140
+ if isinstance(stmt, On):
141
+ program.handlers.append(Handler(
142
+ target=stmt.target, event=stmt.event, bind=stmt.bind,
143
+ when=stmt.when, actions=stmt.actions, line=line))
144
+ return changed
145
+
146
+ if isinstance(stmt, Listen):
147
+ if stmt.target not in program.nodes and stmt.target not in program.data:
148
+ diags.append(Diagnostic("TARGET_MISSING", ERROR,
149
+ "订阅的目标 %s 不存在" % _hash(stmt.target), line=line))
150
+ return changed
151
+ program.listens.append(ListenDecl(target=stmt.target, event=stmt.event, line=line))
152
+ return changed
153
+
154
+ if isinstance(stmt, CallStmt):
155
+ program.calls.append(stmt)
156
+ return changed
157
+
158
+ if isinstance(stmt, Probe):
159
+ program.probes.append(stmt)
160
+ return changed
161
+
162
+ # 顶层动作(集合原语)由引擎执行:它们改的是状态,不是程序。
163
+ if isinstance(stmt, ActionStmt):
164
+ return changed
165
+
166
+ return changed
167
+
168
+
169
+ def _hash(name: str) -> str:
170
+ return "#" + name
171
+
172
+
173
+ def _apply_add(program: Program, stmt: Add, diags: List[Diagnostic]) -> List[str]:
174
+ line = stmt.line
175
+ parent = program.nodes.get(stmt.parent)
176
+ if parent is None:
177
+ diags.append(Diagnostic("PARENT_MISSING", ERROR,
178
+ "父 %s 不存在" % _hash(stmt.parent), line=line))
179
+ return []
180
+ if stmt.self_id in program.nodes or stmt.self_id in program.data:
181
+ diags.append(Diagnostic("ID_DUP", ERROR,
182
+ "地址 %s 已被占用" % _hash(stmt.self_id), line=line))
183
+ return []
184
+ if stmt.anchor is not None:
185
+ anchor = program.nodes.get(stmt.anchor[1])
186
+ if anchor is None or anchor.parent != parent.id:
187
+ diags.append(Diagnostic("ANCHOR_MISSING", ERROR,
188
+ "锚点 %s 不存在" % _hash(stmt.anchor[1]), line=line))
189
+ return []
190
+ node = program.nodes.get(stmt.self_id)
191
+ if node is None:
192
+ node = Node(id=stmt.self_id, type=stmt.type, line=line)
193
+ node.type = stmt.type
194
+ node.parent = parent.id
195
+ node.as_name = stmt.as_name
196
+ node.attrs.update(stmt.attrs)
197
+ program.nodes[stmt.self_id] = node
198
+ if stmt.self_id not in parent.children:
199
+ _insert(program, parent, stmt.self_id, stmt.anchor)
200
+ return []
201
+
202
+
203
+ def _insert(program: Program, parent: Node, node_id: str, anchor):
204
+ if anchor is None:
205
+ parent.children.append(node_id)
206
+ return
207
+ kind, anchor_id = anchor
208
+ if anchor_id in parent.children:
209
+ idx = parent.children.index(anchor_id)
210
+ parent.children.insert(idx if kind == "before" else idx + 1, node_id)
211
+ else:
212
+ parent.children.append(node_id)
213
+
214
+
215
+ def _detach(program: Program, node: Node) -> None:
216
+ parent = program.nodes.get(node.parent) if node.parent else None
217
+ if parent and node.id in parent.children:
218
+ parent.children.remove(node.id)
219
+ for child in list(node.children):
220
+ _detach(program, program.nodes[child]) if child in program.nodes else None
221
+ program.nodes.pop(node.id, None)
222
+
223
+
224
+ def _ancestors(program: Program, node_id: str) -> List[str]:
225
+ out, cur = [], program.nodes.get(node_id)
226
+ seen = set()
227
+ while cur is not None and cur.parent is not None:
228
+ if cur.parent in seen:
229
+ break
230
+ seen.add(cur.parent)
231
+ out.append(cur.parent)
232
+ cur = program.nodes.get(cur.parent)
233
+ return out
234
+
235
+
236
+ def ancestors_of(program: Program, node_id: str) -> List[str]:
237
+ return _ancestors(program, node_id)
238
+
239
+
240
+ def in_template(program: Program, node_id: str) -> bool:
241
+ for aid in _ancestors(program, node_id):
242
+ node = program.nodes.get(aid)
243
+ if node is not None and node.type == "template":
244
+ return True
245
+ return False
246
+
247
+
248
+ def _unknown_attr(name: str, line: int) -> Diagnostic:
249
+ guess = vocab.suggest_from(name, vocab.ALL_ATTRS)
250
+ return Diagnostic("UNKNOWN_ATTR", WARNING,
251
+ "未知属性 %s" % name, line=line,
252
+ suggest=("是不是想写 %s?" % guess) if guess else "")
253
+
254
+
255
+ # ------------------------------------------------------------------ 校验
256
+
257
+ def validate(program: Program) -> List[Diagnostic]:
258
+ diags: List[Diagnostic] = []
259
+ used_templates = set()
260
+
261
+ for node in program.nodes.values():
262
+ if node.id == ROOT:
263
+ continue
264
+ if node.type not in vocab.NODE_TYPES:
265
+ guess = vocab.suggest_from(node.type, vocab.NODE_TYPES)
266
+ diags.append(Diagnostic("UNKNOWN_TYPE", WARNING,
267
+ "未知节点类型 %s" % node.type, line=node.line,
268
+ suggest=("是不是想写 %s?" % guess) if guess else ""))
269
+ for name, expr in node.attrs.items():
270
+ if name not in vocab.ALL_ATTRS:
271
+ diags.append(_unknown_attr(name, node.line))
272
+ continue
273
+ only = vocab.TYPE_ONLY_ATTRS.get(name)
274
+ if only and node.type not in only:
275
+ diags.append(Diagnostic("ATTR_ON_TYPE", WARNING,
276
+ "属性 %s 不适用于 %s" % (name, node.type),
277
+ line=node.line))
278
+ if name in vocab.REF_ATTRS:
279
+ if not isinstance(expr, (Ref, Lit)):
280
+ diags.append(Diagnostic("REF_ATTR_EXPR", ERROR,
281
+ "引用类属性 %s 不能写表达式" % name,
282
+ line=node.line))
283
+ elif isinstance(expr, Ref) and expr.field is None:
284
+ used_templates.add(expr.addr)
285
+ if name == "icon" and isinstance(expr, Lit) and isinstance(expr.value, str):
286
+ if expr.value not in vocab.CORE_ICONS:
287
+ guess = vocab.suggest_from(expr.value, vocab.CORE_ICONS)
288
+ diags.append(Diagnostic("UNKNOWN_ICON", WARNING,
289
+ "未知图标 %s" % expr.value, line=node.line,
290
+ suggest=("是不是想写 %s?" % guess) if guess else ""))
291
+ if name == "states":
292
+ diags += _check_states(expr, node)
293
+ if name == "animate":
294
+ diags += _check_animate(expr, node)
295
+
296
+ for node in program.nodes.values():
297
+ if node.type == "template" and node.id not in used_templates:
298
+ diags.append(Diagnostic("TEMPLATE_UNUSED", WARNING,
299
+ "模板 %s 未被任何列表使用" % _hash(node.id), line=node.line))
300
+
301
+ diags += _check_handlers(program)
302
+ diags += _check_interaction_coverage(program)
303
+ diags += _check_controlled_writeback(program)
304
+ diags += _check_bindings(program)
305
+ return diags
306
+
307
+
308
+ MUTATING_ACTIONS = ("append", "remove", "remove_where", "update_where", "clear", "sort")
309
+
310
+
311
+ def _referenced_pairs(program: Program) -> set:
312
+ """全程序里被引用过的 (地址, 属性) 对。"""
313
+ pairs: set = set()
314
+
315
+ def take(expr) -> None:
316
+ if expr is None:
317
+ return
318
+ for ref in collect_refs(expr):
319
+ pairs.add((ref.addr, ref.field))
320
+
321
+ for node in program.nodes.values():
322
+ for expr in node.attrs.values():
323
+ take(expr)
324
+ for handler in program.handlers:
325
+ take(handler.when)
326
+ for action in handler.actions:
327
+ take(action.item)
328
+ take(action.where)
329
+ for expr in action.attrs.values():
330
+ take(expr)
331
+ for expr in action.params.values():
332
+ take(expr)
333
+ for call in program.calls:
334
+ for expr in call.params.values():
335
+ take(expr)
336
+ return pairs
337
+
338
+
339
+ def _template_data_sources(program: Program, template: Node) -> set:
340
+ """模板的行数据来自哪个数据源(由使用它的列表决定)。"""
341
+ out = set()
342
+ for node in program.nodes.values():
343
+ ref = node.attrs.get("template")
344
+ src = node.attrs.get("source")
345
+ if isinstance(ref, Ref) and ref.addr == template.id \
346
+ and isinstance(src, Ref) and src.addr in program.data:
347
+ out.add(src.addr)
348
+ return out
349
+
350
+
351
+ def _check_controlled_writeback(program: Program) -> List[Diagnostic]:
352
+ """受控控件的值绑定了数据源,但它的 change 处理器没有回写那个数据源 → 警告。
353
+
354
+ 绑定值是**派生值**:不回写,用户改动就会被重算覆盖(界面弹回)。
355
+ 三条件同时成立才报,避免噪音:
356
+ ① 值类属性是绑定、且能追溯到某个数据源;
357
+ ② 该控件有 change 处理器;
358
+ ③ 这些处理器一个都没写该数据源,且没有 call(能力可能写,无法静态判定)。
359
+ """
360
+ diags: List[Diagnostic] = []
361
+ for node in program.nodes.values():
362
+ for field in ("value", "selected"):
363
+ expr = node.attrs.get(field)
364
+ if expr is None or is_static(expr):
365
+ continue
366
+ sources = {r.addr for r in collect_refs(expr) if r.addr in program.data}
367
+ template = _owning_template(program, node.id)
368
+ if template is not None:
369
+ sources |= _template_data_sources(program, template)
370
+ if not sources:
371
+ continue
372
+ handlers = [h for h in program.handlers
373
+ if h.target == node.id and h.event == "change"]
374
+ if not handlers:
375
+ continue
376
+ written, uncertain = set(), False
377
+ for handler in handlers:
378
+ for action in handler.actions:
379
+ if action.kind == "call":
380
+ uncertain = True
381
+ elif action.kind in MUTATING_ACTIONS:
382
+ written.add(action.target)
383
+ if uncertain or (sources & written):
384
+ continue
385
+ diags.append(Diagnostic(
386
+ "BOUND_VALUE_NOT_WRITTEN", WARNING,
387
+ "#%s 的 %s 绑定了 %s,但它的 change 处理器没有写回该数据源:"
388
+ "用户的改动会被绑定重算覆盖(界面弹回)"
389
+ % (node.id, field, "、".join("#" + s for s in sorted(sources))),
390
+ line=node.line))
391
+ return diags
392
+
393
+
394
+ def _owning_template(program: Program, node_id: str):
395
+ for aid in [node_id] + _ancestors(program, node_id):
396
+ node = program.nodes.get(aid)
397
+ if node is not None and node.type == "template":
398
+ return node
399
+ return None
400
+
401
+
402
+ def _check_interaction_coverage(program: Program) -> List[Diagnostic]:
403
+ """主交互事件没有任何处理器 → 警告。
404
+
405
+ 用户的动作不会有任何反应,而"没反应"必须被说出来,不能靠猜。
406
+ """
407
+ diags: List[Diagnostic] = []
408
+ covered = {(h.target, h.event) for h in program.handlers}
409
+ watched = {(l.target, l.event) for l in program.listens}
410
+ read_pairs = _referenced_pairs(program)
411
+ for node in program.nodes.values():
412
+ event = vocab.PRIMARY_EVENT.get(node.type)
413
+ if event is None:
414
+ continue
415
+ # 被订阅也算"有人管":外部驱动者(LLM / 调度者)会响应,不属静默失败
416
+ if (node.id, event) in covered or (node.id, event) in watched:
417
+ continue
418
+ # 值类控件的值若在别处被读到,交互是有去处的
419
+ # ("提交时才读控件的值"是极常见且完全正当的写法)
420
+ if event == "change" and ((node.id, "value") in read_pairs
421
+ or (node.id, "selected") in read_pairs):
422
+ continue
423
+ diags.append(Diagnostic(
424
+ "UNCOVERED_INTERACTION", WARNING,
425
+ "%s #%s 的 %s 既没有处理器也没有被订阅:用户操作它不会有任何反应"
426
+ % (node.type, node.id, event), line=node.line))
427
+ return diags
428
+
429
+
430
+ def _check_states(expr: Expr, node: Node) -> List[Diagnostic]:
431
+ diags: List[Diagnostic] = []
432
+ if not isinstance(expr, DictLit):
433
+ return diags
434
+ for state, value in expr.fields.items():
435
+ if not isinstance(value, DictLit):
436
+ continue
437
+ for key in value.fields:
438
+ if key in vocab.STATE_APPEARANCE_LAYOUT:
439
+ diags.append(Diagnostic("STATE_LAYOUT_ATTR", WARNING,
440
+ "状态 %s 中不允许布局属性 %s" % (state, key),
441
+ line=node.line))
442
+ elif key not in vocab.STATE_APPEARANCE:
443
+ diags.append(Diagnostic("STATE_UNKNOWN_ATTR", WARNING,
444
+ "状态 %s 中的 %s 不是外观属性" % (state, key),
445
+ line=node.line))
446
+ return diags
447
+
448
+
449
+ def _check_animate(expr: Expr, node: Node) -> List[Diagnostic]:
450
+ names: List[str] = []
451
+ if isinstance(expr, Lit) and isinstance(expr.value, str):
452
+ names = [expr.value]
453
+ elif isinstance(expr, ListLit):
454
+ names = [i.value for i in expr.items if isinstance(i, Lit) and isinstance(i.value, str)]
455
+ return [Diagnostic("ANIMATE_UNKNOWN", WARNING,
456
+ "属性 %s 不可动画" % name, line=node.line)
457
+ for name in names if name not in vocab.ANIMATABLE]
458
+
459
+
460
+ def _check_handlers(program: Program) -> List[Diagnostic]:
461
+ diags: List[Diagnostic] = []
462
+ for h in program.handlers:
463
+ if h.event not in vocab.EVENTS:
464
+ diags.append(Diagnostic("UNKNOWN_EVENT", WARNING,
465
+ "未知事件 %s" % h.event, line=h.line))
466
+ node = program.nodes.get(h.target)
467
+ if node is not None and h.event in vocab.INTERACTION_EVENTS:
468
+ disabled = node.attrs.get("disabled")
469
+ if isinstance(disabled, Lit) and disabled.value is True:
470
+ diags.append(Diagnostic("DISABLED_HANDLER", WARNING,
471
+ "静态禁用的节点 #%s 绑定了 %s" % (h.target, h.event),
472
+ line=h.line))
473
+ if h.when is not None and isinstance(h.when, Lit) and not isinstance(h.when.value, bool):
474
+ diags.append(Diagnostic("WHEN_NOT_BOOL", ERROR,
475
+ "when 守卫必须是布尔", line=h.line))
476
+ for listen in program.listens:
477
+ if listen.event not in vocab.EVENTS:
478
+ diags.append(Diagnostic("UNKNOWN_EVENT", WARNING,
479
+ "未知事件 %s" % listen.event, line=listen.line))
480
+ return diags
481
+
482
+
483
+ def _check_bindings(program: Program) -> List[Diagnostic]:
484
+ diags: List[Diagnostic] = []
485
+ graph: Dict[tuple, set] = {}
486
+
487
+ def known(addr: str) -> bool:
488
+ return addr in program.nodes or addr in program.data
489
+
490
+ for node in program.nodes.values():
491
+ for name, expr in node.attrs.items():
492
+ if is_static(expr):
493
+ continue
494
+ deps = set()
495
+ for ref in collect_refs(expr):
496
+ if not known(ref.addr):
497
+ diags.append(Diagnostic("BIND_UNKNOWN_REF", ERROR,
498
+ "引用 %s 不存在" % _hash(ref.addr),
499
+ line=node.line))
500
+ continue
501
+ if ref.field is not None and ref.addr in program.nodes:
502
+ target = program.nodes[ref.addr]
503
+ if ref.field not in target.attrs and ref.field not in vocab.STATE_FLAGS:
504
+ diags.append(Diagnostic("BIND_UNKNOWN_REF", ERROR,
505
+ "节点 %s 没有属性 %s" % (_hash(ref.addr), ref.field),
506
+ line=node.line))
507
+ continue
508
+ deps.add((ref.addr, ref.field))
509
+ graph[(node.id, name)] = deps
510
+
511
+ cycles = _find_cycle(graph)
512
+ for key in sorted(cycles):
513
+ diags.append(Diagnostic("BIND_CYCLE", ERROR,
514
+ "绑定成环:%s.%s" % (_hash(key[0]), key[1])))
515
+ return diags
516
+
517
+
518
+ def _find_cycle(graph: Dict[tuple, set]) -> set:
519
+ """返回参与环的键集合。"""
520
+ color: Dict[tuple, int] = {}
521
+ bad: set = set()
522
+
523
+ def visit(node, stack):
524
+ color[node] = 1
525
+ for dep in graph.get(node, ()):
526
+ if dep not in graph:
527
+ continue
528
+ state = color.get(dep, 0)
529
+ if state == 1:
530
+ bad.update(stack[stack.index(dep):] if dep in stack else [dep, node])
531
+ elif state == 0:
532
+ visit(dep, stack + [dep])
533
+ color[node] = 2
534
+
535
+ for node in list(graph):
536
+ if color.get(node, 0) == 0:
537
+ visit(node, [node])
538
+ return bad