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

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions