Help us shape the future of Go! Take the 2026 Go Developer Survey

qudy

package module
v0.0.6 Latest Latest
Warning

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

Go to latest
Published: Sep 23, 2026 License: MIT Imports: 12 Imported by: 0

README

qudy

qudy is a Go library and command for writing Go code generators. Keep loops, conditions, and helper calls in Go, and write the source you want to produce in comments next to that logic. qudy compiles those comments into calls to a printf-style function.

If you have written a generator full of escaped strings, the difference looks like this. Given names := []string{"Red", "Green", "Blue"}, a template can say:

for _, name := range names {
    //`case ~name:
    //`    return "~name"
}

Running the resulting generator writes:

case Red:
    return "Red"
case Green:
    return "Green"
case Blue:
    return "Blue"

The loop is ordinary Go. The comments describe the output, and ~name inserts the current value.

Use it when your generator mixes Go logic with repetitive source code: methods for a set of types, handlers for a list of routes, or boilerplate derived from a schema. The template keeps the output visible beside the logic that produces it, without a separate language for loops and expressions.

There are two steps: qudy compiles your template into a Go generator; you run that generator to write the final source. You supply the data and the logic that reads it. The generator contains ordinary Go calls and needs no qudy runtime dependency.

Three nesting dolls show qudy producing a stringer generator, which produces colors_gen.go.

The example below takes an enum's constant names as arguments. After that, read about interpolation, integration with existing generators, and writing several files.

A stringer in one file

Suppose you want fmt.Sprint(Green) to print Green rather than 1. In an existing Go module, save this enum as color.go:

// color.go
package colors

type Color int

const (
    Red Color = iota
    Green
    Blue
)

The template below generates its String method. It takes the package, type, and constant names from the command line; it does not discover them in Go source. Create a stringer directory and save this as stringer/stringer.go:

//go:build qudy

package main

import "os"

func main() {
    pkg, typ, names := os.Args[1], os.Args[2], os.Args[3:]
    //`package ~pkg
    //`
    //`import "fmt"
    //`
    //`func (v# ~typ) String() string {
    //`	switch v# {
    for _, name := range names {
        //`	case ~name:
        //`		return "~name"
    }
    //`	}
    //`	return fmt.Sprintf("~typ(%d)", v#)
    //`}
}

The Go code runs in the generator. Each comment that starts with a backtick writes an output line, ~name inserts a Go value, and the loop writes one case for each constant. v# writes a generated name, something like v_qd1, to reduce the chance of clashing with names in the generated code. See generated names for the limits.

See examples/stringer for a slightly longer version.

Run the example

Install the command with Go 1.25 or later:

go install github.com/bilus/qudy/cmd/qudy@latest

First, compile the template into a Go program:

qudy -o stringer/stringer_gen.go stringer/stringer.go

Then run that program to generate the method:

go run ./stringer colors Color Red Green Blue > color_string_gen.go

color_string_gen.go now holds:

package colors

import "fmt"

func (v_qd1 Color) String() string {
	switch v_qd1 {
	case Red:
		return "Red"
	case Green:
		return "Green"
	case Blue:
		return "Blue"
	}
	return fmt.Sprintf("Color(%d)", v_qd1)
}

Run go test ./... to check that the package builds with its new method.

How the template becomes a generator

qudy turns the template's two output lines inside the loop into a call equivalent to:

fmt.Printf("\tcase %v:\n\t\treturn \"%v\"\n", name, name)

The -emit flag chooses that function. It defaults to fmt.Printf. and qudy adds the fmt import to the generator when the template lacks it. You can use an existing generator's method to write to a buffer or file. qudy combines a run of consecutive output lines into one call, uses %v for inserted values, and escapes literal percent signs, which is why the template can say %d. Your emit function must accept a format string and its arguments and interpret them like fmt.Printf.

The generator emits //line directives to make compilation errors point to the template.

Build tags and editor support

A template parses as Go, so gofmt can format it and your editor can read it. It may not build as Go: variables used only in output lines look unused to the Go compiler. So start a template with //go:build qudy, and a normal build ignores it. qudy writes //go:build !qudy into the generator, so the two never build together. Name generated Go files with the _gen.go suffix. For a compiled template, replace .go with _gen.go: stringer.go becomes stringer_gen.go. The CLI writes to the path supplied with -o.

To get completion and navigation inside a template, give your editor the tag, for example -tags=qudy in the buildFlags of gopls. It then loads the templates in place of the generators, and it reports a name that only output lines use as unused.

The quick start runs qudy by hand. To run it with go generate, see examples/stringer.

Insert values into the output

Output lines combine literal text, values inserted with ~, and names generated with #:

~name is an interpolation: qudy inserts the value of name there. For example, inside a template function:

foo := "XXX"
count := 3
//`~foo
//`~count

Produces:

XXX
3

qudy formats each inserted value with %v, so numbers need no conversion. You can also capture a selector:

p := struct{ Name string }{Name: "Color"}
//`type ~p.Name int

Produces:

type Color int

Without braces, ~ captures the next Go identifier and any selectors that follow it, as in ~p.Name. Each identifier must use only ASCII letters, digits, and underscores, and cannot start with a digit.

Use a braced interpolation, ~{expr}, to capture a call, index, or other Go expression. This example uses strconv.Quote from the standard library:

label := "hello"
values := []int{10, 20}
//`var label = ~{strconv.Quote(label)}
//`var first = ~{values[0]}

Produces:

var label = "hello"
var first = 10

Without braces, capture stops before a bracket or parenthesis. Compare:

name := "items"
//`~name[0]
//`~{name[0]}

Produces:

items[0]
105

~name[0] captures only name, inserting its value followed by the literal text [0]. ~{name[0]} evaluates the index in the generator: the first byte of "items" is 'i', whose numeric value is 105. This distinction lets you write indexing and calls in the generated code as well as evaluate them in the generator.

Insert a Go string literal

~"name inserts a value as a Go string literal, with its quotes and every escape it needs:

path := `C:\temp`
//`var quoted = ~"path
//`var naive = "~path"

Produces:

var quoted = "C:\\temp"
var naive = "C:\temp"

The two lines differ, and only the first is right. "~path" puts the raw value between two literal quotes, so the Go compiler reads the value again as source text. Here it reads \t as a tab, and the generated program compiles with the wrong string. A quote or a newline in the value breaks the literal, and then the generated code does not compile at all. ~"path escapes the value, so the literal always holds the string you inserted. Keep "~name" for a value that needs no escaping, such as a Go identifier.

~"{expr} takes any expression. qudy formats the value with %v first, so ~"count writes "3".

Insert a list

~@xs splices a slice: it inserts the elements with , between them. Most lists in Go source have that shape: parameters, call arguments, and the elements of a literal.

This template writes a method that logs a call and forwards it to the wrapped value. Its parameters and its arguments come from one slice:

type Param struct{ Name, Type string }

func (p Param) String() string { return p.Name + " " + p.Type }

func writeWrapper(method string, params []Param) {
    //`func (s *Logged) ~method(~@params) error {
    //`	log.Println(~"method, ~@{names(params)})
    //`	return s.inner.~method(~@{names(params)})
    //`}
}

writeWrapper("Get", []Param{{"ctx", "context.Context"}, {"id", "int"}}) produces:

func (s *Logged) Get(ctx context.Context, id int) error {
	log.Println("Get", ctx, id)
	return s.inner.Get(ctx, id)
}

~@params writes the parameter list. It can take a []Param directly, because every interpolation formats its value with fmt, and Param has a String method. ~@{names(params)} writes the call arguments: the braces take any expression, here a helper that returns the names as a []string.

An empty slice inserts nothing, so writeWrapper("Close", nil) writes Close(). After a fixed argument, the result has a dangling comma: log.Println("Close", ). Go accepts that, and gofmt removes it.

~@"xs writes each element as a Go string literal, which suits the elements of a []string:

tags := []string{"<p>", `say "hi"`}
//`var tags = []string{~@"tags}

Produces:

var tags = []string{"<p>", "say \"hi\""}

A splice writes one line. For one element per line, such as the fields of a struct or the cases of a switch, put a Go loop around an output line, as in the stringer above. For another separator, join the elements yourself: ~{strings.Join(xs, " | ")}.

One use of String gives a surprising result. A splice ranges over the slice by value, so with a String method on a pointer receiver, the elements of a []Tag come out as structs, such as {p}. Give String a value receiver, or splice a []*Tag.

Output lines and comments

An output line must be a comment on its own line inside a function body. Both //`text and the spelling that gofmt produces, // `text, work. qudy copies the template's other Go code into the generator, with three exceptions: it negates the qudy build tag, it leaves out go:generate lines, and it handles gensyms as described below. It formats the generator with go/format.

Each output line writes a trailing newline. End it with a backslash to continue on the same line, for example when a loop writes an argument list:

//`handle(ctx\
for _, arg := range args {
    //`, ~arg\
}
//`)

Two trailing backslashes write one backslash and keep the newline.

To put a comment in the generated code, include it in an output line:

//`// Event represents a change to a user.

To annotate only the template, use an ordinary Go comment or ~// inside an output line. qudy drops ~//, the rest of that line, and the spaces before it:

//`} ~// close the generated handler

Write ~~ for a literal tilde and ## for a literal hash. Reserve comments that open with a backtick for output lines; qudy rejects them outside function bodies.

Generate names with fewer collisions

A generator often needs its own local names in the code it produces. Write name# to request a name with a numbered suffix, often called a gensym:

//`value, errName# := parse(input)
//`if errName# != nil {
//`    return errName#
//`}

The first use of errName# in a block declares a variable in the generator, equivalent to errName_qd := qudyGensym("errName"). Later uses in that block insert the same name.

errName# never collides with a variable of the template. qudy keeps the gensym in a generator variable of its own, errName_qd, so errName# and ~errName can stand in one output line and mean different things. For the same reason a template may not declare a name that ends in _qd.

name# belongs in output lines. When the template's Go code needs the gensym, call qudyGensym, as shown below.

qudy supplies the symbol generator. It declares qudyGensym, a small function value, at the start of each function that uses name# or calls qudyGensym, so the generator stays one self-contained file. Each generated symbol gets a numbered suffix, so errName# comes out as errName_qd1, which is unlikely to collide with a name in your code.

The numbers follow the order of generation, so a new gensym renumbers the ones after it, which shows up as churn in generated code under version control. To confine that, tell qudy where a scope of the generated program starts. The numbering restarts in a scope, and the call that qudyScope returns ends it:

func writeStringer(typ string, names []string) {
    defer qudyScope()()
    //`func (v# ~typ) String() string {
    ...
}

Each generated function then numbers from one, and a gensym added in one of them cannot renumber another. A loop that writes several functions ends the scope at the end of each iteration, with end() in place of the defer. Scopes nest: a scope inside another continues the numbering and restores it when it ends, so a name from an enclosing scope is never reused while it is live.

The default generator does not inspect the output's scope or reserve existing identifiers. A user-defined errName_qd1 can still collide with its result. If your generator knows which names are taken, supply a custom symbol generator that checks them.

To choose the names yourself, declare qudyGensym in the template, as a local variable, a parameter, or a package-level variable. It is a func(want string) string. qudy then uses yours and declares none:

qudyGensym := newSeededGensym(taken)

A function literal shares the symbol generator of the function around it.

Call qudyGensym(want) directly inside a function body when you compute the desired name or need to declare the variable yourself:

local := qudyGensym(param.Name)
//`var ~local ~param.Type.Text

Generate several files

Have your generator print a txtar archive: a line such as -- color_string_gen.go -- starts a new file. Inside a template function, file markers are ordinary output lines:

//`-- color_string_gen.go --
//`package colors
//`// String methods go here.
//`-- color_parse_gen.go --
//`package colors
//`// Parse functions go here.

Pipe the generator's output into qudy -txtar -o ./generated. qudy splits the archive into files under that directory and formats the .go files with go/format. For a complete example, run this from the qudy repository:

go run ./examples/enums -package colors Color=Red,Green,Blue | qudy -txtar -o ./generated

See examples/enums for a generator that writes both String methods and Parse functions. The generated methods expect the enum types and constants to be defined in the same package.

Extend a generator with a runtime file

