From 9be25f972eb3a194ccfbe17b2b6ca23abc2fdb8f Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Sun, 16 Aug 2026 20:04:54 -0500 Subject: [PATCH] interp: make the timeout recoverable, per package initializer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A package initializer that cannot be computed at compile time — crypto AES key expansion, secp256k1 table setup — would run the interpreter out of its budget and fail the BUILD. The only recourse was to raise the timeout for everything, which just moves the cliff. The timeout is now an ordinary recoverable error: the initializer that exceeded it reverts to a runtime call, exactly as it would on native Go, and the build continues. Each initializer also gets its own budget rather than sharing one, so a single expensive one no longer starves the remaining packages of precomputation they CAN do. Under -debug it says which package timed out and after how long, because "this init is now happening at startup instead of compile time" is a performance fact worth being able to see. --- interp/errors.go | 4 +++- interp/interp.go | 5 +++++ interp/interpreter.go | 9 ++++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/interp/errors.go b/interp/errors.go index 37a8e6d7d7..58486cd82d 100644 --- a/interp/errors.go +++ b/interp/errors.go @@ -20,6 +20,7 @@ var ( errMapAlreadyCreated = errors.New("interp: map already created") errLoopUnrolled = errors.New("interp: loop unrolled") errLoopTooLong = errors.New("interp: loop ran too many iterations") + errTimeout = errors.New("interp: timeout exceeded") ) // This is one of the errors that can be returned from toLLVMValue when the @@ -30,7 +31,8 @@ var errInvalidPtrToIntSize = errors.New("interp: ptrtoint integer size does not func isRecoverableError(err error) bool { return err == errIntegerAsPointer || err == errUnsupportedInst || err == errUnsupportedRuntimeInst || err == errMapAlreadyCreated || - err == errLoopUnrolled || err == errLoopTooLong || err == errInvalidPtrToIntSize + err == errLoopUnrolled || err == errLoopTooLong || err == errInvalidPtrToIntSize || + err == errTimeout } // ErrorLine is one line in a traceback. The position may be missing. diff --git a/interp/interp.go b/interp/interp.go index fa590ce7f9..d2cc0fdaf9 100644 --- a/interp/interp.go +++ b/interp/interp.go @@ -121,6 +121,11 @@ func Run(mod llvm.Module, timeout time.Duration, maxLoopIterations int, debug bo if r.debug { fmt.Fprintln(os.Stderr, "call:", fn.Name()) } + // Give each package initializer its own timeout budget (errTimeout is + // recoverable): one heavy compile-time-uncomputable init — e.g. crypto + // AES/secp256k1 key expansion — reverts to a runtime call without + // starving the remaining packages of precomputation time. + r.start = time.Now() _, mem, callErr := r.run(r.getFunction(fn), nil, nil, " ") call.EraseFromParentAsInstruction() if callErr != nil { diff --git a/interp/interpreter.go b/interp/interpreter.go index 4c8c36fe75..f0813dfd77 100644 --- a/interp/interpreter.go +++ b/interp/interpreter.go @@ -167,7 +167,14 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent if time.Since(r.start) > r.timeout { // Running for more than the allowed timeout; This shouldn't happen, but it does. // See github.com/tinygo-org/tinygo/issues/2124 - return nil, mem, r.errorAt(fn.blocks[0].instructions[0], fmt.Errorf("interp: running for more than %s, timing out (executed calls: %d)", r.timeout, r.callsExecuted)) + // errTimeout is recoverable: rather than abort the whole build, + // the caller reverts this package initializer to a runtime call + // (so heavy compile-time-uncomputable init like crypto AES/secp256k1 + // key expansion just runs at program startup, as on native Go). + if r.debug { + fmt.Fprintf(os.Stderr, "interp: %s: timing out after %s (executed calls: %d); leaving as runtime init\n", r.pkgName, r.timeout, r.callsExecuted) + } + return nil, mem, r.errorAt(fn.blocks[0].instructions[0], errTimeout) } if len(operands) != 0 {