@lingfan-engine/editor 0.1.0 → 0.1.3

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.
@@ -1,370 +1,370 @@
1
- /**
2
- * 表达式审计:把装配时产出的 token 流按引擎的类型规则判类,违规记入警告池。
3
- *
4
- * 判类器是一个整体(方法共享游标 `pos` 与警告池),所以实现文件刻意只留一条导入。
5
- */
6
-
7
- import {
8
- BAIL,
9
- type AuditToken,
10
- type ExpressionWarning,
11
- type OperandKind,
12
- type VarKind,
13
- } from "./token.ts";
14
- import type { EditorTexts } from "../../../contracts/index.ts";
15
- import { resolveEditorTexts } from "../../../contracts/index.ts";
16
-
17
- /**
18
- * 递归下降判类器——**优先级链与引擎逐层一致**:`?: → || → && → ==/!= → 比较 →
19
- * + - → * / % → 一元 → 成员/调用`。相邻配对会在 `isVIP || gold == 1` 这类
20
- * 优先级场景错怪操作数,故必须按结构配对。
21
- *
22
- * 类型传播口径(与引擎「输入合型 ⇒ 有结果必为输出型」一致):
23
- * - 违规(已知操作数不合运算符要求)⇒ 记警告,结果按 unknown(防连锁噪声);
24
- * - 无违规 ⇒ 结果 = 运算符输出型(算术 num / 比较·相等·逻辑·`!` bool / 一元减 num),
25
- * 即使另一侧 unknown——引擎在「不报错」的前提下结果型是确定的。
26
- */
27
- export class ExpressionAuditor {
28
- private pos = 0;
29
-
30
- private readonly tokens: readonly AuditToken[];
31
- private readonly pending: Array<{
32
- rule: ExpressionWarning["rule"];
33
- message: string;
34
- }>;
35
- private readonly texts: EditorTexts;
36
-
37
- constructor(
38
- tokens: readonly AuditToken[],
39
- pending: Array<{
40
- rule: ExpressionWarning["rule"];
41
- message: string;
42
- }>,
43
- texts: Partial<EditorTexts> = {},
44
- ) {
45
- this.tokens = tokens;
46
- this.pending = pending;
47
- this.texts = resolveEditorTexts(texts);
48
- }
49
-
50
- /** 入口:整体解析一次;任何结构不理解 / 残留 token ⇒ 全部放弃(零误报) */
51
- audit(): void {
52
- const saved = [...this.pending];
53
- try {
54
- this.ternary();
55
- if (this.pos !== this.tokens.length) throw BAIL;
56
- } catch {
57
- this.pending.length = 0;
58
- this.pending.push(...saved);
59
- }
60
- }
61
-
62
- private peek(): AuditToken | undefined {
63
- return this.tokens[this.pos];
64
- }
65
-
66
- private isOp(text: string): boolean {
67
- const tok = this.peek();
68
- return tok !== undefined && tok.t === "op" && tok.text === text;
69
- }
70
-
71
- private isPunct(text: string): boolean {
72
- const tok = this.peek();
73
- return tok !== undefined && tok.t === "punct" && tok.text === text;
74
- }
75
-
76
- private eatOp(text: string): boolean {
77
- if (this.isOp(text)) {
78
- this.pos += 1;
79
- return true;
80
- }
81
- return false;
82
- }
83
-
84
- private describe(operand: { kind: OperandKind; label: string }): string {
85
- return `${operand.kind === "unknown" ? this.texts.unknownOperandKind() : operand.kind}「${operand.label}」`;
86
- }
87
-
88
- /** 单侧核对:已知操作数必须为 want,否则记警告并返回 true(违规)。note 追加引擎语义注记 */
89
- private requireKind(
90
- op: string,
91
- rule: ExpressionWarning["rule"],
92
- side: string,
93
- operand: { kind: OperandKind; label: string } | undefined,
94
- want: VarKind,
95
- note = "",
96
- ): boolean {
97
- if (operand === undefined || operand.kind === "unknown") return false;
98
- if (operand.kind === want) return false;
99
- this.pending.push({
100
- rule,
101
- message: this.texts.operandKindMismatch(
102
- op,
103
- side,
104
- want,
105
- this.describe(operand),
106
- note,
107
- ),
108
- });
109
- return true;
110
- }
111
-
112
- private ternary(): OperandKind {
113
- const condKind = this.or();
114
- if (!this.isPunct("?")) return condKind;
115
- this.pos += 1;
116
- const whenTrue = this.ternary();
117
- if (!this.isPunct(":")) throw BAIL;
118
- this.pos += 1;
119
- const whenFalse = this.ternary();
120
- let violated = false;
121
- if (condKind !== "unknown" && condKind !== "bool") {
122
- this.pending.push({
123
- rule: "ternary-cond-non-bool",
124
- message: this.texts.ternaryCondNotBool(
125
- this.describe({ kind: condKind, label: "条件表达式" }),
126
- ),
127
- });
128
- violated = true;
129
- }
130
- if (violated) return "unknown";
131
- return whenTrue !== "unknown" ? whenTrue : whenFalse;
132
- }
133
-
134
- private or(): OperandKind {
135
- let left = this.and();
136
- while (this.eatOp("||")) {
137
- const right = this.and();
138
- const violated =
139
- this.requireKind(
140
- "||",
141
- "logical-non-bool",
142
- "左",
143
- { kind: left, label: "左操作数" },
144
- "bool",
145
- ) ||
146
- this.requireKind(
147
- "||",
148
- "logical-non-bool",
149
- "右",
150
- { kind: right, label: "右操作数" },
151
- "bool",
152
- ";右支仅在被求值时检查——'||' 左支为真即短路",
153
- );
154
- left = violated ? "unknown" : "bool";
155
- }
156
- return left;
157
- }
158
-
159
- private and(): OperandKind {
160
- let left = this.equality();
161
- while (this.eatOp("&&")) {
162
- const right = this.equality();
163
- const violated =
164
- this.requireKind(
165
- "&&",
166
- "logical-non-bool",
167
- "左",
168
- { kind: left, label: "左操作数" },
169
- "bool",
170
- ) ||
171
- this.requireKind(
172
- "&&",
173
- "logical-non-bool",
174
- "右",
175
- { kind: right, label: "右操作数" },
176
- "bool",
177
- ";右支仅在被求值时检查——'&&' 左支为假即短路",
178
- );
179
- left = violated ? "unknown" : "bool";
180
- }
181
- return left;
182
- }
183
-
184
- /** 相等级:至多一个 ==/!=(引擎不支持链式比较);仅同类型标量相等 */
185
- private equality(): OperandKind {
186
- const left = this.comparison();
187
- let op: "==" | "!=" | null = null;
188
- if (this.eatOp("==")) op = "==";
189
- else if (this.eatOp("!=")) op = "!=";
190
- if (op === null) return left;
191
- const right = this.comparison();
192
- if (left !== "unknown" && right !== "unknown" && left !== right) {
193
- this.pending.push({
194
- rule: "equality-kind-mismatch",
195
- message: this.texts.crossTypeComparison(op, left, right),
196
- });
197
- return "unknown";
198
- }
199
- return "bool";
200
- }
201
-
202
- /** 比较级:至多一个比较符(引擎不支持链式比较);只支持 number */
203
- private comparison(): OperandKind {
204
- const left = this.additive();
205
- let op: string | null = null;
206
- for (const candidate of [">=", "<=", ">", "<"]) {
207
- if (this.eatOp(candidate)) {
208
- op = candidate;
209
- break;
210
- }
211
- }
212
- if (op === null) return left;
213
- const right = this.additive();
214
- const violated =
215
- this.requireKind(
216
- op,
217
- "compare-non-num",
218
- "左",
219
- { kind: left, label: "左操作数" },
220
- "num",
221
- ) ||
222
- this.requireKind(
223
- op,
224
- "compare-non-num",
225
- "右",
226
- { kind: right, label: "右操作数" },
227
- "num",
228
- );
229
- return violated ? "unknown" : "bool";
230
- }
231
-
232
- private additive(): OperandKind {
233
- let left = this.multiplicative();
234
- for (;;) {
235
- const op = this.eatOp("+") ? "+" : this.eatOp("-") ? "-" : null;
236
- if (op === null) return left;
237
- const right = this.multiplicative();
238
- const violated =
239
- this.requireKind(
240
- op,
241
- "arith-non-num",
242
- "左",
243
- { kind: left, label: "左操作数" },
244
- "num",
245
- ) ||
246
- this.requireKind(
247
- op,
248
- "arith-non-num",
249
- "右",
250
- { kind: right, label: "右操作数" },
251
- "num",
252
- );
253
- left = violated ? "unknown" : "num";
254
- }
255
- }
256
-
257
- private multiplicative(): OperandKind {
258
- let left = this.unary();
259
- for (;;) {
260
- const op = this.eatOp("*")
261
- ? "*"
262
- : this.eatOp("/")
263
- ? "/"
264
- : this.eatOp("%")
265
- ? "%"
266
- : null;
267
- if (op === null) return left;
268
- const right = this.unary();
269
- const violated =
270
- this.requireKind(
271
- op,
272
- "arith-non-num",
273
- "左",
274
- { kind: left, label: "左操作数" },
275
- "num",
276
- ) ||
277
- this.requireKind(
278
- op,
279
- "arith-non-num",
280
- "右",
281
- { kind: right, label: "右操作数" },
282
- "num",
283
- );
284
- left = violated ? "unknown" : "num";
285
- }
286
- }
287
-
288
- private unary(): OperandKind {
289
- if (this.eatOp("!")) {
290
- const inner = this.unary();
291
- if (
292
- this.requireKind(
293
- "!",
294
- "unary-not-non-bool",
295
- "",
296
- { kind: inner, label: "操作数" },
297
- "bool",
298
- )
299
- ) {
300
- return "unknown";
301
- }
302
- return "bool";
303
- }
304
- if (this.eatOp("-")) {
305
- const inner = this.unary();
306
- if (
307
- this.requireKind(
308
- "-",
309
- "unary-minus-non-num",
310
- "",
311
- { kind: inner, label: "操作数" },
312
- "num",
313
- )
314
- ) {
315
- return "unknown";
316
- }
317
- return "num";
318
- }
319
- return this.primary();
320
- }
321
-
322
- private primary(): OperandKind {
323
- const tok = this.peek();
324
- if (tok === undefined) throw BAIL;
325
- if (tok.t === "operand") {
326
- this.pos += 1;
327
- // 函数调用:ident( … ) ⇒ 结果未知(内置函数参数规则不进本层)
328
- if (this.isPunct("(")) {
329
- this.skipBalancedParens();
330
- return "unknown";
331
- }
332
- // 成员路径:ident.ident … ⇒ 结果未知(字典下钻的值型构建期不可知)
333
- if (this.isPunct(".") && tok.kind === "unknown") {
334
- while (this.isPunct(".")) {
335
- this.pos += 1;
336
- const seg = this.peek();
337
- if (seg === undefined || seg.t !== "operand") throw BAIL;
338
- this.pos += 1;
339
- }
340
- return "unknown";
341
- }
342
- return tok.kind;
343
- }
344
- if (tok.t === "punct" && tok.text === "(") {
345
- this.pos += 1;
346
- const inner = this.ternary();
347
- if (!this.isPunct(")")) throw BAIL;
348
- this.pos += 1;
349
- return inner;
350
- }
351
- throw BAIL;
352
- }
353
-
354
- /** 括号平衡跳过(函数调用参数整体不判型) */
355
- private skipBalancedParens(): void {
356
- let depth = 0;
357
- for (;;) {
358
- const tok = this.peek();
359
- if (tok === undefined) throw BAIL;
360
- this.pos += 1;
361
- if (tok.t === "punct") {
362
- if (tok.text === "(") depth += 1;
363
- if (tok.text === ")") {
364
- depth -= 1;
365
- if (depth === 0) return;
366
- }
367
- }
368
- }
369
- }
370
- }
1
+ /**
2
+ * 表达式审计:把装配时产出的 token 流按引擎的类型规则判类,违规记入警告池。
3
+ *
4
+ * 判类器是一个整体(方法共享游标 `pos` 与警告池),所以实现文件刻意只留一条导入。
5
+ */
6
+
7
+ import {
8
+ BAIL,
9
+ type AuditToken,
10
+ type ExpressionWarning,
11
+ type OperandKind,
12
+ type VarKind,
13
+ } from "./token.ts";
14
+ import type { EditorTexts } from "../../../contracts/index.ts";
15
+ import { resolveEditorTexts } from "../../../contracts/index.ts";
16
+
17
+ /**
18
+ * 递归下降判类器——**优先级链与引擎逐层一致**:`?: → || → && → ==/!= → 比较 →
19
+ * + - → * / % → 一元 → 成员/调用`。相邻配对会在 `isVIP || gold == 1` 这类
20
+ * 优先级场景错怪操作数,故必须按结构配对。
21
+ *
22
+ * 类型传播口径(与引擎「输入合型 ⇒ 有结果必为输出型」一致):
23
+ * - 违规(已知操作数不合运算符要求)⇒ 记警告,结果按 unknown(防连锁噪声);
24
+ * - 无违规 ⇒ 结果 = 运算符输出型(算术 num / 比较·相等·逻辑·`!` bool / 一元减 num),
25
+ * 即使另一侧 unknown——引擎在「不报错」的前提下结果型是确定的。
26
+ */
27
+ export class ExpressionAuditor {
28
+ private pos = 0;
29
+
30
+ private readonly tokens: readonly AuditToken[];
31
+ private readonly pending: Array<{
32
+ rule: ExpressionWarning["rule"];
33
+ message: string;
34
+ }>;
35
+ private readonly texts: EditorTexts;
36
+
37
+ constructor(
38
+ tokens: readonly AuditToken[],
39
+ pending: Array<{
40
+ rule: ExpressionWarning["rule"];
41
+ message: string;
42
+ }>,
43
+ texts: Partial<EditorTexts> = {},
44
+ ) {
45
+ this.tokens = tokens;
46
+ this.pending = pending;
47
+ this.texts = resolveEditorTexts(texts);
48
+ }
49
+
50
+ /** 入口:整体解析一次;任何结构不理解 / 残留 token ⇒ 全部放弃(零误报) */
51
+ audit(): void {
52
+ const saved = [...this.pending];
53
+ try {
54
+ this.ternary();
55
+ if (this.pos !== this.tokens.length) throw BAIL;
56
+ } catch {
57
+ this.pending.length = 0;
58
+ this.pending.push(...saved);
59
+ }
60
+ }
61
+
62
+ private peek(): AuditToken | undefined {
63
+ return this.tokens[this.pos];
64
+ }
65
+
66
+ private isOp(text: string): boolean {
67
+ const tok = this.peek();
68
+ return tok !== undefined && tok.t === "op" && tok.text === text;
69
+ }
70
+
71
+ private isPunct(text: string): boolean {
72
+ const tok = this.peek();
73
+ return tok !== undefined && tok.t === "punct" && tok.text === text;
74
+ }
75
+
76
+ private eatOp(text: string): boolean {
77
+ if (this.isOp(text)) {
78
+ this.pos += 1;
79
+ return true;
80
+ }
81
+ return false;
82
+ }
83
+
84
+ private describe(operand: { kind: OperandKind; label: string }): string {
85
+ return `${operand.kind === "unknown" ? this.texts.unknownOperandKind() : operand.kind}「${operand.label}」`;
86
+ }
87
+
88
+ /** 单侧核对:已知操作数必须为 want,否则记警告并返回 true(违规)。note 追加引擎语义注记 */
89
+ private requireKind(
90
+ op: string,
91
+ rule: ExpressionWarning["rule"],
92
+ side: string,
93
+ operand: { kind: OperandKind; label: string } | undefined,
94
+ want: VarKind,
95
+ note = "",
96
+ ): boolean {
97
+ if (operand === undefined || operand.kind === "unknown") return false;
98
+ if (operand.kind === want) return false;
99
+ this.pending.push({
100
+ rule,
101
+ message: this.texts.operandKindMismatch(
102
+ op,
103
+ side,
104
+ want,
105
+ this.describe(operand),
106
+ note,
107
+ ),
108
+ });
109
+ return true;
110
+ }
111
+
112
+ private ternary(): OperandKind {
113
+ const condKind = this.or();
114
+ if (!this.isPunct("?")) return condKind;
115
+ this.pos += 1;
116
+ const whenTrue = this.ternary();
117
+ if (!this.isPunct(":")) throw BAIL;
118
+ this.pos += 1;
119
+ const whenFalse = this.ternary();
120
+ let violated = false;
121
+ if (condKind !== "unknown" && condKind !== "bool") {
122
+ this.pending.push({
123
+ rule: "ternary-cond-non-bool",
124
+ message: this.texts.ternaryCondNotBool(
125
+ this.describe({ kind: condKind, label: "条件表达式" }),
126
+ ),
127
+ });
128
+ violated = true;
129
+ }
130
+ if (violated) return "unknown";
131
+ return whenTrue !== "unknown" ? whenTrue : whenFalse;
132
+ }
133
+
134
+ private or(): OperandKind {
135
+ let left = this.and();
136
+ while (this.eatOp("||")) {
137
+ const right = this.and();
138
+ const violated =
139
+ this.requireKind(
140
+ "||",
141
+ "logical-non-bool",
142
+ "左",
143
+ { kind: left, label: "左操作数" },
144
+ "bool",
145
+ ) ||
146
+ this.requireKind(
147
+ "||",
148
+ "logical-non-bool",
149
+ "右",
150
+ { kind: right, label: "右操作数" },
151
+ "bool",
152
+ ";右支仅在被求值时检查——'||' 左支为真即短路",
153
+ );
154
+ left = violated ? "unknown" : "bool";
155
+ }
156
+ return left;
157
+ }
158
+
159
+ private and(): OperandKind {
160
+ let left = this.equality();
161
+ while (this.eatOp("&&")) {
162
+ const right = this.equality();
163
+ const violated =
164
+ this.requireKind(
165
+ "&&",
166
+ "logical-non-bool",
167
+ "左",
168
+ { kind: left, label: "左操作数" },
169
+ "bool",
170
+ ) ||
171
+ this.requireKind(
172
+ "&&",
173
+ "logical-non-bool",
174
+ "右",
175
+ { kind: right, label: "右操作数" },
176
+ "bool",
177
+ ";右支仅在被求值时检查——'&&' 左支为假即短路",
178
+ );
179
+ left = violated ? "unknown" : "bool";
180
+ }
181
+ return left;
182
+ }
183
+
184
+ /** 相等级:至多一个 ==/!=(引擎不支持链式比较);仅同类型标量相等 */
185
+ private equality(): OperandKind {
186
+ const left = this.comparison();
187
+ let op: "==" | "!=" | null = null;
188
+ if (this.eatOp("==")) op = "==";
189
+ else if (this.eatOp("!=")) op = "!=";
190
+ if (op === null) return left;
191
+ const right = this.comparison();
192
+ if (left !== "unknown" && right !== "unknown" && left !== right) {
193
+ this.pending.push({
194
+ rule: "equality-kind-mismatch",
195
+ message: this.texts.crossTypeComparison(op, left, right),
196
+ });
197
+ return "unknown";
198
+ }
199
+ return "bool";
200
+ }
201
+
202
+ /** 比较级:至多一个比较符(引擎不支持链式比较);只支持 number */
203
+ private comparison(): OperandKind {
204
+ const left = this.additive();
205
+ let op: string | null = null;
206
+ for (const candidate of [">=", "<=", ">", "<"]) {
207
+ if (this.eatOp(candidate)) {
208
+ op = candidate;
209
+ break;
210
+ }
211
+ }
212
+ if (op === null) return left;
213
+ const right = this.additive();
214
+ const violated =
215
+ this.requireKind(
216
+ op,
217
+ "compare-non-num",
218
+ "左",
219
+ { kind: left, label: "左操作数" },
220
+ "num",
221
+ ) ||
222
+ this.requireKind(
223
+ op,
224
+ "compare-non-num",
225
+ "右",
226
+ { kind: right, label: "右操作数" },
227
+ "num",
228
+ );
229
+ return violated ? "unknown" : "bool";
230
+ }
231
+
232
+ private additive(): OperandKind {
233
+ let left = this.multiplicative();
234
+ for (;;) {
235
+ const op = this.eatOp("+") ? "+" : this.eatOp("-") ? "-" : null;
236
+ if (op === null) return left;
237
+ const right = this.multiplicative();
238
+ const violated =
239
+ this.requireKind(
240
+ op,
241
+ "arith-non-num",
242
+ "左",
243
+ { kind: left, label: "左操作数" },
244
+ "num",
245
+ ) ||
246
+ this.requireKind(
247
+ op,
248
+ "arith-non-num",
249
+ "右",
250
+ { kind: right, label: "右操作数" },
251
+ "num",
252
+ );
253
+ left = violated ? "unknown" : "num";
254
+ }
255
+ }
256
+
257
+ private multiplicative(): OperandKind {
258
+ let left = this.unary();
259
+ for (;;) {
260
+ const op = this.eatOp("*")
261
+ ? "*"
262
+ : this.eatOp("/")
263
+ ? "/"
264
+ : this.eatOp("%")
265
+ ? "%"
266
+ : null;
267
+ if (op === null) return left;
268
+ const right = this.unary();
269
+ const violated =
270
+ this.requireKind(
271
+ op,
272
+ "arith-non-num",
273
+ "左",
274
+ { kind: left, label: "左操作数" },
275
+ "num",
276
+ ) ||
277
+ this.requireKind(
278
+ op,
279
+ "arith-non-num",
280
+ "右",
281
+ { kind: right, label: "右操作数" },
282
+ "num",
283
+ );
284
+ left = violated ? "unknown" : "num";
285
+ }
286
+ }
287
+
288
+ private unary(): OperandKind {
289
+ if (this.eatOp("!")) {
290
+ const inner = this.unary();
291
+ if (
292
+ this.requireKind(
293
+ "!",
294
+ "unary-not-non-bool",
295
+ "",
296
+ { kind: inner, label: "操作数" },
297
+ "bool",
298
+ )
299
+ ) {
300
+ return "unknown";
301
+ }
302
+ return "bool";
303
+ }
304
+ if (this.eatOp("-")) {
305
+ const inner = this.unary();
306
+ if (
307
+ this.requireKind(
308
+ "-",
309
+ "unary-minus-non-num",
310
+ "",
311
+ { kind: inner, label: "操作数" },
312
+ "num",
313
+ )
314
+ ) {
315
+ return "unknown";
316
+ }
317
+ return "num";
318
+ }
319
+ return this.primary();
320
+ }
321
+
322
+ private primary(): OperandKind {
323
+ const tok = this.peek();
324
+ if (tok === undefined) throw BAIL;
325
+ if (tok.t === "operand") {
326
+ this.pos += 1;
327
+ // 函数调用:ident( … ) ⇒ 结果未知(内置函数参数规则不进本层)
328
+ if (this.isPunct("(")) {
329
+ this.skipBalancedParens();
330
+ return "unknown";
331
+ }
332
+ // 成员路径:ident.ident … ⇒ 结果未知(字典下钻的值型构建期不可知)
333
+ if (this.isPunct(".") && tok.kind === "unknown") {
334
+ while (this.isPunct(".")) {
335
+ this.pos += 1;
336
+ const seg = this.peek();
337
+ if (seg === undefined || seg.t !== "operand") throw BAIL;
338
+ this.pos += 1;
339
+ }
340
+ return "unknown";
341
+ }
342
+ return tok.kind;
343
+ }
344
+ if (tok.t === "punct" && tok.text === "(") {
345
+ this.pos += 1;
346
+ const inner = this.ternary();
347
+ if (!this.isPunct(")")) throw BAIL;
348
+ this.pos += 1;
349
+ return inner;
350
+ }
351
+ throw BAIL;
352
+ }
353
+
354
+ /** 括号平衡跳过(函数调用参数整体不判型) */
355
+ private skipBalancedParens(): void {
356
+ let depth = 0;
357
+ for (;;) {
358
+ const tok = this.peek();
359
+ if (tok === undefined) throw BAIL;
360
+ this.pos += 1;
361
+ if (tok.t === "punct") {
362
+ if (tok.text === "(") depth += 1;
363
+ if (tok.text === ")") {
364
+ depth -= 1;
365
+ if (depth === 0) return;
366
+ }
367
+ }
368
+ }
369
+ }
370
+ }