inco

module
v0.1.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 2, 2026 License: MIT

README

Inco

Inco is a compile-time assertion engine for Go. Write contract directives as plain comments; they are transformed into runtime guards in shadow files, wired in via go build -overlay.

Philosophy

Business logic should be pure. Defensive noise — if x == nil, if err != nil — belongs in the shadow, not in your source.

Write the intent; Inco generates the shield.

if is for logic, not for guarding

In an Inco codebase, if should express logic flow — branching on business conditions, selecting behavior. Not for:

  • Nil guards// @inco: ptr != nil
  • Value validation// @inco: x > 0
  • Range checks// @inco: i < len(s)

When every defensive check is a directive, the remaining if statements carry real semantic weight — genuine decisions, not boilerplate.

Directive Syntax

Two prefixes — @inco: (contract, condition inverted) and @if: (guard, same as if).

@inco: is the core — write the condition you expect to hold, inco generates the inverse guard. @if: exists to lower the migration barrier: existing if guard clauses can be converted to directives with zero mental overhead — just copy the condition as-is.

Two forms — standalone and inline:

Standalone (entire line is directive)
// @inco: <expr>
// @inco: <expr>, -panic("msg")
// @inco: <expr>, -return(values...)
// @if: <expr>, -continue
// @if: <expr>, -break
Inline (code + trailing directive)
_ = err // @if: err != nil, -panic(err)
_ = skip // @inco: !skip, -return(filepath.SkipDir)

Inline directives attach to a code statement at the end of the line. The engine uses AST analysis to distinguish inline directives from decorative comments (e.g. struct field comments are ignored).

The default action is -panic with an auto-generated message.

Example: Bank Transfer
// Transfer moves amount cents from one account to another.
func Transfer(from *Account, to *Account, amount int) error {
    // @inco: from != nil
    // @inco: to != nil
    // @inco: from != to, -panic("cannot transfer to self")
    // @inco: amount > 0, -panic("amount must be positive")
    // @inco: from.Balance >= amount, -return(fmt.Errorf("insufficient funds: have %d, need %d", from.Balance, amount))

    from.Balance -= amount
    to.Balance += amount

    fmt.Printf("transferred %d from %s to %s\n", amount, from.ID, to.ID)
    return nil
}

Five directives, each expressing a precondition the caller must satisfy. No if noise — just intent.

Directive Semantics

@inco: is a contract — "I expect this to hold". Generated code is if !(<expr>) { action }. Write the condition you expect to be true. This is the idiomatic way to express preconditions in Inco.

@if: is a migration helper — same condition as if. Generated code is if <expr> { action }. When migrating an existing codebase, if err != nil { return err } becomes // @if: err != nil, -return(err) with no condition inversion needed.

// @inco: err == nil, -panic(err)    // contract: I expect no error
// @inco: n > 0, -continue           // contract: n must be positive
// @if: err != nil, -return(err)     // guard: if error, return
// @if: x == nil, -panic("x nil")    // guard: if nil, panic
Actions
Action Syntax Meaning
panic (default) // @inco: <expr> Panic with auto message
panic (custom) // @inco: <expr>, -panic("msg") Panic with custom message
return // @if: <expr>, -return(vals...) Return specified values
return (bare) // @if: <expr>, -return Bare return
continue // @if: <expr>, -continue Continue enclosing loop
break // @if: <expr>, -break Break enclosing loop
log // @if: <expr>, -log(args...) log.Println(args...)
Generated Output

After inco gen, the above becomes a shadow file in .inco_cache/:

func Transfer(from *Account, to *Account, amount int) error {
    if !(from != nil) {
        panic("inco violation: from != nil (at transfer.inco.go:14)")
    }
    if !(to != nil) {
        panic("inco violation: to != nil (at transfer.inco.go:15)")
    }
    if !(from != to) {
        panic("cannot transfer to self")
    }
    if !(amount > 0) {
        panic("amount must be positive")
    }
    if !(from.Balance >= amount) {
        return fmt.Errorf("insufficient funds: have %d, need %d", from.Balance, amount)
    }

    from.Balance -= amount
    to.Balance += amount

    fmt.Printf("transferred %d from %s to %s\n", amount, from.ID, to.ID)
    return nil
}

Shadow files live in .inco_cache/ and are wired in via go build -overlay.

Auto-Import

When directive arguments reference packages (e.g. fmt.Sprintf, errors.New), Inco automatically adds the corresponding import to the shadow file via astutil.AddImport. No manual import management needed.

