Patch DSL

June 1, 2026 · View on GitHub

The Patch DSL is a declarative, AST-aware language for modifying Go source files. It is used to instrument the Go standard library for xgo's mocking, tracing, and interception features.

Patch files live under patches/<go-version>/src/ and mirror the GOROOT directory structure. Files with the .xgo.patch extension are applied to their corresponding GOROOT source files at build time.

See patches/DESIGN_FILE_PATCH.md for internal architecture and design decisions.

Block Structure

A .xgo.patch file contains one or more <patch> blocks:

<patch patch-name>
# comment (starting with #)
command
command

command
</patch>
  • # starts a comment line (ignored).
  • Blank lines are ignored.
  • Each block has a unique name used for idempotent re-application.
  • Blocks are applied sequentially; each block re-parses the file so later blocks see earlier modifications.

Command Reference

Positioning Commands

Move the cursor to a location in the source file. Each positioning command starts a new sequence number for stacking edits.

CommandDescription
goto struct <name>Cursor at the struct declaration
goto func <name>Cursor at the start of a function declaration
goto func (<receiver>) <name>Cursor at a method declaration
goto interface <name>Cursor at an interface declaration
goto opening {Cursor at the opening brace { of the current declaration (struct, func, interface)
goto closing }Cursor at the closing brace } of the current declaration
goto field <name>Cursor at a named field within the current struct (requires prior goto struct)
match <text>Cursor at the start of matching text within current scope (for subsequent insert_before/insert_after)
find_for_replace <text>Same as match, but selects a range for replace

Editing Commands

Modify the source at the cursor position.

CommandEffect
insert_before <text>Insert text at cursor.offset (before the cursor)
insert_after <text>Insert text at cursor.endOffset (after the cursor)
insert_after_line <text>Like insert_after, but skips the trailing \n so the insert lands on the next line
replace <text>Replace the range selected by a prior find_for_replace
replace_directive <old> with <new>Replace a compiler directive (e.g. //go:linkname) while preserving its positional constraints
newlineAppend \n to the current accumulated text segment
copy_func <source> as <target> append to file endCopy a function body, rename it, and append to the end of the file

Requirements:

  • insert_before, insert_after, insert_after_line, replace all require non-empty text.
  • replace requires a prior find_for_replace command.
  • replace_directive requires a prior goto command (it searches the full file).
  • goto opening/closing/field requires a prior positioning command (struct, func, or interface).

Examples

Add a field to a struct

<patch xgo_add_field>
goto struct g
goto closing }
insert_before newField string
newline
</patch>

Before:

type g struct {
    a int
}

After:

type g struct {
    a int
    /*<begin xgo_add_field>*/newField string/*<end xgo_add_field>*/}

Insert after a function's opening brace

<patch xgo_callback>
goto func goexit1
goto opening {
insert_after notifyCallback()
newline
</patch>

Before:

func goexit1() {
    ...
}

After:

func goexit1() {/*<begin xgo_callback>*/
    notifyCallback();/*<end xgo_callback>*/
    ...
}

Insert before a matched line

<patch xgo_before_cmd>
goto func run
match cmd := exec.Command(name)
insert_before log.Printf("running: %v", name)
newline
</patch>

Insert after a matched line

<patch xgo_mock_enable>
goto func runTest
match pkgArgs)
insert_after ;pkgs = xgoUnify(ctx, pkgs)
</patch>

Replace a line

<patch xgo_linkname>
find_for_replace //go:linkname timeSleep time.Sleep
replace //go:linkname timeSleep time.runtimeSleep
</patch>

Before:

//go:linkname timeSleep time.Sleep
func timeSleep(...)

After:

/*<begin xgo_linkname>*//*old://go:linkname timeSleep time.Sleep>*///go:linkname timeSleep time.runtimeSleep/*<end xgo_linkname>*/
func timeSleep(...)

Replace a function signature

<patch xgo_sleep_wrap>
find_for_replace func Sleep(d Duration)
replace func XgoRealSleep(d Duration);func /*xgo_instr*/Sleep(d Duration){ XgoRealSleep(d); }
</patch>

Replace a compiler directive

<patch xgo_runtime_linkname>
goto func timeSleep
replace_directive //go:linkname timeSleep time.Sleep with //go:linkname timeSleep time.XgoRealSleep
</patch>

Before:

//go:linkname timeSleep time.Sleep
func timeSleep(...)

After:

/*<next-line-original xgo_runtime_linkname>//go:linkname timeSleep time.Sleep</next-line-original>*/
//go:linkname timeSleep time.XgoRealSleep
func timeSleep(...)

replace_directive preserves the directive's positional relationship with the function it annotates, while find_for_replace + replace does not.

Insert after a line (skipping trailing newline)

<patch xgo_import_io>
goto func LoadPackage
match "lines")
insert_after_line ;import io "io"
</patch>