By default a generator is self-contained: the generator file holds all of its code. Pass -runtime, and qudy also writes qudy_runtime.go beside the generator. That file is a small runtime for the package, and every template of the package shares it. Compile all templates of a package with -runtime, or none of them.

qudy -runtime -o stringer/stringer_gen.go stringer/stringer.go

The runtime adds two things.

One symbol generator for the package

Without the runtime, every function counts its own gensyms. Two functions that write into the same generated function can then both produce err_qd1. With the runtime, one symbol generator counts for the whole run of the generator, so each gensym differs from every other one: err_qd1, err_qd2, and so on. name# and calls of qudyGensym need no change. With the runtime, such a call may also stand outside a function body.

qudyScope works across functions here: a helper called inside a scope numbers on from the caller's count, and the scope's end restores the count for the whole package. Without the runtime, a scope reaches only the function that opens it, because every function has a symbol generator of its own.

One limit remains. A gensym can still collide with a name of your own in the output, such as a hand-written err_qd1.

Capture and redirect the output

qudyCapture runs a function and returns the text of its output lines as a string. Nothing reaches the output:

func path(segments []string) string {
    return qudyCapture(func() {
        for _, s := range segments {
            //`~s/\
        }
    })
}

path([]string{"usr", "bin"}) returns usr/bin/. A capture also takes the output lines of every function that runs inside it, and captures nest.

More generally, qudyEmit is a variable of the package. While it holds a function, every run of output lines goes to that function, in place of the -emit function. qudyPush sets it and returns the call that restores the previous value, which suits defer:

func path(segments []string) string {
    var b strings.Builder
    defer qudyPush(func(format string, args ...any) { fmt.Fprintf(&b, format, args...) })()
    for _, s := range segments {
        segment(s)
    }
    return b.String()
}

Both work with any emit function, a method such as g.generate included.

The runtime file declares qudyGensym, qudyCapture and qudyPush as ordinary functions, so an editor with the qudy tag resolves them in a template.

What it costs
  • The generator is no longer one self-contained file. go run stringer_gen.go on that file alone fails, and go run ./stringer works.
  • The runtime file comes from the most recent qudy compile. After an upgrade of qudy, compile every template of the package again.
  • qudyEmit and the symbol generator are variables of the package. Two generators that run at the same time in one process share them, so keep the runtime away from concurrent generation.

Use qudy as a library

Call qudy.Compile when you want to compile templates from your own Go program:

generator, err := qudy.Compile("stringer/stringer.go", src, "fmt.Printf")

Import github.com/bilus/qudy and pass the template as src []byte. Compile returns the generator's formatted Go source and an error. It does not run the generator.

Further examples

  • stringer: a complete enum generator and go generate integration.
  • enums: several output files from one template.
  • sumtype: a larger generator that reads Go source.

The name stands for “quick and dirty quasiquoting”: quote the code you want to produce, and insert Go values where it varies.

Documentation

Overview

Package qudy compiles a template into the Go source of its generator.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Compile

func Compile(filename string, src []byte, emit string) ([]byte, error)

Compile turns a template into the Go source of its generator.

func CompileWithRuntime added in v0.0.3

func CompileWithRuntime(filename string, src []byte, emit string) (generator, runtime []byte, err error)

CompileWithRuntime returns a template's generator and the runtime file of its package.

func Extract

func Extract(archive []byte, dir string) ([]byte, error)

Extract writes an archive's files under dir, gofmt-formatted, and returns the comment.

Types

This section is empty.

Directories

Path Synopsis
cmd
qudy command
Command qudy compiles a template into the Go source of its generator.
Command qudy compiles a template into the Go source of its generator.
examples
enums command
Command enums writes two files for each enum, as one txtar archive.
Command enums writes two files for each enum, as one txtar archive.
lispg command
stringer command
Command stringer writes the String method of an enum, as an example of qudy.
Command stringer writes the String method of an enum, as an example of qudy.
sumtype
Package sumtype generates the boilerplate for a package's sum types.
Package sumtype generates the boilerplate for a package's sum types.
sumtype/cmd/sumtype command
Command sumtype generates the boilerplate for a package's sum types.
Command sumtype generates the boilerplate for a package's sum types.

Jump to

Keyboard shortcuts

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