Standard library auto-imports are restricted to a curated whitelist of common packages:

Category Packages
Core fmt errors strings strconv bytes regexp sort slices maps math cmp
OS / IO os io filepath path bufio
Time / Sync time context sync
Encoding json xml csv base64 hex
Net http url
Log log slog

Third-party dependencies used in your module are resolved dynamically via go list -e -deps ./... (results are cached across files). Ambiguous package names (e.g. a local package shadowing a stdlib name) are removed from the mapping to prevent incorrect imports. Internal and vendored packages are also filtered out.

Usage

# Install
go install github.com/incogno-design/inco/cmd/inco@latest

# Generate overlay
inco gen [dir]

# Build / Test / Run with contracts enforced
inco build ./...
inco test ./...
inco run .

# Format source files (normalizes directive spacing)
inco fmt ./...

# Release: bake guards into source tree (no overlay needed)
inco release [dir]

# Revert release
inco release clean [dir]

# Contract coverage audit
inco audit [dir]

# Clean cache
inco clean [dir]

Formatting — inco fmt

inco fmt ./...

Normalizes blank-line spacing around directives. Runs a three-step pipeline:

  1. gofmt -w — canonical Go formatting
  2. Directive spacing — adjust blank lines per rules below
  3. gofmt -w — re-format after spacing changes (skipped when nothing changed)
Spacing rules
Situation Rule
Between consecutive directives No blank line
After directive block → non-directive code Exactly one blank line
After directive → closing } No blank line

Before:

func foo(x int, y int) {
    // @inco: x > 0

    // @inco: y > 0
    println(x, y)
}

After inco fmt:

func foo(x int, y int) {
    // @inco: x > 0
    // @inco: y > 0

    println(x, y)
}

Release Mode

inco release bakes guards into your source tree — no overlay, no build tags, no inco tool needed at build time.

Convention: .inco.go files

Name source files that contain directives with a .inco.go extension:

foo.inco.go   ← source with @inco:/@if: directives

inco gen and inco build treat .inco.go files exactly like .go files (they end in .go, so the scanner picks them up).

Release workflow
inco release .

