fix(minecraft): semicolon now works in single line
Extract normalizeReplCode/rewriteTrailingExpressionToReturn from Brain to new repl-code-normalizer module, add test verifying single-line statements with trailing expressions return values (const nearestLog = [...]; nearestLog)
This commit is contained in:
@@ -131,6 +131,15 @@ inv;
|
||||
expect(result.returnValue).toContain('oak_sapling')
|
||||
})
|
||||
|
||||
it('returns trailing expression values from single-line statements', async () => {
|
||||
const brain: any = new Brain(createDeps('await skip()'))
|
||||
|
||||
const result = await brain.executeDebugRepl('const nearestLog = [{ name: "oak_log" }]; nearestLog')
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.returnValue).toContain('oak_log')
|
||||
})
|
||||
|
||||
it('queues exactly one synthetic follow-up on no-action result', async () => {
|
||||
const brain: any = new Brain(createDeps('1 + 1'))
|
||||
const enqueueSpy = vi.fn(async () => undefined)
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
toErrorMessage,
|
||||
} from './llmlogic'
|
||||
import { generateBrainSystemPrompt } from './prompts/brain-prompt'
|
||||
import { normalizeReplScript } from './repl-code-normalizer'
|
||||
import { createCancellationToken } from './task-state'
|
||||
|
||||
interface BrainDeps {
|
||||
@@ -507,46 +508,7 @@ export class Brain {
|
||||
}
|
||||
|
||||
private normalizeReplCode(code: string): string {
|
||||
// Simple top-level var persistence for REPL UX:
|
||||
// const/let/var foo = ... -> globalThis.foo = ...
|
||||
const normalized = code.replace(
|
||||
/^\s*(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*([^\n]+?)\s*(?:;\s*)?$/gm,
|
||||
(_full: string, name: string, valueExpr: string) => `globalThis.${name} = ${valueExpr}`,
|
||||
)
|
||||
return this.rewriteTrailingExpressionToReturn(normalized)
|
||||
}
|
||||
|
||||
private rewriteTrailingExpressionToReturn(code: string): string {
|
||||
if (/\breturn\b/.test(code))
|
||||
return code
|
||||
|
||||
const lines = code.split('\n')
|
||||
let lastIndex = -1
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
if (lines[i].trim().length > 0) {
|
||||
lastIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (lastIndex < 0)
|
||||
return code
|
||||
|
||||
const rawLine = lines[lastIndex]
|
||||
const trimmedLine = rawLine.trim()
|
||||
if (trimmedLine.length === 0 || trimmedLine.startsWith('//'))
|
||||
return code
|
||||
if (trimmedLine === '}' || trimmedLine.endsWith('{'))
|
||||
return code
|
||||
|
||||
const expression = trimmedLine.replace(/;$/, '').trim()
|
||||
const blocked = /^(?:if|for|while|switch|const|let|var|function|class|try|catch|finally|return|throw|import|export)\b/
|
||||
if (blocked.test(expression))
|
||||
return code
|
||||
|
||||
const indent = rawLine.match(/^\s*/)?.[0] ?? ''
|
||||
lines[lastIndex] = `${indent}return (${expression})`
|
||||
return lines.join('\n')
|
||||
return normalizeReplScript(code)
|
||||
}
|
||||
|
||||
private toDebugReplActions(actions: Array<{
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { normalizeReplScript } from './repl-code-normalizer'
|
||||
|
||||
describe('normalizeReplScript', () => {
|
||||
it('rewrites top-level variable declarations and trailing expression', () => {
|
||||
const normalized = normalizeReplScript('const nearestLog = query.blocks().first(); nearestLog')
|
||||
|
||||
expect(normalized).toContain('globalThis.nearestLog = query.blocks().first();')
|
||||
expect(normalized).toContain('return (nearestLog)')
|
||||
expect(normalized).not.toContain('return (globalThis.nearestLog = query.blocks().first(); nearestLog)')
|
||||
})
|
||||
|
||||
it('does not rewrite trailing expression when explicit return exists', () => {
|
||||
const normalized = normalizeReplScript(`
|
||||
const inv = [{ name: 'oak_log' }]
|
||||
return inv
|
||||
`)
|
||||
|
||||
expect(normalized).toContain('globalThis.inv = [{ name: \'oak_log\' }];')
|
||||
expect(normalized).toContain('return inv')
|
||||
expect(normalized).not.toContain('return (inv)')
|
||||
})
|
||||
|
||||
it('leaves scripts unchanged when parser reports diagnostics', () => {
|
||||
const script = 'const broken = ;'
|
||||
expect(normalizeReplScript(script)).toBe(script)
|
||||
})
|
||||
|
||||
it('keeps non-expression trailing statements without implicit return', () => {
|
||||
const normalized = normalizeReplScript(`
|
||||
const x = 1
|
||||
if (x > 0) {
|
||||
x
|
||||
}
|
||||
`)
|
||||
|
||||
expect(normalized).toContain('globalThis.x = 1;')
|
||||
expect(normalized).not.toContain('return (')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,91 @@
|
||||
import ts from 'typescript'
|
||||
|
||||
interface TextReplacement {
|
||||
start: number
|
||||
end: number
|
||||
value: string
|
||||
}
|
||||
|
||||
function getNodeText(sourceFile: ts.SourceFile, node: ts.Node): string {
|
||||
const start = node.getStart(sourceFile, false)
|
||||
const end = node.getEnd()
|
||||
return sourceFile.text.slice(start, end)
|
||||
}
|
||||
|
||||
function getVariableKeyword(flags: ts.NodeFlags): 'const' | 'let' | 'var' {
|
||||
if (flags & ts.NodeFlags.Const)
|
||||
return 'const'
|
||||
if (flags & ts.NodeFlags.Let)
|
||||
return 'let'
|
||||
return 'var'
|
||||
}
|
||||
|
||||
function normalizeVariableStatement(sourceFile: ts.SourceFile, statement: ts.VariableStatement): string {
|
||||
const keyword = getVariableKeyword(statement.declarationList.flags)
|
||||
const fragments: string[] = []
|
||||
|
||||
for (const declaration of statement.declarationList.declarations) {
|
||||
if (ts.isIdentifier(declaration.name)) {
|
||||
const initializer = declaration.initializer ? getNodeText(sourceFile, declaration.initializer) : 'undefined'
|
||||
fragments.push(`globalThis.${declaration.name.text} = ${initializer};`)
|
||||
continue
|
||||
}
|
||||
|
||||
const declarationText = getNodeText(sourceFile, declaration)
|
||||
fragments.push(`${keyword} ${declarationText};`)
|
||||
}
|
||||
|
||||
return fragments.join('\n')
|
||||
}
|
||||
|
||||
function applyReplacements(code: string, replacements: TextReplacement[]): string {
|
||||
if (replacements.length === 0)
|
||||
return code
|
||||
|
||||
let output = code
|
||||
const sorted = [...replacements].sort((a, b) => b.start - a.start)
|
||||
for (const replacement of sorted) {
|
||||
output = `${output.slice(0, replacement.start)}${replacement.value}${output.slice(replacement.end)}`
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
export function normalizeReplScript(code: string): string {
|
||||
const sourceFile = ts.createSourceFile('repl.ts', code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
||||
const diagnostics = (sourceFile as ts.SourceFile & {
|
||||
parseDiagnostics?: readonly ts.DiagnosticWithLocation[]
|
||||
}).parseDiagnostics
|
||||
|
||||
if (diagnostics && diagnostics.length > 0)
|
||||
return code
|
||||
|
||||
const replacements: TextReplacement[] = []
|
||||
|
||||
for (const statement of sourceFile.statements) {
|
||||
if (!ts.isVariableStatement(statement))
|
||||
continue
|
||||
|
||||
const start = statement.getStart(sourceFile, false)
|
||||
const end = statement.getEnd()
|
||||
replacements.push({
|
||||
start,
|
||||
end,
|
||||
value: normalizeVariableStatement(sourceFile, statement),
|
||||
})
|
||||
}
|
||||
|
||||
const hasTopLevelReturn = sourceFile.statements.some(statement => ts.isReturnStatement(statement))
|
||||
if (!hasTopLevelReturn && sourceFile.statements.length > 0) {
|
||||
const lastStatement = sourceFile.statements[sourceFile.statements.length - 1]
|
||||
if (ts.isExpressionStatement(lastStatement)) {
|
||||
const expressionText = getNodeText(sourceFile, lastStatement.expression)
|
||||
replacements.push({
|
||||
start: lastStatement.getStart(sourceFile, false),
|
||||
end: lastStatement.getEnd(),
|
||||
value: `return (${expressionText})`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return applyReplacements(code, replacements)
|
||||
}
|
||||
Reference in New Issue
Block a user