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/lang.py ADDED
@@ -0,0 +1,755 @@
1
+ """词法与语法:把源文本行解析为语句与表达式(`spec/01-grammar.md`)。
2
+
3
+ 行是语句的最小单位;裸换行不续行。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ from .diag import Diagnostic, ERROR
12
+
13
+ # ------------------------------------------------------------------ 表达式
14
+
15
+ class Expr:
16
+ pass
17
+
18
+
19
+ @dataclass
20
+ class Lit(Expr):
21
+ value: Any
22
+ raw: str = ""
23
+
24
+
25
+ @dataclass
26
+ class Ref(Expr):
27
+ addr: str # 不含 '#'
28
+ field: Optional[str] = None
29
+
30
+
31
+ @dataclass
32
+ class Local(Expr):
33
+ name: str
34
+ field: Optional[str] = None
35
+
36
+
37
+ @dataclass
38
+ class Bin(Expr):
39
+ op: str
40
+ left: Expr
41
+ right: Expr
42
+
43
+
44
+ @dataclass
45
+ class Un(Expr):
46
+ op: str
47
+ operand: Expr
48
+
49
+
50
+ @dataclass
51
+ class Call(Expr):
52
+ name: str
53
+ args: List[Expr] = field(default_factory=list)
54
+
55
+
56
+ @dataclass
57
+ class ListLit(Expr):
58
+ items: List[Expr] = field(default_factory=list)
59
+
60
+
61
+ @dataclass
62
+ class DictLit(Expr):
63
+ fields: Dict[str, Expr] = field(default_factory=dict)
64
+
65
+
66
+ def is_static(expr: Expr) -> bool:
67
+ """纯字面量(含由字面量构成的列表/字典)——不构成绑定。"""
68
+ if isinstance(expr, Lit):
69
+ return True
70
+ if isinstance(expr, ListLit):
71
+ return all(is_static(i) for i in expr.items)
72
+ if isinstance(expr, DictLit):
73
+ return all(is_static(v) for v in expr.fields.values())
74
+ return False
75
+
76
+
77
+ def collect_refs(expr: Expr, out=None) -> List[Ref]:
78
+ out = [] if out is None else out
79
+ if isinstance(expr, Ref):
80
+ out.append(expr)
81
+ elif isinstance(expr, (Bin,)):
82
+ collect_refs(expr.left, out)
83
+ collect_refs(expr.right, out)
84
+ elif isinstance(expr, Un):
85
+ collect_refs(expr.operand, out)
86
+ elif isinstance(expr, Call):
87
+ for a in expr.args:
88
+ collect_refs(a, out)
89
+ elif isinstance(expr, ListLit):
90
+ for a in expr.items:
91
+ collect_refs(a, out)
92
+ elif isinstance(expr, DictLit):
93
+ for a in expr.fields.values():
94
+ collect_refs(a, out)
95
+ return out
96
+
97
+
98
+ def expr_source(expr: Expr) -> str:
99
+ """把表达式还原为规范源文本(用于状态/程序写回与诊断)。"""
100
+ if isinstance(expr, Lit):
101
+ if isinstance(expr.value, bool):
102
+ return "true" if expr.value else "false"
103
+ if isinstance(expr.value, str):
104
+ return '"%s"' % expr.value.replace('"', '\\"')
105
+ return str(expr.value)
106
+ if isinstance(expr, Ref):
107
+ return "#" + expr.addr + ("." + expr.field if expr.field else "")
108
+ if isinstance(expr, Local):
109
+ return expr.name + ("." + expr.field if expr.field else "")
110
+ if isinstance(expr, Bin):
111
+ return "%s %s %s" % (expr_source(expr.left), expr.op, expr_source(expr.right))
112
+ if isinstance(expr, Un):
113
+ return "%s %s" % (expr.op, expr_source(expr.operand))
114
+ if isinstance(expr, Call):
115
+ return "%s(%s)" % (expr.name, ", ".join(expr_source(a) for a in expr.args))
116
+ if isinstance(expr, ListLit):
117
+ return "[" + ", ".join(expr_source(i) for i in expr.items) + "]"
118
+ if isinstance(expr, DictLit):
119
+ return "{" + ", ".join("%s: %s" % (k, expr_source(v)) for k, v in expr.fields.items()) + "}"
120
+ return ""
121
+
122
+
123
+ # ------------------------------------------------------------------ 语句
124
+
125
+ @dataclass
126
+ class Stmt:
127
+ line: int = 0
128
+
129
+
130
+ @dataclass
131
+ class Add(Stmt):
132
+ parent: str = ""
133
+ type: str = ""
134
+ self_id: str = ""
135
+ attrs: Dict[str, Expr] = field(default_factory=dict)
136
+ as_name: str = ""
137
+ anchor: Optional[tuple] = None # ("before"|"after", addr)
138
+ is_upsert: bool = False
139
+
140
+
141
+ @dataclass
142
+ class SetStmt(Stmt):
143
+ target: str = ""
144
+ attrs: Dict[str, Expr] = field(default_factory=dict)
145
+
146
+
147
+ @dataclass
148
+ class Del(Stmt):
149
+ target: str = ""
150
+
151
+
152
+ @dataclass
153
+ class Move(Stmt):
154
+ target: str = ""
155
+ new_parent: str = ""
156
+ anchor: Optional[tuple] = None
157
+
158
+
159
+ @dataclass
160
+ class Data(Stmt):
161
+ source: str = ""
162
+ persist: bool = False
163
+ initial: Optional[Expr] = None
164
+ schema: Dict[str, tuple] = field(default_factory=dict) # 字段 -> (类型, 默认表达式|None)
165
+
166
+
167
+ @dataclass
168
+ class Action:
169
+ kind: str = ""
170
+ target: str = ""
171
+ attrs: Dict[str, Expr] = field(default_factory=dict)
172
+ item: Optional[Expr] = None
173
+ bind: str = ""
174
+ where: Optional[Expr] = None
175
+ by: str = ""
176
+ func: str = ""
177
+ params: Dict[str, Expr] = field(default_factory=dict)
178
+ into: str = ""
179
+
180
+
181
+ @dataclass
182
+ class On(Stmt):
183
+ target: str = ""
184
+ event: str = ""
185
+ bind: str = ""
186
+ when: Optional[Expr] = None
187
+ actions: List[Action] = field(default_factory=list)
188
+
189
+
190
+ @dataclass
191
+ class Listen(Stmt):
192
+ target: str = ""
193
+ event: str = ""
194
+
195
+
196
+ @dataclass
197
+ class CallStmt(Stmt):
198
+ func: str = ""
199
+ params: Dict[str, Expr] = field(default_factory=dict)
200
+ into: str = ""
201
+
202
+
203
+ @dataclass
204
+ class Probe(Stmt):
205
+ verb: str = ""
206
+ target: str = ""
207
+
208
+
209
+ @dataclass
210
+ class ActionStmt(Stmt):
211
+ """顶层动作:集合原语也可以直接作为语句驱动数据。"""
212
+
213
+ action: Optional[Action] = None
214
+
215
+
216
+ # ------------------------------------------------------------------ 词法
217
+
218
+ _PUNCT = set("=,:;{}[]().")
219
+ _OPS2 = ("==", "!=", "<=", ">=")
220
+ _OPS1 = ("+", "-", "*", "/", "<", ">")
221
+ _HEX = set("0123456789abcdefABCDEF")
222
+ _ID0 = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_")
223
+ _IDN = _ID0 | set("0123456789")
224
+
225
+
226
+ @dataclass
227
+ class Tok:
228
+ kind: str
229
+ text: str
230
+ value: Any = None
231
+ pos: int = 0
232
+
233
+
234
+ class ParseError(Exception):
235
+ def __init__(self, message, pos=0):
236
+ super().__init__(message)
237
+ self.message = message
238
+ self.pos = pos
239
+
240
+
241
+ def tokenize(line: str) -> List[Tok]:
242
+ toks: List[Tok] = []
243
+ i, n = 0, len(line)
244
+ while i < n:
245
+ ch = line[i]
246
+ if ch.isspace():
247
+ i += 1
248
+ continue
249
+ if ch == "/" and line[i:i + 2] == "//":
250
+ break # 注释到行尾(规范 01 第 2 节);`//` 无其他含义,故与地址无歧义
251
+ if ch == "#":
252
+ j = i + 1
253
+ while j < n and line[j] in _IDN:
254
+ j += 1
255
+ word = line[i + 1:j]
256
+ if not word:
257
+ raise ParseError("井号后需要地址", i)
258
+ # 只认 6/8 位:3 位会与 `#bad` / `#dad` / `#abc` 这类短地址撞车
259
+ if all(c in _HEX for c in word) and len(word) in (6, 8):
260
+ toks.append(Tok("COLOR", "#" + word, "#" + word, i))
261
+ else:
262
+ toks.append(Tok("ADDR", word, word, i))
263
+ i = j
264
+ continue
265
+ if ch == '"':
266
+ j, buf = i + 1, []
267
+ while True:
268
+ if j >= n:
269
+ raise ParseError("字符串未闭合", i)
270
+ if line[j] == "\\" and j + 1 < n:
271
+ buf.append(line[j + 1])
272
+ j += 2
273
+ continue
274
+ if line[j] == '"':
275
+ break
276
+ buf.append(line[j])
277
+ j += 1
278
+ toks.append(Tok("STRING", line[i:j + 1], "".join(buf), i))
279
+ i = j + 1
280
+ continue
281
+ if ch.isdigit():
282
+ j = i
283
+ while j < n and line[j].isdigit():
284
+ j += 1
285
+ isf = False
286
+ if j < n and line[j] == "." and j + 1 < n and line[j + 1].isdigit():
287
+ isf = True
288
+ j += 1
289
+ while j < n and line[j].isdigit():
290
+ j += 1
291
+ text = line[i:j]
292
+ toks.append(Tok("NUMBER", text, float(text) if isf else int(text), i))
293
+ i = j
294
+ continue
295
+ if ch in _ID0:
296
+ j = i
297
+ while j < n and line[j] in _IDN:
298
+ j += 1
299
+ toks.append(Tok("IDENT", line[i:j], line[i:j], i))
300
+ i = j
301
+ continue
302
+ two = line[i:i + 2]
303
+ if two in _OPS2:
304
+ toks.append(Tok("OP", two, two, i))
305
+ i += 2
306
+ continue
307
+ if ch in _OPS1:
308
+ toks.append(Tok("OP", ch, ch, i))
309
+ i += 1
310
+ continue
311
+ if ch in _PUNCT:
312
+ toks.append(Tok("PUNCT", ch, ch, i))
313
+ i += 1
314
+ continue
315
+ raise ParseError("无法识别的字符 %r" % ch, i)
316
+ toks.append(Tok("END", "", None, n))
317
+ return toks
318
+
319
+
320
+ # ------------------------------------------------------------------ 语法
321
+
322
+ class Parser:
323
+ def __init__(self, toks: List[Tok], line: int):
324
+ self.toks = toks
325
+ self.i = 0
326
+ self.line = line
327
+
328
+ # -- 基础
329
+ def peek(self, k: int = 0) -> Tok:
330
+ j = min(self.i + k, len(self.toks) - 1)
331
+ return self.toks[j]
332
+
333
+ def next(self) -> Tok:
334
+ tok = self.peek()
335
+ if tok.kind != "END":
336
+ self.i += 1
337
+ return tok
338
+
339
+ def at(self, kind: str, text: Optional[str] = None) -> bool:
340
+ tok = self.peek()
341
+ return tok.kind == kind and (text is None or tok.text == text)
342
+
343
+ def accept(self, kind: str, text: Optional[str] = None) -> Optional[Tok]:
344
+ if self.at(kind, text):
345
+ return self.next()
346
+ return None
347
+
348
+ def expect(self, kind: str, text: Optional[str] = None) -> Tok:
349
+ if not self.at(kind, text):
350
+ tok = self.peek()
351
+ raise ParseError("期望 %s%s,实际 %r" % (kind, "/" + text if text else "", tok.text), tok.pos)
352
+ return self.next()
353
+
354
+ def expect_addr(self) -> str:
355
+ return self.expect("ADDR").text
356
+
357
+ def done(self) -> bool:
358
+ return self.peek().kind == "END"
359
+
360
+ # -- 表达式
361
+ def expr(self) -> Expr:
362
+ return self.or_expr()
363
+
364
+ def or_expr(self) -> Expr:
365
+ node = self.and_expr()
366
+ while self.at("IDENT", "or"):
367
+ self.next()
368
+ node = Bin("or", node, self.and_expr())
369
+ return node
370
+
371
+ def and_expr(self) -> Expr:
372
+ node = self.not_expr()
373
+ while self.at("IDENT", "and"):
374
+ self.next()
375
+ node = Bin("and", node, self.not_expr())
376
+ return node
377
+
378
+ def not_expr(self) -> Expr:
379
+ if self.at("IDENT", "not"):
380
+ self.next()
381
+ return Un("not", self.not_expr())
382
+ return self.cmp_expr()
383
+
384
+ def cmp_expr(self) -> Expr:
385
+ node = self.add_expr()
386
+ while self.at("OP") and self.peek().text in ("==", "!=", "<", "<=", ">", ">="):
387
+ op = self.next().text
388
+ node = Bin(op, node, self.add_expr())
389
+ return node
390
+
391
+ def add_expr(self) -> Expr:
392
+ node = self.mul_expr()
393
+ while self.at("OP") and self.peek().text in ("+", "-"):
394
+ op = self.next().text
395
+ node = Bin(op, node, self.mul_expr())
396
+ return node
397
+
398
+ def mul_expr(self) -> Expr:
399
+ node = self.unary()
400
+ while self.at("OP") and self.peek().text in ("*", "/"):
401
+ op = self.next().text
402
+ node = Bin(op, node, self.unary())
403
+ return node
404
+
405
+ def unary(self) -> Expr:
406
+ if self.at("OP", "-"):
407
+ self.next()
408
+ return Un("-", self.unary())
409
+ return self.primary()
410
+
411
+ def primary(self) -> Expr:
412
+ tok = self.peek()
413
+ if tok.kind == "NUMBER":
414
+ self.next()
415
+ return Lit(tok.value, tok.text)
416
+ if tok.kind == "STRING":
417
+ self.next()
418
+ return Lit(tok.value, tok.text)
419
+ if tok.kind == "COLOR":
420
+ self.next()
421
+ return Lit(tok.value, tok.text)
422
+ if tok.kind == "ADDR":
423
+ self.next()
424
+ field = None
425
+ if self.accept("PUNCT", "."):
426
+ field = self.expect("IDENT").text
427
+ return Ref(tok.text, field)
428
+ if tok.kind == "PUNCT" and tok.text == "(":
429
+ self.next()
430
+ inner = self.expr()
431
+ self.expect("PUNCT", ")")
432
+ return inner
433
+ if tok.kind == "PUNCT" and tok.text == "[":
434
+ self.next()
435
+ items: List[Expr] = []
436
+ if not self.at("PUNCT", "]"):
437
+ items.append(self.expr())
438
+ while self.accept("PUNCT", ","):
439
+ items.append(self.expr())
440
+ self.expect("PUNCT", "]")
441
+ return ListLit(items)
442
+ if tok.kind == "PUNCT" and tok.text == "{":
443
+ return self.dict_lit()
444
+ if tok.kind == "IDENT":
445
+ if tok.text == "true":
446
+ self.next()
447
+ return Lit(True, "true")
448
+ if tok.text == "false":
449
+ self.next()
450
+ return Lit(False, "false")
451
+ if self.peek(1).kind == "PUNCT" and self.peek(1).text == "(":
452
+ self.next()
453
+ self.next()
454
+ args: List[Expr] = []
455
+ if not self.at("PUNCT", ")"):
456
+ args.append(self.expr())
457
+ while self.accept("PUNCT", ","):
458
+ args.append(self.expr())
459
+ self.expect("PUNCT", ")")
460
+ return Call(tok.text, args)
461
+ if self.peek(1).kind == "PUNCT" and self.peek(1).text == ".":
462
+ self.next()
463
+ self.next()
464
+ return Local(tok.text, self.expect("IDENT").text)
465
+ self.next()
466
+ return Lit(tok.text, tok.text)
467
+ raise ParseError("无法解析的表达式起点 %r" % tok.text, tok.pos)
468
+
469
+ def dict_lit(self) -> DictLit:
470
+ self.expect("PUNCT", "{")
471
+ fields: Dict[str, Expr] = {}
472
+ if not self.at("PUNCT", "}"):
473
+ while True:
474
+ key = self.next()
475
+ if key.kind not in ("IDENT", "STRING"):
476
+ raise ParseError("字典键必须是名字或字符串", key.pos)
477
+ self.expect("PUNCT", ":")
478
+ fields[key.value if key.kind == "STRING" else key.text] = self.expr()
479
+ if not self.accept("PUNCT", ","):
480
+ break
481
+ self.expect("PUNCT", "}")
482
+ return DictLit(fields)
483
+
484
+ # -- 属性
485
+ def attr_list(self, stop_words=()) -> Dict[str, Expr]:
486
+ attrs: Dict[str, Expr] = {}
487
+ while self.at("IDENT") and self.peek().text not in stop_words:
488
+ if not (self.peek(1).kind == "PUNCT" and self.peek(1).text == "="):
489
+ break
490
+ key = self.next().text
491
+ self.next() # '='
492
+ attrs[key] = self.expr()
493
+ return attrs
494
+
495
+ # -- 语句
496
+ def statement(self) -> Optional[Stmt]:
497
+ if self.done():
498
+ return None
499
+ head = self.expect("IDENT")
500
+ verb = head.text
501
+ if verb == "add":
502
+ return self.add_stmt(is_upsert=False)
503
+ if verb == "upsert":
504
+ return self.add_stmt(is_upsert=True)
505
+ if verb == "set":
506
+ st = SetStmt(line=self.line)
507
+ st.target = self.expect_addr()
508
+ st.attrs = self.attr_list()
509
+ return st
510
+ if verb == "del":
511
+ st = Del(line=self.line)
512
+ st.target = self.expect_addr()
513
+ return st
514
+ if verb == "move":
515
+ st = Move(line=self.line)
516
+ st.target = self.expect_addr()
517
+ st.new_parent = self.expect_addr()
518
+ st.anchor = self.anchor()
519
+ return st
520
+ if verb == "data":
521
+ return self.data_stmt()
522
+ if verb == "on":
523
+ return self.on_stmt()
524
+ if verb == "listen":
525
+ st = Listen(line=self.line)
526
+ st.target = self.expect_addr()
527
+ st.event = self.expect("IDENT").text
528
+ return st
529
+ if verb == "call":
530
+ return self.call_stmt()
531
+ if verb in ("append", "remove", "remove_where", "update_where", "clear", "sort"):
532
+ st = ActionStmt(line=self.line)
533
+ self.i -= 1 # 把动词交还给动作解析器
534
+ st.action = self.action()
535
+ return st
536
+ if verb in ("tree", "get", "where"):
537
+ st = Probe(line=self.line, verb=verb)
538
+ if self.at("ADDR"):
539
+ st.target = self.next().text
540
+ return st
541
+ raise ParseError("未知动词 %r" % verb, head.pos)
542
+
543
+ def anchor(self) -> Optional[tuple]:
544
+ if self.at("IDENT", "before") or self.at("IDENT", "after"):
545
+ word = self.next().text
546
+ return (word, self.expect_addr())
547
+ return None
548
+
549
+ def add_stmt(self, is_upsert: bool) -> Add:
550
+ st = Add(line=self.line, is_upsert=is_upsert)
551
+ st.parent = self.expect_addr()
552
+ st.type = self.expect("IDENT").text
553
+ st.self_id = self.expect_addr()
554
+ if self.at("IDENT", "as"):
555
+ self.next()
556
+ st.as_name = self.expect("IDENT").text
557
+ st.attrs = self.attr_list(stop_words=("before", "after", "as"))
558
+ st.anchor = self.anchor()
559
+ if self.at("IDENT", "as"):
560
+ self.next()
561
+ st.as_name = self.expect("IDENT").text
562
+ return st
563
+
564
+ def data_stmt(self) -> Data:
565
+ st = Data(line=self.line)
566
+ st.source = self.expect_addr()
567
+ if self.at("IDENT", "persist"):
568
+ self.next()
569
+ self.expect("PUNCT", "=")
570
+ tok = self.expect("IDENT")
571
+ st.persist = tok.text == "true"
572
+ self.expect("PUNCT", "=")
573
+ st.initial = self.expr()
574
+ if self.at("IDENT", "of"):
575
+ self.next()
576
+ st.schema = self.schema()
577
+ return st
578
+
579
+ def schema(self) -> Dict[str, tuple]:
580
+ self.expect("PUNCT", "{")
581
+ out: Dict[str, tuple] = {}
582
+ if not self.at("PUNCT", "}"):
583
+ while True:
584
+ name = self.expect("IDENT").text
585
+ self.expect("PUNCT", ":")
586
+ typ = self.expect("IDENT").text
587
+ default = None
588
+ if self.accept("PUNCT", "="):
589
+ default = self.expr()
590
+ out[name] = (typ, default)
591
+ if not self.accept("PUNCT", ","):
592
+ break
593
+ self.expect("PUNCT", "}")
594
+ return out
595
+
596
+ def on_stmt(self) -> On:
597
+ st = On(line=self.line)
598
+ st.target = self.expect_addr()
599
+ st.event = self.expect("IDENT").text
600
+ if self.at("IDENT", "as"):
601
+ self.next()
602
+ st.bind = self.expect("IDENT").text
603
+ if self.at("IDENT", "when"):
604
+ self.next()
605
+ st.when = self.expr()
606
+ self.expect("PUNCT", ":")
607
+ st.actions = self.action_list()
608
+ return st
609
+
610
+ def action_list(self) -> List[Action]:
611
+ groups: List[List[Tok]] = [[]]
612
+ depth = 0
613
+ while not self.done():
614
+ tok = self.next()
615
+ if tok.kind == "PUNCT" and tok.text in "([{":
616
+ depth += 1
617
+ elif tok.kind == "PUNCT" and tok.text in ")]}":
618
+ depth -= 1
619
+ if tok.kind == "PUNCT" and tok.text == ";" and depth == 0:
620
+ groups.append([])
621
+ continue
622
+ groups[-1].append(tok)
623
+ actions = []
624
+ for group in groups:
625
+ if not group:
626
+ continue
627
+ sub = Parser(group + [Tok("END", "", None, 0)], self.line)
628
+ actions.append(sub.action())
629
+ return actions
630
+
631
+ def action(self) -> Action:
632
+ head = self.expect("IDENT")
633
+ kind = head.text
634
+ act = Action(kind=kind)
635
+ if kind == "set":
636
+ act.target = self.expect_addr()
637
+ act.attrs = self.attr_list()
638
+ elif kind == "append":
639
+ act.target = self.expect_addr()
640
+ act.item = self.named_expr("item")
641
+ elif kind == "remove":
642
+ act.target = self.expect_addr()
643
+ act.item = self.named_expr("item")
644
+ elif kind == "remove_where":
645
+ act.target = self.expect_addr()
646
+ self.expect("IDENT", "as")
647
+ act.bind = self.expect("IDENT").text
648
+ self.expect("IDENT", "where")
649
+ act.where = self.expr()
650
+ elif kind == "update_where":
651
+ act.target = self.expect_addr()
652
+ self.expect("IDENT", "as")
653
+ act.bind = self.expect("IDENT").text
654
+ self.expect("IDENT", "set")
655
+ act.attrs = self.attr_list(stop_words=("where",))
656
+ self.expect("IDENT", "where")
657
+ act.where = self.expr()
658
+ elif kind == "clear":
659
+ act.target = self.expect_addr()
660
+ elif kind == "sort":
661
+ act.target = self.expect_addr()
662
+ self.expect("IDENT", "by")
663
+ act.by = self.expect("IDENT").text
664
+ elif kind == "call":
665
+ act.func = self.next().text
666
+ self.expect("IDENT", "with")
667
+ act.params = self.dict_lit().fields
668
+ self.expect("IDENT", "into")
669
+ act.into = self.expect_addr()
670
+ else:
671
+ raise ParseError("未知动作 %r" % kind, head.pos)
672
+ return act
673
+
674
+ def named_expr(self, name: str) -> Expr:
675
+ self.expect("IDENT", name)
676
+ self.expect("PUNCT", "=")
677
+ return self.expr()
678
+
679
+ def call_stmt(self) -> CallStmt:
680
+ st = CallStmt(line=self.line)
681
+ st.func = self.next().text
682
+ self.expect("IDENT", "with")
683
+ st.params = self.dict_lit().fields
684
+ self.expect("IDENT", "into")
685
+ st.into = self.expect_addr()
686
+ return st
687
+
688
+
689
+ def _is_comment(line: str) -> bool:
690
+ return line.startswith("//")
691
+
692
+
693
+ def _indent(line: str) -> int:
694
+ return len(line) - len(line.lstrip(" \t"))
695
+
696
+
697
+ def parse_line(text: str, line_no: int = 0) -> Optional[Stmt]:
698
+ """解析一行;注释与空行返回 None。"""
699
+ stripped = text.strip()
700
+ if not stripped or _is_comment(stripped):
701
+ return None
702
+ parser = Parser(tokenize(stripped), line_no)
703
+ stmt = parser.statement()
704
+ if not parser.done():
705
+ tok = parser.peek()
706
+ raise ParseError("语句后有多余记号 %r" % tok.text, tok.pos)
707
+ if stmt is not None:
708
+ stmt.line = line_no
709
+ return stmt
710
+
711
+
712
+ def parse_program(lines: List[str]):
713
+ """返回 (语句列表, 诊断列表)。语法错误不阻断后续行。
714
+
715
+ 源文本形状(规范 01 第 1 节):一条语句默认写在一行内;
716
+ **唯一例外是行为头**——以 `on … :` 结尾的行,其动作体可以写在
717
+ 紧随其后、缩进更深的多行上;遇到第一行缩进不深于该行为头即结束。
718
+ 空行与注释行被忽略,不参与缩进判定。
719
+ """
720
+ stmts: List[Stmt] = []
721
+ diags: List[Diagnostic] = []
722
+ total = len(lines)
723
+ index = 0
724
+ while index < total:
725
+ raw = lines[index]
726
+ line_no = index + 1
727
+ index += 1
728
+ stripped = raw.strip()
729
+ if not stripped or _is_comment(stripped):
730
+ continue
731
+
732
+ body: List[str] = []
733
+ if stripped.startswith("on ") and stripped.endswith(":"):
734
+ base = _indent(raw)
735
+ while index < total:
736
+ nxt = lines[index]
737
+ s = nxt.strip()
738
+ if not s or _is_comment(s):
739
+ index += 1
740
+ continue
741
+ if _indent(nxt) <= base:
742
+ break
743
+ body.append(s)
744
+ index += 1
745
+
746
+ text = stripped if not body else stripped + " " + " ; ".join(body)
747
+ try:
748
+ stmt = parse_line(text, line_no)
749
+ except ParseError as ex:
750
+ diags.append(Diagnostic("SYNTAX", ERROR, str(ex.message),
751
+ line=line_no, col=ex.pos + 1))
752
+ continue
753
+ if stmt is not None:
754
+ stmts.append(stmt)
755
+ return stmts, diags