insert_after_line is like insert_after but skips past the \n after the matched position, causing the inserted text to land on the next line. This is useful for adding code after a matched line without creating inline chunks.

Copy a function to the end of the file

<patch add_real_now>
copy_func Now as XgoRealNow append to file end
</patch>

This copies the body of Now(), renames the function to XgoRealNow(), and appends it to the file end with markers.

Insert a multi-line block (stacking insert_after + newline)

<patch xgo_noder_syntax>
goto func LoadPackage
match lines), "lines")
insert_after // auto gen
newline
insert_after if os.Getenv("FLAG")=="true" {
newline
insert_after     doWork()
newline
insert_after }
</patch>

Each newline appends \n to the preceding text segment.

Stacking Behavior

When multiple editing commands operate at the same cursor position (same sequence number), they stack:

Insert modeStack orderExample
insert_beforeReverse (last command = closest to original code)insert_before A then insert_before BAB placed before the cursor
insert_afterForward (first command = closest to original code)insert_after A then insert_after BAB placed after the cursor
newlineAppends \n to the preceding segmentinsert_after A then newlineA\n

Leading/trailing newlines from the stacking artifact are trimmed.

Marker System & Idempotency

All edits are wrapped with inline markers so the system can:

  • Detect existing edits and clear them before re-applying.
  • Preserve original text for replace operations.

Markers never occupy their own line — they share lines with code to keep panic file:line references accurate.

Insert Marker

/*<begin patch-name>*/inserted-content/*<end patch-name>*/

Replace Marker

/*<begin patch-name>*//*old:original-text*/replacement-content/*<end patch-name>*/

Replace Directive Marker

/*<next-line-original patch-name>original-directive</next-line-original>*/
replacement-directive

The /*<old:...>*/ sub-comment stores the original text so the system can restore it on re-application.

Clearing

  • Before applying a patch, any existing markers with that patch's name are removed.
  • For insert edits: markers and their content are deleted.
  • For replace edits: the /*old:...*/ content is restored.
  • For replace_directive edits: the /*<next-line-original>*/ annotation's original text is restored and the replacement line is removed.
  • Applying the same patch twice has the same effect as applying it once.

__config__.json

Each patch directory (e.g. patches/go1.25/) may have a __config__.json for directory copy and generate instructions:

{
  "version": "go1.25+",
  "copy": [
    {"from": "relative/to/xgo_repo", "to": "relative/to/GOROOT", "ignore_files": ["path/to/skip"]}
  ],
  "generate": [
    {
      "kind": "rebuild-compiler",
      "cmd": "shell command",
      "outputs": ["file/path"]
    }
  ]
}
  • copy: copies a directory from the xgo repo into the instrumented GOROOT. Each entry has:
    • from: source path relative to xgo repo root
    • to: destination path relative to GOROOT
    • ignore_files (optional): file paths to remove after copy (relative to GOROOT)
  • generate: runs shell commands (supports ${VAR} substitution from environment). Each entry has:
    • kind (optional): category label for selective skipping (e.g. "rebuild-compiler", "rebuild-go")
    • cmd: the shell command to run
    • outputs: list of output file paths (informational)
  • Non-.xgo.patch files (except __config__.json) are copied one-to-one into GOROOT.

Error Cases

ConditionError
insert_before with no textinsert_before requires text
insert_after with no textinsert_after requires text
insert_after_line with no textinsert_after_line requires text
replace with no textreplace requires text
replace without find_for_replacereplace requires prior find_for_replace
replace_directive with no old or new textreplace_directive requires old and new text
replace_directive missing with keywordreplace_directive requires 'with' keyword: "..."
Unknown goto targetunknown goto target: "..."
match not found in scopetext not found in scope: "..."
Struct/function/interface not founddeclaration not found: ... / function not found: ...
Missing </patch> tagmissing </patch> for "..."
copy_func missing as keywordcopy_func requires 'as' keyword
Unknown commandunknown command: "..."

Walkthrough: Adding a New Patch

  1. Identify the target — find the GOROOT source file to modify and the exact location (struct, function, or text to match).

  2. Create the patch file — mirror the GOROOT path under patches/<go-version>/src/ with a .xgo.patch suffix.
    Example: to patch src/runtime/runtime2.go, create patches/go1.25/src/runtime/runtime2.go.xgo.patch.

  3. Write the patch block — use positioning commands to navigate, then editing commands to modify:

    <patch my_new_patch>
    # Describe what this patch does
    goto func targetFunc
    match theLine
    insert_after myExtraCode()
    </patch>
    
  4. Test — run the xgo test suite to verify your changes:

    go test ./instrument/patch/... -v -count=1
    
  5. For new Go versions — copy the previous version's patch directory and adjust selectors as needed:

    cp -r patches/go1.25 patches/go1.26