Summary
When an EXPECT timeout or an error-locked keyboard state occurs while execution is inside a function body, the executor attempts to jump to the registered ON TIMEOUT / ON ERROR label via gotoLabel(). gotoLabel() refuses to run while the call stack is non-empty and aborts execution with the misleading message "GOTO is not allowed inside functions", even though the script author never wrote a GOTO — they registered an ON handler.
Location
- File:
src/script_executor.cpp
- Lines / functions:
ScriptExecutor::gotoLabel() at L733–L738 (early-returns with error when !m_callStack.isEmpty())
ScriptExecutor::notifyTerminalStateChanged() at L120–L128 (invokes gotoLabel(m_onErrorLabel) unconditionally)
ScriptExecutor::endExpect() at L612–L620 (invokes gotoLabel(m_onTimeoutLabel) unconditionally)
Category
functional
Severity
high
Impact: any timeout or error that happens inside a CALLed function will terminate the script with an incorrect diagnostic instead of running the registered handler, defeating the purpose of ON TIMEOUT / ON ERROR.
Reproduction / Evidence
Verified by code analysis.
Script:
ON ERROR GOTO fail
CALL myfunc()
LABEL fail
LOG "recovered"
DEF myfunc()
EXPECT KEYBOARD UNLOCKED
ENDDEF
Execution path when the keyboard enters the ErrorLocked state during the EXPECT inside myfunc:
notifyTerminalStateChanged() observes ErrorLocked (L118).
m_onErrorLabel is non-empty, so gotoLabel("fail") is called (L122).
gotoLabel() sees !m_callStack.isEmpty() (the active call to myfunc) and emits executionError(0, "GOTO is not allowed inside functions"), then stop() (L734–L738).
- The script terminates; the
fail handler never runs.
The equivalent path exists for ON TIMEOUT via endExpect(false) at L614–L616 when the EXPECT was entered from inside a function.
Expected Behavior
When ON TIMEOUT / ON ERROR fires, the runtime should unwind the call stack (and any nested block exec frames) down to the top-level script, then jump to the registered label — because LABELs can only be top-level (per PROMPT.md), the handler necessarily lives there.
Actual Behavior
gotoLabel() refuses to run while inside a function and stops the script with a misleading "GOTO is not allowed inside functions" message. The registered handler never executes.
Root Cause
gotoLabel() was written as a defense against user-written GOTO inside a function (where label indices are valid only in root->children), but it is shared by the internal ON-handler dispatch path without distinguishing the two callers. The ON-handler path should be allowed to unwind the call stack first and then jump.
Summary
When an
EXPECTtimeout or an error-locked keyboard state occurs while execution is inside a function body, the executor attempts to jump to the registeredON TIMEOUT/ON ERRORlabel viagotoLabel().gotoLabel()refuses to run while the call stack is non-empty and aborts execution with the misleading message"GOTO is not allowed inside functions", even though the script author never wrote aGOTO— they registered anONhandler.Location
src/script_executor.cppScriptExecutor::gotoLabel()at L733–L738 (early-returns with error when!m_callStack.isEmpty())ScriptExecutor::notifyTerminalStateChanged()at L120–L128 (invokesgotoLabel(m_onErrorLabel)unconditionally)ScriptExecutor::endExpect()at L612–L620 (invokesgotoLabel(m_onTimeoutLabel)unconditionally)Category
functionalSeverity
highImpact: any timeout or error that happens inside a
CALLed function will terminate the script with an incorrect diagnostic instead of running the registered handler, defeating the purpose ofON TIMEOUT/ON ERROR.Reproduction / Evidence
Verified by code analysis.
Script:
Execution path when the keyboard enters the
ErrorLockedstate during theEXPECTinsidemyfunc:notifyTerminalStateChanged()observesErrorLocked(L118).m_onErrorLabelis non-empty, sogotoLabel("fail")is called (L122).gotoLabel()sees!m_callStack.isEmpty()(the active call tomyfunc) and emitsexecutionError(0, "GOTO is not allowed inside functions"), thenstop()(L734–L738).failhandler never runs.The equivalent path exists for
ON TIMEOUTviaendExpect(false)at L614–L616 when theEXPECTwas entered from inside a function.Expected Behavior
When
ON TIMEOUT/ON ERRORfires, the runtime should unwind the call stack (and any nested block exec frames) down to the top-level script, then jump to the registered label — becauseLABELs can only be top-level (perPROMPT.md), the handler necessarily lives there.Actual Behavior
gotoLabel()refuses to run while inside a function and stops the script with a misleading"GOTO is not allowed inside functions"message. The registered handler never executes.Root Cause
gotoLabel()was written as a defense against user-writtenGOTOinside a function (where label indices are valid only inroot->children), but it is shared by the internal ON-handler dispatch path without distinguishing the two callers. The ON-handler path should be allowed to unwind the call stack first and then jump.