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.
- openpuppet_language-2.0.data/data/share/puppet/conformance/README.md +274 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/binding.json +238 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/capabilities.json +155 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/grammar.json +248 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/interaction.json +220 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/listen.json +133 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/platform.json +274 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/probes.json +117 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/render.json +180 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/tabs.json +39 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/templates.json +143 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_bad_result.py +11 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_basic.py +24 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_deps_mismatch.py +11 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_no_doc.py +10 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/runner.py +721 -0
- openpuppet_language-2.0.data/data/share/puppet/spec/01-grammar.md +283 -0
- openpuppet_language-2.0.data/data/share/puppet/spec/02-ir.md +77 -0
- openpuppet_language-2.0.data/data/share/puppet/spec/03-semantics.md +167 -0
- openpuppet_language-2.0.data/data/share/puppet/spec/04-vocabulary.md +227 -0
- openpuppet_language-2.0.data/data/share/puppet/spec/05-render-contract.md +126 -0
- openpuppet_language-2.0.data/data/share/puppet/spec/06-diagnostics.md +151 -0
- openpuppet_language-2.0.data/data/share/puppet/spec/README.md +73 -0
- openpuppet_language-2.0.dist-info/METADATA +50 -0
- openpuppet_language-2.0.dist-info/RECORD +40 -0
- openpuppet_language-2.0.dist-info/WHEEL +5 -0
- openpuppet_language-2.0.dist-info/entry_points.txt +2 -0
- openpuppet_language-2.0.dist-info/top_level.txt +1 -0
- puppet/__init__.py +19 -0
- puppet/adapter.py +112 -0
- puppet/capabilities.py +172 -0
- puppet/cli.py +132 -0
- puppet/diag.py +42 -0
- puppet/engine.py +1038 -0
- puppet/eval.py +243 -0
- puppet/ir.py +538 -0
- puppet/lang.py +755 -0
- puppet/raster_adapter.py +348 -0
- puppet/tk_adapter.py +374 -0
- 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
|