@hoplogic/hopjit 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/act-body-interpreter.d.ts +24 -0
- package/dist/act-body-interpreter.js +159 -0
- package/dist/act-body-parser.d.ts +10 -0
- package/dist/act-body-parser.js +577 -0
- package/dist/act-builtins.d.ts +12 -0
- package/dist/act-builtins.js +104 -0
- package/dist/ast-helpers.d.ts +26 -0
- package/dist/ast-helpers.js +58 -0
- package/dist/ast-runtime.d.ts +49 -0
- package/dist/ast-runtime.js +224 -0
- package/dist/ast-types.d.ts +260 -0
- package/dist/ast-types.js +4 -0
- package/dist/cli-types.d.ts +190 -0
- package/dist/cli-types.js +4 -0
- package/dist/cli.d.ts +26 -0
- package/dist/cli.js +623 -0
- package/dist/dispatcher.d.ts +92 -0
- package/dist/dispatcher.js +692 -0
- package/dist/doc-ref.d.ts +41 -0
- package/dist/doc-ref.js +214 -0
- package/dist/engine-traverse.d.ts +37 -0
- package/dist/engine-traverse.js +385 -0
- package/dist/engine.d.ts +141 -0
- package/dist/engine.js +1792 -0
- package/dist/errors.d.ts +47 -0
- package/dist/errors.js +34 -0
- package/dist/hoplog.d.ts +120 -0
- package/dist/hoplog.js +501 -0
- package/dist/parser.d.ts +7 -0
- package/dist/parser.js +802 -0
- package/dist/persistence.d.ts +60 -0
- package/dist/persistence.js +208 -0
- package/dist/prompt.d.ts +130 -0
- package/dist/prompt.js +1014 -0
- package/dist/provider-types.d.ts +134 -0
- package/dist/provider-types.js +3 -0
- package/dist/runtime-types.d.ts +84 -0
- package/dist/runtime-types.js +4 -0
- package/dist/tools.d.ts +16 -0
- package/dist/tools.js +141 -0
- package/dist/validator.d.ts +23 -0
- package/dist/validator.js +959 -0
- package/driver/codex/AGENTS.md +54 -0
- package/driver/hopskill-build/SKILL.md +137 -0
- package/driver/hopskill-build/references/spec-skeleton.md +132 -0
- package/driver/hopskill-build/references/step-type-cheatsheet.md +56 -0
- package/driver/hopskill-build/references/three-focus-rules.md +148 -0
- package/driver/hopspec-skill.md +187 -0
- package/driver/references/cli-discovery.md +36 -0
- package/driver/references/discovery.md +45 -0
- package/driver/references/driver-subagent.md +50 -0
- package/driver/references/parallel-worker.md +50 -0
- package/driver/references/step-execution-rules.md +47 -0
- package/package.json +52 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
// @module: act-body ^anc-struct-act-body
|
|
2
|
+
// act body 解析器:``` hop_python 围栏 → ActBody AST
|
|
3
|
+
// 设计见 design/spec-ast.md ^anc-ast-act-body;概念 HopSpec V3核心规范 ^anc-step-act。
|
|
4
|
+
// 无推理编排:赋值 + 白名单调用 + if-else 分支(确定性条件),禁循环(for/while → 错误)。
|
|
5
|
+
// Python 风格:缩进块、if cond: / elif cond: / else:、无花括号。
|
|
6
|
+
import { ACT_BUILTIN_NAMES } from './act-builtins.js';
|
|
7
|
+
const FENCE_OPEN = /^```hop_python\s*$/;
|
|
8
|
+
const FENCE_CLOSE = /^```\s*$/;
|
|
9
|
+
const LOOP_KEYWORDS = /\b(for|while)\b/;
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// 围栏提取:从 instruction 行数组中切出 hop_python body 与剩余自然语言行
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
export function extractHopPythonFence(instructionLines) {
|
|
14
|
+
const narrative = [];
|
|
15
|
+
let bodyLines = null;
|
|
16
|
+
let bodyStartIndex = -1;
|
|
17
|
+
let inBody = false;
|
|
18
|
+
for (let i = 0; i < instructionLines.length; i++) {
|
|
19
|
+
const line = instructionLines[i];
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
if (!inBody && FENCE_OPEN.test(trimmed)) {
|
|
22
|
+
inBody = true;
|
|
23
|
+
bodyLines = [];
|
|
24
|
+
bodyStartIndex = i + 1;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (inBody && FENCE_CLOSE.test(trimmed)) {
|
|
28
|
+
inBody = false;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (inBody) {
|
|
32
|
+
bodyLines.push(line);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
narrative.push(line);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { bodyLines, bodyStartIndex, narrative };
|
|
39
|
+
}
|
|
40
|
+
const KEYWORDS = new Set(['if', 'elif', 'else', 'and', 'or', 'not', 'true', 'false']);
|
|
41
|
+
// 多字符运算符优先匹配
|
|
42
|
+
const MULTI_OPS = ['==', '!=', '<=', '>='];
|
|
43
|
+
const SINGLE_OPS = new Set(['+', '-', '*', '/', '<', '>', '=']);
|
|
44
|
+
function tokenize(bodyLines, baseLine, errors) {
|
|
45
|
+
const tokens = [];
|
|
46
|
+
const indentStack = [0];
|
|
47
|
+
let ok = true;
|
|
48
|
+
for (let ln = 0; ln < bodyLines.length; ln++) {
|
|
49
|
+
const raw = bodyLines[ln];
|
|
50
|
+
const absLine = baseLine + ln;
|
|
51
|
+
// 禁循环:早拦
|
|
52
|
+
if (LOOP_KEYWORDS.test(raw)) {
|
|
53
|
+
errors.push({ kind: 'parse', line: absLine, message: 'act body 禁止循环(for/while)——循环须升 HopSpec loop 步骤' });
|
|
54
|
+
ok = false;
|
|
55
|
+
}
|
|
56
|
+
const content = raw.replace(/\s+$/, '');
|
|
57
|
+
const trimmedStart = content.replace(/^\s*/, '');
|
|
58
|
+
if (trimmedStart === '' || trimmedStart.startsWith('#'))
|
|
59
|
+
continue; // 空行/纯注释跳过
|
|
60
|
+
// 缩进只接受空格、禁 tab(hop_python 词法契约,见概念 ^anc-step-act-body-lang)。
|
|
61
|
+
// 注:act body 在 `> ` 区,`>` 紧跟的 tab 会被 RE_INSTRUCTION 吞掉,但「> 空格+tab」
|
|
62
|
+
// 形态的 tab 会残留进此处的行首空白——必须显式拦,否则缩进列数被污染却静默接受。
|
|
63
|
+
const leadingWs = content.slice(0, content.length - trimmedStart.length);
|
|
64
|
+
if (leadingWs.includes('\t')) {
|
|
65
|
+
errors.push({ kind: 'parse', line: absLine, message: 'act body 缩进只接受空格,不接受 tab' });
|
|
66
|
+
ok = false;
|
|
67
|
+
}
|
|
68
|
+
const indent = content.length - trimmedStart.length;
|
|
69
|
+
const top = indentStack[indentStack.length - 1];
|
|
70
|
+
if (indent > top) {
|
|
71
|
+
indentStack.push(indent);
|
|
72
|
+
tokens.push({ kind: 'indent', value: '', line: absLine });
|
|
73
|
+
}
|
|
74
|
+
else if (indent < top) {
|
|
75
|
+
while (indentStack.length > 1 && indent < indentStack[indentStack.length - 1]) {
|
|
76
|
+
indentStack.pop();
|
|
77
|
+
tokens.push({ kind: 'dedent', value: '', line: absLine });
|
|
78
|
+
}
|
|
79
|
+
if (indent !== indentStack[indentStack.length - 1]) {
|
|
80
|
+
errors.push({ kind: 'parse', line: absLine, message: 'act body 缩进不一致' });
|
|
81
|
+
ok = false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// 行内 tokenize(剥行尾注释)
|
|
85
|
+
let s = trimmedStart;
|
|
86
|
+
const hashIdx = findCommentStart(s);
|
|
87
|
+
if (hashIdx >= 0)
|
|
88
|
+
s = s.slice(0, hashIdx).replace(/\s+$/, '');
|
|
89
|
+
let i = 0;
|
|
90
|
+
while (i < s.length) {
|
|
91
|
+
const c = s[i];
|
|
92
|
+
if (c === ' ' || c === '\t') {
|
|
93
|
+
i++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// string
|
|
97
|
+
if (c === '"' || c === "'") {
|
|
98
|
+
const quote = c;
|
|
99
|
+
let j = i + 1;
|
|
100
|
+
let val = '';
|
|
101
|
+
while (j < s.length && s[j] !== quote) {
|
|
102
|
+
if (s[j] === '\\' && j + 1 < s.length) {
|
|
103
|
+
val += s[j + 1];
|
|
104
|
+
j += 2;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
val += s[j];
|
|
108
|
+
j++;
|
|
109
|
+
}
|
|
110
|
+
if (j >= s.length) {
|
|
111
|
+
errors.push({ kind: 'parse', line: absLine, message: '未闭合字符串' });
|
|
112
|
+
ok = false;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
tokens.push({ kind: 'string', value: val, line: absLine });
|
|
116
|
+
i = j + 1;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// number
|
|
120
|
+
if (/[0-9]/.test(c) || (c === '.' && /[0-9]/.test(s[i + 1] ?? ''))) {
|
|
121
|
+
let j = i;
|
|
122
|
+
while (j < s.length && /[0-9.]/.test(s[j]))
|
|
123
|
+
j++;
|
|
124
|
+
tokens.push({ kind: 'number', value: s.slice(i, j), line: absLine });
|
|
125
|
+
i = j;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
// ident / keyword
|
|
129
|
+
if (/[A-Za-z_]/.test(c)) {
|
|
130
|
+
let j = i;
|
|
131
|
+
while (j < s.length && /[A-Za-z0-9_]/.test(s[j]))
|
|
132
|
+
j++;
|
|
133
|
+
tokens.push({ kind: 'ident', value: s.slice(i, j), line: absLine });
|
|
134
|
+
i = j;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// multi-char op
|
|
138
|
+
const two = s.slice(i, i + 2);
|
|
139
|
+
if (MULTI_OPS.includes(two)) {
|
|
140
|
+
tokens.push({ kind: 'op', value: two, line: absLine });
|
|
141
|
+
i += 2;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
// single
|
|
145
|
+
if (c === ':') {
|
|
146
|
+
tokens.push({ kind: 'colon', value: ':', line: absLine });
|
|
147
|
+
i++;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (c === '(') {
|
|
151
|
+
tokens.push({ kind: 'lparen', value: '(', line: absLine });
|
|
152
|
+
i++;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (c === ')') {
|
|
156
|
+
tokens.push({ kind: 'rparen', value: ')', line: absLine });
|
|
157
|
+
i++;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (c === ',') {
|
|
161
|
+
tokens.push({ kind: 'comma', value: ',', line: absLine });
|
|
162
|
+
i++;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (c === '.') {
|
|
166
|
+
tokens.push({ kind: 'dot', value: '.', line: absLine });
|
|
167
|
+
i++;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (SINGLE_OPS.has(c)) {
|
|
171
|
+
tokens.push({ kind: 'op', value: c, line: absLine });
|
|
172
|
+
i++;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
errors.push({ kind: 'parse', line: absLine, message: `act body 非法字符: ${c}` });
|
|
176
|
+
ok = false;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
tokens.push({ kind: 'newline', value: '', line: absLine });
|
|
180
|
+
}
|
|
181
|
+
// 收尾 dedent
|
|
182
|
+
while (indentStack.length > 1) {
|
|
183
|
+
indentStack.pop();
|
|
184
|
+
tokens.push({ kind: 'dedent', value: '', line: baseLine + bodyLines.length });
|
|
185
|
+
}
|
|
186
|
+
tokens.push({ kind: 'eof', value: '', line: baseLine + bodyLines.length });
|
|
187
|
+
return ok ? tokens : null;
|
|
188
|
+
}
|
|
189
|
+
// 找到行内注释 # 的起点(跳过字符串内的 #)
|
|
190
|
+
function findCommentStart(s) {
|
|
191
|
+
let inStr = null;
|
|
192
|
+
for (let i = 0; i < s.length; i++) {
|
|
193
|
+
const c = s[i];
|
|
194
|
+
if (inStr) {
|
|
195
|
+
if (c === inStr)
|
|
196
|
+
inStr = null;
|
|
197
|
+
else if (c === '\\')
|
|
198
|
+
i++;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (c === '"' || c === "'") {
|
|
202
|
+
inStr = c;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (c === '#')
|
|
206
|
+
return i;
|
|
207
|
+
}
|
|
208
|
+
return -1;
|
|
209
|
+
}
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// 递归下降 parser
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
class Parser {
|
|
214
|
+
toks;
|
|
215
|
+
errors;
|
|
216
|
+
pos = 0;
|
|
217
|
+
constructor(toks, errors) {
|
|
218
|
+
this.toks = toks;
|
|
219
|
+
this.errors = errors;
|
|
220
|
+
}
|
|
221
|
+
peek() { return this.toks[this.pos]; }
|
|
222
|
+
next() { return this.toks[this.pos++]; }
|
|
223
|
+
at(kind) { return this.peek().kind === kind; }
|
|
224
|
+
atKeyword(kw) { const t = this.peek(); return t.kind === 'ident' && t.value === kw; }
|
|
225
|
+
err(msg) { this.errors.push({ kind: 'parse', line: this.peek().line, message: 'act body: ' + msg }); }
|
|
226
|
+
skipNewlines() { while (this.at('newline'))
|
|
227
|
+
this.next(); }
|
|
228
|
+
// 顶层语句块(直到 eof)
|
|
229
|
+
parseProgram() {
|
|
230
|
+
const stmts = [];
|
|
231
|
+
this.skipNewlines();
|
|
232
|
+
while (!this.at('eof')) {
|
|
233
|
+
const s = this.parseStatement();
|
|
234
|
+
if (!s)
|
|
235
|
+
break;
|
|
236
|
+
stmts.push(s);
|
|
237
|
+
this.skipNewlines();
|
|
238
|
+
}
|
|
239
|
+
return stmts;
|
|
240
|
+
}
|
|
241
|
+
// 缩进块:INDENT statement+ DEDENT
|
|
242
|
+
parseBlock() {
|
|
243
|
+
const stmts = [];
|
|
244
|
+
if (!this.at('indent')) {
|
|
245
|
+
this.err('期望缩进块');
|
|
246
|
+
return stmts;
|
|
247
|
+
}
|
|
248
|
+
this.next(); // indent
|
|
249
|
+
this.skipNewlines();
|
|
250
|
+
while (!this.at('dedent') && !this.at('eof')) {
|
|
251
|
+
const s = this.parseStatement();
|
|
252
|
+
if (!s)
|
|
253
|
+
break;
|
|
254
|
+
stmts.push(s);
|
|
255
|
+
this.skipNewlines();
|
|
256
|
+
}
|
|
257
|
+
if (this.at('dedent'))
|
|
258
|
+
this.next();
|
|
259
|
+
return stmts;
|
|
260
|
+
}
|
|
261
|
+
parseStatement() {
|
|
262
|
+
const t = this.peek();
|
|
263
|
+
if (t.kind === 'ident' && (t.value === 'if'))
|
|
264
|
+
return this.parseIf();
|
|
265
|
+
if (t.kind === 'ident' && (t.value === 'elif' || t.value === 'else')) {
|
|
266
|
+
this.err(`意外的 ${t.value}(无匹配 if)`);
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
// ident 开头:可能是赋值 (ident = ...) 或调用语句 (ident(...))
|
|
270
|
+
if (t.kind === 'ident' && !KEYWORDS.has(t.value)) {
|
|
271
|
+
// 向前看:下一个非该 ident 的 token
|
|
272
|
+
const save = this.pos;
|
|
273
|
+
const name = this.next().value;
|
|
274
|
+
if (this.at('op') && this.peek().value === '=') {
|
|
275
|
+
this.next(); // =
|
|
276
|
+
const value = this.parseExpr();
|
|
277
|
+
const line = t.line;
|
|
278
|
+
this.expectNewlineOrEnd();
|
|
279
|
+
return { type: 'assign', target: name, value, line };
|
|
280
|
+
}
|
|
281
|
+
// 回退,作为表达式语句(应是 call)
|
|
282
|
+
this.pos = save;
|
|
283
|
+
const expr = this.parseExpr();
|
|
284
|
+
this.expectNewlineOrEnd();
|
|
285
|
+
if (expr.type === 'call')
|
|
286
|
+
return { type: 'call', call: expr, line: t.line };
|
|
287
|
+
this.err('表达式语句只允许工具/函数调用');
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
this.err(`非法语句起始: ${t.value || t.kind}`);
|
|
291
|
+
this.next();
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
parseIf() {
|
|
295
|
+
const line = this.peek().line;
|
|
296
|
+
this.next(); // if
|
|
297
|
+
const condition = this.parseExpr();
|
|
298
|
+
this.expectColon();
|
|
299
|
+
this.expectNewline();
|
|
300
|
+
const then_body = this.parseBlock();
|
|
301
|
+
let else_body;
|
|
302
|
+
this.skipNewlines();
|
|
303
|
+
if (this.atKeyword('elif')) {
|
|
304
|
+
// elif 展开为 else_body: [嵌套 IfStmt]
|
|
305
|
+
else_body = [this.parseIf()];
|
|
306
|
+
}
|
|
307
|
+
else if (this.atKeyword('else')) {
|
|
308
|
+
this.next(); // else
|
|
309
|
+
this.expectColon();
|
|
310
|
+
this.expectNewline();
|
|
311
|
+
else_body = this.parseBlock();
|
|
312
|
+
}
|
|
313
|
+
return { type: 'if', condition, then_body, else_body, line };
|
|
314
|
+
}
|
|
315
|
+
// 表达式优先级:or > and > not > cmp > add > mul > unary > postfix > primary
|
|
316
|
+
parseExpr() { return this.parseOr(); }
|
|
317
|
+
parseOr() {
|
|
318
|
+
let left = this.parseAnd();
|
|
319
|
+
while (this.atKeyword('or')) {
|
|
320
|
+
this.next();
|
|
321
|
+
const right = this.parseAnd();
|
|
322
|
+
left = bin('or', left, right);
|
|
323
|
+
}
|
|
324
|
+
return left;
|
|
325
|
+
}
|
|
326
|
+
parseAnd() {
|
|
327
|
+
let left = this.parseNot();
|
|
328
|
+
while (this.atKeyword('and')) {
|
|
329
|
+
this.next();
|
|
330
|
+
const right = this.parseNot();
|
|
331
|
+
left = bin('and', left, right);
|
|
332
|
+
}
|
|
333
|
+
return left;
|
|
334
|
+
}
|
|
335
|
+
parseNot() {
|
|
336
|
+
if (this.atKeyword('not')) {
|
|
337
|
+
this.next();
|
|
338
|
+
return { type: 'unary', op: 'not', operand: this.parseNot() };
|
|
339
|
+
}
|
|
340
|
+
return this.parseCmp();
|
|
341
|
+
}
|
|
342
|
+
parseCmp() {
|
|
343
|
+
let left = this.parseAdd();
|
|
344
|
+
if (this.at('op') && ['==', '!=', '<', '>', '<=', '>='].includes(this.peek().value)) {
|
|
345
|
+
const op = this.next().value;
|
|
346
|
+
const right = this.parseAdd();
|
|
347
|
+
left = bin(op, left, right);
|
|
348
|
+
}
|
|
349
|
+
return left;
|
|
350
|
+
}
|
|
351
|
+
parseAdd() {
|
|
352
|
+
let left = this.parseMul();
|
|
353
|
+
while (this.at('op') && (this.peek().value === '+' || this.peek().value === '-')) {
|
|
354
|
+
const op = this.next().value;
|
|
355
|
+
const right = this.parseMul();
|
|
356
|
+
left = bin(op, left, right);
|
|
357
|
+
}
|
|
358
|
+
return left;
|
|
359
|
+
}
|
|
360
|
+
parseMul() {
|
|
361
|
+
let left = this.parseUnary();
|
|
362
|
+
while (this.at('op') && (this.peek().value === '*' || this.peek().value === '/')) {
|
|
363
|
+
const op = this.next().value;
|
|
364
|
+
const right = this.parseUnary();
|
|
365
|
+
left = bin(op, left, right);
|
|
366
|
+
}
|
|
367
|
+
return left;
|
|
368
|
+
}
|
|
369
|
+
parseUnary() {
|
|
370
|
+
if (this.at('op') && this.peek().value === '-') {
|
|
371
|
+
this.next();
|
|
372
|
+
return { type: 'unary', op: '-', operand: this.parseUnary() };
|
|
373
|
+
}
|
|
374
|
+
return this.parsePostfix();
|
|
375
|
+
}
|
|
376
|
+
parsePostfix() {
|
|
377
|
+
let e = this.parsePrimary();
|
|
378
|
+
for (;;) {
|
|
379
|
+
if (this.at('dot')) {
|
|
380
|
+
this.next();
|
|
381
|
+
if (!this.at('ident')) {
|
|
382
|
+
this.err('. 后期望字段名');
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
e = { type: 'field', object: e, field: this.next().value };
|
|
386
|
+
}
|
|
387
|
+
else if (this.at('lparen')) {
|
|
388
|
+
// 调用:e 必须是 var(callee 名)
|
|
389
|
+
if (e.type !== 'var') {
|
|
390
|
+
this.err('只能调用具名函数/工具');
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
e = this.parseCallArgs(e.name);
|
|
394
|
+
}
|
|
395
|
+
else
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
return e;
|
|
399
|
+
}
|
|
400
|
+
parseCallArgs(callee) {
|
|
401
|
+
this.next(); // (
|
|
402
|
+
const args = [];
|
|
403
|
+
if (!this.at('rparen')) {
|
|
404
|
+
for (;;) {
|
|
405
|
+
// 命名参数 name: expr 或 位置参数 expr
|
|
406
|
+
if (this.at('ident') && this.toks[this.pos + 1]?.kind === 'colon') {
|
|
407
|
+
const name = this.next().value;
|
|
408
|
+
this.next(); // ident :
|
|
409
|
+
args.push({ name, value: this.parseExpr() });
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
args.push({ value: this.parseExpr() });
|
|
413
|
+
}
|
|
414
|
+
if (this.at('comma')) {
|
|
415
|
+
this.next();
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (this.at('rparen'))
|
|
422
|
+
this.next();
|
|
423
|
+
else
|
|
424
|
+
this.err(') 未闭合');
|
|
425
|
+
return { type: 'call', callee, args };
|
|
426
|
+
}
|
|
427
|
+
parsePrimary() {
|
|
428
|
+
const t = this.peek();
|
|
429
|
+
if (t.kind === 'number') {
|
|
430
|
+
this.next();
|
|
431
|
+
return { type: 'literal', value: Number(t.value), literal_kind: 'number' };
|
|
432
|
+
}
|
|
433
|
+
if (t.kind === 'string') {
|
|
434
|
+
this.next();
|
|
435
|
+
return { type: 'literal', value: t.value, literal_kind: 'string' };
|
|
436
|
+
}
|
|
437
|
+
if (t.kind === 'ident' && (t.value === 'true' || t.value === 'false')) {
|
|
438
|
+
this.next();
|
|
439
|
+
return { type: 'literal', value: t.value === 'true', literal_kind: 'bool' };
|
|
440
|
+
}
|
|
441
|
+
if (t.kind === 'ident' && !KEYWORDS.has(t.value)) {
|
|
442
|
+
this.next();
|
|
443
|
+
return { type: 'var', name: t.value };
|
|
444
|
+
}
|
|
445
|
+
if (t.kind === 'lparen') {
|
|
446
|
+
this.next();
|
|
447
|
+
const e = this.parseExpr();
|
|
448
|
+
if (this.at('rparen'))
|
|
449
|
+
this.next();
|
|
450
|
+
else
|
|
451
|
+
this.err(') 未闭合');
|
|
452
|
+
return e;
|
|
453
|
+
}
|
|
454
|
+
this.err(`非法表达式起始: ${t.value || t.kind}`);
|
|
455
|
+
this.next();
|
|
456
|
+
return { type: 'literal', value: '', literal_kind: 'string' }; // 占位,错误已记
|
|
457
|
+
}
|
|
458
|
+
expectColon() { if (this.at('colon'))
|
|
459
|
+
this.next();
|
|
460
|
+
else
|
|
461
|
+
this.err('期望 :'); }
|
|
462
|
+
expectNewline() { if (this.at('newline'))
|
|
463
|
+
this.next();
|
|
464
|
+
else
|
|
465
|
+
this.err('期望换行'); }
|
|
466
|
+
expectNewlineOrEnd() { if (this.at('newline'))
|
|
467
|
+
this.next();
|
|
468
|
+
else if (!this.at('eof') && !this.at('dedent'))
|
|
469
|
+
this.err('语句后期望换行'); }
|
|
470
|
+
}
|
|
471
|
+
function bin(op, left, right) {
|
|
472
|
+
return { type: 'binary', op, left, right };
|
|
473
|
+
}
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
// 主入口
|
|
476
|
+
// ---------------------------------------------------------------------------
|
|
477
|
+
export function parseActBody(bodyLines, baseLine, errors) {
|
|
478
|
+
const before = errors.length;
|
|
479
|
+
const toks = tokenize(bodyLines, baseLine, errors);
|
|
480
|
+
if (!toks)
|
|
481
|
+
return null;
|
|
482
|
+
const parser = new Parser(toks, errors);
|
|
483
|
+
const statements = parser.parseProgram();
|
|
484
|
+
if (errors.length > before)
|
|
485
|
+
return null; // 有解析错误则不产出 body
|
|
486
|
+
return { statements, source_location: { line_start: baseLine, line_end: baseLine + bodyLines.length } };
|
|
487
|
+
}
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
// 序列化:ActBody AST → hop_python 文本(复用模式交付 CC 用;不要求与原文逐字符等价,
|
|
490
|
+
// 语义等价即可——见 spec-ast.md serialize 是辅助工具)。@a: anc-ast-act-body
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
export function serializeActBody(body) {
|
|
493
|
+
return body.statements.map(s => serializeStmt(s, 0)).join('\n');
|
|
494
|
+
}
|
|
495
|
+
function indent(level) { return ' '.repeat(level); }
|
|
496
|
+
function serializeStmt(stmt, level) {
|
|
497
|
+
const pad = indent(level);
|
|
498
|
+
if (stmt.type === 'assign') {
|
|
499
|
+
return `${pad}${stmt.target} = ${serializeExpr(stmt.value)}`;
|
|
500
|
+
}
|
|
501
|
+
if (stmt.type === 'call') {
|
|
502
|
+
return `${pad}${serializeExpr(stmt.call)}`;
|
|
503
|
+
}
|
|
504
|
+
// if
|
|
505
|
+
const ifs = stmt;
|
|
506
|
+
const lines = [`${pad}if ${serializeExpr(ifs.condition)}:`];
|
|
507
|
+
for (const s of ifs.then_body)
|
|
508
|
+
lines.push(serializeStmt(s, level + 1));
|
|
509
|
+
if (ifs.else_body) {
|
|
510
|
+
// elif 展开还原:else_body 恰为单个 IfStmt → 输出 elif
|
|
511
|
+
if (ifs.else_body.length === 1 && ifs.else_body[0].type === 'if') {
|
|
512
|
+
const elif = ifs.else_body[0];
|
|
513
|
+
const elifLines = serializeStmt(elif, level).split('\n');
|
|
514
|
+
elifLines[0] = `${pad}elif ${serializeExpr(elif.condition)}:`;
|
|
515
|
+
lines.push(...elifLines.slice(0));
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
lines.push(`${pad}else:`);
|
|
519
|
+
for (const s of ifs.else_body)
|
|
520
|
+
lines.push(serializeStmt(s, level + 1));
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return lines.join('\n');
|
|
524
|
+
}
|
|
525
|
+
function serializeExpr(expr) {
|
|
526
|
+
switch (expr.type) {
|
|
527
|
+
case 'literal':
|
|
528
|
+
if (expr.literal_kind === 'string')
|
|
529
|
+
return JSON.stringify(expr.value);
|
|
530
|
+
return String(expr.value);
|
|
531
|
+
case 'var': return expr.name;
|
|
532
|
+
case 'field': return `${serializeExpr(expr.object)}.${expr.field}`;
|
|
533
|
+
case 'unary': return expr.op === 'not' ? `not ${serializeExpr(expr.operand)}` : `-${serializeExpr(expr.operand)}`;
|
|
534
|
+
case 'binary': return `${serializeExpr(expr.left)} ${expr.op} ${serializeExpr(expr.right)}`;
|
|
535
|
+
case 'call': {
|
|
536
|
+
const c = expr;
|
|
537
|
+
const args = c.args.map(a => a.name ? `${a.name}: ${serializeExpr(a.value)}` : serializeExpr(a.value)).join(', ');
|
|
538
|
+
return `${c.callee}(${args})`;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// ---------------------------------------------------------------------------
|
|
543
|
+
// 判定 body 是否含工具调用(callee 不在内置白名单 = 工具调用,需 caller 执行)。
|
|
544
|
+
// CLI next 据此分流:无工具 body 引擎消化,含工具 body 交 caller。
|
|
545
|
+
// 见 design/hop-cli.md ^anc-cli-next-drives-body。@a: anc-cli-next-drives-body
|
|
546
|
+
// ---------------------------------------------------------------------------
|
|
547
|
+
export function bodyHasToolCall(body) {
|
|
548
|
+
return body.statements.some(stmtHasToolCall);
|
|
549
|
+
}
|
|
550
|
+
function stmtHasToolCall(stmt) {
|
|
551
|
+
if (stmt.type === 'assign')
|
|
552
|
+
return exprHasToolCall(stmt.value);
|
|
553
|
+
if (stmt.type === 'call')
|
|
554
|
+
return exprHasToolCall(stmt.call);
|
|
555
|
+
// if
|
|
556
|
+
if (exprHasToolCall(stmt.condition))
|
|
557
|
+
return true;
|
|
558
|
+
if (stmt.then_body.some(stmtHasToolCall))
|
|
559
|
+
return true;
|
|
560
|
+
if (stmt.else_body && stmt.else_body.some(stmtHasToolCall))
|
|
561
|
+
return true;
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
function exprHasToolCall(expr) {
|
|
565
|
+
switch (expr.type) {
|
|
566
|
+
case 'literal':
|
|
567
|
+
case 'var': return false;
|
|
568
|
+
case 'field': return exprHasToolCall(expr.object);
|
|
569
|
+
case 'unary': return exprHasToolCall(expr.operand);
|
|
570
|
+
case 'binary': return exprHasToolCall(expr.left) || exprHasToolCall(expr.right);
|
|
571
|
+
case 'call':
|
|
572
|
+
// callee 不在内置白名单 = 工具调用
|
|
573
|
+
if (!ACT_BUILTIN_NAMES.has(expr.callee))
|
|
574
|
+
return true;
|
|
575
|
+
return expr.args.some(a => exprHasToolCall(a.value));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface BuiltinFn {
|
|
2
|
+
name: string;
|
|
3
|
+
arity: {
|
|
4
|
+
min: number;
|
|
5
|
+
max: number;
|
|
6
|
+
};
|
|
7
|
+
fn: (args: unknown[]) => unknown;
|
|
8
|
+
}
|
|
9
|
+
/** hop_python 内置函数白名单:函数名 → 实现(含 arity),parser 校验 B2 与 interpreter 执行共用。见 [[act-body#^anc-exec-act-body-interp]] */
|
|
10
|
+
export declare const ACT_BUILTINS: ReadonlyMap<string, BuiltinFn>;
|
|
11
|
+
/** 内置函数名集合,供 validator/parser 快速查白名单成员。见 [[act-body#^anc-exec-act-body-interp]] */
|
|
12
|
+
export declare const ACT_BUILTIN_NAMES: ReadonlySet<string>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// @module: act-body ^anc-struct-act-body
|
|
2
|
+
// act body 内置函数白名单:纯函数、无副作用、确定性。
|
|
3
|
+
// 设计见 design/spec-parser.md ^anc-rule-b2;概念 ^anc-step-act-body-lang「能力边界=白名单」。
|
|
4
|
+
// 刻意排除 IO/随机/时间/网络——这些走 ToolProvider 工具(受沙箱约束)。
|
|
5
|
+
// @a: anc-rule-b2
|
|
6
|
+
function num(v) {
|
|
7
|
+
const n = typeof v === 'number' ? v : Number(v);
|
|
8
|
+
if (Number.isNaN(n))
|
|
9
|
+
throw new Error(`期望数字,实际: ${JSON.stringify(v)}`);
|
|
10
|
+
return n;
|
|
11
|
+
}
|
|
12
|
+
function str(v) {
|
|
13
|
+
return typeof v === 'string' ? v : String(v);
|
|
14
|
+
}
|
|
15
|
+
function arr(v) {
|
|
16
|
+
if (Array.isArray(v))
|
|
17
|
+
return v;
|
|
18
|
+
throw new Error(`期望数组,实际: ${JSON.stringify(v)}`);
|
|
19
|
+
}
|
|
20
|
+
const BUILTINS = [
|
|
21
|
+
// 数值
|
|
22
|
+
{ name: 'len', arity: { min: 1, max: 1 }, fn: (a) => {
|
|
23
|
+
const v = a[0];
|
|
24
|
+
if (Array.isArray(v))
|
|
25
|
+
return v.length;
|
|
26
|
+
if (typeof v === 'string')
|
|
27
|
+
return v.length;
|
|
28
|
+
if (v && typeof v === 'object')
|
|
29
|
+
return Object.keys(v).length;
|
|
30
|
+
throw new Error('len 期望数组/字符串/对象');
|
|
31
|
+
} },
|
|
32
|
+
{ name: 'min', arity: { min: 1, max: Infinity }, fn: (a) => Math.min(...a.map(num)) },
|
|
33
|
+
{ name: 'max', arity: { min: 1, max: Infinity }, fn: (a) => Math.max(...a.map(num)) },
|
|
34
|
+
{ name: 'round', arity: { min: 1, max: 2 }, fn: (a) => {
|
|
35
|
+
const n = num(a[0]);
|
|
36
|
+
const d = a.length > 1 ? num(a[1]) : 0;
|
|
37
|
+
const f = Math.pow(10, d);
|
|
38
|
+
return Math.round(n * f) / f;
|
|
39
|
+
} },
|
|
40
|
+
{ name: 'abs', arity: { min: 1, max: 1 }, fn: (a) => Math.abs(num(a[0])) },
|
|
41
|
+
{ name: 'sum', arity: { min: 1, max: 1 }, fn: (a) => arr(a[0]).reduce((s, x) => s + num(x), 0) },
|
|
42
|
+
// 字符串
|
|
43
|
+
{ name: 'lower', arity: { min: 1, max: 1 }, fn: (a) => str(a[0]).toLowerCase() },
|
|
44
|
+
{ name: 'upper', arity: { min: 1, max: 1 }, fn: (a) => str(a[0]).toUpperCase() },
|
|
45
|
+
{ name: 'trim', arity: { min: 1, max: 1 }, fn: (a) => str(a[0]).trim() },
|
|
46
|
+
{ name: 'split', arity: { min: 2, max: 2 }, fn: (a) => str(a[0]).split(str(a[1])) },
|
|
47
|
+
{ name: 'join', arity: { min: 2, max: 2 }, fn: (a) => arr(a[0]).map(str).join(str(a[1])) },
|
|
48
|
+
{ name: 'replace', arity: { min: 3, max: 3 }, fn: (a) => str(a[0]).split(str(a[1])).join(str(a[2])) },
|
|
49
|
+
{ name: 'contains', arity: { min: 2, max: 2 }, fn: (a) => {
|
|
50
|
+
const hay = a[0];
|
|
51
|
+
if (Array.isArray(hay))
|
|
52
|
+
return hay.includes(a[1]);
|
|
53
|
+
return str(hay).includes(str(a[1]));
|
|
54
|
+
} },
|
|
55
|
+
// 结构
|
|
56
|
+
{ name: 'count', arity: { min: 1, max: 1 }, fn: (a) => arr(a[0]).length },
|
|
57
|
+
{ name: 'keys', arity: { min: 1, max: 1 }, fn: (a) => {
|
|
58
|
+
const v = a[0];
|
|
59
|
+
if (v && typeof v === 'object' && !Array.isArray(v))
|
|
60
|
+
return Object.keys(v);
|
|
61
|
+
throw new Error('keys 期望对象');
|
|
62
|
+
} },
|
|
63
|
+
{ name: 'values', arity: { min: 1, max: 1 }, fn: (a) => {
|
|
64
|
+
const v = a[0];
|
|
65
|
+
if (v && typeof v === 'object' && !Array.isArray(v))
|
|
66
|
+
return Object.values(v);
|
|
67
|
+
throw new Error('values 期望对象');
|
|
68
|
+
} },
|
|
69
|
+
{ name: 'get', arity: { min: 2, max: 3 }, fn: (a) => {
|
|
70
|
+
const obj = a[0];
|
|
71
|
+
const key = str(a[1]);
|
|
72
|
+
const def = a.length > 2 ? a[2] : null;
|
|
73
|
+
if (obj && typeof obj === 'object' && key in obj)
|
|
74
|
+
return obj[key];
|
|
75
|
+
return def;
|
|
76
|
+
} },
|
|
77
|
+
// 类型转换
|
|
78
|
+
{ name: 'to_number', arity: { min: 1, max: 1 }, fn: (a) => num(a[0]) },
|
|
79
|
+
{ name: 'to_string', arity: { min: 1, max: 1 }, fn: (a) => str(a[0]) },
|
|
80
|
+
{ name: 'to_bool', arity: { min: 1, max: 1 }, fn: (a) => {
|
|
81
|
+
const v = a[0];
|
|
82
|
+
if (typeof v === 'boolean')
|
|
83
|
+
return v;
|
|
84
|
+
if (typeof v === 'string')
|
|
85
|
+
return /^(true|yes|1)$/i.test(v.trim());
|
|
86
|
+
return Boolean(v);
|
|
87
|
+
} },
|
|
88
|
+
// 判定
|
|
89
|
+
{ name: 'is_null', arity: { min: 1, max: 1 }, fn: (a) => a[0] === null || a[0] === undefined },
|
|
90
|
+
{ name: 'is_empty', arity: { min: 1, max: 1 }, fn: (a) => {
|
|
91
|
+
const v = a[0];
|
|
92
|
+
if (v === null || v === undefined)
|
|
93
|
+
return true;
|
|
94
|
+
if (Array.isArray(v) || typeof v === 'string')
|
|
95
|
+
return v.length === 0;
|
|
96
|
+
if (typeof v === 'object')
|
|
97
|
+
return Object.keys(v).length === 0;
|
|
98
|
+
return false;
|
|
99
|
+
} },
|
|
100
|
+
];
|
|
101
|
+
/** hop_python 内置函数白名单:函数名 → 实现(含 arity),parser 校验 B2 与 interpreter 执行共用。见 [[act-body#^anc-exec-act-body-interp]] */
|
|
102
|
+
export const ACT_BUILTINS = new Map(BUILTINS.map(b => [b.name, b]));
|
|
103
|
+
/** 内置函数名集合,供 validator/parser 快速查白名单成员。见 [[act-body#^anc-exec-act-body-interp]] */
|
|
104
|
+
export const ACT_BUILTIN_NAMES = new Set(BUILTINS.map(b => b.name));
|