Skip to content

runtime, testing: support Goexit, SkipNow, FailNow#5493

Open
jakebailey wants to merge 5 commits into
tinygo-org:devfrom
jakebailey:goexit
Open

runtime, testing: support Goexit, SkipNow, FailNow#5493
jakebailey wants to merge 5 commits into
tinygo-org:devfrom
jakebailey:goexit

Conversation

@jakebailey

@jakebailey jakebailey commented Jul 4, 2026

Copy link
Copy Markdown
Member

In #5438 I expanded out panic/recover behavior to cover a lot of (all?) cases.

I forgot about Goexit.

This PR adds support for it, including the work to make it exit pthreads.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds end-to-end support for runtime.Goexit in TinyGo, including integrating it with the testing package’s fatal/skip paths and ensuring thread-based schedulers can terminate goroutines cleanly.

Changes:

  • Implement Goexit as a special unwind/termination path in the panic/defer machinery and schedulers.
  • Update testing/benchmark runners to execute test bodies in goroutines so FailNow/SkipNow can terminate via Goexit without killing the parent runner.
  • Add/extend test coverage and golden outputs for Goexit + fatal/skip behaviors.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
testdata/testing.txt Updates expected output for new fatal subtest behavior.
testdata/testing.go Adds t.Fatal/t.SkipNow subtests with arch/OS gating.
testdata/testing-wasm.txt New expected output variant for wasm / windows-386 gating.
testdata/recover.txt Extends expected output for Goexit + recovered deferred panics.
testdata/recover.go Adds Goexit scenarios covering recovered/nested/loop defers.
testdata/goexit.go New standalone Goexit scenario program for crash/deadlock assertions.
src/testing/testing.go Implements FailNow/SkipNow via runtime.Goexit; runs subtests in goroutine.
src/testing/benchmark.go Runs benchmarks in goroutine to support Goexit semantics.
src/runtime/scheduler_threads.go Adds scheduler goexit() path for thread-backed tasks.
src/runtime/scheduler_none.go Adds goexit() implementation for no-scheduler builds.
src/runtime/scheduler_cores.go Adds goexit() for multicore scheduler builds.
src/runtime/scheduler_cooperative.go Adds goexit() for tasks/asyncify cooperative scheduler.
src/runtime/runtime_windows.go Defines abort() on Windows instead of relying on an external symbol.
src/runtime/panic.go Tracks pending-Goexit across recovered deferred panics; calls goexit() on Goexit completion.
src/internal/task/task_threads.go Adds task-level Exit() and refactors deadlock tracking for thread scheduler.
src/internal/task/task_threads.c Adds tinygo_task_exit() using pthread_exit.
src/internal/task/task_stack.go Hooks task lifecycle accounting into stack-based schedulers.
src/internal/task/task_exit.go Introduces task lifecycle/deadlock logic for tasks/asyncify/cores schedulers.
src/internal/task/task_asyncify.go Hooks task lifecycle accounting into asyncify scheduler start.
main_test.go Adds Goexit crash/deadlock regression test; adjusts expected-output routing for testing.go on wasm/win386.
GNUmakefile Adds crypto/ecdh to Linux test packages; updates skip rationale comment.
compiler/testdata/defer-cortex-m-qemu.ll Updates deferFrame layout expectations for new Goexit flag field.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +5 to +28
import "sync/atomic"

var (
mainTask *Task
liveTasks uint32
mainExitedByGoexit uint32
)

func addLiveTask(t *Task) {
if mainTask == nil {
mainTask = t
}
atomic.AddUint32(&liveTasks, 1)
}

// Exit exits the current task because runtime.Goexit was called.
func Exit() {
exit(true)
}

func exit(goexit bool) {
t := Current()
remaining := atomic.AddUint32(&liveTasks, ^uint32(0))
if t == mainTask {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so? the main task is the main task, so it should only happen once, first.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.

Comment thread src/testing/testing.go
Comment on lines +526 to +531
done := make(chan struct{})
go func() {
defer close(done)
tRunner(&sub, f)
}()
<-done

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that this needs to be done is why I had to add more memory to the encoding/xml tests.

Now that tests run on a new goroutine, their stacks are way smaller. On my mac (where I was debugging this), the main thread has something like 8MB, but the new goroutine only has like 512KB or something. The xml tests have a big test that seems to take somewhere between 4MB and 8MB based on what worked in CI.

Comment thread src/runtime/panic.go Outdated
@jakebailey jakebailey force-pushed the goexit branch 2 times, most recently from e631e70 to 429be4b Compare July 5, 2026 05:07
@jakebailey jakebailey marked this pull request as ready for review July 5, 2026 15:22
@jakebailey jakebailey requested a review from Copilot July 5, 2026 15:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 1 comment.

Comment thread src/runtime/panic.go

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 2 comments.

Comment thread compiler/defer.go
Comment thread src/internal/task/task_threads.go Outdated
@jakebailey jakebailey force-pushed the goexit branch 2 times, most recently from 39ed9eb to ee3f918 Compare July 6, 2026 04:16
Keep a pending Goexit separate from the active panic state so recovered
deferred panics do not cancel the original Goexit unwind. Goexit should
also ignore -panic=trap, since it is not a panic.

Wire testing.FailNow and SkipNow through runtime.Goexit, and run tests
and benchmarks in goroutines. This lets Goexit terminate the user
function without skipping test bookkeeping. Add Goexit/recover coverage
and enable crypto/ecdh in the Linux stdlib test list.
The threads scheduler handled runtime.Goexit using the generic deadlock
path, which parked the current pthread forever. Add a scheduler-specific
goexit hook so it can remove the current task and exit the pthread while
ordinary blocking deadlocks still block.

Track main goroutine Goexit so the last remaining goroutine reports the
expected deadlock instead of letting the process exit successfully.
Expand the crash coverage with the Goexit cases covered by Big Go's
runtime tests.
Top-level tests now run in goroutines, which puts stdlib tests on
TinyGo's default host goroutine stack. encoding/xml's depth-limit
test needs a larger stack on Linux and Darwin, so run just that
package with a larger stack while leaving the rest of the host stdlib
tests on the default stack.
Treat panicState as a bitmask so a recovered panic during Goexit
can clear the panic bit while preserving the pending Goexit bit.
This removes the separate deferFrame Goexit field and restores the
previous defer frame size.
Comment thread compiler/defer.go
Comment on lines +133 to +138
movl %ebx, 8(%ebx)
movl %esi, 12(%ebx)
movl %edi, 16(%ebx)
movl %ebp, 20(%ebx)
1:`
constraints = "={eax},{ebx},~{ebx},~{ecx},~{edx},~{esi},~{edi},~{ebp},~{xmm0},~{xmm1},~{xmm2},~{xmm3},~{xmm4},~{xmm5},~{xmm6},~{xmm7},~{fpsr},~{fpcr},~{flags},~{dirflag},~{memory}"
constraints = "={eax},{ebx},~{ecx},~{edx},~{xmm0},~{xmm1},~{xmm2},~{xmm3},~{xmm4},~{xmm5},~{xmm6},~{xmm7},~{fpsr},~{fpcr},~{flags},~{dirflag},~{memory}"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll be honest and say I don't have a great understanding of this; sort of like how I had to deal with clobbering problems in my previous PR, the crash is also clobbering, but seemingly so much so that I can't make LLVM do the right thing, and instead need to manually save everything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants