@lingfan-engine/stories-build 0.0.0-stage → 0.1.2

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 (110) hide show
  1. package/LICENSE +201 -0
  2. package/bin/stories-build.mjs +16 -0
  3. package/bin/stories-watch.mjs +16 -0
  4. package/package.json +50 -3
  5. package/src/authoring.d.ts +51 -0
  6. package/src/cell/errors.ts +12 -0
  7. package/src/cell/imports.ts +127 -0
  8. package/src/cell/index.ts +33 -0
  9. package/src/cell/render-register.ts +33 -0
  10. package/src/cell/scan-cells.ts +63 -0
  11. package/src/cell/scan-file.ts +108 -0
  12. package/src/cell/types.ts +27 -0
  13. package/src/cell/whitelist.ts +22 -0
  14. package/src/cli/cli.ts +99 -0
  15. package/src/cli/watch-cli.ts +42 -0
  16. package/src/compiler/cell-scan.ts +27 -0
  17. package/src/compiler/compiler-contract.ts +87 -0
  18. package/src/compiler/extension-load.ts +67 -0
  19. package/src/compiler/index.ts +22 -0
  20. package/src/compiler/story-compiler/assemble/functions.ts +202 -0
  21. package/src/compiler/story-compiler/assemble/index.ts +8 -0
  22. package/src/compiler/story-compiler/assemble/session.ts +207 -0
  23. package/src/compiler/story-compiler/assemble/types.ts +37 -0
  24. package/src/compiler/story-compiler/assemble.ts +5 -0
  25. package/src/compiler/story-compiler/graph/build.ts +208 -0
  26. package/src/compiler/story-compiler/graph/core.ts +102 -0
  27. package/src/compiler/story-compiler/graph/fold.ts +121 -0
  28. package/src/compiler/story-compiler/graph/index.ts +17 -0
  29. package/src/compiler/story-compiler/graph/scan/bindings.ts +277 -0
  30. package/src/compiler/story-compiler/graph/scan/imports.ts +170 -0
  31. package/src/compiler/story-compiler/graph/scan/index.ts +9 -0
  32. package/src/compiler/story-compiler/graph/scan/module-scan.ts +199 -0
  33. package/src/compiler/story-compiler/graph/state-root.ts +147 -0
  34. package/src/compiler/story-compiler/graph.ts +18 -0
  35. package/src/compiler/story-compiler/index.ts +34 -0
  36. package/src/compiler/story-compiler/translate/expr/assign.ts +204 -0
  37. package/src/compiler/story-compiler/translate/expr/engine-text.ts +16 -0
  38. package/src/compiler/story-compiler/translate/expr/expr.ts +174 -0
  39. package/src/compiler/story-compiler/translate/expr/index.ts +24 -0
  40. package/src/compiler/story-compiler/translate/expr/lower/index.ts +4 -0
  41. package/src/compiler/story-compiler/translate/expr/lower/lower.ts +266 -0
  42. package/src/compiler/story-compiler/translate/expr/lower/numeric.ts +56 -0
  43. package/src/compiler/story-compiler/translate/expr/probe.ts +23 -0
  44. package/src/compiler/story-compiler/translate/expr/state-path.ts +47 -0
  45. package/src/compiler/story-compiler/translate/expr/words/arg.ts +87 -0
  46. package/src/compiler/story-compiler/translate/expr/words/call.ts +125 -0
  47. package/src/compiler/story-compiler/translate/expr/words/callee.ts +43 -0
  48. package/src/compiler/story-compiler/translate/expr/words/command.ts +93 -0
  49. package/src/compiler/story-compiler/translate/expr/words/dispatch.ts +16 -0
  50. package/src/compiler/story-compiler/translate/expr/words/index.ts +8 -0
  51. package/src/compiler/story-compiler/translate/expr/words/static-for.ts +96 -0
  52. package/src/compiler/story-compiler/translate/font-check.ts +94 -0
  53. package/src/compiler/story-compiler/translate/index.ts +10 -0
  54. package/src/compiler/story-compiler/translate/shared.ts +132 -0
  55. package/src/compiler/story-compiler/translate/stmt/control/branch.ts +71 -0
  56. package/src/compiler/story-compiler/translate/stmt/control/index.ts +11 -0
  57. package/src/compiler/story-compiler/translate/stmt/control/iter.ts +72 -0
  58. package/src/compiler/story-compiler/translate/stmt/control/loop.ts +114 -0
  59. package/src/compiler/story-compiler/translate/stmt/control/switch.ts +75 -0
  60. package/src/compiler/story-compiler/translate/stmt/control-flow.ts +25 -0
  61. package/src/compiler/story-compiler/translate/stmt/index.ts +17 -0
  62. package/src/compiler/story-compiler/translate/stmt/statements.ts +145 -0
  63. package/src/compiler/story-compiler/translate.ts +15 -0
  64. package/src/compiler/story-compiler.ts +119 -0
  65. package/src/config/index.ts +15 -0
  66. package/src/config/manifest-read.ts +26 -0
  67. package/src/config/state-type.ts +85 -0
  68. package/src/config/story-config.ts +113 -0
  69. package/src/const-eval/config-eval.ts +181 -0
  70. package/src/const-eval/const-eval-pass.ts +133 -0
  71. package/src/const-eval/const-eval.ts +114 -0
  72. package/src/const-eval/const-expr.ts +358 -0
  73. package/src/const-eval/contracts.ts +31 -0
  74. package/src/const-eval/define-merge.ts +195 -0
  75. package/src/const-eval/diag/compare.ts +39 -0
  76. package/src/const-eval/diag/index.ts +5 -0
  77. package/src/const-eval/diag/locate.ts +45 -0
  78. package/src/const-eval/index.ts +44 -0
  79. package/src/const-eval/module-characters.ts +353 -0
  80. package/src/const-eval/module-define.ts +300 -0
  81. package/src/const-eval/round/evaluate.ts +149 -0
  82. package/src/const-eval/round/index.ts +4 -0
  83. package/src/const-eval/round/source.ts +134 -0
  84. package/src/const-eval/state-schema.ts +269 -0
  85. package/src/discovery/index.ts +7 -0
  86. package/src/discovery/source-discovery.ts +193 -0
  87. package/src/emit/compile-roundtrip.ts +45 -0
  88. package/src/emit/emit.ts +147 -0
  89. package/src/emit/index.ts +9 -0
  90. package/src/emit/report.ts +29 -0
  91. package/src/fonts/font-registry.ts +53 -0
  92. package/src/fonts/index.ts +7 -0
  93. package/src/index.ts +76 -0
  94. package/src/pipeline/gates.ts +107 -0
  95. package/src/pipeline/index.ts +18 -0
  96. package/src/pipeline/orchestrate.ts +459 -0
  97. package/src/pipeline/watch.ts +244 -0
  98. package/src/shared/errors.ts +2 -0
  99. package/src/shared/index.ts +11 -0
  100. package/src/shared/texts.ts +85 -0
  101. package/src/shared/walk-files.ts +18 -0
  102. package/src/typegen/collect.ts +107 -0
  103. package/src/typegen/font-render.ts +30 -0
  104. package/src/typegen/index.ts +18 -0
  105. package/src/typegen/render.ts +68 -0
  106. package/src/typegen/script-render.ts +21 -0
  107. package/src/typegen/state-enum-ast.ts +365 -0
  108. package/src/typegen/state-render.ts +242 -0
  109. package/src/typegen/types.ts +21 -0
  110. package/README.md +0 -3
@@ -0,0 +1,174 @@
1
+ import * as ts from "typescript";
2
+ import { diag, lookupBinding, lookupScope } from "../shared.ts";
3
+ import type { BodyCtx } from "../shared.ts";
4
+ import { probeExpression } from "./probe.ts";
5
+ import { lowerExpr } from "./lower/index.ts";
6
+ import { statePath } from "./state-path.ts";
7
+ import { quoteEngineString } from "./engine-text.ts";
8
+
9
+ export type Lowered =
10
+ | { kind: "lit"; value: string | number | boolean }
11
+ | { kind: "expr"; text: string }
12
+ | undefined;
13
+
14
+ export function lowerCond(
15
+ ctx: BodyCtx,
16
+ expr: ts.Expression,
17
+ ): string | undefined {
18
+ const inner = lowerExpr(ctx, expr);
19
+ if (inner === undefined) return undefined;
20
+ if (!probeExpression(inner.text)) {
21
+ diag(ctx, expr, `表达式 \`${inner.text}\` 不符合引擎表达式文法`);
22
+ return undefined;
23
+ }
24
+ return `{${inner.text}}`;
25
+ }
26
+
27
+ export function lowerValue(ctx: BodyCtx, expr: ts.Expression): Lowered {
28
+ const root = ctx.shared.stateRoot;
29
+ if (ts.isNumericLiteral(expr)) {
30
+ const n = Number(expr.text);
31
+ if (!Number.isFinite(n)) {
32
+ diag(ctx, expr, `数字字面量 \`${expr.text}\` 不是有限数`);
33
+ return undefined;
34
+ }
35
+ return { kind: "lit", value: n };
36
+ }
37
+ if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
38
+ return { kind: "lit", value: expr.text };
39
+ }
40
+ if (expr.kind === ts.SyntaxKind.TrueKeyword)
41
+ return { kind: "lit", value: true };
42
+ if (expr.kind === ts.SyntaxKind.FalseKeyword)
43
+ return { kind: "lit", value: false };
44
+ if (expr.kind === ts.SyntaxKind.NullKeyword) {
45
+ diag(ctx, expr, "null 不可作为值");
46
+ return undefined;
47
+ }
48
+ if (ts.isParenthesizedExpression(expr))
49
+ return lowerValue(ctx, expr.expression);
50
+ if (ts.isTemplateExpression(expr)) {
51
+ const parts: string[] = [expr.head.text];
52
+ for (const span of expr.templateSpans) {
53
+ if (
54
+ ts.isStringLiteral(span.expression) ||
55
+ ts.isNoSubstitutionTemplateLiteral(span.expression)
56
+ ) {
57
+ parts.push(span.expression.text);
58
+ } else if (
59
+ span.expression.kind === ts.SyntaxKind.TrueKeyword ||
60
+ span.expression.kind === ts.SyntaxKind.FalseKeyword
61
+ ) {
62
+ parts.push(
63
+ span.expression.kind === ts.SyntaxKind.TrueKeyword ? "true" : "false",
64
+ );
65
+ } else if (ts.isNumericLiteral(span.expression)) {
66
+ parts.push(String(Number(span.expression.text)));
67
+ } else {
68
+ const lowered = lowerExpr(ctx, span.expression);
69
+ if (lowered === undefined) return undefined;
70
+ if (!probeExpression(lowered.text)) {
71
+ diag(
72
+ ctx,
73
+ span.expression,
74
+ `插值表达式 \`${lowered.text}\` 不符合引擎表达式文法`,
75
+ );
76
+ return undefined;
77
+ }
78
+ parts.push(`{${lowered.text}}`);
79
+ }
80
+ parts.push(span.literal.text);
81
+ }
82
+ return { kind: "lit", value: parts.join("") };
83
+ }
84
+ if (ts.isIdentifier(expr)) {
85
+ const name = expr.text;
86
+ if (name === root) {
87
+ diag(ctx, expr, `${root} 必须以成员路径使用:${root}.player.gold`);
88
+ return undefined;
89
+ }
90
+ const scoped = lookupScope(ctx, name);
91
+ if (scoped !== undefined) {
92
+ if (scoped.kind === "index")
93
+ return { kind: "lit", value: scoped.value ?? 0 };
94
+ return { kind: "expr", text: name };
95
+ }
96
+ const binding = lookupBinding(ctx, name);
97
+ if (binding === undefined) {
98
+ diag(ctx, expr, `未知标识符 \`${name}\``);
99
+ return undefined;
100
+ }
101
+ if (binding.kind === "const") {
102
+ if (binding.value === null) {
103
+ diag(ctx, expr, "常量值为 null,不可作为值");
104
+ return undefined;
105
+ }
106
+ return { kind: "lit", value: binding.value };
107
+ }
108
+ if (binding.kind === "enum") {
109
+ diag(ctx, expr, `枚举 \`${name}\` 必须以成员形式使用:${name}.成员`);
110
+ return undefined;
111
+ }
112
+ diag(ctx, expr, `\`${name}\` 不是值(${binding.kind} 绑定不能作为值)`);
113
+ return undefined;
114
+ }
115
+ if (ts.isPropertyAccessExpression(expr)) {
116
+ const object = expr.expression;
117
+ if (ts.isIdentifier(object) && object.text !== root) {
118
+ const binding = lookupBinding(ctx, object.text);
119
+ if (binding?.kind === "enum") {
120
+ const value = binding.members.get(expr.name.text);
121
+ if (value === undefined) {
122
+ diag(
123
+ ctx,
124
+ expr,
125
+ `枚举 ${binding.name} 没有成员 \`${expr.name.text}\``,
126
+ );
127
+ return undefined;
128
+ }
129
+ if (value === null) {
130
+ diag(ctx, expr, "枚举成员值为 null,不可作为值");
131
+ return undefined;
132
+ }
133
+ return { kind: "lit", value };
134
+ }
135
+ }
136
+ const target = statePath(ctx, expr);
137
+ if (target === undefined) return undefined;
138
+ // 可选段脱糖:state.player?.nick → optGet("player.nick")(路径缺失返回 null,配 ?? 兜底)
139
+ if (target.optional) {
140
+ return {
141
+ kind: "expr",
142
+ text: `optGet(${quoteEngineString(target.path)})`,
143
+ };
144
+ }
145
+ return { kind: "expr", text: target.path };
146
+ }
147
+ if (
148
+ ts.isBinaryExpression(expr) ||
149
+ ts.isPrefixUnaryExpression(expr) ||
150
+ ts.isConditionalExpression(expr) ||
151
+ ts.isCallExpression(expr) ||
152
+ ts.isElementAccessExpression(expr)
153
+ ) {
154
+ const lowered = lowerExpr(ctx, expr);
155
+ if (lowered === undefined) return undefined;
156
+ if (!probeExpression(lowered.text)) {
157
+ diag(ctx, expr, `表达式 \`${lowered.text}\` 不符合引擎表达式文法`);
158
+ return undefined;
159
+ }
160
+ return { kind: "expr", text: lowered.text };
161
+ }
162
+ diag(
163
+ ctx,
164
+ expr,
165
+ "值表达式不支持此形态(数组请用 newArray 词、对象请用 dict 词)",
166
+ );
167
+ return undefined;
168
+ }
169
+
170
+ export function loweredToScriptValue(
171
+ lowered: NonNullable<Lowered>,
172
+ ): string | number | boolean {
173
+ return lowered.kind === "lit" ? lowered.value : `{${lowered.text}}`;
174
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 表达式与赋值族出口:把 TS 表达式 / 赋值语句降成引擎命令与脚本值。
3
+ *
4
+ * 轴 = 值层四件——条件与取值降级(`expr.ts`)、表达式降级与路径归约
5
+ * (`lower/` + `state-path.ts` + `probe.ts` + `engine-text.ts`)、
6
+ * 词表调用与命令发射(`words/`)、赋值与自增(`assign.ts`)。
7
+ */
8
+ export {
9
+ emitAssignment,
10
+ translateIncrement,
11
+ translateVariableStatement,
12
+ } from "./assign.ts";
13
+ export { lowerCond, loweredToScriptValue, lowerValue } from "./expr.ts";
14
+ export type { Lowered } from "./expr.ts";
15
+ export { lowerExpr } from "./lower/index.ts";
16
+ export { probeExpression } from "./probe.ts";
17
+ export { statePath } from "./state-path.ts";
18
+ export type { StatePath } from "./state-path.ts";
19
+ export {
20
+ callWord,
21
+ emitCallCommand,
22
+ emitCommand,
23
+ resolveFunctionCallee,
24
+ } from "./words/index.ts";
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 表达式降级族出口:外部只从这里取 `lowerExpr`。
3
+ */
4
+ export { lowerExpr } from "./lower.ts";
@@ -0,0 +1,266 @@
1
+ /**
2
+ * 表达式降级主分发:把 TS 表达式逐个形态降成引擎表达式文本 + 优先级。
3
+ *
4
+ * 白名单外的形态一律 `diag` 报诊断后返回 undefined(fail-closed,不做猜测性转换);
5
+ * 值位置不支持函数调用(词函数只能作为命令)。递归自调用,所以必须在同一文件内。
6
+ */
7
+ import * as ts from "typescript";
8
+ import { diag, lookupBinding, lookupScope } from "../../shared.ts";
9
+ import type { BodyCtx } from "../../shared.ts";
10
+ import { quoteEngineString, engineValueText } from "../engine-text.ts";
11
+ import { statePath } from "../state-path.ts";
12
+ import { PRECEDENCE, wrapOperand, foldNumericBinary } from "./numeric.ts";
13
+
14
+ const BUILTIN_EXPR_FNS = new Set([
15
+ "random",
16
+ "min",
17
+ "max",
18
+ "abs",
19
+ "clamp",
20
+ "arrayGet",
21
+ "dictGet",
22
+ "optGet",
23
+ "len",
24
+ ]);
25
+ /** 路径型内建:首参必须是路径字符串字面量(路径在构建期定型,不接受变量) */
26
+ const PATH_BUILTINS = new Set(["arrayGet", "dictGet", "optGet", "len"]);
27
+
28
+ export function lowerExpr(
29
+ ctx: BodyCtx,
30
+ expr: ts.Expression,
31
+ ): { text: string; prec: number } | undefined {
32
+ const sf = ctx.shared.mod.sf;
33
+ const root = ctx.shared.stateRoot;
34
+ if (ts.isNumericLiteral(expr)) {
35
+ const n = Number(expr.text);
36
+ if (!Number.isFinite(n)) {
37
+ diag(ctx, expr, `数字字面量 \`${expr.text}\` 不是有限数`);
38
+ return undefined;
39
+ }
40
+ return { text: String(n), prec: 9 };
41
+ }
42
+ if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
43
+ return { text: quoteEngineString(expr.text), prec: 9 };
44
+ }
45
+ if (expr.kind === ts.SyntaxKind.TrueKeyword) return { text: "true", prec: 9 };
46
+ if (expr.kind === ts.SyntaxKind.FalseKeyword)
47
+ return { text: "false", prec: 9 };
48
+ if (ts.isParenthesizedExpression(expr)) {
49
+ const inner = lowerExpr(ctx, expr.expression);
50
+ return inner === undefined ? undefined : { text: inner.text, prec: 9 };
51
+ }
52
+ if (ts.isIdentifier(expr)) {
53
+ const name = expr.text;
54
+ if (name === root) {
55
+ diag(ctx, expr, `${root} 必须以成员路径使用:${root}.player.gold`);
56
+ return undefined;
57
+ }
58
+ const scoped = lookupScope(ctx, name);
59
+ if (scoped !== undefined) {
60
+ if (scoped.kind === "index") {
61
+ return { text: String(scoped.value), prec: 9 };
62
+ }
63
+ if (scoped.kind === "function") {
64
+ diag(ctx, expr, `函数 \`${name}\` 不能作为值`);
65
+ return undefined;
66
+ }
67
+ return { text: name, prec: 9 };
68
+ }
69
+ const binding = lookupBinding(ctx, name);
70
+ if (binding === undefined) {
71
+ diag(
72
+ ctx,
73
+ expr,
74
+ `未知标识符 \`${name}\`(未声明的 let、模块 const、枚举或词函数)`,
75
+ );
76
+ return undefined;
77
+ }
78
+ switch (binding.kind) {
79
+ case "const": {
80
+ const v = binding.value;
81
+ if (v === null) {
82
+ diag(ctx, expr, "常量值为 null,不可下沉到表达式");
83
+ return undefined;
84
+ }
85
+ return { text: engineValueText(v), prec: 9 };
86
+ }
87
+ case "enum":
88
+ diag(ctx, expr, `枚举 \`${name}\` 必须以成员形式使用:${name}.成员`);
89
+ return undefined;
90
+ default:
91
+ diag(
92
+ ctx,
93
+ expr,
94
+ `\`${name}\` 不能用于表达式(${binding.kind} 绑定不可求值)`,
95
+ );
96
+ return undefined;
97
+ }
98
+ }
99
+ if (ts.isPropertyAccessExpression(expr)) {
100
+ const object = expr.expression;
101
+ if (ts.isIdentifier(object) && object.text !== root) {
102
+ const binding = lookupBinding(ctx, object.text);
103
+ if (binding?.kind === "enum") {
104
+ const value = binding.members.get(expr.name.text);
105
+ if (value === undefined) {
106
+ diag(
107
+ ctx,
108
+ expr,
109
+ `枚举 ${binding.name} 没有成员 \`${expr.name.text}\``,
110
+ );
111
+ return undefined;
112
+ }
113
+ if (value === null) {
114
+ diag(ctx, expr, "枚举成员值为 null,不可下沉到表达式");
115
+ return undefined;
116
+ }
117
+ return { text: engineValueText(value), prec: 9 };
118
+ }
119
+ }
120
+ const target = statePath(ctx, expr);
121
+ if (target === undefined) return undefined;
122
+ // 可选段脱糖:state.player?.nick → optGet("player.nick")(路径缺失返回 null,配 ?? 兜底)
123
+ if (target.optional) {
124
+ return { text: `optGet(${quoteEngineString(target.path)})`, prec: 9 };
125
+ }
126
+ return { text: target.path, prec: 9 };
127
+ }
128
+ if (ts.isElementAccessExpression(expr)) {
129
+ const target = statePath(ctx, expr.expression);
130
+ if (target === undefined) return undefined;
131
+ // `?.[` 不支持:内建缺失语义(null)已覆盖可选需求,不再叠加可选层
132
+ if (expr.questionDotToken !== undefined || target.optional) {
133
+ diag(ctx, expr, `${root} 下标访问不支持可选链 ?.[]`);
134
+ return undefined;
135
+ }
136
+ const arg = expr.argumentExpression;
137
+ if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) {
138
+ return {
139
+ text: `dictGet(${quoteEngineString(target.path)}, ${quoteEngineString(arg.text)})`,
140
+ prec: 9,
141
+ };
142
+ }
143
+ const index = lowerExpr(ctx, arg);
144
+ if (index === undefined) return undefined;
145
+ return {
146
+ text: `arrayGet(${quoteEngineString(target.path)}, ${index.text})`,
147
+ prec: 9,
148
+ };
149
+ }
150
+ if (ts.isPrefixUnaryExpression(expr)) {
151
+ const opText =
152
+ expr.operator === ts.SyntaxKind.ExclamationToken
153
+ ? "!"
154
+ : expr.operator === ts.SyntaxKind.MinusToken
155
+ ? "-"
156
+ : undefined;
157
+ if (opText === undefined) {
158
+ diag(ctx, expr, "一元表达式只支持 ! 与 -(自增自减只能作为独立语句)");
159
+ return undefined;
160
+ }
161
+ const operand = lowerExpr(ctx, expr.operand);
162
+ if (operand === undefined) return undefined;
163
+ return {
164
+ text: opText + wrapOperand(operand.text, operand.prec, 8),
165
+ prec: 8,
166
+ };
167
+ }
168
+ if (ts.isBinaryExpression(expr)) {
169
+ const raw = expr.operatorToken.getText(sf);
170
+ const op = raw === "===" ? "==" : raw === "!==" ? "!=" : raw;
171
+ const prec = PRECEDENCE[op];
172
+ if (prec === undefined) {
173
+ diag(ctx, expr, `二元运算符 \`${raw}\` 不在引擎表达式文法内`);
174
+ return undefined;
175
+ }
176
+ const left = lowerExpr(ctx, expr.left);
177
+ const right = lowerExpr(ctx, expr.right);
178
+ if (left === undefined || right === undefined) return undefined;
179
+ const folded = foldNumericBinary(left.text, right.text, op);
180
+ if (folded !== undefined) return { text: folded, prec: 9 };
181
+ return {
182
+ text: `${wrapOperand(left.text, left.prec, prec)} ${op} ${wrapOperand(right.text, right.prec, prec + 1)}`,
183
+ prec,
184
+ };
185
+ }
186
+ if (ts.isConditionalExpression(expr)) {
187
+ const cond = lowerExpr(ctx, expr.condition);
188
+ const whenTrue = lowerExpr(ctx, expr.whenTrue);
189
+ const whenFalse = lowerExpr(ctx, expr.whenFalse);
190
+ if (cond === undefined || whenTrue === undefined || whenFalse === undefined)
191
+ return undefined;
192
+ return {
193
+ text: `${wrapOperand(cond.text, cond.prec, 1)} ? ${wrapOperand(whenTrue.text, whenTrue.prec, 2)} : ${wrapOperand(whenFalse.text, whenFalse.prec, 1)}`,
194
+ prec: 1,
195
+ };
196
+ }
197
+ if (ts.isCallExpression(expr)) {
198
+ if (
199
+ ts.isIdentifier(expr.expression) &&
200
+ lookupBinding(ctx, expr.expression.text) === undefined &&
201
+ BUILTIN_EXPR_FNS.has(expr.expression.text)
202
+ ) {
203
+ const fnName = expr.expression.text;
204
+ const args: string[] = [];
205
+ if (PATH_BUILTINS.has(fnName)) {
206
+ // 路径型内建首参在构建期定型:state 路径降级成路径串,或直接写字符串字面量;
207
+ // let 变量等运行期值当路径无法进快照 ⇒ fail-closed
208
+ const first = expr.arguments[0];
209
+ const asPath =
210
+ first !== undefined && ts.isPropertyAccessExpression(first)
211
+ ? statePath(ctx, first)
212
+ : undefined;
213
+ let firstText: string | undefined;
214
+ if (asPath !== undefined) {
215
+ if (asPath.optional) {
216
+ diag(ctx, first!, `${fnName} 路径不支持可选链`);
217
+ return undefined;
218
+ }
219
+ firstText = quoteEngineString(asPath.path);
220
+ } else if (
221
+ first !== undefined &&
222
+ (ts.isStringLiteral(first) ||
223
+ ts.isNoSubstitutionTemplateLiteral(first))
224
+ ) {
225
+ firstText = quoteEngineString(first.text);
226
+ } else {
227
+ diag(
228
+ ctx,
229
+ expr,
230
+ `${fnName} 首参必须是路径(${root} 路径或字符串字面量)`,
231
+ );
232
+ return undefined;
233
+ }
234
+ args.push(firstText);
235
+ for (const arg of expr.arguments.slice(1)) {
236
+ const lowered = lowerExpr(ctx, arg);
237
+ if (lowered === undefined) return undefined;
238
+ args.push(lowered.text);
239
+ }
240
+ return { text: `${fnName}(${args.join(", ")})`, prec: 9 };
241
+ }
242
+ for (const arg of expr.arguments) {
243
+ const lowered = lowerExpr(ctx, arg);
244
+ if (lowered === undefined) return undefined;
245
+ args.push(lowered.text);
246
+ }
247
+ return { text: `${fnName}(${args.join(", ")})`, prec: 9 };
248
+ }
249
+ diag(
250
+ ctx,
251
+ expr,
252
+ "值位置的函数调用不支持:词函数只能作为命令,子程序返回值请用语句级 `const x = f(1)`(call into 结果通道),表达式内建函数仅 random/min/max/abs/clamp/arrayGet/dictGet/optGet/len",
253
+ );
254
+ return undefined;
255
+ }
256
+ if (ts.isTemplateExpression(expr)) {
257
+ diag(
258
+ ctx,
259
+ expr,
260
+ "模板串只能作为字符串值(say 文本等),不能用于条件/算术表达式",
261
+ );
262
+ return undefined;
263
+ }
264
+ diag(ctx, expr, "表达式不在引擎文法白名单内(TS-only 结构不支持)");
265
+ return undefined;
266
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 数字与优先级的构建期处理:运算符优先级表、按优先级加括号、数字字面量折叠。
3
+ *
4
+ * 折叠只处理「两侧都是数字字面量」的算术式;除/模零不折叠,留给运行期文法校验(fail-closed)。
5
+ */
6
+ export const PRECEDENCE: Record<string, number> = {
7
+ "||": 2,
8
+ "??": 1,
9
+ "&&": 3,
10
+ "==": 4,
11
+ "!=": 4,
12
+ "<": 5,
13
+ ">": 5,
14
+ "<=": 5,
15
+ ">=": 5,
16
+ "+": 6,
17
+ "-": 6,
18
+ "*": 7,
19
+ "/": 7,
20
+ "%": 7,
21
+ };
22
+
23
+ export function wrapOperand(
24
+ child: string,
25
+ childPrec: number,
26
+ parentPrec: number,
27
+ ): string {
28
+ return childPrec < parentPrec ? `(${child})` : child;
29
+ }
30
+
31
+ const NUMERIC_TEXT = /^-?\d+(?:\.\d+)?$/;
32
+
33
+ /** 两侧均为数字字面量的算术二元式在构建期折叠成单值(除/模零不折叠,留给运行期文法校验)。 */
34
+ export function foldNumericBinary(
35
+ left: string,
36
+ right: string,
37
+ op: string,
38
+ ): string | undefined {
39
+ if (!NUMERIC_TEXT.test(left) || !NUMERIC_TEXT.test(right)) return undefined;
40
+ const a = Number(left);
41
+ const b = Number(right);
42
+ switch (op) {
43
+ case "+":
44
+ return String(a + b);
45
+ case "-":
46
+ return String(a - b);
47
+ case "*":
48
+ return String(a * b);
49
+ case "/":
50
+ return b === 0 ? undefined : String(a / b);
51
+ case "%":
52
+ return b === 0 ? undefined : String(a % b);
53
+ default:
54
+ return undefined;
55
+ }
56
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 引擎表达式文法探针:把候选文本交给引擎求值器试跑,判断是否落在引擎文法内。
3
+ *
4
+ * 探针只关心文法(`evaluateExpression` 抛错即不合法),结果按源串缓存——
5
+ * 同一表达式在构建期会被反复试探,缓存是必须的(模块级单例,不可复制成两份)。
6
+ */
7
+ import { evaluateExpression } from "@lingfan-engine/core";
8
+
9
+ const probeCache = new Set<string>();
10
+
11
+ /** 探针免检前缀:路径型内建的实参由编译器构造性生成(文法必然合法),运行期语义不归探针管 */
12
+ const PROBE_LENIENT = /^(arrayGet|dictGet|optGet|len)\(/;
13
+
14
+ export function probeExpression(src: string): boolean {
15
+ if (probeCache.has(src) || PROBE_LENIENT.test(src)) return true;
16
+ try {
17
+ evaluateExpression(src, () => ({ found: true as const, value: 1 }));
18
+ probeCache.add(src);
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * state 成员表达式的路径归约:把 `state.a.b?.c` 收成引擎侧的点路径 + 可选段标记。
3
+ *
4
+ * 为什么单独一层:值降级(`lower/lower.ts`)与命令族(`assign.ts` / `stmt/control-flow.ts`)
5
+ * 都要判「这是不是 state 路径」,判错就会把运行期值当构建期路径写进快照。
6
+ */
7
+ import * as ts from "typescript";
8
+ import { diag } from "../shared.ts";
9
+ import type { BodyCtx } from "../shared.ts";
10
+
11
+ export interface StatePath {
12
+ /** 根名剥离后的点路径(如 state.player.gold → "player.gold") */
13
+ path: string;
14
+ /** 链上存在 `?.`(可选段);值位置会脱糖为 optGet,写位置/for-of 集合不支持 */
15
+ optional: boolean;
16
+ }
17
+
18
+ export function statePath(
19
+ ctx: BodyCtx,
20
+ expr: ts.Expression,
21
+ ): StatePath | undefined {
22
+ const root = ctx.shared.stateRoot;
23
+ const parts: string[] = [];
24
+ let cur: ts.Expression = expr;
25
+ let optional = false;
26
+ for (;;) {
27
+ if (ts.isPropertyAccessExpression(cur)) {
28
+ if (cur.questionDotToken !== undefined) optional = true;
29
+ parts.unshift(cur.name.text);
30
+ cur = cur.expression;
31
+ continue;
32
+ }
33
+ if (ts.isIdentifier(cur) && cur.text === root) {
34
+ if (parts.length === 0) {
35
+ diag(ctx, expr, `${root} 必须以成员路径使用:${root}.player.gold`);
36
+ return undefined;
37
+ }
38
+ return { path: parts.join("."), optional };
39
+ }
40
+ diag(
41
+ ctx,
42
+ expr,
43
+ `成员表达式只支持 ${root} 路径(如 ${root}.player.gold)或枚举成员访问`,
44
+ );
45
+ return undefined;
46
+ }
47
+ }
@@ -0,0 +1,87 @@
1
+ import * as ts from "typescript";
2
+ import { diag, lookupBinding } from "../../shared.ts";
3
+ import type { BodyCtx } from "../../shared.ts";
4
+ import { lowerValue, loweredToScriptValue } from "../expr.ts";
5
+ import { wordCalleeName } from "./callee.ts";
6
+ import type { WordDispatch } from "./dispatch.ts";
7
+
8
+ // —— 词实参求值(静态求值 + 嵌套命令递归)——
9
+
10
+ export function evaluateWordArg(
11
+ ctx: BodyCtx,
12
+ expr: ts.Expression,
13
+ dispatch: WordDispatch,
14
+ ): unknown {
15
+ if (ts.isIdentifier(expr) && expr.text === "undefined") return undefined;
16
+ if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) {
17
+ diag(ctx, expr, "函数实参仅 staticFor 支持");
18
+ return undefined;
19
+ }
20
+ if (ts.isIdentifier(expr)) {
21
+ const binding = lookupBinding(ctx, expr.text);
22
+ if (binding?.kind === "cell") {
23
+ return { name: binding.guard };
24
+ }
25
+ }
26
+ if (ts.isArrayLiteralExpression(expr)) {
27
+ const out: unknown[] = [];
28
+ for (const el of expr.elements) {
29
+ if (ts.isSpreadElement(el)) {
30
+ diag(ctx, el, "展开实参不支持");
31
+ return undefined;
32
+ }
33
+ const item = evaluateWordArg(ctx, el, dispatch);
34
+ const isExplicitUndefined =
35
+ ts.isIdentifier(el) && el.text === "undefined";
36
+ if (item === undefined && !isExplicitUndefined) return undefined;
37
+ out.push(item);
38
+ }
39
+ return out;
40
+ }
41
+ if (ts.isObjectLiteralExpression(expr)) {
42
+ const out: Record<string, unknown> = {};
43
+ for (const prop of expr.properties) {
44
+ if (!ts.isPropertyAssignment(prop)) {
45
+ diag(
46
+ ctx,
47
+ prop,
48
+ "对象实参只支持 key: value 形式(不支持展开/方法/计算键)",
49
+ );
50
+ return undefined;
51
+ }
52
+ const key =
53
+ ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)
54
+ ? prop.name.text
55
+ : undefined;
56
+ if (key === undefined) {
57
+ diag(ctx, prop, "对象实参键必须是标识符或字符串字面量");
58
+ return undefined;
59
+ }
60
+ const value = evaluateWordArg(ctx, prop.initializer, dispatch);
61
+ const isExplicitUndefined =
62
+ ts.isIdentifier(prop.initializer) &&
63
+ prop.initializer.text === "undefined";
64
+ if (value === undefined && !isExplicitUndefined) return undefined;
65
+ out[key] = value;
66
+ }
67
+ return out;
68
+ }
69
+ if (ts.isCallExpression(expr)) {
70
+ // 值位置的词调用也得分具名与 `script.` 命名空间两种写法——`option(...)` 能当菜单
71
+ // 选项、`script.option(...)` 同样是同一个词,落到 lowerValue 只会得到「值位置的函数
72
+ // 调用不支持」,而真正该报的是这个词自己的实参问题。
73
+ const wordName = wordCalleeName(ctx, expr);
74
+ if (wordName !== undefined) {
75
+ const ops = dispatch.command(ctx, expr);
76
+ if (ops.length === 0) return undefined;
77
+ if (ops.length !== 1) {
78
+ diag(ctx, expr, `值位置词调用 \`${wordName}\` 必须恰好产出一个命令`);
79
+ return undefined;
80
+ }
81
+ return ops[0];
82
+ }
83
+ }
84
+ const lowered = lowerValue(ctx, expr);
85
+ if (lowered === undefined) return undefined;
86
+ return loweredToScriptValue(lowered);
87
+ }