@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.
- package/LICENSE +201 -0
- package/bin/stories-build.mjs +16 -0
- package/bin/stories-watch.mjs +16 -0
- package/package.json +50 -3
- package/src/authoring.d.ts +51 -0
- package/src/cell/errors.ts +12 -0
- package/src/cell/imports.ts +127 -0
- package/src/cell/index.ts +33 -0
- package/src/cell/render-register.ts +33 -0
- package/src/cell/scan-cells.ts +63 -0
- package/src/cell/scan-file.ts +108 -0
- package/src/cell/types.ts +27 -0
- package/src/cell/whitelist.ts +22 -0
- package/src/cli/cli.ts +99 -0
- package/src/cli/watch-cli.ts +42 -0
- package/src/compiler/cell-scan.ts +27 -0
- package/src/compiler/compiler-contract.ts +87 -0
- package/src/compiler/extension-load.ts +67 -0
- package/src/compiler/index.ts +22 -0
- package/src/compiler/story-compiler/assemble/functions.ts +202 -0
- package/src/compiler/story-compiler/assemble/index.ts +8 -0
- package/src/compiler/story-compiler/assemble/session.ts +207 -0
- package/src/compiler/story-compiler/assemble/types.ts +37 -0
- package/src/compiler/story-compiler/assemble.ts +5 -0
- package/src/compiler/story-compiler/graph/build.ts +208 -0
- package/src/compiler/story-compiler/graph/core.ts +102 -0
- package/src/compiler/story-compiler/graph/fold.ts +121 -0
- package/src/compiler/story-compiler/graph/index.ts +17 -0
- package/src/compiler/story-compiler/graph/scan/bindings.ts +277 -0
- package/src/compiler/story-compiler/graph/scan/imports.ts +170 -0
- package/src/compiler/story-compiler/graph/scan/index.ts +9 -0
- package/src/compiler/story-compiler/graph/scan/module-scan.ts +199 -0
- package/src/compiler/story-compiler/graph/state-root.ts +147 -0
- package/src/compiler/story-compiler/graph.ts +18 -0
- package/src/compiler/story-compiler/index.ts +34 -0
- package/src/compiler/story-compiler/translate/expr/assign.ts +204 -0
- package/src/compiler/story-compiler/translate/expr/engine-text.ts +16 -0
- package/src/compiler/story-compiler/translate/expr/expr.ts +174 -0
- package/src/compiler/story-compiler/translate/expr/index.ts +24 -0
- package/src/compiler/story-compiler/translate/expr/lower/index.ts +4 -0
- package/src/compiler/story-compiler/translate/expr/lower/lower.ts +266 -0
- package/src/compiler/story-compiler/translate/expr/lower/numeric.ts +56 -0
- package/src/compiler/story-compiler/translate/expr/probe.ts +23 -0
- package/src/compiler/story-compiler/translate/expr/state-path.ts +47 -0
- package/src/compiler/story-compiler/translate/expr/words/arg.ts +87 -0
- package/src/compiler/story-compiler/translate/expr/words/call.ts +125 -0
- package/src/compiler/story-compiler/translate/expr/words/callee.ts +43 -0
- package/src/compiler/story-compiler/translate/expr/words/command.ts +93 -0
- package/src/compiler/story-compiler/translate/expr/words/dispatch.ts +16 -0
- package/src/compiler/story-compiler/translate/expr/words/index.ts +8 -0
- package/src/compiler/story-compiler/translate/expr/words/static-for.ts +96 -0
- package/src/compiler/story-compiler/translate/font-check.ts +94 -0
- package/src/compiler/story-compiler/translate/index.ts +10 -0
- package/src/compiler/story-compiler/translate/shared.ts +132 -0
- package/src/compiler/story-compiler/translate/stmt/control/branch.ts +71 -0
- package/src/compiler/story-compiler/translate/stmt/control/index.ts +11 -0
- package/src/compiler/story-compiler/translate/stmt/control/iter.ts +72 -0
- package/src/compiler/story-compiler/translate/stmt/control/loop.ts +114 -0
- package/src/compiler/story-compiler/translate/stmt/control/switch.ts +75 -0
- package/src/compiler/story-compiler/translate/stmt/control-flow.ts +25 -0
- package/src/compiler/story-compiler/translate/stmt/index.ts +17 -0
- package/src/compiler/story-compiler/translate/stmt/statements.ts +145 -0
- package/src/compiler/story-compiler/translate.ts +15 -0
- package/src/compiler/story-compiler.ts +119 -0
- package/src/config/index.ts +15 -0
- package/src/config/manifest-read.ts +26 -0
- package/src/config/state-type.ts +85 -0
- package/src/config/story-config.ts +113 -0
- package/src/const-eval/config-eval.ts +181 -0
- package/src/const-eval/const-eval-pass.ts +133 -0
- package/src/const-eval/const-eval.ts +114 -0
- package/src/const-eval/const-expr.ts +358 -0
- package/src/const-eval/contracts.ts +31 -0
- package/src/const-eval/define-merge.ts +195 -0
- package/src/const-eval/diag/compare.ts +39 -0
- package/src/const-eval/diag/index.ts +5 -0
- package/src/const-eval/diag/locate.ts +45 -0
- package/src/const-eval/index.ts +44 -0
- package/src/const-eval/module-characters.ts +353 -0
- package/src/const-eval/module-define.ts +300 -0
- package/src/const-eval/round/evaluate.ts +149 -0
- package/src/const-eval/round/index.ts +4 -0
- package/src/const-eval/round/source.ts +134 -0
- package/src/const-eval/state-schema.ts +269 -0
- package/src/discovery/index.ts +7 -0
- package/src/discovery/source-discovery.ts +193 -0
- package/src/emit/compile-roundtrip.ts +45 -0
- package/src/emit/emit.ts +147 -0
- package/src/emit/index.ts +9 -0
- package/src/emit/report.ts +29 -0
- package/src/fonts/font-registry.ts +53 -0
- package/src/fonts/index.ts +7 -0
- package/src/index.ts +76 -0
- package/src/pipeline/gates.ts +107 -0
- package/src/pipeline/index.ts +18 -0
- package/src/pipeline/orchestrate.ts +459 -0
- package/src/pipeline/watch.ts +244 -0
- package/src/shared/errors.ts +2 -0
- package/src/shared/index.ts +11 -0
- package/src/shared/texts.ts +85 -0
- package/src/shared/walk-files.ts +18 -0
- package/src/typegen/collect.ts +107 -0
- package/src/typegen/font-render.ts +30 -0
- package/src/typegen/index.ts +18 -0
- package/src/typegen/render.ts +68 -0
- package/src/typegen/script-render.ts +21 -0
- package/src/typegen/state-enum-ast.ts +365 -0
- package/src/typegen/state-render.ts +242 -0
- package/src/typegen/types.ts +21 -0
- package/README.md +0 -3
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import type { StoryCommand } from "@lingfan-engine/core";
|
|
3
|
+
import { WORDS, diag, lookupBinding, lookupScope } from "../../shared.ts";
|
|
4
|
+
import type { BodyCtx } from "../../shared.ts";
|
|
5
|
+
import { lowerValue, loweredToScriptValue } from "../expr.ts";
|
|
6
|
+
import { checkFontReferences } from "../../font-check.ts";
|
|
7
|
+
|
|
8
|
+
export function callWord(
|
|
9
|
+
ctx: BodyCtx,
|
|
10
|
+
node: ts.Node,
|
|
11
|
+
name: string,
|
|
12
|
+
args: unknown[],
|
|
13
|
+
): StoryCommand | undefined {
|
|
14
|
+
const word = WORDS[name];
|
|
15
|
+
if (word === undefined) {
|
|
16
|
+
// 未知词的第一现场(ambient 直呼也汇到这里),文案单点指路两条调用形态。
|
|
17
|
+
diag(
|
|
18
|
+
ctx,
|
|
19
|
+
node,
|
|
20
|
+
`词汇层不存在词 \`${name}\`——词函数经 \`script.\` 命名空间或具名导入调用,请核对词名`,
|
|
21
|
+
);
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const product = word(...args);
|
|
26
|
+
if (product === null || typeof product !== "object") {
|
|
27
|
+
diag(ctx, node, `词 \`${name}\` 未返回命令对象`);
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
// 字体引用检查单点:所有词命令都经此处产出(jump/值位置词调用同走),静态可达面全覆盖
|
|
31
|
+
checkFontReferences(ctx, node, product);
|
|
32
|
+
return product;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
35
|
+
diag(ctx, node, `词 \`${name}\` 实参形态非法:${message}`);
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function emitJump(
|
|
41
|
+
ctx: BodyCtx,
|
|
42
|
+
call: ts.CallExpression,
|
|
43
|
+
): StoryCommand | undefined {
|
|
44
|
+
if (call.arguments.length !== 1) {
|
|
45
|
+
diag(ctx, call, "jump 恰好接受一个实参(导入的故事标识符或列名字符串)");
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const arg = call.arguments[0];
|
|
49
|
+
if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) {
|
|
50
|
+
return callWord(ctx, call, "jump", [arg.text]);
|
|
51
|
+
}
|
|
52
|
+
if (ts.isIdentifier(arg)) {
|
|
53
|
+
const binding = lookupBinding(ctx, arg.text);
|
|
54
|
+
if (binding?.kind === "story") {
|
|
55
|
+
return callWord(ctx, call, "jump", [binding.name]);
|
|
56
|
+
}
|
|
57
|
+
diag(
|
|
58
|
+
ctx,
|
|
59
|
+
arg,
|
|
60
|
+
`jump 实参 \`${arg.text}\` 不是导入的故事(default 导出函数)`,
|
|
61
|
+
);
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
diag(ctx, arg, "jump 实参必须是导入的故事标识符或字符串字面量");
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 解析「函数 callee」:具名标识符,绑定面(词表 import)或函数作用域里命中
|
|
70
|
+
* kind 为 function 的绑定。返回 undefined = 这个 callee 不是子程序调用
|
|
71
|
+
* (词调用、`obj.method()`、未知名字),由各调用方走自己的分支。
|
|
72
|
+
*
|
|
73
|
+
* emitCommand 与语句级 `const x = f(1)` 拦截共用,保证两种位置对「什么算函数」
|
|
74
|
+
* 的判定逐字节一致。
|
|
75
|
+
*/
|
|
76
|
+
export function resolveFunctionCallee(
|
|
77
|
+
ctx: BodyCtx,
|
|
78
|
+
call: ts.CallExpression,
|
|
79
|
+
): { name: string; file: string } | undefined {
|
|
80
|
+
const callee = call.expression;
|
|
81
|
+
if (!ts.isIdentifier(callee)) return undefined;
|
|
82
|
+
const name = callee.text;
|
|
83
|
+
let binding = lookupBinding(ctx, name);
|
|
84
|
+
const scopedFn = lookupScope(ctx, name);
|
|
85
|
+
if (scopedFn?.kind === "function") {
|
|
86
|
+
binding = {
|
|
87
|
+
kind: "function",
|
|
88
|
+
name,
|
|
89
|
+
file: scopedFn.file ?? ctx.shared.file,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return binding?.kind === "function"
|
|
93
|
+
? { name: binding.name, file: binding.file }
|
|
94
|
+
: undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function emitCallCommand(
|
|
98
|
+
ctx: BodyCtx,
|
|
99
|
+
call: ts.CallExpression,
|
|
100
|
+
fn: { name: string; file: string },
|
|
101
|
+
into?: string,
|
|
102
|
+
): StoryCommand | undefined {
|
|
103
|
+
const args: Array<string | number | boolean> = [];
|
|
104
|
+
for (const arg of call.arguments) {
|
|
105
|
+
const lowered = lowerValue(ctx, arg);
|
|
106
|
+
if (lowered === undefined) continue;
|
|
107
|
+
args.push(loweredToScriptValue(lowered));
|
|
108
|
+
}
|
|
109
|
+
// refs 的键与 call.target 都用**声明名**:`import { alpha as ah }` 后调用点是 `ah()`,
|
|
110
|
+
// 但已发射的 func 声明名叫 alpha,函数体登记(funcs.body)也按声明名索引。用调用点
|
|
111
|
+
// 局部名会得到一个永远解析不到的函数引用——产物里留下悬空的 call。
|
|
112
|
+
// fn 由 resolveFunctionCallee 给出(携带声明名 + 声明文件),此处不再二次查绑定面:
|
|
113
|
+
// 别名场景按声明名回查必然落空,会把声明文件错记成当前文件、refs 归位到错误文件。
|
|
114
|
+
ctx.shared.funcs.refs.set(`${fn.file}#${fn.name}`, {
|
|
115
|
+
file: fn.file,
|
|
116
|
+
name: fn.name,
|
|
117
|
+
});
|
|
118
|
+
// into = 结果绑定名(语句级 const x = f(1) 的降级形态):引擎把 ret 带值
|
|
119
|
+
// 声明成调用方局部变量,无带值 ret / 自然跑完则绑 null
|
|
120
|
+
return callWord(ctx, call, "call", [
|
|
121
|
+
fn.name,
|
|
122
|
+
args,
|
|
123
|
+
...(into === undefined ? [] : [into]),
|
|
124
|
+
]);
|
|
125
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import { lookupBinding, lookupScope } from "../../shared.ts";
|
|
3
|
+
import type { BodyCtx } from "../../shared.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 取调用表达式的**词名**——具名标识符、`script.` 属性访问与免导入 ambient 直呼
|
|
7
|
+
* 三种形态在这里归一,免得下面的特判(jump / staticFor / cell)、实参求值与值位置
|
|
8
|
+
* 通道各写三遍、漏掉一边。
|
|
9
|
+
*
|
|
10
|
+
* ambient 直呼(作者不写 `import { script }` 也用 `script.bgm(...)`,类型来自
|
|
11
|
+
* gen/script.gen.d.ts 的全局声明)只在接收者恰为标识符 `script`、且绑定表与函数
|
|
12
|
+
* 作用域都没有同名遮蔽时兜底——模块层自有同名声明按 TS 遮蔽语义优先,不在此解析。
|
|
13
|
+
*
|
|
14
|
+
* 返回 undefined 表示这个 callee 不是词调用(普通函数、`obj.method()` 等),该走别的分支。
|
|
15
|
+
*/
|
|
16
|
+
export function wordCalleeName(
|
|
17
|
+
ctx: BodyCtx,
|
|
18
|
+
call: ts.CallExpression,
|
|
19
|
+
): string | undefined {
|
|
20
|
+
const callee = call.expression;
|
|
21
|
+
if (ts.isIdentifier(callee)) {
|
|
22
|
+
const binding = lookupBinding(ctx, callee.text);
|
|
23
|
+
// 用 binding.name(声明名)而非 callee.text:`import { say as 说 }` 后调用点是 `说()`,
|
|
24
|
+
// 但词表里登记的名字仍是 say。
|
|
25
|
+
return binding?.kind === "word" ? binding.name : undefined;
|
|
26
|
+
}
|
|
27
|
+
if (
|
|
28
|
+
ts.isPropertyAccessExpression(callee) &&
|
|
29
|
+
ts.isIdentifier(callee.expression)
|
|
30
|
+
) {
|
|
31
|
+
const receiver = callee.expression.text;
|
|
32
|
+
const target = lookupBinding(ctx, receiver);
|
|
33
|
+
if (target?.kind === "namespace") return callee.name.text;
|
|
34
|
+
if (
|
|
35
|
+
receiver === "script" &&
|
|
36
|
+
target === undefined &&
|
|
37
|
+
lookupScope(ctx, receiver) === undefined
|
|
38
|
+
) {
|
|
39
|
+
return callee.name.text;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import type { StoryCommand } from "@lingfan-engine/core";
|
|
3
|
+
import { diag, lookupBinding } from "../../shared.ts";
|
|
4
|
+
import type { BodyCtx } from "../../shared.ts";
|
|
5
|
+
import {
|
|
6
|
+
callWord,
|
|
7
|
+
emitCallCommand,
|
|
8
|
+
emitJump,
|
|
9
|
+
resolveFunctionCallee,
|
|
10
|
+
} from "./call.ts";
|
|
11
|
+
import { emitStaticFor } from "./static-for.ts";
|
|
12
|
+
import { evaluateWordArg } from "./arg.ts";
|
|
13
|
+
import { wordCalleeName } from "./callee.ts";
|
|
14
|
+
import type { WordDispatch } from "./dispatch.ts";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 发射一条词命令。三条特判(jump / staticFor / cell)按**词名**判定,与 callee 写成
|
|
18
|
+
* 具名标识符还是 `script.` 属性访问无关——同一个词换个写法必须落到同一条路径,
|
|
19
|
+
* 否则 `script.staticFor(...)` 会被当普通命令词去求值实参,报出与 `staticFor(...)`
|
|
20
|
+
* 完全不同的文案。
|
|
21
|
+
*
|
|
22
|
+
* 返回数组而非单条命令:staticFor 会静态展开成多条(`staticFor(3, …)` → 3 条命令),
|
|
23
|
+
* 是这条通道里唯一的展开点。调用方负责扁平化。
|
|
24
|
+
*/
|
|
25
|
+
function emitWordCommand(
|
|
26
|
+
ctx: BodyCtx,
|
|
27
|
+
call: ts.CallExpression,
|
|
28
|
+
wordName: string,
|
|
29
|
+
): StoryCommand[] {
|
|
30
|
+
if (wordName === "jump") {
|
|
31
|
+
const op = emitJump(ctx, call);
|
|
32
|
+
return op === undefined ? [] : [op];
|
|
33
|
+
}
|
|
34
|
+
if (wordName === "staticFor") return emitStaticFor(ctx, call, DISPATCH);
|
|
35
|
+
if (wordName === "cell") {
|
|
36
|
+
diag(ctx, call, "cell 是构建期声明词,只能用于模块层 const,不能作为命令");
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const args = call.arguments.map((arg) => evaluateWordArg(ctx, arg, DISPATCH));
|
|
40
|
+
const op = callWord(ctx, call, wordName, args);
|
|
41
|
+
return op === undefined ? [] : [op];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 命令发射总入口:本文件即递归入口,自引用注入给实参求值与 staticFor 展开 */
|
|
45
|
+
const DISPATCH: WordDispatch = {
|
|
46
|
+
command(ctx, call) {
|
|
47
|
+
return emitCommand(ctx, call);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function emitCommand(
|
|
52
|
+
ctx: BodyCtx,
|
|
53
|
+
call: ts.CallExpression,
|
|
54
|
+
): StoryCommand[] {
|
|
55
|
+
const callee = call.expression;
|
|
56
|
+
if (ts.isIdentifier(callee)) {
|
|
57
|
+
const fn = resolveFunctionCallee(ctx, call);
|
|
58
|
+
if (fn !== undefined) {
|
|
59
|
+
const op = emitCallCommand(ctx, call, fn);
|
|
60
|
+
return op === undefined ? [] : [op];
|
|
61
|
+
}
|
|
62
|
+
const name = callee.text;
|
|
63
|
+
const binding = lookupBinding(ctx, name);
|
|
64
|
+
if (binding === undefined) {
|
|
65
|
+
diag(ctx, call, `未知调用目标 \`${name}\`(须是词函数或声明的函数)`);
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
if (binding.kind === "word") {
|
|
69
|
+
return emitWordCommand(ctx, call, binding.name);
|
|
70
|
+
}
|
|
71
|
+
if (binding.kind === "declare" && name === "jump") {
|
|
72
|
+
const op = emitJump(ctx, call);
|
|
73
|
+
return op === undefined ? [] : [op];
|
|
74
|
+
}
|
|
75
|
+
diag(ctx, call, `\`${name}\` 是 ${binding.kind} 绑定,不是可发射的命令`);
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
const word = wordCalleeName(ctx, call);
|
|
79
|
+
if (word !== undefined) {
|
|
80
|
+
return emitWordCommand(ctx, call, word);
|
|
81
|
+
}
|
|
82
|
+
// 属性访问但接收者不是词汇层命名空间(`obj.method()` 之类):这条文案比末尾的通用文案更能
|
|
83
|
+
// 指路,故单独保留,避免重构时遗漏。
|
|
84
|
+
if (
|
|
85
|
+
ts.isPropertyAccessExpression(callee) &&
|
|
86
|
+
ts.isIdentifier(callee.expression)
|
|
87
|
+
) {
|
|
88
|
+
diag(ctx, call, "方法调用不支持:词函数通过具名导入或 script 命名空间调用");
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
diag(ctx, call, "命令只能是词函数/函数调用");
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type * as ts from "typescript";
|
|
2
|
+
import type { StoryCommand } from "@lingfan-engine/core";
|
|
3
|
+
import type { BodyCtx } from "../../shared.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 词命令递归入口:命令发射总入口由 `command.ts` 注入给实参求值(`arg.ts`)与
|
|
7
|
+
* staticFor 展开(`static-for.ts`)。
|
|
8
|
+
*
|
|
9
|
+
* 值位置的词调用(`option(...)`)与 staticFor 生成器元素都要回到命令发射总入口,
|
|
10
|
+
* 而总入口又要靠实参求值 ⇒ 两处若直接 import `command.ts` 即成环(环判据白名单为空)。
|
|
11
|
+
* 与 `stmt/control-flow.ts` 的 `StatementDispatch` 同法:**只注入、不反向 import**。
|
|
12
|
+
*/
|
|
13
|
+
export interface WordDispatch {
|
|
14
|
+
/** 调用表达式 → 命令序列(staticFor 的展开点已扁平化) */
|
|
15
|
+
command(ctx: BodyCtx, call: ts.CallExpression): StoryCommand[];
|
|
16
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 词命令族出口:命令发射总入口、词表调用(含字体检查单点)与函数 callee 解析。
|
|
3
|
+
*
|
|
4
|
+
* `emitCommand` 是族内递归入口,经 `WordDispatch` 注入给 `arg.ts` /
|
|
5
|
+
* `static-for.ts`(见 `dispatch.ts`);族外只需这四个名字。
|
|
6
|
+
*/
|
|
7
|
+
export { callWord, emitCallCommand, resolveFunctionCallee } from "./call.ts";
|
|
8
|
+
export { emitCommand } from "./command.ts";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import type { StoryCommand } from "@lingfan-engine/core";
|
|
3
|
+
import { foldConst } from "../../../graph.ts";
|
|
4
|
+
import { diag, lookupBinding, lookupScope } from "../../shared.ts";
|
|
5
|
+
import type { BodyCtx, Scope } from "../../shared.ts";
|
|
6
|
+
import type { WordDispatch } from "./dispatch.ts";
|
|
7
|
+
|
|
8
|
+
const STATIC_FOR_MAX = 100000;
|
|
9
|
+
|
|
10
|
+
function foldStaticCount(
|
|
11
|
+
ctx: BodyCtx,
|
|
12
|
+
expr: ts.Expression,
|
|
13
|
+
): number | undefined {
|
|
14
|
+
const value = foldConst(
|
|
15
|
+
expr,
|
|
16
|
+
ctx.shared.mod.sf,
|
|
17
|
+
ctx.shared.diags,
|
|
18
|
+
ctx.shared.file,
|
|
19
|
+
(name) => {
|
|
20
|
+
const scoped = lookupScope(ctx, name);
|
|
21
|
+
if (scoped?.kind === "index" && typeof scoped.value === "number") {
|
|
22
|
+
return { kind: "const", value: scoped.value };
|
|
23
|
+
}
|
|
24
|
+
return lookupBinding(ctx, name);
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
if (
|
|
28
|
+
value === undefined ||
|
|
29
|
+
typeof value !== "number" ||
|
|
30
|
+
!Number.isInteger(value)
|
|
31
|
+
) {
|
|
32
|
+
diag(ctx, expr, "staticFor 次数必须是编译期可折叠的整数");
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function emitStaticFor(
|
|
39
|
+
ctx: BodyCtx,
|
|
40
|
+
call: ts.CallExpression,
|
|
41
|
+
dispatch: WordDispatch,
|
|
42
|
+
): StoryCommand[] {
|
|
43
|
+
if (call.arguments.length !== 2) {
|
|
44
|
+
diag(ctx, call, "staticFor(count, generator) 恰好接受两个实参");
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
const count = foldStaticCount(ctx, call.arguments[0]);
|
|
48
|
+
if (count === undefined) return [];
|
|
49
|
+
if (count < 0) {
|
|
50
|
+
diag(ctx, call.arguments[0], "staticFor 次数不能为负");
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
if (count > STATIC_FOR_MAX) {
|
|
54
|
+
diag(ctx, call.arguments[0], `staticFor 展开量超限(>${STATIC_FOR_MAX})`);
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const generator = call.arguments[1];
|
|
58
|
+
if (
|
|
59
|
+
!ts.isArrowFunction(generator) ||
|
|
60
|
+
generator.parameters.length !== 1 ||
|
|
61
|
+
!ts.isIdentifier(generator.parameters[0].name)
|
|
62
|
+
) {
|
|
63
|
+
diag(ctx, generator, "staticFor 生成器必须是单参数箭头函数");
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
if (!ts.isArrayLiteralExpression(generator.body)) {
|
|
67
|
+
diag(
|
|
68
|
+
ctx,
|
|
69
|
+
generator.body,
|
|
70
|
+
"staticFor 生成器函数体必须是数组字面量(命令列表)",
|
|
71
|
+
);
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
const paramName = generator.parameters[0].name.text;
|
|
75
|
+
const out: StoryCommand[] = [];
|
|
76
|
+
for (let i = 0; i < count; i += 1) {
|
|
77
|
+
const scope: Scope = new Map([[paramName, { kind: "index", value: i }]]);
|
|
78
|
+
ctx.scopes.push(scope);
|
|
79
|
+
try {
|
|
80
|
+
for (const element of generator.body.elements) {
|
|
81
|
+
if (!ts.isCallExpression(element)) {
|
|
82
|
+
diag(
|
|
83
|
+
ctx,
|
|
84
|
+
element,
|
|
85
|
+
"staticFor 生成器元素只能是词函数调用或 staticFor 调用",
|
|
86
|
+
);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
out.push(...dispatch.command(ctx, element));
|
|
90
|
+
}
|
|
91
|
+
} finally {
|
|
92
|
+
ctx.scopes.pop();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 构建期字体名引用检查:词命令产物里的静态字体引用对照字体名注册表,未注册名 fail-closed。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖两类静态可达面:
|
|
5
|
+
* 1. 文本里的 `{font=名}` 行内标签(模板串已整体降糖为静态字符串,标签可见);
|
|
6
|
+
* 2. `say` 行参 `font`(角色字体字段的静态源)。
|
|
7
|
+
*
|
|
8
|
+
* 动态值不在此档:模板插值与标识符实参在降糖后是 `{…}` 包裹形态(编译期不可见真值),
|
|
9
|
+
* 编译期放行,由渲染层注册表回退 + 诊断兜底(两档语义)。
|
|
10
|
+
*/
|
|
11
|
+
import * as ts from "typescript";
|
|
12
|
+
import type { BodyCtx } from "./shared.ts";
|
|
13
|
+
import { diag } from "./shared.ts";
|
|
14
|
+
|
|
15
|
+
/** 降糖形态的插值段:`{…}` 包裹(模板插值与标识符实参都落成这个形态) */
|
|
16
|
+
const INTERP_SEGMENT = /\{[^{}]*\}/;
|
|
17
|
+
const FONT_TAG = /\{font=([^}]*)\}/g;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 检查一个词命令产物里的字体引用;每个未注册的静态名报一条诊断(一次报全部)。
|
|
21
|
+
* 注册表为空集(工程未声明字体)时同样 fail-closed——引用了就报。
|
|
22
|
+
*/
|
|
23
|
+
export function checkFontReferences(
|
|
24
|
+
ctx: BodyCtx,
|
|
25
|
+
node: ts.Node,
|
|
26
|
+
product: unknown,
|
|
27
|
+
): void {
|
|
28
|
+
const registry = ctx.shared.fontNames;
|
|
29
|
+
const bad = new Set<string>();
|
|
30
|
+
collectFontRefs(product, registry, bad);
|
|
31
|
+
for (const name of bad) {
|
|
32
|
+
diag(
|
|
33
|
+
ctx,
|
|
34
|
+
node,
|
|
35
|
+
`未注册的字体名 "${name}"——Resources/Fonts 里没有对应字体文件(字体名 = 文件名去扩展名)`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function collectFontRefs(
|
|
41
|
+
value: unknown,
|
|
42
|
+
registry: ReadonlySet<string>,
|
|
43
|
+
bad: Set<string>,
|
|
44
|
+
): void {
|
|
45
|
+
if (typeof value === "string") {
|
|
46
|
+
collectFromString(value, registry, bad);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
for (const item of value) {
|
|
51
|
+
collectFontRefs(item, registry, bad);
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === "object" && value !== null) {
|
|
56
|
+
for (const [key, item] of Object.entries(value)) {
|
|
57
|
+
// 角色字体字段的静态源(say 行参 font):裸字符串名直接对照注册表;
|
|
58
|
+
// 插值降糖形态({…} 包裹)= 动态值,编译期放行
|
|
59
|
+
if (
|
|
60
|
+
key === "font" &&
|
|
61
|
+
typeof item === "string" &&
|
|
62
|
+
!INTERP_SEGMENT.test(item)
|
|
63
|
+
) {
|
|
64
|
+
noteName(item, registry, bad);
|
|
65
|
+
}
|
|
66
|
+
collectFontRefs(item, registry, bad);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function collectFromString(
|
|
72
|
+
text: string,
|
|
73
|
+
registry: ReadonlySet<string>,
|
|
74
|
+
bad: Set<string>,
|
|
75
|
+
): void {
|
|
76
|
+
for (const match of text.matchAll(FONT_TAG)) {
|
|
77
|
+
const name = match[1]!.trim();
|
|
78
|
+
// 空名 / 含插值段的标签 = 动态,运行期档负责
|
|
79
|
+
if (name === "" || name.includes("{")) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
noteName(name, registry, bad);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function noteName(
|
|
87
|
+
name: string,
|
|
88
|
+
registry: ReadonlySet<string>,
|
|
89
|
+
bad: Set<string>,
|
|
90
|
+
): void {
|
|
91
|
+
if (!registry.has(name)) {
|
|
92
|
+
bad.add(name);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 翻译族出口:TS 源到引擎命令的翻译。
|
|
3
|
+
*
|
|
4
|
+
* 轴 = 三段——表达式与赋值(`expr/`)、语句与控制流(`stmt/`)、共享上下文与词表
|
|
5
|
+
* (`shared.ts`),外加一处字体引用检查(`font-check.ts`)。
|
|
6
|
+
*/
|
|
7
|
+
export type { FuncRegistry, Scope, ScopeEntry, SharedCtx } from "./shared.ts";
|
|
8
|
+
export { createBodyCtx, createSharedCtx, makeFuncOp } from "./shared.ts";
|
|
9
|
+
export { checkFontReferences } from "./font-check.ts";
|
|
10
|
+
export { translateStatements } from "./stmt/index.ts";
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import type { StoryCommand } from "@lingfan-engine/core";
|
|
3
|
+
import { script } from "@lingfan-engine/editor";
|
|
4
|
+
import {
|
|
5
|
+
defaultStoriesBuildTexts,
|
|
6
|
+
type StoriesBuildTexts,
|
|
7
|
+
} from "../../../shared/index.ts";
|
|
8
|
+
import type { Binding, DiagBag, ModuleRecord } from "../graph.ts";
|
|
9
|
+
|
|
10
|
+
export const WORDS = script as unknown as Record<
|
|
11
|
+
string,
|
|
12
|
+
(...args: unknown[]) => StoryCommand
|
|
13
|
+
>;
|
|
14
|
+
|
|
15
|
+
export interface ScopeEntry {
|
|
16
|
+
kind: "let" | "param" | "index" | "function";
|
|
17
|
+
value?: string | number | boolean;
|
|
18
|
+
file?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type Scope = Map<string, ScopeEntry>;
|
|
22
|
+
|
|
23
|
+
export interface FuncRegistry {
|
|
24
|
+
refs: Map<string, { file: string; name: string }>;
|
|
25
|
+
names: Map<string, { file: string; line: number; col: number }>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SharedCtx {
|
|
29
|
+
mod: ModuleRecord;
|
|
30
|
+
file: string;
|
|
31
|
+
diags: DiagBag;
|
|
32
|
+
funcs: FuncRegistry;
|
|
33
|
+
/** 状态根名(发现自 `export declare const <根名>: StoryState`;无声明回退 "state") */
|
|
34
|
+
readonly stateRoot: string;
|
|
35
|
+
/** 字体名注册表(Resources/Fonts 文件名去扩展名;构建期字体引用检查的对照源) */
|
|
36
|
+
readonly fontNames: ReadonlySet<string>;
|
|
37
|
+
/** 集中文案表(编译入口注入;已含缺省合并,诊断文案只从这里取) */
|
|
38
|
+
readonly texts: StoriesBuildTexts;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface BodyCtx {
|
|
42
|
+
shared: SharedCtx;
|
|
43
|
+
scopes: Scope[];
|
|
44
|
+
/** 本函数体自身作用域的起始下标;之下的条目继承自外层函数(只读),赋值目标不得落在这里 */
|
|
45
|
+
readonly ownBase: number;
|
|
46
|
+
breakables: Array<"loop" | "switch">;
|
|
47
|
+
inFunction: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function diag(ctx: BodyCtx, node: ts.Node, message: string): void {
|
|
51
|
+
ctx.shared.diags.add(ctx.shared.file, ctx.shared.mod.sf, node, message);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function lookupBinding(ctx: BodyCtx, name: string): Binding | undefined {
|
|
55
|
+
return ctx.shared.mod.bindings.get(name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function lookupScope(
|
|
59
|
+
ctx: BodyCtx,
|
|
60
|
+
name: string,
|
|
61
|
+
): ScopeEntry | undefined {
|
|
62
|
+
for (let i = ctx.scopes.length - 1; i >= 0; i -= 1) {
|
|
63
|
+
const hit = ctx.scopes[i].get(name);
|
|
64
|
+
if (hit !== undefined) return hit;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 只查本函数体自身的作用域层(含参数层),不含继承自外层的条目 */
|
|
70
|
+
export function lookupOwnScope(
|
|
71
|
+
ctx: BodyCtx,
|
|
72
|
+
name: string,
|
|
73
|
+
): ScopeEntry | undefined {
|
|
74
|
+
for (let i = ctx.scopes.length - 1; i >= ctx.ownBase; i -= 1) {
|
|
75
|
+
const hit = ctx.scopes[i].get(name);
|
|
76
|
+
if (hit !== undefined) return hit;
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function declareLocal(
|
|
82
|
+
ctx: BodyCtx,
|
|
83
|
+
name: string,
|
|
84
|
+
entry: ScopeEntry,
|
|
85
|
+
): void {
|
|
86
|
+
ctx.scopes[ctx.scopes.length - 1].set(name, entry);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// —— 表达式内联(引擎文法唯一来源 runtime/expr)——
|
|
90
|
+
|
|
91
|
+
export function isAssignmentOperator(text: string): boolean {
|
|
92
|
+
return ["=", "+=", "-=", "*=", "/=", "%="].includes(text);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function createBodyCtx(
|
|
96
|
+
shared: SharedCtx,
|
|
97
|
+
options: {
|
|
98
|
+
inFunction: boolean;
|
|
99
|
+
params?: string[];
|
|
100
|
+
parentScopes?: readonly Scope[];
|
|
101
|
+
},
|
|
102
|
+
): BodyCtx {
|
|
103
|
+
const parents = options.parentScopes ?? [];
|
|
104
|
+
const root: Scope = new Map();
|
|
105
|
+
for (const param of options.params ?? []) root.set(param, { kind: "param" });
|
|
106
|
+
return {
|
|
107
|
+
shared,
|
|
108
|
+
scopes: [...parents, root],
|
|
109
|
+
ownBase: parents.length,
|
|
110
|
+
breakables: [],
|
|
111
|
+
inFunction: options.inFunction,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function makeFuncOp(
|
|
116
|
+
name: string,
|
|
117
|
+
params: string[],
|
|
118
|
+
body: StoryCommand[],
|
|
119
|
+
): StoryCommand {
|
|
120
|
+
return WORDS.func(name, params, body);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function createSharedCtx(
|
|
124
|
+
mod: ModuleRecord,
|
|
125
|
+
diags: DiagBag,
|
|
126
|
+
funcs: FuncRegistry,
|
|
127
|
+
stateRoot: string,
|
|
128
|
+
fontNames: ReadonlySet<string> = new Set(),
|
|
129
|
+
texts: StoriesBuildTexts = defaultStoriesBuildTexts,
|
|
130
|
+
): SharedCtx {
|
|
131
|
+
return { mod, file: mod.rel, diags, funcs, stateRoot, fontNames, texts };
|
|
132
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import type { StoryCommand } from "@lingfan-engine/core";
|
|
3
|
+
import type { BodyCtx } from "../../shared.ts";
|
|
4
|
+
import { lowerCond } from "../../expr/expr.ts";
|
|
5
|
+
import { callWord } from "../../expr/words/index.ts";
|
|
6
|
+
import { translateBlockBody } from "../control-flow.ts";
|
|
7
|
+
import type { StatementDispatch } from "../control-flow.ts";
|
|
8
|
+
|
|
9
|
+
interface IfChainInput {
|
|
10
|
+
branches: Array<{ cond: ts.Expression; then: ts.Statement }>;
|
|
11
|
+
elseStmt: ts.Statement | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function collectIfChain(stmt: ts.IfStatement): IfChainInput {
|
|
15
|
+
const branches: Array<{ cond: ts.Expression; then: ts.Statement }> = [
|
|
16
|
+
{ cond: stmt.expression, then: stmt.thenStatement },
|
|
17
|
+
];
|
|
18
|
+
let current = stmt;
|
|
19
|
+
while (
|
|
20
|
+
current.elseStatement !== undefined &&
|
|
21
|
+
ts.isIfStatement(current.elseStatement)
|
|
22
|
+
) {
|
|
23
|
+
current = current.elseStatement;
|
|
24
|
+
branches.push({ cond: current.expression, then: current.thenStatement });
|
|
25
|
+
}
|
|
26
|
+
return { branches, elseStmt: current.elseStatement };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface Chain {
|
|
30
|
+
elif(cond: string, body: StoryCommand[]): Chain;
|
|
31
|
+
else(body: StoryCommand[]): StoryCommand;
|
|
32
|
+
end(): StoryCommand;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function translateIf(
|
|
36
|
+
ctx: BodyCtx,
|
|
37
|
+
stmt: ts.IfStatement,
|
|
38
|
+
dispatch: StatementDispatch,
|
|
39
|
+
): StoryCommand[] {
|
|
40
|
+
const { branches, elseStmt } = collectIfChain(stmt);
|
|
41
|
+
const rendered: Array<[string, StoryCommand[]]> = [];
|
|
42
|
+
for (const branch of branches) {
|
|
43
|
+
const cond = lowerCond(ctx, branch.cond);
|
|
44
|
+
if (cond === undefined) return [];
|
|
45
|
+
ctx.scopes.push(new Map());
|
|
46
|
+
const body = translateBlockBody(ctx, branch.then, dispatch);
|
|
47
|
+
ctx.scopes.pop();
|
|
48
|
+
rendered.push([cond, body]);
|
|
49
|
+
}
|
|
50
|
+
let elseBody: StoryCommand[] = [];
|
|
51
|
+
if (elseStmt !== undefined) {
|
|
52
|
+
ctx.scopes.push(new Map());
|
|
53
|
+
elseBody = translateBlockBody(ctx, elseStmt, dispatch);
|
|
54
|
+
ctx.scopes.pop();
|
|
55
|
+
}
|
|
56
|
+
const [firstCond, firstThen] = rendered[0];
|
|
57
|
+
if (branches.length === 1 && elseStmt === undefined) {
|
|
58
|
+
const op = callWord(ctx, stmt, "when", [firstCond, firstThen]);
|
|
59
|
+
return op === undefined ? [] : [op];
|
|
60
|
+
}
|
|
61
|
+
const chainOp = callWord(ctx, stmt, "whenChain", [firstCond, firstThen]);
|
|
62
|
+
if (chainOp === undefined) return [];
|
|
63
|
+
const chain = chainOp as unknown as Chain;
|
|
64
|
+
let result = chain;
|
|
65
|
+
for (let i = 1; i < rendered.length; i += 1) {
|
|
66
|
+
result = result.elif(rendered[i][0], rendered[i][1]) as Chain;
|
|
67
|
+
}
|
|
68
|
+
const finished =
|
|
69
|
+
elseStmt !== undefined ? result.else(elseBody) : result.end();
|
|
70
|
+
return [finished];
|
|
71
|
+
}
|