@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,199 @@
1
+ import * as ts from "typescript";
2
+ import { scanEnum } from "../fold.ts";
3
+ import type { Binding, DiagBag, ModuleRecord } from "../core.ts";
4
+
5
+ /**
6
+ * 模块语句扫描:把单个 .ts 模块的顶层语句归类登记进 ModuleRecord
7
+ * (导入/导出/函数/enum/const/构建期 define 与角色声明/故事 default 导出),
8
+ * 遇到不支持的语句形态记诊断。状态根名发现独立在 ./state-root.ts。
9
+ */
10
+
11
+ /**
12
+ * node_modules 值导入的本地名登记:扫描期即收集(scanConstStatement
13
+ * 先于第二遍导入解析运行,const 初始化链的专用提示在登记时就要可用)。是否放行
14
+ * 该导入由 resolveImports 依求值喂入决定;`import type` 与纯类型元素不登记。
15
+ */
16
+ function collectNpmNames(
17
+ record: ModuleRecord,
18
+ imp: ts.ImportDeclaration,
19
+ ): void {
20
+ const spec = ts.isStringLiteral(imp.moduleSpecifier)
21
+ ? imp.moduleSpecifier.text
22
+ : "";
23
+ if (
24
+ spec.startsWith(".") ||
25
+ spec === "@lingfan-engine/editor" ||
26
+ spec === "@lingfan-engine/core"
27
+ )
28
+ return;
29
+ const clause = imp.importClause;
30
+ if (clause === undefined || clause.isTypeOnly) return;
31
+ if (clause.name !== undefined) record.npmNames.add(clause.name.text);
32
+ if (
33
+ clause.namedBindings !== undefined &&
34
+ ts.isNamespaceImport(clause.namedBindings)
35
+ ) {
36
+ record.npmNames.add(clause.namedBindings.name.text);
37
+ }
38
+ if (
39
+ clause.namedBindings !== undefined &&
40
+ ts.isNamedImports(clause.namedBindings)
41
+ ) {
42
+ for (const el of clause.namedBindings.elements) {
43
+ if (el.isTypeOnly) continue;
44
+ record.npmNames.add(el.propertyName?.text ?? el.name.text);
45
+ }
46
+ }
47
+ }
48
+
49
+ export function scanModule(record: ModuleRecord, diags: DiagBag): void {
50
+ const { sf, rel } = record;
51
+ for (const stmt of sf.statements) {
52
+ if (ts.isImportDeclaration(stmt)) {
53
+ record.imports.push(stmt);
54
+ collectNpmNames(record, stmt);
55
+ continue;
56
+ }
57
+ if (ts.isExportDeclaration(stmt)) {
58
+ if (
59
+ stmt.moduleSpecifier !== undefined ||
60
+ stmt.exportClause === undefined ||
61
+ !ts.isNamedExports(stmt.exportClause)
62
+ ) {
63
+ diags.add(rel, sf, stmt, "转发导出不支持:请直接从来源模块导入");
64
+ continue;
65
+ }
66
+ for (const el of stmt.exportClause.elements) {
67
+ const wanted = el.propertyName?.text ?? el.name.text;
68
+ const binding = record.bindings.get(wanted);
69
+ if (binding === undefined) {
70
+ diags.add(rel, sf, el, `本地未声明 \`${wanted}\`,无法导出`);
71
+ continue;
72
+ }
73
+ record.exports.set(el.name.text, binding);
74
+ }
75
+ continue;
76
+ }
77
+ if (ts.isExportAssignment(stmt)) {
78
+ if (stmt.isExportEquals) {
79
+ diags.add(rel, sf, stmt, "不支持 export = 形式");
80
+ continue;
81
+ }
82
+ if (
83
+ ts.isFunctionDeclaration(stmt.expression) &&
84
+ stmt.expression.name !== undefined &&
85
+ stmt.expression.body !== undefined
86
+ ) {
87
+ record.story = {
88
+ name: stmt.expression.name.text,
89
+ node: stmt.expression,
90
+ };
91
+ } else {
92
+ diags.add(
93
+ rel,
94
+ sf,
95
+ stmt,
96
+ "default 导出必须是具名函数声明:export default function 名字() { … }",
97
+ );
98
+ }
99
+ continue;
100
+ }
101
+ if (ts.isFunctionDeclaration(stmt)) {
102
+ if (stmt.name === undefined) continue;
103
+ const hasDefault =
104
+ stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) ??
105
+ false;
106
+ if (hasDefault) {
107
+ if (stmt.body === undefined) {
108
+ diags.add(
109
+ rel,
110
+ sf,
111
+ stmt,
112
+ "default 导出必须是具名函数声明:export default function 名字() { … }",
113
+ );
114
+ continue;
115
+ }
116
+ record.story = { name: stmt.name.text, node: stmt };
117
+ continue;
118
+ }
119
+ if (stmt.body === undefined) {
120
+ const declared: Binding = { kind: "declare", name: stmt.name.text };
121
+ record.bindings.set(stmt.name.text, declared);
122
+ if (
123
+ stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
124
+ ) {
125
+ record.exports.set(stmt.name.text, declared);
126
+ }
127
+ continue;
128
+ }
129
+ const binding: Binding = {
130
+ kind: "function",
131
+ file: rel,
132
+ name: stmt.name.text,
133
+ };
134
+ record.functions.set(stmt.name.text, stmt);
135
+ record.bindings.set(stmt.name.text, binding);
136
+ if (stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
137
+ record.exports.set(stmt.name.text, binding);
138
+ }
139
+ continue;
140
+ }
141
+ if (ts.isEnumDeclaration(stmt)) {
142
+ const members = scanEnum(stmt, sf, diags, rel);
143
+ if (members === null) continue;
144
+ const binding: Binding = {
145
+ kind: "enum",
146
+ file: rel,
147
+ name: stmt.name.text,
148
+ members,
149
+ };
150
+ record.bindings.set(stmt.name.text, binding);
151
+ if (stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
152
+ record.exports.set(stmt.name.text, binding);
153
+ }
154
+ continue;
155
+ }
156
+ if (ts.isVariableStatement(stmt)) {
157
+ record.consts.push(stmt);
158
+ continue;
159
+ }
160
+ if (ts.isExpressionStatement(stmt) && isBuildDefineStatement(stmt)) {
161
+ continue; // 构建期 define 语句:由模块层收集器求值合并,图谱层不展开
162
+ }
163
+ if (ts.isExpressionStatement(stmt) && isBuildCharacterStatement(stmt)) {
164
+ continue; // 构建期角色声明:由模块层收集器求值前置,图谱层不展开
165
+ }
166
+ if (
167
+ ts.isInterfaceDeclaration(stmt) ||
168
+ ts.isTypeAliasDeclaration(stmt) ||
169
+ ts.isModuleDeclaration(stmt) ||
170
+ stmt.kind === ts.SyntaxKind.ImportEqualsDeclaration
171
+ ) {
172
+ continue;
173
+ }
174
+ diags.add(
175
+ rel,
176
+ sf,
177
+ stmt,
178
+ "模块层不支持此语句:只允许 const/函数/enum/导入/类型声明/default 导出、构建期 define 与构建期角色声明",
179
+ );
180
+ }
181
+ }
182
+
183
+ /** 顶层 `define(…)` 表达式语句(构建期合并树词;参数合法性由收集器判定) */
184
+ function isBuildDefineStatement(stmt: ts.ExpressionStatement): boolean {
185
+ return (
186
+ ts.isCallExpression(stmt.expression) &&
187
+ ts.isIdentifier(stmt.expression.expression) &&
188
+ stmt.expression.expression.text === "define"
189
+ );
190
+ }
191
+
192
+ /** 顶层 `character(…)` 表达式语句(构建期角色声明;参数合法性由收集器判定) */
193
+ function isBuildCharacterStatement(stmt: ts.ExpressionStatement): boolean {
194
+ return (
195
+ ts.isCallExpression(stmt.expression) &&
196
+ ts.isIdentifier(stmt.expression.expression) &&
197
+ stmt.expression.expression.text === "character"
198
+ );
199
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * 状态根名发现(SSOT):全树唯一的 `export declare const <根名>: StoryState`
3
+ * (或 ambient 桥 `declare global { var <根名>: StoryState }`,注解均可是本文件
4
+ * type 别名链)决定源侧状态前缀,转译剥根按该名进行。无声明回退 DEFAULT_STATE_ROOT;
5
+ * 根名与词函数冲突、或多个异名声明并存 → fail-closed 诊断(定位声明处)。
6
+ */
7
+ import * as ts from "typescript";
8
+ import { DEFAULT_STATE_ROOT, WORD_NAMES } from "./core.ts";
9
+ import type { DiagBag, ModuleRecord } from "./core.ts";
10
+
11
+ const STATE_ROOT_ANNOTATION = "StoryState";
12
+ const MAX_ALIAS_HOPS = 10;
13
+
14
+ interface StateRootCandidate {
15
+ rel: string;
16
+ sf: ts.SourceFile;
17
+ node: ts.Node;
18
+ name: string;
19
+ }
20
+
21
+ /** 本文件 type 别名表:别名 → 目标注解文本(仅无限定标识符;限定名与复合类型不是候选注解) */
22
+ function collectStateAliases(sf: ts.SourceFile): Map<string, string> {
23
+ const aliases = new Map<string, string>();
24
+ for (const stmt of sf.statements) {
25
+ if (!ts.isTypeAliasDeclaration(stmt)) continue;
26
+ const target =
27
+ ts.isTypeReferenceNode(stmt.type) && ts.isIdentifier(stmt.type.typeName)
28
+ ? stmt.type.typeName.text
29
+ : undefined;
30
+ if (target !== undefined) aliases.set(stmt.name.text, target);
31
+ }
32
+ return aliases;
33
+ }
34
+
35
+ function resolvesToStateAnnotation(
36
+ name: string,
37
+ aliases: ReadonlyMap<string, string>,
38
+ ): boolean {
39
+ let cur = name;
40
+ for (let hop = 0; hop <= MAX_ALIAS_HOPS; hop += 1) {
41
+ if (cur === STATE_ROOT_ANNOTATION) return true;
42
+ const next = aliases.get(cur);
43
+ if (next === undefined) return false;
44
+ cur = next;
45
+ }
46
+ return false;
47
+ }
48
+
49
+ /** 单声明的状态根判形:恰一声明、标识符名、标识符 TypeReference 注解且解析到 StoryState */
50
+ function stateRootDeclaration(
51
+ stmt: ts.VariableStatement,
52
+ aliases: ReadonlyMap<string, string>,
53
+ ): { name: string; decl: ts.VariableDeclaration } | undefined {
54
+ if (stmt.declarationList.declarations.length !== 1) return undefined;
55
+ const decl = stmt.declarationList.declarations[0];
56
+ if (!ts.isIdentifier(decl.name) || decl.type === undefined) return undefined;
57
+ const annotation =
58
+ ts.isTypeReferenceNode(decl.type) && ts.isIdentifier(decl.type.typeName)
59
+ ? decl.type.typeName.text
60
+ : undefined;
61
+ if (
62
+ annotation === undefined ||
63
+ !resolvesToStateAnnotation(annotation, aliases)
64
+ ) {
65
+ return undefined;
66
+ }
67
+ return { name: decl.name.text, decl };
68
+ }
69
+
70
+ /**
71
+ * 状态根候选扫描:两种拼写都算声明——
72
+ * 1. 顶层 `export declare const <根名>: StoryState`(直接形态);
73
+ * 2. ambient 全局增补块 `declare global { var <根名>: StoryState }`(桥文件形态:
74
+ * 根名不进模块作用域,只挂全局,供运行期裸名访问;块内无需 export/declare 修饰)。
75
+ */
76
+ function scanStateRootCandidates(
77
+ rel: string,
78
+ sf: ts.SourceFile,
79
+ ): StateRootCandidate[] {
80
+ const candidates: StateRootCandidate[] = [];
81
+ const aliases = collectStateAliases(sf);
82
+ for (const stmt of sf.statements) {
83
+ if (!ts.isVariableStatement(stmt)) continue;
84
+ const modifiers = stmt.modifiers ?? [];
85
+ if (!modifiers.some((m) => m.kind === ts.SyntaxKind.ExportKeyword))
86
+ continue;
87
+ if (!modifiers.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword))
88
+ continue;
89
+ const found = stateRootDeclaration(stmt, aliases);
90
+ if (found === undefined) continue;
91
+ candidates.push({ rel, sf, node: found.decl, name: found.name });
92
+ }
93
+ for (const stmt of sf.statements) {
94
+ // declare global 的名字节点是普通 Identifier("global"),用 GlobalAugmentation
95
+ // 旗标判别增补块(TS 6.0 AST:name.kind 仍为 Identifier)。
96
+ if (!ts.isModuleDeclaration(stmt)) continue;
97
+ if ((stmt.flags & ts.NodeFlags.GlobalAugmentation) === 0) continue;
98
+ if (stmt.body === undefined || !ts.isModuleBlock(stmt.body)) continue;
99
+ for (const inner of stmt.body.statements) {
100
+ if (!ts.isVariableStatement(inner)) continue;
101
+ const found = stateRootDeclaration(inner, aliases);
102
+ if (found === undefined) continue;
103
+ candidates.push({ rel, sf, node: found.decl, name: found.name });
104
+ }
105
+ }
106
+ return candidates;
107
+ }
108
+
109
+ export function discoverStateRoot(
110
+ records: ReadonlyMap<string, ModuleRecord>,
111
+ diags: DiagBag,
112
+ ): string {
113
+ const found: StateRootCandidate[] = [];
114
+ for (const record of records.values()) {
115
+ found.push(...scanStateRootCandidates(record.rel, record.sf));
116
+ }
117
+ const byName = new Map<string, StateRootCandidate>();
118
+ for (const candidate of found) {
119
+ if (!byName.has(candidate.name)) byName.set(candidate.name, candidate);
120
+ }
121
+ if (byName.size === 0) return DEFAULT_STATE_ROOT;
122
+ if (byName.size > 1) {
123
+ const names = [...byName.keys()]
124
+ .sort()
125
+ .map((n) => `\`${n}\``)
126
+ .join("、");
127
+ for (const candidate of byName.values()) {
128
+ diags.add(
129
+ candidate.rel,
130
+ candidate.sf,
131
+ candidate.node,
132
+ `状态根声明不唯一(${names}):只允许一个 \`export declare const <根名>: ${STATE_ROOT_ANNOTATION}\`(ambient 桥为 \`var <根名>: ${STATE_ROOT_ANNOTATION}\`)`,
133
+ );
134
+ }
135
+ return DEFAULT_STATE_ROOT;
136
+ }
137
+ const [name, candidate] = [...byName.entries()][0];
138
+ if (WORD_NAMES.has(name)) {
139
+ diags.add(
140
+ candidate.rel,
141
+ candidate.sf,
142
+ candidate.node,
143
+ `状态根名 \`${name}\` 与词汇层词函数冲突:请另选标识符(\`export declare const <根名>: ${STATE_ROOT_ANNOTATION}\`)`,
144
+ );
145
+ }
146
+ return name;
147
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * 图谱族的对上游出口(本文件与 `graph/` 目录同族,导出面与目录出口一致)。
3
+ */
4
+ export type {
5
+ Binding,
6
+ CompileDiag,
7
+ ConstValue,
8
+ Graph,
9
+ ModuleRecord,
10
+ StoryConfig,
11
+ } from "./graph/index.ts";
12
+ export {
13
+ buildGraph,
14
+ DiagBag,
15
+ foldConst,
16
+ resolveStart,
17
+ toPosix,
18
+ } from "./graph/index.ts";
@@ -0,0 +1,34 @@
1
+ /**
2
+ * story-compiler 族出口:图谱、装配、翻译三个部分的对外面。
3
+ *
4
+ * 轴 = 编译流水线三段——先建图(`graph.ts`)、再装配(`assemble.ts`)、表达式与语句翻译(`translate.ts`)。
5
+ */
6
+ export type {
7
+ Binding,
8
+ CompileDiag,
9
+ ConstValue,
10
+ Graph,
11
+ ModuleRecord,
12
+ StoryConfig,
13
+ } from "./graph.ts";
14
+ export {
15
+ buildGraph,
16
+ DiagBag,
17
+ foldConst,
18
+ resolveStart,
19
+ toPosix,
20
+ } from "./graph.ts";
21
+ export type { CompiledStory } from "./assemble.ts";
22
+ export { compileStoryFile, createAssembleSession } from "./assemble.ts";
23
+ export type {
24
+ FuncRegistry,
25
+ Scope,
26
+ ScopeEntry,
27
+ SharedCtx,
28
+ } from "./translate.ts";
29
+ export {
30
+ createBodyCtx,
31
+ createSharedCtx,
32
+ makeFuncOp,
33
+ translateStatements,
34
+ } from "./translate.ts";
@@ -0,0 +1,204 @@
1
+ import * as ts from "typescript";
2
+ import type { StoryCommand } from "@lingfan-engine/core";
3
+ import {
4
+ diag,
5
+ lookupBinding,
6
+ lookupScope,
7
+ lookupOwnScope,
8
+ declareLocal,
9
+ } from "../shared.ts";
10
+ import type { BodyCtx } from "../shared.ts";
11
+ import { statePath } from "./state-path.ts";
12
+ import { lowerValue, loweredToScriptValue } from "./expr.ts";
13
+ import {
14
+ callWord,
15
+ emitCallCommand,
16
+ resolveFunctionCallee,
17
+ } from "./words/index.ts";
18
+
19
+ function assignmentTarget(
20
+ ctx: BodyCtx,
21
+ expr: ts.Expression,
22
+ ):
23
+ | { kind: "local"; name: string }
24
+ | { kind: "state"; path: string }
25
+ | undefined {
26
+ if (ts.isIdentifier(expr)) {
27
+ if (expr.text === ctx.shared.stateRoot) {
28
+ diag(
29
+ ctx,
30
+ expr,
31
+ `${ctx.shared.stateRoot} 必须以成员路径使用:${ctx.shared.stateRoot}.gold`,
32
+ );
33
+ return undefined;
34
+ }
35
+ const own = lookupOwnScope(ctx, expr.text);
36
+ if (own !== undefined) {
37
+ if (own.kind !== "let" && own.kind !== "param") {
38
+ diag(
39
+ ctx,
40
+ expr,
41
+ `赋值目标 \`${expr.text}\` 必须是已声明的 let 局部变量`,
42
+ );
43
+ return undefined;
44
+ }
45
+ return { kind: "local", name: expr.text };
46
+ }
47
+ if (lookupScope(ctx, expr.text) !== undefined) {
48
+ // 运行时 letVar 只声明进当前函数帧(外层变量原样不动),跨层赋值必然是假写入 → 构建期拦截
49
+ diag(
50
+ ctx,
51
+ expr,
52
+ `不能在内层函数给外层变量 \`${expr.text}\` 赋值:内层赋值只会声明内层新变量,不会写回外层`,
53
+ );
54
+ return undefined;
55
+ }
56
+ diag(ctx, expr, `赋值目标 \`${expr.text}\` 必须是已声明的 let 局部变量`);
57
+ return undefined;
58
+ }
59
+ if (ts.isPropertyAccessExpression(expr)) {
60
+ const target = statePath(ctx, expr);
61
+ if (target === undefined) return undefined;
62
+ if (target.optional) {
63
+ // 写位置不允许缺失语义(optGet 只读),fail-closed
64
+ diag(ctx, expr, `赋值目标不支持可选链:${target.path}`);
65
+ return undefined;
66
+ }
67
+ return { kind: "state", path: target.path };
68
+ }
69
+ diag(
70
+ ctx,
71
+ expr,
72
+ `赋值目标只能是 let 局部变量或 ${ctx.shared.stateRoot} 单层路径`,
73
+ );
74
+ return undefined;
75
+ }
76
+
77
+ function compoundValue(
78
+ ctx: BodyCtx,
79
+ node: ts.Node,
80
+ op: string,
81
+ rhs: ts.Expression,
82
+ ): string | undefined {
83
+ const lowered = lowerValue(ctx, rhs);
84
+ if (lowered === undefined) return undefined;
85
+ if (lowered.kind === "lit" && typeof lowered.value !== "number") {
86
+ diag(ctx, node, "复合赋值右侧必须是数值");
87
+ return undefined;
88
+ }
89
+ const rhsText = lowered.kind === "lit" ? String(lowered.value) : lowered.text;
90
+ return `${op} {${rhsText}}`;
91
+ }
92
+
93
+ export function emitAssignment(
94
+ ctx: BodyCtx,
95
+ node: ts.Node,
96
+ target: ts.Expression,
97
+ op: string,
98
+ rhs: ts.Expression,
99
+ ): StoryCommand[] {
100
+ const resolved = assignmentTarget(ctx, target);
101
+ if (resolved === undefined) return [];
102
+ const word = resolved.kind === "state" ? "set" : "letVar";
103
+ const key = resolved.kind === "state" ? resolved.path : resolved.name;
104
+ let value: string | number | boolean;
105
+ if (op === "=") {
106
+ const lowered = lowerValue(ctx, rhs);
107
+ if (lowered === undefined) return [];
108
+ value = loweredToScriptValue(lowered);
109
+ } else {
110
+ const compound = compoundValue(ctx, node, op, rhs);
111
+ if (compound === undefined) return [];
112
+ value = compound;
113
+ }
114
+ const op2 = callWord(ctx, node, word, [key, value]);
115
+ return op2 === undefined ? [] : [op2];
116
+ }
117
+
118
+ export function translateIncrement(
119
+ ctx: BodyCtx,
120
+ expr: ts.PrefixUnaryExpression | ts.PostfixUnaryExpression,
121
+ ): StoryCommand[] {
122
+ const op =
123
+ expr.operator === ts.SyntaxKind.PlusPlusToken
124
+ ? "+="
125
+ : expr.operator === ts.SyntaxKind.MinusMinusToken
126
+ ? "-="
127
+ : undefined;
128
+ if (op === undefined) {
129
+ diag(ctx, expr, "自增/自减只支持 ++ 与 --");
130
+ return [];
131
+ }
132
+ return emitAssignment(
133
+ ctx,
134
+ expr,
135
+ expr.operand,
136
+ op,
137
+ ts.factory.createNumericLiteral("1"),
138
+ );
139
+ }
140
+
141
+ // —— 语句转译 ——
142
+
143
+ export function translateVariableStatement(
144
+ ctx: BodyCtx,
145
+ stmt: ts.VariableStatement,
146
+ ): StoryCommand[] {
147
+ const out: StoryCommand[] = [];
148
+ for (const decl of stmt.declarationList.declarations) {
149
+ if (!ts.isIdentifier(decl.name)) {
150
+ diag(ctx, decl, "解构声明不支持:数组请用 newArray 词、对象请用 dict 词");
151
+ continue;
152
+ }
153
+ if (decl.initializer === undefined) {
154
+ diag(ctx, decl, "局部变量声明必须带初始化(引擎没有未初始化的槽位)");
155
+ continue;
156
+ }
157
+ if (ts.isArrayLiteralExpression(decl.initializer)) {
158
+ diag(ctx, decl.initializer, "数组字面量不支持:请用 newArray 词");
159
+ continue;
160
+ }
161
+ if (ts.isObjectLiteralExpression(decl.initializer)) {
162
+ diag(ctx, decl.initializer, "对象字面量不支持:请用 dict 词");
163
+ continue;
164
+ }
165
+ // 语句级 `const x = f(1)` / `let x = f(1)`:子程序调用的**唯一**取值形态——
166
+ // 降级为 `call into`(结果绑定名),引擎把返回值声明成本地变量。表达式位
167
+ // (say 插值、赋值右值)仍拒绝:求值是命令内同步一步,中途不能挂起等 call 返回。
168
+ if (ts.isCallExpression(decl.initializer)) {
169
+ const fn = resolveFunctionCallee(ctx, decl.initializer);
170
+ if (fn !== undefined) {
171
+ const op = emitCallCommand(ctx, decl.initializer, fn, decl.name.text);
172
+ if (op !== undefined) out.push(op);
173
+ // 声明放在发射之后:实参在当前作用域里求值,不读这个新名字
174
+ declareLocal(ctx, decl.name.text, { kind: "let" });
175
+ continue;
176
+ }
177
+ // 未解析的具名 callee:与命令位同文案。值位置那句「请用语句级 const x = f(1)」
178
+ // 对已经在语句级的写法是错误指路,这里必须报出真实病因
179
+ if (ts.isIdentifier(decl.initializer.expression)) {
180
+ const calleeName = decl.initializer.expression.text;
181
+ if (
182
+ lookupBinding(ctx, calleeName) === undefined &&
183
+ lookupScope(ctx, calleeName) === undefined
184
+ ) {
185
+ diag(
186
+ ctx,
187
+ decl.initializer,
188
+ `未知调用目标 \`${calleeName}\`(须是词函数或声明的函数)`,
189
+ );
190
+ continue;
191
+ }
192
+ }
193
+ }
194
+ const lowered = lowerValue(ctx, decl.initializer);
195
+ if (lowered === undefined) continue;
196
+ declareLocal(ctx, decl.name.text, { kind: "let" });
197
+ const op = callWord(ctx, decl, "letVar", [
198
+ decl.name.text,
199
+ loweredToScriptValue(lowered),
200
+ ]);
201
+ if (op !== undefined) out.push(op);
202
+ }
203
+ return out;
204
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * 引擎文本字面量的渲染:把构建期已知值写成引擎源码里的字面量文本。
3
+ */
4
+ export function quoteEngineString(text: string): string {
5
+ const escaped = text
6
+ .replace(/\\/g, "\\\\")
7
+ .replace(/\n/g, "\\n")
8
+ .replace(/\r/g, "\\r")
9
+ .replace(/\t/g, "\\t")
10
+ .replace(/"/g, '\\"');
11
+ return `"${escaped}"`;
12
+ }
13
+
14
+ export function engineValueText(value: string | number | boolean): string {
15
+ return typeof value === "string" ? quoteEngineString(value) : String(value);
16
+ }