Skip to content

Refactor debug package to inline funcs everywhere#3145

Open
jakebailey wants to merge 22 commits intomainfrom
jabaile/debug-cleanup
Open

Refactor debug package to inline funcs everywhere#3145
jakebailey wants to merge 22 commits intomainfrom
jabaile/debug-cleanup

Conversation

@jakebailey
Copy link
Member

@jakebailey jakebailey commented Mar 18, 2026

I've been wanting to see if we can speed up the debug asserts such that they could be always on.

To do this I've made a few changes to satisfy the Go compiler:

  • Eliminated reflect for checking nil-ness.
  • Made all funcs generic, no interface indirection.
  • Used "function outlining" to make it so that the fast path is always inlined at the caller.
  • Eliminate "tests", moving them to their callers, as they cannot be inlined.
  • Eliminated the "Node" function variants, as they are covered by existing funcs that inline better.

Regarding "function outlining", the pattern is to do something like this:

func AssertZero[T comparable](value T, message ...any) {
	var zero T
	if value == zero {
		return
	}
	assertZeroSlow(message...)
}

func assertZeroSlow(message ...any) {
	var msg string
	if len(message) > 0 {
		msg = "Expected zero value: " + fmt.Sprint(message...)
	} else {
		msg = "Expected zero value."
	}
	Fail(msg)
}

Go supports "mid stack inlining", where instead of it just being leaf functions that can be inlined, small functions in the middle can be inlined. In this case, restructuring such that the slow code is in another function lets the compiler inline the outer function in its caller, which is typically just a branch (which, should always predict false).

Now, when calling these, -m=2 says things like:

internal/debug/assert.go:15:6: can inline debug.Assert[*int] with cost 80 as: func(*int, ...any) { debug.Assert[go.shape.*int](&debug..dict.Assert[*int], debug.value, debug.message...) }
internal/debug/assert.go:15:6: can inline debug.Assert[bool] with cost 80 as: func(bool, ...any) { debug.Assert[go.shape.bool](&debug..dict.Assert[bool], debug.value, debug.message...) }
internal/debug/assert.go:72:6: can inline debug.AssertLessThan[int] with cost 78 as: func(int, int, ...any) { debug.AssertLessThan[go.shape.int](&debug..dict.AssertLessThan[int], debug.a, debug.b, debug.message...) }
internal/debug/assert.go:15:6: can inline debug.Assert[bool] with cost 80 as: func(bool, ...any) { debug.Assert[go.shape.bool](&debug..dict.Assert[bool], debug.value, debug.message...) }
internal/debug/assert.go:15:6: can inline debug.Assert[go.shape.bool] with cost 74 as: func(*[2]uintptr, go.shape.bool, ...any) { debug.zero = <nil>; if debug.value != debug.zero { return  }; debug.assertSlow[go.shape.bool]((*[2]uintptr)(debug..dict[0]), debug.value, debug.message...) }
internal/debug/assert.go:132:6: can inline debug.CheckNonZero[*github.com/microsoft/typescript-go/internal/ast.Node] with cost 77 as: func(*ast.Node, ...any) *ast.Node { return debug.CheckNonZero[go.shape.*github.com/microsoft/typescript-go/internal/ast.Node](&debug..dict.CheckNonZero[*github.com/microsoft/typescript-go/internal/ast.Node], debug.value, debug.message...) }
internal/debug/assert.go:132:6: can inline debug.CheckNonZero[go.shape.*github.com/microsoft/typescript-go/internal/ast.Node] with cost 70 as: func(*[1]uintptr, go.shape.*github.com/microsoft/typescript-go/internal/ast.Node, ...any) go.shape.*github.com/microsoft/typescript-go/internal/ast.Node { debug.zero = <nil>; if debug.value == debug.zero { debug.checkNonZeroSlow(debug.message...) }; return debug.value }

The PR's been updated to go further. Now there is just debug.Assert. Asserts are now always enabled.

Copilot AI review requested due to automatic review settings March 18, 2026 17:23
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors the internal/debug assertion utilities to use generics + “mid-stack inlining” patterns, and updates call sites across the compiler/LS to the new APIs with the goal of making debug assertions cheaper (potentially always-on).

Changes:

  • Replaced reflect-based “defined/nil” assertions with generic Assert/AssertZero/CheckNonZero and ordered comparisons.
  • Updated many assertion call sites to use boolean expressions or direct non-zero checks instead of specialized helpers (AssertNode, AssertIsDefined, etc.).
  • Removed/renamed debug helpers and adjusted transformer/checker/LS/printer/format code accordingly.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
internal/debug/assert.go New generic assertion APIs + slow-path outlining for inlining.
internal/debug/noassert.go Updated no-op implementations to match new debug API surface.
internal/transformers/estransforms/optionalchain.go Replaced AssertNotNode usage with explicit boolean assertions.
internal/transformers/estransforms/objectrestspread.go Replaced AssertNode with Assert(ast.IsX(...)) style checks.
internal/transformers/estransforms/esdecorator.go Updated various assertions to new debug.Assert usage.
internal/transformers/estransforms/classfields.go Updated assertions; one site replaced an assert-only helper with FailBadSyntaxKind.
internal/printer/namegenerator.go Updated identifier-kind assertion.
internal/printer/factory.go Updated computed temp variables assertion to Assert/non-zero pattern.
internal/ls/inlay_hints.go Updated type node “must exist” assertions.
internal/ls/importTracker.go Replaced CheckDefined with CheckNonZero.
internal/ls/findallreferences.go Updated symbol/value declaration assertions.
internal/ls/completions.go Updated assertions including AssertZero for nil tokens.
internal/ls/change/delete.go Updated various “must exist” assertions.
internal/ls/callhierarchy.go Updated declaration-name “must exist” assertion.
internal/ls/autoimport/import_adder.go Updated parent-module-symbol assertion.
internal/ls/autoimport/fix.go Updated assertions for nil/default-import/type-only import handling.
internal/format/span.go Updated list presence assertion for formatting span processing.
internal/format/indent.go Updated else-keyword presence assertion.
internal/checker/nodebuilderscopes.go Updated scope assertions; replaced optional-node helper with explicit condition.
internal/checker/nodebuilderimpl.go Replaced CheckDefined usage with explicit non-nil assertion.
internal/checker/jsx.go Updated inference-context assertion.
internal/checker/checker.go Updated multiple “must exist” assertions (symbols, declarations, type params, etc.).

You can also share your feedback on Copilot code review. Take the survey.

@jakebailey jakebailey force-pushed the jabaile/debug-cleanup branch from 01dbb62 to 00e75ce Compare March 19, 2026 18:17
@jakebailey
Copy link
Member Author

I'm refactoring this package even more, down to just having Assert as a boolean taking function. And, just deleting nodebug now.

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