For each .inco.go file in the overlay:

  1. Generate foo.go — shadow content with guards injected (the // Code generated by inco. DO NOT EDIT. header is prepended; //line directives are preserved so stack traces still point to original source lines)
  2. Backup foo.inco.gofoo.inco — renamed so the Go compiler ignores it

After release:

go build ./...    # compiles foo.go (with guards) — no overlay, no inco needed
Dry run

Preview what release would do without writing any files:

inco release --dry-run .

Prints [dry-run] <path> for each file that would be generated or renamed.

Restore
inco release clean .

This removes each generated foo.go and restores foo.incofoo.inco.go.

When to use
  • Distribution: ship a self-contained project with contracts baked in
  • CI/CD: build with guards without installing inco
  • One-click restore: inco release clean brings you back to development mode
Single-repo distribution (CI example)

If you want to develop on an inco branch (with .inco.go sources) and automatically publish released code to main, add a workflow like .github/workflows/release-single-repo.yml:

name: Release inco → main

on:
  push:
    branches: [inco]

permissions:
  contents: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: inco

      - uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - name: Install inco
        run: go install github.com/incogno-design/inco/cmd/inco@latest

      - name: Release
        run: |
          inco release .

          # Rename remaining .inco.go files (those without directives) → .go
          find . -name '*.inco.go' -not -path './.inco_cache/*' | while read -r f; do
            target="${f%.inco.go}.go"
            mv "$f" "$target"
          done

          rm -rf .inco_cache

      - name: Push to main
        run: |
          git config user.name  "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add -A
          git commit -m "release: inco@${GITHUB_SHA::7}" --allow-empty
          git push origin HEAD:main --force

This gives you a clean separation: inco branch holds the .inco.go sources with directives, main branch always contains plain .go files with guards baked in — consumers can go install or go get from main without ever knowing about inco.

Build from Source

go install github.com/incogno-design/inco/cmd/inco@latest
make build      # inco build → bin/inco
make test       # Run tests with contracts enforced
make gen        # Regenerate overlay
make clean      # Remove .inco_cache/ and bin/
make install    # Install to $GOPATH/bin

Audit

inco audit scans your codebase and reports:

  • @inco: coverage: percentage of functions guarded by at least one directive
  • inco/(if+inco) ratio: what fraction of all conditional guards are directives
  • Per-file breakdown: directive count, if count, function count, and guarded function count per file
  • Unguarded functions: list of functions without any directive (closures excluded)
  • Ignored files: files/dirs excluded by .incoignore

Test files (_test.go), hidden directories, vendor/, and testdata/ are always skipped.

$ inco audit .
inco audit — contract coverage report
======================================

  Files scanned:  9
  Functions:      52

@inco: coverage:
  With @inco::     30 / 52  (57.7%)
  Without @inco::  22 / 52  (42.3%)

Directive vs if:
  @inco::             67
  @if::               0
  ─────────────────────
  Total directives:   67
  Native if stmts:    59
  inco/(if+inco):     53.2%

Per-file breakdown:
  File              @inco:  if  funcs  guarded
  ────────────────  ──────  ──  ─────  ───────
  engine.inco.go        40  20     15       12
  directive.inco.go      4   8      5        3
  ...

Functions without @inco: (22):
  internal/inco/types.inco.go:15  String

Ignored by .incoignore (4):
  example/demo.inco.go
  example/edge_cases.inco.go
  example/generics.inco.go
  example/transfer.inco.go

The inco/(if+inco) ratio reflects the degree of separation between guards and business logic — higher is not necessarily better. A ratio that is too high suggests business branches may have been incorrectly converted to directives. The ideal state is: all guards use @inco: or @if:, all business logic stays in if, and the ratio naturally settles at a reasonable value.

How It Works

  1. inco gen scans all .go files for // @inco: / // @if: comments (respecting .incoignore; test files, hidden directories, vendor/, and testdata/ are always skipped)
  2. Uses go/ast to classify each directive as standalone (comment-only line) or inline (attached to a statement)
  3. Generates shadow files in .inco_cache/ — standalone directives become if-blocks in place; inline directives keep the code line and inject the if-block after it
  4. Injects //line directives so panic stack traces point back to original source lines
  5. Produces overlay.json for go build -overlay
  6. Shadow files replace originals via overlay — source files are not modified on disk
AST-Based Classification

The engine parses each source file as an AST and collects the set of line numbers that contain Go statements (AssignStmt, ExprStmt, ReturnStmt, IncDecStmt, SendStmt, GoStmt, DeferStmt, BranchStmt). When a directive comment is found:

  • Comment-only line → standalone directive (full line replaced by if-block)
  • Line in statement set → inline directive (code preserved, if-block injected after)
  • Other (struct field comment, etc.) → ignored

This prevents false matches on decorative comments like RequireCount int // @inco: directives.

Incremental Builds

The engine maintains a manifest.json in .inco_cache/ that records a SHA-256 hash for each source file. On subsequent runs, files with unchanged hashes are skipped entirely — only modified files are re-parsed and re-generated. Orphaned shadow files (whose source has been deleted) are automatically cleaned up.

Parallel Processing

File parsing and shadow generation run in parallel across GOMAXPROCS worker goroutines, each with an independent token.FileSet to avoid contention. The first error is propagated atomically.

Shadow File Naming

Shadow files use content-hash naming: <basename>_<sha256[:16]>.go. This ensures stable Go build cache keys — editing a file produces a new shadow name, preventing stale cache hits.

Project Structure

cmd/inco/           CLI: gen, build, test, run, fmt, audit, release, clean
internal/inco/      Core engine:
  audit.inco.go       Contract coverage auditing
  directive.inco.go   Directive parsing (@inco:, @if:)
  engine.inco.go      AST processing, code generation, overlay I/O
  format.inco.go      Directive spacing formatter (inco fmt)
  ignore.inco.go      .incoignore file parsing and hierarchical matching
  import.inco.go      Auto-import management (stdlib whitelist, go list)
  release.inco.go     Release mode: bake guards into source
  types.inco.go       Core types (Directive, ActionKind, Overlay)
  walk.inco.go        Shared file traversal logic

Notes

Inco is self-hosting — it uses @inco: directives in its own source code. Since directives are plain Go comments, the code compiles with or without expansion.

The Acknowledgement Pattern

_ = var is the acknowledgement pattern — you explicitly tell the compiler "I know this variable exists; its guard is handled by inco." When a variable is only used in a directive, _ = var makes the intent clear:

_ = err // @inco: err == nil, -panic(err)

_ = err acknowledges the variable in source; the directive generates the real guard in the overlay. The code compiles cleanly with or without inco.

Design

  • Comment-based: Plain Go comments — no custom syntax, no broken IDE support
  • Fail-fast: panic by default — or return, continue, break as needed
  • Zero-overhead option: Strip directives in production, or keep for fail-fast
  • Incremental: SHA-256 manifest — only changed files are re-processed
  • Parallel: Worker goroutines scale to GOMAXPROCS with independent token.FileSet
  • Cache-friendly: Content-hash (SHA-256) based shadow filenames for stable build cache
  • Source-mapped: //line directives preserve original file:line in stack traces
  • Auto-import: Package references in directive args are auto-imported (with disambiguation)

Best Practices & Common Pitfalls

1. Long Expression Optimization

If the expression is too long or complex, extract it into a boolean variable first — this improves readability and works well with _ = var:

// ❌ Verbose and hard to read
// @inco: !(si.ParentStateInfo != nil && si.ParentStateInfo != parentStateInfo), -panic(...)

// ✅ Clear
isInvalid := si.ParentStateInfo != nil && si.ParentStateInfo != parentStateInfo
_ = isInvalid // @inco: !isInvalid, -panic(...)
2. Repeated Var Assignment for Multiple Guards

When applying multiple directives to the same variable consecutively (e.g., log first, then panic), repeat _ = var before each directive line:

// ✅ Best practice: explicitly suppress unused checks for each line
_ = err // @inco: err == nil, -log("error occurred:", err)
_ = err // @inco: err == nil, -panic(err)
3. Group Directives Together

Directives should be clustered, not scattered among logic. When all validations are independent, group declarations first, then directives:

// ✅ Independent — group declarations, then directives
a, errA := doA()
b, errB := doB()

_ = errA // @inco: errA == nil, -panic(errA)
_ = errB // @inco: errB == nil, -panic(errB)

use(a, b)

Use unique names (errA, errB) to avoid shadowing.

When validations are sequential (each step depends on the previous), interleave declaration → directive pairs with a blank line between each pair:

// ✅ Sequential — declare, validate, blank, declare, validate, blank, logic
absDir, err := filepath.Abs(dir)
_ = err // @inco: err == nil, -panic(err)

err = inco.NewEngine(absDir).Run()
_ = err // @inco: err == nil, -panic(err)

fmt.Println("done")

Do not scatter directives far from their declarations or mix them into unrelated logic.

3b. Unused Variables: Suppress Before the Directive

When a variable is only referenced inside a directive's action (not in later code), place the _ = var above the directive, not below:

// ✅ CORRECT — suppressor before directive
s, ok := actionNames[k]
_ = s
_ = ok // @inco: !ok, -return(s)
return "unknown"

// ❌ WRONG — suppressor after directive breaks formatting
s, ok := actionNames[k]
_ = ok // @inco: !ok, -return(s)
_ = s
return "unknown"

This keeps the directive at the boundary between guard and code, and inco fmt spacing rules work naturally.

4. @inco: is for Function Parameter Validation

The core purpose of @inco: is parameter validation and precondition checks at function entry. The function signature declares types; @inco: declares value constraints:

func Transfer(from, to *Account, amount int) error {
    // @inco: from != nil
    // @inco: to != nil
    // @inco: from != to, -panic("cannot transfer to self")
    // @inco: amount > 0, -panic("amount must be positive")
    // @inco: from.Balance >= amount, -return(fmt.Errorf("insufficient funds"))

    from.Balance -= amount
    to.Balance += amount
    return nil
}

Mid-flow error handling@if: works for inline guard clauses:

result, err := doWork()
_ = err // @if: err != nil, -return(nil, err)

Or use @inco: to express contract semantics ("I expect no error"):

result, err := doWork()
_ = err // @inco: err == nil, -return(nil, err)

License

MIT

Directories

Path Synopsis
cmd
inco command
internal
codegen
Package codegen produces shadow file content from parsed Go source files and their @inco:/@if: directives.
Package codegen produces shadow file content from parsed Go source files and their @inco:/@if: directives.
directive
Package directive provides parsing for @inco: and @if: directive comments.
Package directive provides parsing for @inco: and @if: directive comments.
fsutil
Package fsutil provides file-system traversal and ignore-list support for the inco engine and analysis tools.
Package fsutil provides file-system traversal and ignore-list support for the inco engine and analysis tools.
inco
Package inco implements a compile-time code injection engine.
Package inco implements a compile-time code injection engine.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL