Skip to content

ON TIMEOUT / ON ERROR handlers cannot fire when triggered from inside a function #3

@p0dalirius

Description

@p0dalirius

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:

  1. notifyTerminalStateChanged() observes ErrorLocked (L118).
  2. m_onErrorLabel is non-empty, so gotoLabel("fail") is called (L122).
  3. gotoLabel() sees !m_callStack.isEmpty() (the active call to myfunc) and emits executionError(0, "GOTO is not allowed inside functions"), then stop() (L734–L738).
  4. 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.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions