feat: yarn return

This commit is contained in:
hypercross 2026-03-03 11:36:23 +08:00
parent 4d3faa3e4e
commit a226a9516c
5 changed files with 84 additions and 120 deletions

View File

@ -1,4 +1,4 @@
import type { YarnDocument, Statement, Line, Option } from "../model/ast";
import type {YarnDocument, Statement, Line, Option, YarnNode} from "../model/ast";
import type { IRProgram, IRNode, IRNodeGroup, IRInstruction } from "./ir";
export interface CompileOptions {
@ -31,13 +31,8 @@ export function compile(doc: YarnDocument, opts: CompileOptions = {}): IRProgram
nodesByTitle.get(node.title)!.push(node);
}
for (const [title, nodesWithSameTitle] of nodesByTitle) {
// If only one node with this title, treat as regular node
if (nodesWithSameTitle.length === 1) {
const node = nodesWithSameTitle[0];
const instructions: IRInstruction[] = [];
let onceCounter = 0;
function emitBlock(stmts: Statement[]): IRInstruction[] {
function emitBlock(stmts: Statement[], node: YarnNode): IRInstruction[] {
const block: IRInstruction[] = [];
for (const s of stmts) {
switch (s.type) {
@ -56,6 +51,9 @@ export function compile(doc: YarnDocument, opts: CompileOptions = {}): IRProgram
case "Detour":
block.push({ op: "detour", target: s.target });
break;
case "Return":
block.push({ op: "return" });
break;
case "OptionGroup": {
// Add #lastline tag to the most recent line, if present
for (let i = block.length - 1; i >= 0; i--) {
@ -72,18 +70,18 @@ export function compile(doc: YarnDocument, opts: CompileOptions = {}): IRProgram
}
block.push({
op: "options",
options: s.options.map((o: Option) => ({ text: o.text, tags: ensureLineId(o.tags), css: (o as any).css, markup: o.markup, condition: o.condition, block: emitBlock(o.body) })),
options: s.options.map((o: Option) => ({ text: o.text, tags: ensureLineId(o.tags), css: (o as any).css, markup: o.markup, condition: o.condition, block: emitBlock(o.body, node) })),
});
break;
}
case "If":
block.push({
op: "if",
branches: s.branches.map((b) => ({ condition: b.condition, block: emitBlock(b.body) })),
branches: s.branches.map((b) => ({ condition: b.condition, block: emitBlock(b.body, node) })),
});
break;
case "Once":
block.push({ op: "once", id: genOnce({ node: node.title, index: onceCounter++ }), block: emitBlock(s.body) });
block.push({ op: "once", id: genOnce({ node: node.title, index: onceCounter++ }), block: emitBlock(s.body, node) });
break;
case "Enum":
// Enums are metadata, skip during compilation (already stored in program.enums)
@ -92,7 +90,15 @@ export function compile(doc: YarnDocument, opts: CompileOptions = {}): IRProgram
}
return block;
}
instructions.push(...emitBlock(node.body));
for (const [title, nodesWithSameTitle] of nodesByTitle) {
// If only one node with this title, treat as regular node
if (nodesWithSameTitle.length === 1) {
const node = nodesWithSameTitle[0];
const instructions: IRInstruction[] = [];
onceCounter = 0;
instructions.push(...emitBlock(node.body, node));
const irNode: IRNode = {
title: node.title,
instructions,
@ -106,61 +112,9 @@ export function compile(doc: YarnDocument, opts: CompileOptions = {}): IRProgram
const groupNodes: IRNode[] = [];
for (const node of nodesWithSameTitle) {
const instructions: IRInstruction[] = [];
let onceCounter = 0;
function emitBlock(stmts: Statement[]): IRInstruction[] {
const block: IRInstruction[] = [];
for (const s of stmts) {
switch (s.type) {
case "Line":
{
const line = s as Line;
block.push({ op: "line", speaker: line.speaker, text: line.text, tags: ensureLineId(line.tags), markup: line.markup });
}
break;
case "Command":
block.push({ op: "command", content: s.content });
break;
case "Jump":
block.push({ op: "jump", target: s.target });
break;
case "Detour":
block.push({ op: "detour", target: s.target });
break;
case "OptionGroup": {
for (let i = block.length - 1; i >= 0; i--) {
const ins = block[i];
if (ins.op === "line") {
const tags = new Set(ins.tags ?? []);
if (![...tags].some((x) => x === "lastline" || x === "#lastline")) {
tags.add("lastline");
}
ins.tags = Array.from(tags);
break;
}
if (ins.op !== "command") break;
}
block.push({
op: "options",
options: s.options.map((o: Option) => ({ text: o.text, tags: ensureLineId(o.tags), css: (o as any).css, markup: o.markup, condition: o.condition, block: emitBlock(o.body) })),
});
break;
}
case "If":
block.push({
op: "if",
branches: s.branches.map((b) => ({ condition: b.condition, block: emitBlock(b.body) })),
});
break;
case "Once":
block.push({ op: "once", id: genOnce({ node: node.title, index: onceCounter++ }), block: emitBlock(s.body) });
break;
case "Enum":
break;
}
}
return block;
}
instructions.push(...emitBlock(node.body));
onceCounter = 0;
instructions.push(...emitBlock(node.body, node));
groupNodes.push({
title: node.title,
instructions,

View File

@ -22,6 +22,7 @@ export type IRInstruction =
| { op: "command"; content: string }
| { op: "jump"; target: string }
| { op: "detour"; target: string }
| { op: "return"; }
| { op: "options"; options: Array<{ text: string; tags?: string[]; css?: string; markup?: MarkupParseResult; condition?: string; block: IRInstruction[] }> }
| { op: "if"; branches: Array<{ condition: string | null; block: IRInstruction[] }> }
| { op: "once"; id: string; block: IRInstruction[] };

View File

@ -34,6 +34,7 @@ export type Statement =
| OnceBlock
| Jump
| Detour
| Return
| EnumBlock;
import type { MarkupParseResult } from "../markup/types.js";
@ -61,6 +62,11 @@ export interface Detour {
target: string;
}
export interface Return {
type: "Return";
target: string;
}
export interface OptionGroup {
type: "OptionGroup";
options: Option[];

View File

@ -13,6 +13,7 @@ import type {
OnceBlock,
Jump,
Detour,
Return,
EnumBlock,
} from "../model/ast";
@ -174,6 +175,7 @@ class Parser {
const cmd = this.take("COMMAND").text;
if (cmd.startsWith("jump ")) return { type: "Jump", target: cmd.slice(5).trim() } as Jump;
if (cmd.startsWith("detour ")) return { type: "Detour", target: cmd.slice(7).trim() } as Detour;
if (cmd.startsWith("return")) return { type: "Return" } as Return;
if (cmd.startsWith("if ")) return this.parseIfCommandBlock(cmd);
if (cmd === "once") return this.parseOnceBlock();
if (cmd.startsWith("enum ")) {

View File

@ -122,13 +122,6 @@ export class YarnRunner {
this.step();
}
/**
* Get the current node title (may resolve to a node group).
*/
getCurrentNodeTitle(): string {
return this.nodeTitle;
}
/**
* Resolve a node title to an actual node (handling node groups).
*/
@ -498,6 +491,14 @@ export class YarnRunner {
// resolveNode will handle node groups
continue;
}
case "return": {
const top = this.callStack.pop();
if(!top) throw new Error("No call stack to return to");
this.nodeTitle = top.title;
this.ip = top.ip;
this.currentNodeIndex = -1; // Reset node index for new resolution
continue;
}
case "options": {
const available = this.filterOptions(ins.options);
if (available.length === 0) {