q

module
v0.0.139 Latest Latest
Warning

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

Go to latest
Published: May 14, 2026 License: MIT

README

Go wild with Q, the funkiest -toolexec preprocessor

CI Status Go Report Card Docs

image

Experimental — APIs and internals may change. Use at your own risk.

q is a -toolexec preprocessor that implements rejected Go language proposals. Most q.* calls are rewritten at compile time into ordinary Go — call sites read flat, generated code is identical to hand-written error forwarding, runtime overhead is zero.

// Without q
func loadUser(id int) (User, error) {
    row, err := db.Query(id)
    if err != nil {
        return User{}, fmt.Errorf("loading user %d: %w", id, err)
    }
    user, err := parse(row)
    if err != nil {
        return User{}, err
    }
    return user, nil
}

// With q
func loadUser(id int) (User, error) {
    row  := q.TryE(db.Query(id)).Wrapf("loading user %d", id)
    user := q.Try(parse(row))
    return user, nil
}

The withdrawn Go try proposal is the seed; q ships that idea (and a handful of others) as a preprocessor instead of waiting on a language change.

Things you can do with q

A few situations where the flat shape pays off. Each snippet rewrites to ordinary Go at compile time — no runtime overhead, no closures, no panic/recover.

Wrap an error with context, in one line
user := q.TryE(loadUser(id)).Wrapf("loading user %d", id)

%w is appended automatically — the original error stays unwrappable via errors.Is / errors.As. Skip the Wrapf and use q.Try(...) for a bare bubble.

Recover from a specific failure mode mid-call
n := q.TryE(strconv.Atoi(s)).
    RecoverIs(strconv.ErrSyntax, 0).
    Wrapf("parsing %q", s)

.RecoverIs(sentinel, value) recovers when the captured err matches the sentinel via errors.Is. .RecoverAs((*MyErr)(nil), value) does the same via errors.As for typed errors. Both continue the chain — pair them with a terminal (Wrap, Wrapf, Err, ErrF, Catch) for the non-matching path. Multiple Recover* steps may be chained in source order.

For full control, .Catch(fn) takes a func(error) (T, error) — return (v, nil) to recover, (zero, err) to bubble. q.Const(v) is a shortcut: q.TryE(call).Catch(q.Const(0)) always recovers to 0.

Acquire and release a resource in one statement
conn := q.Open(dial(addr)).DeferCleanup((*Conn).Close)
file := q.Open(os.Open(path)).DeferCleanup()       // auto: defer file.Close()
ch   := q.Open(makeChan()).DeferCleanup()          // auto: defer close(ch)
return process(conn, file, ch)
// On return, the defers fire LIFO.

.DeferCleanup(cleanup) takes the cleanup explicitly. .DeferCleanup() (no args) lets the preprocessor infer it from the resource type — channel close, Close() error (close-time error discarded), or Close() (no return). For "we acquired this and we're not closing it":

val := q.Open(loadValue(key)).NoDeferCleanup()

If os.Open fails, conn was already opened and conn.Close runs. Same semantics as hand-written defer file.Close() chains, half the lines.

Bubble nil pointers, channel closes, type-assertion misses
user := q.NotNil(table[id])           // bubble q.ErrNil if id isn't in the map
msg  := q.Recv(inbox)                 // bubble q.ErrChanClosed when inbox closes
admin := q.AsE[Admin](user).Wrapf("%T is not an admin", user)

Each helper picks a different failure shape; the rewrite is the same if X { return zero, err } pattern.

Cancellation as a one-statement checkpoint
func sync(ctx context.Context, items []Item) error {
    for _, it := range items {
        q.CheckCtx(ctx)                 // bubble ctx.Err() if cancelled OR timed out, no-op otherwise
        q.Try(process(it))
    }
    return nil
}

For ctx-aware blocking ops, q.RecvCtx(ctx, ch) and q.AwaitCtx(ctx, future) bubble whichever fires first — cancel or value.

Auto-cancelled child contexts
ctx = q.Timeout(ctx, 2*time.Second)   // ctx, _qCancel := WithTimeout(...); defer cancel()
ctx = q.Deadline(ctx, deadline)       // same with WithDeadline

The required defer cancel() is wired in by the rewriter — there's no cancel variable to forget about.

JS-flavour futures, with select-style fan-in
fa := q.Async(func() (Sales, error) { return fetchSales(ctx) })
fb := q.Async(func() (Inventory, error) { return fetchInventory(ctx) })

sales := q.AwaitCtxE(ctx, fa).Wrap("sales")
inv   := q.AwaitCtx(ctx, fb)

results := q.AwaitAll(fa, fb, fc)     // []T in input order; bubble first error
fastest := q.AwaitAny(fa, fb, fc)     // first success wins, errors.Join on all-fail
Generators (iter.Seq sugar)
// q.Yield(v) inside the body becomes `if !yield(v) { return }`;
// the whole expression is rewritten to iter.Seq[int](...) at compile time.
fibs := q.Generator[int](func() {
    a, b := 0, 1
    for { q.Yield(a); a, b = b, a+b }
})

