Compile-time code generation for Go. Executes helper functions at build time and replaces placeholders with results.
Core concept: Placeholder comment (//:func:args) + literal value on next line → GoAhead replaces literal with computed result.
goahead/
├── main.go # CLI entry point
├── internal/ # All business logic
│ ├── codegen.go # Orchestration
│ ├── code_processor.go # Placeholder replacement
│ ├── file_processor.go # File I/O, parsing
│ ├── function_executor.go # Helper execution, depth resolution
│ ├── injector.go # Function injection
│ ├── toolexec_manager.go # Toolexec mode
│ ├── types.go # Core types: ProcessorContext, UserFunction, Config
│ └── constants.go # Version, patterns
├── test/ # All tests
│ ├── test_helpers.go # setupTestDir, verifyCompiles, processAndReplace
│ └── *_test.go # Tests by feature
└── examples/ # Feature examples
- Scan -
file_processor.CollectAllGoFiles()walks tree, categorizes files - Load -
file_processor.LoadUserFunctions()parses helpers, registers inProcessorContext.FunctionsByDepth - Prepare -
function_executor.ensurePreparedForDir()builds executable code for each source directory using depth resolution - Process -
code_processor.ProcessFile()finds placeholders, calls helpers, replaces literals - Inject -
injector.ProcessFileInjections()copies functions from helpers to source
Purpose: Allow different implementations of same symbol at different tree depths without conflicts. All helper functions are visible project-wide; depth determines shadowing priority.
Algorithm:
sourceDepth = calculateDepth(sourceFile)
// First: search from source depth down to root (closest wins)
for depth = sourceDepth down to 0:
if symbol exists at depth:
return symbol
// Then: search deeper depths (all helpers visible, lower priority)
for depth = sourceDepth+1 up to maxDepth:
if symbol exists at depth:
return symbol
Rules:
- Only exported symbols (uppercase) are tracked/available for placeholders
- All helper functions are visible from anywhere in the project
- Same-depth symbols pool and share (siblings see each other)
- Closer depth takes priority (child overrides parent for files in child dir)
- Root files can see subdirectory helpers (lower priority than root helpers)
- Duplicate at same depth = FATAL
Implementation: internal/function_executor.go:
collectVisibleHelperFiles()- gathers ALL helpers, ordered by priority (closest depth first)filterShadowedDeclarations()- removes shadowed funcs/vars/consts/typesprocessFunctionFileWithNames()- extracts all exported identifiers usingtoken.IsExported()
Critical:
- Variables, constants, and types follow same shadowing as functions
- Unexported symbols (lowercase) are ignored for placeholder/shadowing but still executable within helpers
- This prevents "redeclared" errors and aligns with Go export conventions
Purpose: Directories with their own go.mod are independent projects - they don't inherit parent helpers and are processed as separate trees.
Detection: During CollectAllGoFiles(), if a subdirectory contains go.mod, it's:
- Added to
ctx.Submodules - Skipped with
filepath.SkipDir(not processed as part of parent)
Processing: After main project completes, RunCodegen() recursively processes each submodule:
for _, submodule := range ctx.Submodules {
RunCodegen(submodule, verbose) // Fresh context, isolated tree
}Behavior:
- Submodule helpers are NOT visible to parent project
- Parent helpers are NOT visible to submodule
- Each submodule has its own depth-based resolution tree starting at depth 0
- Works recursively (submodules can contain submodules)
- Single
goaheadinvocation processes entire workspace including all nested submodules
Use case: Monorepos with multiple Go modules that need independent code generation.
Purpose: Copy runtime functions from helpers to source (e.g., obfuscation: encode at build time, decode at runtime).
Markers:
//:inject:MethodName
type Interface interface {
MethodName(args) returnType
}Behavior:
- Validates method exists in interface
- Removes existing injected code
- Copies function + dependencies from helper
- Preserves marker (repeatable on subsequent builds)
Implementation: internal/injector.go
Critical: Tests verify the SPECIFICATION, not the implementation.
Wrong approach:
// Bad: models current buggy behavior
result := process(input)
if strings.Contains(result, "something") { ... } // Just checks outputCorrect approach:
// Good: verifies specification
result := processAndReplace(t, dir, "main.go")
verifyCompiles(t, result) // Compiles = valid Go
if !strings.Contains(result, `expected = "value"`) {
t.Errorf("Should replace placeholder with value")
}Must test:
- Positive cases (valid inputs work)
- Negative cases (invalid inputs fail with correct error)
- Edge cases (empty, nil, boundary, Unicode)
- Generated code compiles (
verifyCompiles)
Test utilities (test/test_helpers.go):
setupTestDir(t, files)- temp dir with test filesverifyCompiles(t, code)- compile checkprocessAndReplace(t, dir, file)- full pipeline
Stdlib only - no external dependencies
Deterministic - same input = same output always
Error wrapping - fmt.Errorf("context: %w", err)
No panics - return errors (except truly unrecoverable)
Config-driven - CLI flags → internal.Config
- Placeholder needs literal on next line - comment alone does nothing
- Helper files need TWO tags -
//go:build excludeAND//go:ahead functions - Case-sensitive -
Version≠version - Strings need quotes -
//:greet:"World"not//:greet:World - Toolexec + CGO = race condition - use subcommands for CGO
Before changes:
go test ./... # Verify tests passAfter changes:
gofmt -w . # Format
go vet ./... # Static analysis
go build ./... # Build check
go test ./... # All tests
go test -race ./... # Race detectionAdding features:
- Read relevant tests to understand expected behavior
- Add tests for new functionality FIRST
- Implement feature
- Verify generated code compiles (
verifyCompiles) - Run full test suite
Fixing bugs:
- Add failing test demonstrating bug
- Fix bug
- Verify test passes
- Run full suite
- All tests pass (
go test ./...) - No race conditions (
go test -race ./...) - Update README.md for user-facing changes
- Update AGENTS.md for structural/flow changes
- Include tests for new features
- Include regression tests for bug fixes