ON TIMEOUT / ON ERROR handlers cannot fire when triggered from inside a function
@p0dalirius is already working on this.
Since Apr 18, 2026.
- Dominant language
- C++
- Stars
- 0
- Forks
- 1
- PR merge metrics
- No merged PRs in 30d
Description
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 (invokesgotoLabel(m_onErrorLabel)unconditionally)ScriptExecutor::endExpect()at L612–L620 (invokesgotoLabel(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()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).- The script terminates; the
failhandler 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.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.