@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,11 @@
1
+ /**
2
+ * 控制流族出口:if / while / for / for-of / switch 的翻译入口。
3
+ *
4
+ * 族内各文件共用 `StatementDispatch` 契约(注入式递归入口,声明在
5
+ * `../control-flow.ts`),并统一从 `../control-flow.ts` 取 `translateBlockBody`
6
+ * 下探块体——单向依赖,不反向 import 语句分发器(`statements.ts`)。
7
+ */
8
+ export { translateIf } from "./branch.ts";
9
+ export { translateFor, translateWhile } from "./loop.ts";
10
+ export { translateForOf } from "./iter.ts";
11
+ export { translateSwitch } from "./switch.ts";
@@ -0,0 +1,72 @@
1
+ import * as ts from "typescript";
2
+ import type { StoryCommand } from "@lingfan-engine/core";
3
+ import { diag, lookupScope } from "../../shared.ts";
4
+ import type { BodyCtx } from "../../shared.ts";
5
+ import { statePath } from "../../expr/state-path.ts";
6
+ import { callWord } from "../../expr/words/index.ts";
7
+ import { translateBlockBody } from "../control-flow.ts";
8
+ import type { StatementDispatch } from "../control-flow.ts";
9
+
10
+ export function translateForOf(
11
+ ctx: BodyCtx,
12
+ stmt: ts.ForOfStatement,
13
+ dispatch: StatementDispatch,
14
+ ): StoryCommand[] {
15
+ const initializer = stmt.initializer;
16
+ if (
17
+ !ts.isVariableDeclarationList(initializer) ||
18
+ initializer.declarations.length !== 1 ||
19
+ !ts.isIdentifier(initializer.declarations[0].name)
20
+ ) {
21
+ diag(ctx, initializer, "for-of 只支持 (let x of 集合) 形式");
22
+ return [];
23
+ }
24
+ const varName = initializer.declarations[0].name.text;
25
+ const collection = forOfCollection(ctx, stmt.expression);
26
+ if (collection === undefined) return [];
27
+ ctx.scopes.push(new Map([[varName, { kind: "let" }]]));
28
+ ctx.breakables.push("loop");
29
+ const body = translateBlockBody(ctx, stmt.statement, dispatch);
30
+ ctx.breakables.pop();
31
+ ctx.scopes.pop();
32
+ const op = callWord(ctx, stmt, "forIn", [varName, collection, body]);
33
+ return op === undefined ? [] : [op];
34
+ }
35
+
36
+ function forOfCollection(
37
+ ctx: BodyCtx,
38
+ expr: ts.Expression,
39
+ ): string | undefined {
40
+ const root = ctx.shared.stateRoot;
41
+ if (ts.isIdentifier(expr)) {
42
+ if (expr.text === root) {
43
+ diag(ctx, expr, `for-of 集合必须是 ${root} 路径或局部变量名`);
44
+ return undefined;
45
+ }
46
+ const scoped = lookupScope(ctx, expr.text);
47
+ if (
48
+ scoped !== undefined &&
49
+ (scoped.kind === "let" || scoped.kind === "param")
50
+ ) {
51
+ return `{${expr.text}}`;
52
+ }
53
+ diag(
54
+ ctx,
55
+ expr,
56
+ `for-of 集合 \`${expr.text}\` 必须是已声明的局部变量或 ${root} 路径`,
57
+ );
58
+ return undefined;
59
+ }
60
+ if (ts.isPropertyAccessExpression(expr)) {
61
+ const target = statePath(ctx, expr);
62
+ if (target === undefined) return undefined;
63
+ if (target.optional) {
64
+ // 迭代集合必须真实存在(缺失语义无从谈起),fail-closed
65
+ diag(ctx, expr, `for-of 集合不支持可选链:${target.path}`);
66
+ return undefined;
67
+ }
68
+ return `{${target.path}}`;
69
+ }
70
+ diag(ctx, expr, `for-of 集合只能是 ${root} 路径或局部变量名`);
71
+ return undefined;
72
+ }
@@ -0,0 +1,114 @@
1
+ import * as ts from "typescript";
2
+ import type { StoryCommand } from "@lingfan-engine/core";
3
+ import { diag, isAssignmentOperator } from "../../shared.ts";
4
+ import type { BodyCtx, Scope } from "../../shared.ts";
5
+ import {
6
+ lowerCond,
7
+ lowerValue,
8
+ loweredToScriptValue,
9
+ } from "../../expr/expr.ts";
10
+ import { callWord } from "../../expr/words/index.ts";
11
+ import { emitAssignment, translateIncrement } from "../../expr/assign.ts";
12
+ import { translateBlockBody } from "../control-flow.ts";
13
+ import type { StatementDispatch } from "../control-flow.ts";
14
+
15
+ export function translateWhile(
16
+ ctx: BodyCtx,
17
+ stmt: ts.WhileStatement,
18
+ dispatch: StatementDispatch,
19
+ ): StoryCommand[] {
20
+ const cond = lowerCond(ctx, stmt.expression);
21
+ if (cond === undefined) return [];
22
+ ctx.scopes.push(new Map());
23
+ ctx.breakables.push("loop");
24
+ const body = translateBlockBody(ctx, stmt.statement, dispatch);
25
+ ctx.breakables.pop();
26
+ ctx.scopes.pop();
27
+ const op = callWord(ctx, stmt, "whileDo", [cond, body]);
28
+ return op === undefined ? [] : [op];
29
+ }
30
+
31
+ export function translateFor(
32
+ ctx: BodyCtx,
33
+ stmt: ts.ForStatement,
34
+ dispatch: StatementDispatch,
35
+ ): StoryCommand[] {
36
+ if (
37
+ stmt.initializer === undefined ||
38
+ !ts.isVariableDeclarationList(stmt.initializer)
39
+ ) {
40
+ diag(
41
+ ctx,
42
+ stmt,
43
+ "for 初始化只支持单个带初值的 let 声明(for (let i = 0; …))",
44
+ );
45
+ return [];
46
+ }
47
+ const decls = stmt.initializer.declarations;
48
+ if (
49
+ decls.length !== 1 ||
50
+ !ts.isIdentifier(decls[0].name) ||
51
+ decls[0].initializer === undefined
52
+ ) {
53
+ diag(ctx, stmt.initializer, "for 初始化只支持单个带初值的 let 声明");
54
+ return [];
55
+ }
56
+ const lowered = lowerValue(ctx, decls[0].initializer);
57
+ if (lowered === undefined) return [];
58
+ const varName = decls[0].name.text;
59
+ const initOp = callWord(ctx, stmt.initializer, "letVar", [
60
+ varName,
61
+ loweredToScriptValue(lowered),
62
+ ]);
63
+ if (initOp === undefined) return [];
64
+ const cond =
65
+ stmt.condition === undefined ? undefined : lowerCond(ctx, stmt.condition);
66
+ if (cond === undefined) {
67
+ diag(ctx, stmt, "for 缺少循环条件");
68
+ return [];
69
+ }
70
+ const scope: Scope = new Map([[varName, { kind: "let" }]]);
71
+ ctx.scopes.push(scope);
72
+ ctx.breakables.push("loop");
73
+ const body = translateBlockBody(ctx, stmt.statement, dispatch);
74
+ const update = translateForUpdate(ctx, stmt);
75
+ ctx.breakables.pop();
76
+ ctx.scopes.pop();
77
+ if (update.length === 0 && stmt.incrementor !== undefined) return [];
78
+ if (stmt.incrementor === undefined) {
79
+ diag(
80
+ ctx,
81
+ stmt,
82
+ "for 缺少更新表达式(循环会无法推进;固定次数请用 staticFor)",
83
+ );
84
+ return [];
85
+ }
86
+ const op = callWord(ctx, stmt, "whileDo", [cond, [...body, ...update]]);
87
+ return op === undefined ? [] : [initOp, op];
88
+ }
89
+
90
+ function translateForUpdate(
91
+ ctx: BodyCtx,
92
+ stmt: ts.ForStatement,
93
+ ): StoryCommand[] {
94
+ const inc = stmt.incrementor;
95
+ if (inc === undefined) return [];
96
+ const sf = ctx.shared.mod.sf;
97
+ if (
98
+ ts.isBinaryExpression(inc) &&
99
+ isAssignmentOperator(inc.operatorToken.getText(sf))
100
+ ) {
101
+ return emitAssignment(
102
+ ctx,
103
+ inc,
104
+ inc.left,
105
+ inc.operatorToken.getText(sf),
106
+ inc.right,
107
+ );
108
+ }
109
+ if (ts.isPrefixUnaryExpression(inc) || ts.isPostfixUnaryExpression(inc)) {
110
+ return translateIncrement(ctx, inc);
111
+ }
112
+ diag(ctx, inc, "for 更新表达式只支持赋值或 ++/--");
113
+ return [];
114
+ }
@@ -0,0 +1,75 @@
1
+ import * as ts from "typescript";
2
+ import type { StoryCommand } from "@lingfan-engine/core";
3
+ import { foldConst } from "../../../graph.ts";
4
+ import { diag, lookupBinding } from "../../shared.ts";
5
+ import type { BodyCtx } from "../../shared.ts";
6
+ import { lowerCond } from "../../expr/expr.ts";
7
+ import { callWord } from "../../expr/words/index.ts";
8
+ import type { StatementDispatch } from "../control-flow.ts";
9
+
10
+ function isUnlabeledBreak(stmt: ts.Statement): boolean {
11
+ return ts.isBreakStatement(stmt) && stmt.label === undefined;
12
+ }
13
+
14
+ export function translateSwitch(
15
+ ctx: BodyCtx,
16
+ stmt: ts.SwitchStatement,
17
+ dispatch: StatementDispatch,
18
+ ): StoryCommand[] {
19
+ const sf = ctx.shared.mod.sf;
20
+ const on = lowerCond(ctx, stmt.expression);
21
+ if (on === undefined) return [];
22
+ const clauses = stmt.caseBlock.clauses;
23
+ const cases: Array<[string | number | boolean, StoryCommand[]]> = [];
24
+ let defaultBody: StoryCommand[] | undefined;
25
+ for (let i = 0; i < clauses.length; i += 1) {
26
+ const clause = clauses[i];
27
+ const isLast = i === clauses.length - 1;
28
+ const body = clause.statements;
29
+ const endsWithBreak =
30
+ body.length > 0 && isUnlabeledBreak(body[body.length - 1]);
31
+ if (body.length === 0 && !isLast) {
32
+ diag(
33
+ ctx,
34
+ clause,
35
+ "case 体不能为空(引擎 switch 无穿透,请合并条件或补 break)",
36
+ );
37
+ continue;
38
+ }
39
+ if (body.length > 0 && !endsWithBreak && !isLast) {
40
+ diag(ctx, clause, "case 体必须以 break 收尾(引擎 switch 无穿透)");
41
+ continue;
42
+ }
43
+ ctx.scopes.push(new Map());
44
+ ctx.breakables.push("switch");
45
+ const commands = dispatch.statements(ctx, body);
46
+ ctx.breakables.pop();
47
+ ctx.scopes.pop();
48
+ if (ts.isDefaultClause(clause)) {
49
+ if (defaultBody !== undefined) {
50
+ diag(ctx, clause, "重复的 default 分支");
51
+ continue;
52
+ }
53
+ defaultBody = commands;
54
+ continue;
55
+ }
56
+ const value = foldConst(
57
+ clause.expression,
58
+ sf,
59
+ ctx.shared.diags,
60
+ ctx.shared.file,
61
+ (name) => lookupBinding(ctx, name),
62
+ );
63
+ if (value === undefined || value === null) {
64
+ diag(
65
+ ctx,
66
+ clause.expression,
67
+ "case 值必须是字面量或可折叠的模块常量/枚举成员",
68
+ );
69
+ continue;
70
+ }
71
+ cases.push([value, commands]);
72
+ }
73
+ const op = callWord(ctx, stmt, "switchOn", [on, cases, defaultBody]);
74
+ return op === undefined ? [] : [op];
75
+ }
@@ -0,0 +1,25 @@
1
+ import * as ts from "typescript";
2
+ import type { StoryCommand } from "@lingfan-engine/core";
3
+ import type { BodyCtx } from "../shared.ts";
4
+
5
+ /**
6
+ * 语句递归入口:块体与 `case` 体由此下探。
7
+ *
8
+ * 控制流语句的体内可以再出现控制流语句,本族与语句分发器天然互相递归;
9
+ * 入口由调用方**注入**(本模块不 import 分发器)。
10
+ */
11
+ export interface StatementDispatch {
12
+ /** 单条语句 → 命令序列 */
13
+ statement(ctx: BodyCtx, stmt: ts.Statement): StoryCommand[];
14
+ /** 语句列表 → 命令序列 */
15
+ statements(ctx: BodyCtx, stmts: readonly ts.Statement[]): StoryCommand[];
16
+ }
17
+
18
+ export function translateBlockBody(
19
+ ctx: BodyCtx,
20
+ stmt: ts.Statement,
21
+ dispatch: StatementDispatch,
22
+ ): StoryCommand[] {
23
+ if (ts.isBlock(stmt)) return dispatch.statements(ctx, stmt.statements);
24
+ return dispatch.statement(ctx, stmt);
25
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 语句族出口:TS 语句(含控制流)到引擎语句的翻译。
3
+ *
4
+ * 轴 = 语句层两件——语句分派(`statements.ts`)与控制流族(`control-flow.ts`
5
+ * 提供块体递归入口契约 `StatementDispatch` 与 `translateBlockBody`,
6
+ * `control/` 子目录按 if / 循环 / 迭代 / switch 分文件实现)。
7
+ */
8
+ export { translateBlockBody } from "./control-flow.ts";
9
+ export type { StatementDispatch } from "./control-flow.ts";
10
+ export {
11
+ translateFor,
12
+ translateForOf,
13
+ translateIf,
14
+ translateSwitch,
15
+ translateWhile,
16
+ } from "./control/index.ts";
17
+ export { translateStatement, translateStatements } from "./statements.ts";
@@ -0,0 +1,145 @@
1
+ import * as ts from "typescript";
2
+ import type { StoryCommand } from "@lingfan-engine/core";
3
+ import { diag, isAssignmentOperator } from "../shared.ts";
4
+ import type { BodyCtx } from "../shared.ts";
5
+ import {
6
+ callWord,
7
+ emitAssignment,
8
+ emitCommand,
9
+ lowerValue,
10
+ loweredToScriptValue,
11
+ translateIncrement,
12
+ translateVariableStatement,
13
+ } from "../expr/index.ts";
14
+ import type { StatementDispatch } from "./control-flow.ts";
15
+ import {
16
+ translateIf,
17
+ translateWhile,
18
+ translateFor,
19
+ translateForOf,
20
+ translateSwitch,
21
+ } from "./control/index.ts";
22
+
23
+ /** 语句递归入口:本文件即分发器,自引用注入给控制流族(见 `StatementDispatch`) */
24
+ const DISPATCH: StatementDispatch = {
25
+ statement: translateStatement,
26
+ statements: translateStatements,
27
+ };
28
+
29
+ export function translateStatement(
30
+ ctx: BodyCtx,
31
+ stmt: ts.Statement,
32
+ ): StoryCommand[] {
33
+ const sf = ctx.shared.mod.sf;
34
+ if (ts.isVariableStatement(stmt))
35
+ return translateVariableStatement(ctx, stmt);
36
+ if (ts.isExpressionStatement(stmt)) {
37
+ const expr = stmt.expression;
38
+ if (ts.isCallExpression(expr)) return emitCommand(ctx, expr);
39
+ if (
40
+ ts.isBinaryExpression(expr) &&
41
+ isAssignmentOperator(expr.operatorToken.getText(sf))
42
+ ) {
43
+ return emitAssignment(
44
+ ctx,
45
+ expr,
46
+ expr.left,
47
+ expr.operatorToken.getText(sf),
48
+ expr.right,
49
+ );
50
+ }
51
+ if (ts.isPrefixUnaryExpression(expr) || ts.isPostfixUnaryExpression(expr)) {
52
+ return translateIncrement(ctx, expr);
53
+ }
54
+ diag(ctx, stmt, "表达式语句只能是词函数/函数调用或赋值");
55
+ return [];
56
+ }
57
+ if (ts.isIfStatement(stmt)) return translateIf(ctx, stmt, DISPATCH);
58
+ if (ts.isWhileStatement(stmt)) return translateWhile(ctx, stmt, DISPATCH);
59
+ if (ts.isDoStatement(stmt)) {
60
+ diag(ctx, stmt, "do-while 不支持:请改写为 while(先求值再循环)");
61
+ return [];
62
+ }
63
+ if (ts.isForStatement(stmt)) return translateFor(ctx, stmt, DISPATCH);
64
+ if (ts.isForOfStatement(stmt)) return translateForOf(ctx, stmt, DISPATCH);
65
+ if (ts.isForInStatement(stmt)) {
66
+ diag(ctx, stmt, "for-in 不支持:对象遍历请改用明确的状态键读取");
67
+ return [];
68
+ }
69
+ if (ts.isSwitchStatement(stmt)) return translateSwitch(ctx, stmt, DISPATCH);
70
+ if (ts.isReturnStatement(stmt)) {
71
+ if (!ctx.inFunction) {
72
+ diag(ctx, stmt, ctx.shared.texts.topLevelReturn());
73
+ return [];
74
+ }
75
+ if (stmt.expression !== undefined) {
76
+ // 带值 return:调用点用 `call into` 接收(语句级 const x = f(1) 降级形态);
77
+ // 不带 into 的调用点把返回值静默丢弃(向后兼容旧故事)
78
+ const lowered = lowerValue(ctx, stmt.expression);
79
+ if (lowered === undefined) return [];
80
+ const op = callWord(ctx, stmt, "ret", [loweredToScriptValue(lowered)]);
81
+ return op === undefined ? [] : [op];
82
+ }
83
+ const op = callWord(ctx, stmt, "ret", []);
84
+ return op === undefined ? [] : [op];
85
+ }
86
+ if (ts.isBreakStatement(stmt)) {
87
+ if (stmt.label !== undefined) {
88
+ diag(ctx, stmt, "带标签 break 不支持");
89
+ return [];
90
+ }
91
+ if (ctx.breakables.length === 0) {
92
+ diag(ctx, stmt, ctx.shared.texts.breakOutsideLoop());
93
+ return [];
94
+ }
95
+ if (ctx.breakables[ctx.breakables.length - 1] === "switch") {
96
+ return [];
97
+ }
98
+ const op = callWord(ctx, stmt, "breakLoop", []);
99
+ return op === undefined ? [] : [op];
100
+ }
101
+ if (ts.isContinueStatement(stmt)) {
102
+ if (stmt.label !== undefined) {
103
+ diag(ctx, stmt, "带标签 continue 不支持");
104
+ return [];
105
+ }
106
+ for (let i = ctx.breakables.length - 1; i >= 0; i -= 1) {
107
+ if (ctx.breakables[i] === "loop") {
108
+ const op = callWord(ctx, stmt, "continueLoop", []);
109
+ return op === undefined ? [] : [op];
110
+ }
111
+ }
112
+ diag(ctx, stmt, ctx.shared.texts.continueOutsideLoop());
113
+ return [];
114
+ }
115
+ if (ts.isFunctionDeclaration(stmt)) {
116
+ if (stmt.body === undefined) return [];
117
+ diag(
118
+ ctx,
119
+ stmt,
120
+ "块内函数声明不支持:函数请声明在函数体/列体顶层(会前置为 func 命令)",
121
+ );
122
+ return [];
123
+ }
124
+ if (ts.isBlock(stmt)) return translateStatements(ctx, stmt.statements);
125
+ if (ts.isEmptyStatement(stmt)) return [];
126
+ if (ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt))
127
+ return [];
128
+ diag(
129
+ ctx,
130
+ stmt,
131
+ "此语句不支持转译(转译总表:if/while/for/for-of/switch/return/break/continue/let/赋值/调用)",
132
+ );
133
+ return [];
134
+ }
135
+
136
+ export function translateStatements(
137
+ ctx: BodyCtx,
138
+ statements: readonly ts.Statement[],
139
+ ): StoryCommand[] {
140
+ const out: StoryCommand[] = [];
141
+ for (const stmt of statements) {
142
+ out.push(...translateStatement(ctx, stmt));
143
+ }
144
+ return out;
145
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 翻译族的对上游出口(本文件与 `translate/` 目录同族,导出面与目录出口一致)。
3
+ */
4
+ export type {
5
+ FuncRegistry,
6
+ Scope,
7
+ ScopeEntry,
8
+ SharedCtx,
9
+ } from "./translate/index.ts";
10
+ export {
11
+ createBodyCtx,
12
+ createSharedCtx,
13
+ makeFuncOp,
14
+ translateStatements,
15
+ } from "./translate/index.ts";
@@ -0,0 +1,119 @@
1
+ import {
2
+ isSafeFileNameSegment,
3
+ type CharacterDef,
4
+ type Story,
5
+ } from "@lingfan-engine/core";
6
+ import type { StorySourceConfig } from "../config/story-config.ts";
7
+ import {
8
+ defaultStoriesBuildTexts,
9
+ type StoriesBuildTexts,
10
+ } from "../shared/index.ts";
11
+ import {
12
+ buildGraph,
13
+ compileStoryFile,
14
+ createAssembleSession,
15
+ DiagBag,
16
+ resolveStart,
17
+ type CompileDiag,
18
+ type CompiledStory,
19
+ type Graph,
20
+ } from "./story-compiler/index.ts";
21
+ import type { CompilerConstValues } from "./compiler-contract.ts";
22
+
23
+ export interface CompileStoriesResult {
24
+ stories: Array<{ columnId: string; sourceRelPath: string; story: Story }>;
25
+ diags: CompileDiag[];
26
+ entryColumnId?: string;
27
+ }
28
+
29
+ export function compileStories(input: {
30
+ readonly root: string;
31
+ readonly sourcesDir: string;
32
+ /** 布局声明(config.ts 求值产物;folders/start 必需)——图谱层只消费,不再解析 config */
33
+ readonly config: StorySourceConfig;
34
+ readonly stories: readonly string[];
35
+ readonly modules: readonly string[];
36
+ /** 编译期求值喂入;缺省 = 无被求值文件(行为不变) */
37
+ readonly constValues?: CompilerConstValues;
38
+ /** 字体名注册表(Resources/Fonts 文件名去扩展名);缺省 = 空注册表,引用字体名即 fail-closed */
39
+ readonly fontNames?: ReadonlySet<string>;
40
+ /** 全工程角色表(编排层模块层声明收集产物);缺省 = 空表,无前置角色命令 */
41
+ readonly characters?: ReadonlyMap<string, CharacterDef>;
42
+ /** 文案注入(Partial);缺省 = 内建缺省文案(即构建层的既有文案) */
43
+ readonly texts?: Partial<StoriesBuildTexts>;
44
+ }): CompileStoriesResult {
45
+ const texts: StoriesBuildTexts = {
46
+ ...defaultStoriesBuildTexts,
47
+ ...input.texts,
48
+ };
49
+ const diags = new DiagBag();
50
+ const graph = buildGraph(
51
+ input.sourcesDir,
52
+ diags,
53
+ input.config,
54
+ input.constValues,
55
+ texts,
56
+ );
57
+ if (graph === null) return { stories: [], diags: diags.list() };
58
+ const entry = resolveStart(graph, diags);
59
+
60
+ const session = createAssembleSession(
61
+ input.fontNames,
62
+ texts,
63
+ input.characters,
64
+ );
65
+ const stories: CompiledStory[] = [];
66
+ for (const rel of graph.coveredStories) {
67
+ const compiled = compileStoryFile(session, graph, diags, rel);
68
+ if (compiled !== undefined) stories.push(compiled);
69
+ }
70
+ checkColumnIds(graph, diags, stories, texts);
71
+
72
+ const list = diags.list();
73
+ if (list.length > 0) return { stories: [], diags: list };
74
+ return {
75
+ stories,
76
+ diags: [],
77
+ entryColumnId: entry,
78
+ };
79
+ }
80
+
81
+ function checkColumnIds(
82
+ graph: Graph,
83
+ diags: DiagBag,
84
+ stories: readonly CompiledStory[],
85
+ texts: StoriesBuildTexts,
86
+ ): void {
87
+ const seen = new Map<string, string>();
88
+ for (const item of stories) {
89
+ const record = graph.records.get(item.sourceRelPath);
90
+ const storyName = record?.story?.node.name;
91
+ if (
92
+ record === undefined ||
93
+ record.story === null ||
94
+ storyName === undefined
95
+ )
96
+ continue;
97
+ if (item.columnId === "" || !isSafeFileNameSegment(item.columnId)) {
98
+ diags.add(
99
+ record.rel,
100
+ record.sf,
101
+ storyName,
102
+ texts.columnIdUnsafe(item.columnId),
103
+ );
104
+ continue;
105
+ }
106
+ const lower = item.columnId.toLowerCase();
107
+ const prev = seen.get(lower);
108
+ if (prev !== undefined) {
109
+ diags.add(
110
+ record.rel,
111
+ record.sf,
112
+ storyName,
113
+ texts.columnIdDuplicate(item.columnId, prev),
114
+ );
115
+ continue;
116
+ }
117
+ seen.set(lower, item.sourceRelPath);
118
+ }
119
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 配置域:工程清单与布局声明的读取、校验。
3
+ *
4
+ * 两者都是「先读工程声明,再决定后面怎么跑」的前置步骤,与源扫描
5
+ * 分成两域是因为清单读的是 `project.json`、布局读的是 `Stories.src/config.ts`,
6
+ * 校验规则各成一套,混在一个目录里只会让入口面变糊。
7
+ */
8
+ export {
9
+ CONFIG_FILE,
10
+ assertSingleRootScript,
11
+ parseFolders,
12
+ parseStart,
13
+ type StorySourceConfig,
14
+ } from "./story-config.ts";
15
+ export { readManifest } from "./manifest-read.ts";
@@ -0,0 +1,26 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { MANIFEST_FILE } from "@lingfan-engine/core";
4
+ import { StoryBuildError } from "../shared/index.ts";
5
+
6
+ /**
7
+ * 读取工程清单(`<root>/<resourcesDir>/project.json`)。
8
+ * 清单是工程事实(entry/defines/shell),不归 TS 源管:缺文件与非法 JSON 都 fail-closed,
9
+ * 文案带实际路径便于作者定位自己漏了什么。
10
+ */
11
+ export function readManifest(root: string, resourcesDir: string): unknown {
12
+ const manifestPath = join(root, resourcesDir, MANIFEST_FILE);
13
+ if (!existsSync(manifestPath)) {
14
+ throw new StoryBuildError(
15
+ `${resourcesDir}/${MANIFEST_FILE} 不存在——工程清单(entry/defines/shell)不归 TS 源管,请先建工程清单`,
16
+ );
17
+ }
18
+ const manifestText = readFileSync(manifestPath, "utf8");
19
+ try {
20
+ return JSON.parse(manifestText);
21
+ } catch (error) {
22
+ throw new StoryBuildError(
23
+ `${resourcesDir}/${MANIFEST_FILE} 不是合法 JSON:${String(error)}`,
24
+ );
25
+ }
26
+ }