for v := range fibs {
    if v > 100 { break }
    fmt.Println(v)
}

q.Generator[T] produces a stdlib iter.Seq[T]. The type param is required (Go can't infer a result-only type argument). Free interop with for ... range and any other iter.Seq consumer.

Bidirectional coroutines
// Lua / Python-generator-send-style: caller passes in, body sends out.
doubler := q.Coro(func(in <-chan int, out chan<- int) {
    for v := range in {
        out <- v * 2
    }
})
defer doubler.Close()

v, _ := doubler.Resume(21) // 42
v, _  = doubler.Resume(100) // 200

q.Coro wraps a goroutine + two channels into a Resume / Close / Wait / Done API. Useful for stateful conversations iter.Seq (one-way pull) can't express. Pure runtime — no preprocessor work. Reach for q.Generator for the simpler emit-only case.

Multi-channel select and drain
v   := q.RecvAny(chA, chB, chC)       // first value across N channels
all := q.DrainAll(chA, chB, chC)      // [][]T — collected until each closes
Panic → error, function-wide
func handle(req Request) (resp Response, err error) {
    defer q.Recover()                 // any panic becomes a *q.PanicError on err
    return work(req)
}

defer q.RecoverE().Map(func(r any) error {
    return &APIError{Detail: fmt.Sprint(r)}
})

The &err is wired in from the enclosing signature — no need to type it out.

Mutex sugar
func (s *Store) Set(k, v string) {
    q.Lock(&s.mu)                     // Lock + defer Unlock
    s.data[k] = v
}
Runtime preconditions, no panic
func encode(buf []byte) (Frame, error) {
    q.Require(len(buf) >= 16, "header too short")
    // bubble: fmt.Errorf("codec.go:42: %s: %w", "header too short", q.ErrRequireFailed)
    // reads as: "codec.go:42: header too short: q.Require failed"
    ...
}

// callers can identify the failure mode:
if errors.Is(err, q.ErrRequireFailed) { ... }

Validations bubble like every other failure — no defer recover() on the caller's side.

Production-grade slog attrs
slog.Info("request handled",
    q.SlogAttr(reqID),     // → slog.Any("reqID", reqID)
    q.SlogAttr(elapsed),   // → slog.Any("elapsed", elapsed)
    q.SlogFile(),          // → slog.Any("file", "main.go")
    q.SlogLine())          // → slog.Any("line", 42)

Auto-derives keys from the source text / file / line at compile time. No runtime stack walk; everything's a constant the compiler folds in.

Context-attached attrs (correlation IDs, etc.)

Install once at startup, then attach attrs anywhere in request flow:

q.InstallSlogJSON(nil, nil)   // JSON to os.Stderr (or pass your own base handler)

// in request middleware / handler:
ctx = q.SlogCtx(ctx, q.SlogAttr(reqID), q.SlogAttr(userID))
slog.InfoContext(ctx, "processing")  // record auto-carries reqID + userID

Standard Go pattern (a wrapping slog.Handler that pulls attrs out of the ctx) — q just gives you the ctx key, the wrapper, and the install one-liner. Accumulating via repeated q.SlogCtx calls works: deeper attrs add to whatever the parent already had.

Dev-time dbg! and stderr-flavored slog
u := loadUser(q.DebugPrintln(id))
// stderr: "main.go:17 id = 7"  (passes id through unchanged)

slog.Info("loaded", q.DebugSlogAttr(userID))
// → slog.Info("loaded", slog.Any("main.go:42 userID", userID))

The Debug* family carries file:line inside the key text — easy to spot in scrolling stderr, but noisy in shipping logs. Pull these out before merging; reach for q.SlogAttr / q.SlogFile / q.SlogLine for permanent logging.

Compile-time string interpolation
name := "world"
age  := 42

q.F("hi {name}, {age+1} next year")           // → fmt.Sprintf("hi %v, %v next year", name, age+1)
q.F("upper: {strings.ToUpper(name)}")         // → fmt.Sprintf("upper: %v", strings.ToUpper(name))
q.Ferr("user {id} not found")                 // → fmt.Errorf("user %v not found", id)  (type error)
q.Fln("processing {len(items)} items")        // → fmt.Fprintln(q.DebugWriter, …)

{{ / }} escape literal braces. The format must be a Go string literal — dynamic formats are rejected at scan time. Inside {…}, anything that parses as a Go expression goes (selectors, function calls, arithmetic, even nested string literals). Tradeoff: identifiers inside the literal aren't IDE-visible — go-to-definition / rename don't see them.

Value-returning match expression
type Color int
const (Red Color = iota; Green; Blue)

description := q.Match(c,
    q.Case(Red,   "warm"),
    q.Case(Green, "natural"),
    q.Case(Blue,  "cool"),
    // missing Blue → build fails: "missing case(s) for: Blue"
)

Folds to an IIFE-wrapped switch — value-returning switch as an expression, the way Scala / Rust / Swift have it. Coverage-checked when V is an enum (same rules as q.Exhaustive); q.Default(...) opts out for forward-compat scenarios.

Conditional expression
display := q.Tern(user != nil, user.Name, "anonymous")
// → "anonymous" when user is nil; user.Name when not — and user.Name
//   is never evaluated when user is nil (no nil-deref panic).

v := q.Tern(cached, fast(), slowLookup(key))
// → slowLookup(key) only runs when `cached` is false; fast() never
//   runs when `cached` is false either.

// Chains naturally for multi-way picks:
tier := q.Tern(score >= 90, "A",
         q.Tern(score >= 80, "B",
          q.Tern(score >= 70, "C", "F")))

q.Tern(cond, ifTrue, ifFalse) returns ifTrue when cond is true, otherwise ifFalse. The preprocessor splices each branch's source span into its own arm of an IIFE — so only the matching branch is evaluated, despite Go's eager arg-eval semantics. Lazy by source-splicing, not by func-thunks.

Nested-nil safe traversal
theme := q.At(user.Profile.Settings.Theme).Or("light")
// → "light" if user, Profile, or Settings is nil; user.Profile.Settings.Theme otherwise.

// Multiple fallback paths — try in source order, first non-nil wins:
endpoint := q.At(opts.Endpoint).
    OrElse(env.Endpoint).
    OrElse(globalConfig.DefaultEndpoint).
    Or("https://example.com")

// Zero-value terminal:
name := q.At(user.Profile.DisplayName).OrZero()  // "" if any hop is nil

q.At(<chain>) opens an optional-chaining-style traversal. The rewriter walks each hop, asks go/types whether it's nil-checkable, and emits per-hop guards inside an IIFE. .OrElse(<alt>) chains additional paths / values to try; .Or(<fallback>) and .OrZero() close the chain. Each path's expression is single-eval and evaluated lazily — only when reached. See docs/api/at.md.

Deferred evaluation
cfg := q.Lazy(loadConfigFromDisk())  // loadConfigFromDisk() has NOT run.
if userRequested {
    settings := cfg.Value()           // first .Value() runs the thunk
    _ = settings
}
// loadConfigFromDisk() never ran if userRequested was false.

q.Lazy(<expr>) reads as if the expression were eager but the rewriter wraps it in a thunk closure. The first .Value() call evaluates the thunk under sync.Once; later calls return the cached result. Concurrency-safe by construction. q.LazyE(<call>) is the (T, error)-shaped sibling — pair .Value() with q.Try at the consumer. See docs/api/lazy.md.

Discriminated sum types
type Pending struct{}
type Done    struct{ At time.Time }
type Failed  struct{ Err error }

type Status q.OneOf3[Pending, Done, Failed]   // discriminated union of three variants

s := q.AsOneOf[Status](Done{At: time.Now()})

desc := q.Match(s,
    q.Case(Pending{}, "waiting"),
    q.OnType(func(d Done) string   { return "done at " + d.At.String() }),
    q.OnType(func(f Failed) string { return "failed: " + f.Err.Error() }),
)
// missing variant → build fails: "missing arm(s) for: Failed"

Real sum types — the headline rejected proposal. q.OneOfN is the struct-based form (uniform Tag uint8; Value any storage). q.AsOneOf[T](v) rewrites in place to T{Tag: <pos>, Value: v} after validating v's type matches one of T's variants. Dispatch reads through q.Match: q.Case(Variant{}, result) for tag-only arms (drops the payload), q.OnType(func(t T) R { ... }) to bind the typed payload. q.Default opts out of coverage. Statement-level dispatch via switch v := q.Exhaustive(s.Value).(type) { ... } enforces case coverage with the same rules. See docs/api/oneof.md.

The Scala-flavoured 2-arm sibling lives in its own subpackage: either.Either[L, R] with either.Left / either.Right / either.AsEither constructors, right-biased either.Map / either.FlatMap / either.GetOrElse, and either.Fold for the value-returning 2-arm dispatch. Reuses every q.OneOfN integration point — q.Match, q.OnType, q.Exhaustive — for free.

For message-passing systems where each variant carries its own typed payload and you want it to flow through chan Message as itself (no Tag/Value wrapper), use q.Sealed:

type Message interface{ message() }                       // 1-line marker

type Ping       struct{ ID int }
type Pong       struct{ ID int }
type Disconnect struct{ Reason string }

var _ = q.Sealed[Message](Ping{}, Pong{}, Disconnect{})    // synthesises the markers

ch := make(chan Message, 4)
ch <- Ping{ID: 1}                                          // flows as itself

for m := range ch {
    switch v := q.Exhaustive(m).(type) {                    // coverage-checked at build
    case Ping:       handlePing(v)
    case Pong:       handlePong(v)
    case Disconnect: handleDisconnect(v)
    }
}

q.Sealed[I](variants...) registers the closed set; the preprocessor synthesises one marker-method impl per variant in a companion file so each implements I. Variadic value-args = no arity ceiling. Same-package variants only (Go's method-decl restriction). Pick q.Sealed for actor-style message dispatch, q.OneOfN when you need a single concrete carrier (cross-package or primitive variants).

Typed-string atoms (Erlang-flavoured)
type Pending q.Atom              // each atom is its own type — no const decl
type Done    q.Atom

p := q.A[Pending]()              // p: Pending = "<importpath>.Pending" — globally unique across pkgs

func classify(a q.Atom) string {
    switch a {
    case q.AtomOf[Pending]():    // q.Atom-typed sibling for case ergonomics
        return "p"
    case q.AtomOf[Done]():
        return "d"
    }
    return "?"
}

Each atom is its own type — Go's type system protects against mixing — and the value is auto-derived from the type's bare name. No const block to maintain, no central declaration shared across files. The preprocessor rewrites every call site to a typed-string constant cast at compile time. Zero runtime cost. See docs/api/atom.md.

Named parameters for constructors

The "options struct" pattern is Go's stand-in for named arguments — it gets you the readability win at the call site but loses the safety win, since Go can't tell "explicitly zero" from "didn't set it." q.FnParams / q.ValidatedStruct close that gap: required-by-default struct literals, with optional opt-out per field.

type LoadOptions struct {
    _       q.FnParams                          // or q.ValidatedStruct
    Path    string                              // required
    Format  string                              // required
    Timeout time.Duration `q:"opt"`             // optional (also accepts q:"optional")
}

Load(LoadOptions{Path: "/etc", Format: "yaml"}) // OK
Load(LoadOptions{Path: "/etc"})                  // build error: Format required

Add _ q.FnParams (function parameters) or _ q.ValidatedStruct (any other DTO / config / model struct) as a blank marker field. The preprocessor walks every struct literal in the package — top-level and nested — and rejects any literal of a marked type that omits a required field. Optional fields opt out per-field via q:"opt" / q:"optional". See docs/api/fnparams.md.

Auto-derived dependency injection
type Config struct{ DB string }
type DB     struct{ cfg *Config }
type Server struct{ db *DB; cfg *Config }

func newConfig() *Config              { return &Config{DB: "..."} }
func newDB(c *Config) (*DB, error)    { return &DB{cfg: c}, nil }
func newServer(d *DB, c *Config) *Server { return &Server{db: d, cfg: c} }

// List the recipes; the preprocessor reads each signature, builds the
// dep graph, topo-sorts, and emits the inlined construction. ZIO ZLayer
// in spirit, plain Go functions in shape. No codegen step. No runtime
// reflection. The chain terminator picks the resource-lifetime policy:
//
//   .DeferCleanup()   — returns (T, error). Cleanups fire automatically via
//                  a `defer` injected into the enclosing function (in
//                  reverse-topo order). The fast path.
//
//   .NoDeferCleanup() — returns (T, func(), error). Caller takes manual
//                  ownership of the (idempotent) shutdown closure —
//                  use when lifetime spans more than the function
//                  scope (main / signal handlers / background workers).
//
//   .WithScope(s)     — returns (T, error). Built deps cache + cleanup
//                  in `s` (a *q.Scope); subsequent assemblies in the
//                  same scope reuse cached deps. Per-request, per-tenant,
//                  per-session lifetimes — see docs/api/scope.md.
//
// Recipes can be (T), (T, error), (T, func()), (T, func(), error),
// or an inline value. Resource shapes (and types with auto-detected
// Close() / Close() error / writable channel) feed cleanups onto
// the chain; the rest pass through. Wrap a recipe in q.PermitNil
// to opt it out of the runtime nil-check when nil IS a valid output.
server := q.Try(q.Assemble[*Server](newConfig, openDB, newServer).DeferCleanup())

// In main, manage shutdown explicitly:
func main() {
    server, shutdown, err := q.Assemble[*Server](newConfig, openDB, newServer).NoDeferCleanup()
    if err != nil { log.Fatal(err) }
    defer shutdown() // reverse-topo, blocking; idempotent
    server.Run()
}

// Per-request scope: handler-built deps share a request-scoped scope.
func handle(w http.ResponseWriter, r *http.Request) {
    scope := q.NewScope().BoundTo(r.Context())
    server := q.Try(q.Assemble[*Server](newConfig, openDB, newServer).WithScope(scope))
    // Subsequent assemblies in the same scope reuse cached *Config / *DB.
    worker := q.Try(q.Assemble[*Worker](newConfig, openDB, newWorker).WithScope(scope))
    // server, db, worker all close when r.Context() is cancelled.
    _ = worker
    server.Run()
}

// Pass ctx as an inline-value recipe — recipes that take context.Context
// receive it via interface satisfaction. q.WithAssemblyDebug enables
// per-step trace output for diagnosing wiring.
ctx := q.WithAssemblyDebug(context.Background())
server := q.Unwrap(q.Assemble[*Server](ctx, newConfig, newDB, newServer).DeferCleanup())

// q.AssembleAll[T] for plugin / handler / middleware aggregation —
// every recipe whose output is assignable to T contributes one slice
// element, in declaration order.
plugins := q.Unwrap(q.AssembleAll[Plugin](newAuth, newLog, newMetrics).DeferCleanup())

// q.AssembleStruct[T] decomposes T's fields into separate dep targets.
// Useful when several distinct products share a common dep set —
// shared transitive deps (here *Config, *DB) build only once.
type App struct {
    Server *Server
    Worker *Worker
    Stats  *Stats
}
app := q.Unwrap(q.AssembleStruct[App](newConfig, newDB, newServer, newWorker, newStats).DeferCleanup())

When a recipe is missing or duplicated or the graph cycles, the build fails with a tree visualisation of what the resolver sees. See docs/api/assemble.md.

Auto-derived struct conversions (Chimney-style)
type User    struct { ID int; First, Last, Email string; Internal bool }
type UserDTO struct { ID int; Email, FullName, Source string }

dto := q.ConvertTo[UserDTO](user,
    q.Set(UserDTO{}.Source, "v1"),
    q.SetFn(UserDTO{}.Email,    func(u User) string { return strings.ToLower(u.Email) }),
    q.SetFn(UserDTO{}.FullName, func(u User) string { return u.First + " " + u.Last }),
)
// → UserDTO{ID: user.ID, Email: ..., FullName: ..., Source: "v1"}
// `User.Internal` is silently dropped (target-driven).

The override field reference is a typed Go selector expression (UserDTO{}.Field), not a string — Go's type-checker validates the field exists and the value/fn return type matches. Rename a target field and every override site fails to compile. Multi-hop paths (UserDTO{}.Address.City) target nested fields directly. Resolution per target field: override → same-named source field with assignable type → recursive nested derivation when both fields are structs → build-time diagnostic. No runtime reflection; the rewriter emits a plain struct literal.

For derivations that may fail at runtime — database lookups, remote fetches — q.ConvertToE returns (Target, error) and lets q.SetFnE(field, func(Source) (V, error)) overrides bubble. Pair with q.Try for the flat shape: dto := q.Try(q.ConvertToE[UserDTO](u, q.SetFnE(...))). See docs/api/convert.md.

Functional data ops
// Transform, filter, group — pure runtime, no preprocessor magic.
doubled := q.Map(nums, func(n int) int { return n * 2 })
adults  := q.Filter(users, func(u User) bool { return u.Age >= 18 })
byCat   := q.GroupBy(items, func(it Item) string { return it.Cat })
total   := q.Fold(amounts, 0, func(acc, n int) int { return acc + n })
mx      := q.Reduce(scores, func(a, b int) int { if a > b { return a }; return b })

// Fallible variants compose with q.Try / q.TryE — first error short-circuits.
func loadUsers(rows []Row) ([]User, error) {
    return q.TryE(q.MapErr(rows, parseUser)).Wrap("loading users"), nil
}

// Find pairs with q.Ok / q.OkE for "bubble on missing"
func findAdmin(users []User) (User, error) {
    return q.Ok(q.Find(users, isAdmin)), nil
}

Functional data ops over slices: Map, FlatMap, Filter, GroupBy, Exists, ForAll, Find, Fold, Reduce, Distinct, DistinctBy, Partition, Chunk, Count, Take, Drop. Each fallible op ships in two flavours — bare and …Err returning (result, error) — designed to flow through q.Try / q.TryE for the bubble path. Pure runtime helpers; no …E chain flavour because q.TryE(q.MapErr(…)).Wrap(…) already produces that shape. Iterator (iter.Seq) variants are deferred to a follow-up wave. Inspiration: Scala collections and samber/lo.

Parallel data ops
// Bounded concurrency, default = runtime.NumCPU(). Limit travels on ctx.
ctx = q.WithPar(ctx, 8)
results := q.Try(q.ParMapErr(ctx, urls, fetchURL))

// Bare versions — read ctx for the limit but ignore cancellation.
doubled := q.ParMap(ctx, items, expensive)

// One goroutine per item (use sparingly).
ctx2 := q.WithParUnbounded(ctx)
results = q.ParMap(ctx2, items, expensive)

// Side-effect fan-out + first-error-wins.
q.Check(q.ParEachErr(ctx, files, upload))

q.ParMap / q.ParMapErr, q.ParFlatMap / q.ParFlatMapErr, q.ParFilter / q.ParFilterErr, q.ParForEach / q.ParForEachErr, q.ParGroupBy / q.ParGroupByErr, q.ParExists / q.ParExistsErr, q.ParForAll / q.ParForAllErr. The worker count rides on context.Context via q.WithPar(ctx, n) — set once at the request top, every nested ParMap respects it. ctx cancellation triggers ctx.Err() bubble in …Err variants; bare variants stop dispatching and return partial results. First error wins. Inspiration: samber/lo PR #858 and github.com/GiGurra/party.

Compile-time reflection
type User struct {
    ID    int    `json:"id"   db:"user_id"`
    Name  string `json:"name" db:"full_name"`
}

q.Fields[User]()                 // []string{"ID", "Name"}
q.TypeName[User]()               // "User"
q.Tag[User]("Name", "json")      // "name"
q.Tag[User]("Name", "db")        // "full_name"

Each call folds to a literal at compile time. Useful for codegen-free JSON / CSV / SQL row mappers, schema-derived helpers, and other small cases where pulling in reflect is overkill. q.Tag's field+key args must be string literals; field name validated at compile time.

Compile-time string-case transforms
q.Snake("HelloWorld")       // "hello_world"
q.Snake("XMLHttpRequest")   // "xml_http_request"
q.Camel("hello_world")      // "helloWorld"
q.Pascal("hello_world")     // "HelloWorld"
q.Kebab("HelloWorld")       // "hello-world"
q.Upper("db_host")          // "DB_HOST"

Each call site folds to a string literal at compile time. Useful for column names, env vars, URL slugs, JSON field names — the codegen-adjacent stuff Go forces you to spell out by hand. Inputs must be string literals; runtime values use the standard strings package.

Injection-safe SQL
s := q.SQL("SELECT * FROM users WHERE id = {id} AND status = {status}")
// s.Query → "SELECT * FROM users WHERE id = ? AND status = ?"
// s.Args  → []any{id, status}
db.QueryRowContext(ctx, s.Query, s.Args...)

// Or with PostgreSQL-style placeholders:
s := q.PgSQL("SELECT * FROM users WHERE id = {id}")  // → "...$1", []any{id}

// Or named-param style:
s := q.NamedSQL("SELECT * FROM users WHERE id = {id}")  // → "...:name1", []any{id}

Same {expr} surface as q.F, but the rewriter physically can't inline user values into the query — {name} always becomes a placeholder + an entry in Args. The parameterised guarantee is structural, not advisory.

Compile-time enum helpers
type Color int
const (Red Color = iota; Green; Blue)

q.EnumValues[Color]()           // []Color{Red, Green, Blue}
q.EnumNames[Color]()            // []string{"Red", "Green", "Blue"}
q.EnumName[Color](Green)        // "Green"
q.EnumParse[Color]("Blue")      // (Blue, nil)
q.EnumOrdinal[Color](Blue)      // 2

func (c Color) String() string { return q.EnumName[Color](c) }

Each call site folds to a literal slice or an inline switch — no runtime reflection, no companion code-generator, no go generate step. Works for both int-backed and string-backed enums (any const-able comparable type). Constants are discovered by walking the package's *types.Const set at compile time.

Auto-generated enum methods
type Color int
const (Red Color = iota; Green; Blue)

var _ = q.GenStringer[Color]()         // synthesizes Color.String()
var _ = q.GenEnumJSONStrict[Color]()   // name-based JSON, errors on unknown

type Status string
const (Pending Status = "pending"; Done Status = "done")
var _ = q.GenEnumJSONLax[Status]()     // pass-through JSON, preserves unknown wire values

The directives are package-level; the toolexec pass writes a companion _q_gen.go with the methods. Strict rejects wire values your code doesn't know about. Lax preserves them for forward-compat with newer producers — pair with q.Exhaustive's default: arm.

Exhaustive switches
switch q.Exhaustive(c) {        // build fails if any const of c's type is uncovered
case Red:    return "warm"
case Green:  return "natural"
case Blue:   return "cool"
}

q.Exhaustive is only legal as a switch tag; anywhere else is a diagnostic. Default clauses opt out of the check. The wrapper is stripped at rewrite time — zero runtime overhead.

Get the goroutine ID Go won't give you
id := q.GoroutineID()        // uint64, the goid in panic traces
slog.Info("processing", q.SlogAttr(id))

The runtime package deliberately hides goroutine IDs. q's preprocessor injects a one-line accessor (getg().goid) into the stdlib runtime compile and //go:linkname-pulls it from pkg/q. Cost: ~1 ns, just an inlined struct-field read. No stack-walk, no assembly, no pprof-labels dependency. Loses to a future Go release if Go closes the linkname loophole; works on Go 1.26.

Trace a bubble back to its call site
row := q.TraceE(db.Query(id)).Wrapf("loading user %d", id)
// → fmt.Errorf("users.go:42: loading user 7: %w", err) on the bubble

Compile-time file:line prefix; the wrap and underlying error remain unwrappable.

Statement positions

Every value-producing helper works in five positions:

v := q.Try(call())                       // define
v  = q.Try(call())                       // assign (incl. m[k] = …, obj.field = …)
     q.Try(call())                       // discard — bubble fires, value dropped
return q.Try(call()), nil                // return-position
x := f(q.Try(call()), q.NotNil(p))       // hoist — q.* nested inside any expression

Multiple q.* per statement compose:

return q.Try(a()) * q.Try(b()) / q.Try(c()), nil
x := q.Try(Foo(q.Try(Bar())))           // nested q.* inside another q.*'s arg

Why a preprocessor

Three properties fall out of the design:

  • Zero runtime overhead. Each q.* is rewritten at compile time into the same if err != nil { return …, err } shape you would write by hand. No closures, no panic/recover, no reflection.
  • IDE-native. gopls, go vet, and editor analyzers see ordinary Go — completion, refactors, type errors all point at the right places.
  • Loud failure on misuse. Forgetting -toolexec=q doesn't silently produce a binary that drops errors — it fails the link with relocation target _q_atCompileTime not defined. Same for any rewriter bug that leaves a q.* call site untransformed: the helper's body panics with a diagnostic naming itself.

The link gate, the rewrite contract, and the typed-nil-interface guard are documented in docs/design.md.

Quick start

# Install the preprocessor binary
go install github.com/GiGurra/q/cmd/q@latest

# Add the runtime package to your module
go get github.com/GiGurra/q

# Build or test with the preprocessor active
GOFLAGS="-toolexec=q" go build ./...
GOFLAGS="-toolexec=q" go test  ./...

Getting Started covers GOCACHE discipline (toolexec and non-toolexec builds shouldn't share a cache), IDE setup for GoLand and VS Code, and a sample CI workflow.

Read more

Status

Experimental. The public surface is implemented end-to-end across every statement position, with closures, generics, and multi-q.*-per-statement nesting all supported. The only currently-parked shape is multi-LHS where q.* itself produces multiple T values (v, w := q.Try(call())); see TODO #16.

  • proven — compile-time contracts via -toolexec. q reuses proven's link-gate trick.
  • rewire — compile-time mocking via -toolexec. q's preprocessor scaffolding mirrors rewire's shape.

Acknowledgements

100% vibe coded with Claude Code. AST rewriting and compiler toolchains are well outside my comfort zone.

License

MIT — see LICENSE.

Directories

Path Synopsis
cmd
q command
Command q is the q preprocessor — a -toolexec binary that hooks into the Go build pipeline and rewrites every q.NoErr / q.NonNil family call site into the conventional `if err != nil { return … }` shape.
Command q is the q preprocessor — a -toolexec binary that hooks into the Go build pipeline and rewrites every q.NoErr / q.NonNil family call site into the conventional `if err != nil { return … }` shape.
example
app command
Package main is a small in-memory todo service that exercises a large slice of q's surface in one cohesive flow:
Package main is a small in-memory todo service that exercises a large slice of q's surface in one cohesive flow:
as command
example/as mirrors docs/api/as.md one-to-one.
example/as mirrors docs/api/as.md one-to-one.
assemble command
example/assemble mirrors docs/api/assemble.md one-to-one.
example/assemble mirrors docs/api/assemble.md one-to-one.
async command
example/async mirrors docs/api/async.md one-to-one.
example/async mirrors docs/api/async.md one-to-one.
at command
example/at mirrors docs/api/at.md one-to-one.
example/at mirrors docs/api/at.md one-to-one.
atcompiletime command
example/atcompiletime mirrors docs/api/atcompiletime.md one-to-one.
example/atcompiletime mirrors docs/api/atcompiletime.md one-to-one.
atom command
example/atom mirrors docs/api/atom.md one-to-one.
example/atom mirrors docs/api/atom.md one-to-one.
await_ctx command
example/await_ctx mirrors docs/api/await_ctx.md one-to-one.
example/await_ctx mirrors docs/api/await_ctx.md one-to-one.
await_multi command
example/await_multi mirrors docs/api/await_multi.md one-to-one.
example/await_multi mirrors docs/api/await_multi.md one-to-one.
basic command
Package main — the smallest end-to-end demo of q.
Package main — the smallest end-to-end demo of q.
channel_multi command
example/channel_multi mirrors docs/api/channel_multi.md one-to-one.
example/channel_multi mirrors docs/api/channel_multi.md one-to-one.
check command
example/check mirrors docs/api/check.md one-to-one.
example/check mirrors docs/api/check.md one-to-one.
checkctx command
example/checkctx mirrors docs/api/checkctx.md one-to-one.
example/checkctx mirrors docs/api/checkctx.md one-to-one.
convert command
example/convert mirrors docs/api/convert.md one-to-one.
example/convert mirrors docs/api/convert.md one-to-one.
coro command
example/coro mirrors docs/api/coro.md one-to-one.
example/coro mirrors docs/api/coro.md one-to-one.
data command
example/data mirrors docs/api/data.md one-to-one (representative shapes from each section).
example/data mirrors docs/api/data.md one-to-one (representative shapes from each section).
debug command
example/debug mirrors docs/api/debug.md one-to-one.
example/debug mirrors docs/api/debug.md one-to-one.
either command
example/either mirrors docs/api/either.md one-to-one.
example/either mirrors docs/api/either.md one-to-one.
enums command
example/enums mirrors docs/api/enums.md one-to-one.
example/enums mirrors docs/api/enums.md one-to-one.
exhaustive command
example/exhaustive mirrors docs/api/exhaustive.md one-to-one.
example/exhaustive mirrors docs/api/exhaustive.md one-to-one.
fnparams command
example/fnparams mirrors docs/api/fnparams.md one-to-one.
example/fnparams mirrors docs/api/fnparams.md one-to-one.
format command
example/format mirrors docs/api/format.md one-to-one.
example/format mirrors docs/api/format.md one-to-one.
gen command
example/gen mirrors docs/api/gen.md one-to-one.
example/gen mirrors docs/api/gen.md one-to-one.
generator command
example/generator mirrors docs/api/generator.md one-to-one.
example/generator mirrors docs/api/generator.md one-to-one.
getting_started command
example/getting_started mirrors docs/getting-started.md's "First passing build" snippet.
example/getting_started mirrors docs/getting-started.md's "First passing build" snippet.
goroutine_id command
example/goroutine_id mirrors docs/api/goroutine_id.md one-to-one.
example/goroutine_id mirrors docs/api/goroutine_id.md one-to-one.
lazy command
example/lazy mirrors docs/api/lazy.md one-to-one.
example/lazy mirrors docs/api/lazy.md one-to-one.
lock command
example/lock mirrors docs/api/lock.md one-to-one.
example/lock mirrors docs/api/lock.md one-to-one.
match command
example/match mirrors docs/api/match.md one-to-one.
example/match mirrors docs/api/match.md one-to-one.
notnil command
example/notnil mirrors docs/api/notnil.md one-to-one.
example/notnil mirrors docs/api/notnil.md one-to-one.
ok command
example/ok mirrors docs/api/ok.md one-to-one.
example/ok mirrors docs/api/ok.md one-to-one.
oneof command
example/oneof mirrors docs/api/oneof.md one-to-one.
example/oneof mirrors docs/api/oneof.md one-to-one.
open command
example/open mirrors docs/api/open.md one-to-one.
example/open mirrors docs/api/open.md one-to-one.
par command
example/par mirrors docs/api/par.md one-to-one.
example/par mirrors docs/api/par.md one-to-one.
recover command
example/recover mirrors docs/api/recover.md one-to-one.
example/recover mirrors docs/api/recover.md one-to-one.
recv command
example/recv mirrors docs/api/recv.md one-to-one.
example/recv mirrors docs/api/recv.md one-to-one.
recv_ctx command
example/recv_ctx mirrors docs/api/recv_ctx.md one-to-one.
example/recv_ctx mirrors docs/api/recv_ctx.md one-to-one.
reflection command
example/reflection mirrors docs/api/reflection.md one-to-one.
example/reflection mirrors docs/api/reflection.md one-to-one.
require command
example/require mirrors docs/api/require.md one-to-one.
example/require mirrors docs/api/require.md one-to-one.
scope command
example/scope mirrors docs/api/scope.md one-to-one.
example/scope mirrors docs/api/scope.md one-to-one.
slog command
example/slog mirrors docs/api/slog.md one-to-one.
example/slog mirrors docs/api/slog.md one-to-one.
sql command
example/sql mirrors docs/api/sql.md one-to-one.
example/sql mirrors docs/api/sql.md one-to-one.
string_case command
example/string_case mirrors docs/api/string_case.md one-to-one.
example/string_case mirrors docs/api/string_case.md one-to-one.
tern command
example/tern mirrors docs/api/tern.md one-to-one.
example/tern mirrors docs/api/tern.md one-to-one.
timeout command
example/timeout mirrors docs/api/timeout.md one-to-one.
example/timeout mirrors docs/api/timeout.md one-to-one.
todo command
example/todo mirrors docs/api/todo.md one-to-one.
example/todo mirrors docs/api/todo.md one-to-one.
trace command
example/trace mirrors docs/api/trace.md one-to-one.
example/trace mirrors docs/api/trace.md one-to-one.
try command
example/try mirrors docs/api/try.md one-to-one.
example/try mirrors docs/api/try.md one-to-one.
internal
preprocessor
Package preprocessor implements the q -toolexec preprocessor.
Package preprocessor implements the q -toolexec preprocessor.
pkg
q
Package q is "Go wild with Q, the funkiest -toolexec preprocessor" — a -toolexec preprocessor that implements rejected Go language proposals (the ? / try operator) plus a playground of helpers Go didn't ship: ctx cancellation checkpoints, futures and fan-in, panic→error recovery, mutex sugar, runtime preconditions, dbg!-style prints and slog.Attr builders.
Package q is "Go wild with Q, the funkiest -toolexec preprocessor" — a -toolexec preprocessor that implements rejected Go language proposals (the ? / try operator) plus a playground of helpers Go didn't ship: ctx cancellation checkpoints, futures and fan-in, panic→error recovery, mutex sugar, runtime preconditions, dbg!-style prints and slog.Attr builders.
q/either
Package either ships the Scala-flavoured 2-arm sum type: either.Either[L, R] holds exactly one of L (the "left" arm) or R (the "right" arm).
Package either ships the Scala-flavoured 2-arm sum type: either.Either[L, R] holds exactly one of L (the "left" arm) or R (the "right" arm).

Jump to

Keyboard shortcuts

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