Skip to content

reactivego/jig

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

84 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

jig

Go Reference

jig is a tool for generating source code from a generics library.

go install github.com/reactivego/jig@latest

Note: Since Go 1.18, the language has built-in generics support. However, jig remains useful for:

  • Legacy codebases that need to maintain compatibility with older Go versions
  • Code generation patterns not easily expressed with Go's type parameters
  • Existing projects built on jig-based libraries like reactivego/rx

Table of Contents

Install

go install github.com/reactivego/jig/cmd/jig@latest

Verify the installation:

jig -h
Usage of jig [flags] [<dir>]:
  -c, --clean     Remove files generated by jig
  -m, --missing   Only generate code that is missing
  -n, --nodoc     No documentation in generated files
  -v, --verbose   Print details of what jig is doing

Getting Started

Let's implement a simple generic stack to get started with jig.

Step 1: Set Up the Project

mkdir -p ~/go/hellojig/generic
cd ~/go/hellojig
go mod init hellojig
cd generic

Step 2: Write the Generic Template

Create stack.go with the following content:

package stack

type foo int

//jig:template <Foo>Stack

type FooStack []foo

//jig:template <Foo>Stack Push

func (s *FooStack) Push(v foo) {
	*s = append(*s, v)
}

//jig:template <Foo>Stack Pop

func (s *FooStack) Pop() (result foo) {
	slen := len(*s)
	if slen != 0 {
		result = (*s)[slen-1]
		*s = (*s)[:slen-1]
	}
	return
}

Key concepts:

  • foo is a placeholder type
  • Foo in template names (like <Foo>Stack) marks the type variable
  • Each template is a code fragment that jig can specialize

Build to verify the code compiles:

go build

Step 3: Use the Generic Library

Go back to the project root and create main.go:

cd ..
package main

import _ "hellojig/generic"

func main() {
	var stack StringStack
	stack.Push("Hello, World!")
	println(stack.Pop())
}

Step 4: Generate and Run

First, try running without jig (this will fail):

go run *.go
# command-line-arguments
./main.go:6:13: undefined: StringStack

Now run jig to generate the code:

jig -v
found 3 templates in package "stack" (hellojig/generic)
generating "StringStack"
  StringStack
generating "StringStack Push"
  StringStack Push
generating "StringStack Pop"
  StringStack Pop
writing file "stack.go"

Run again:

go run *.go
Hello, World!

Generated Code

The jig tool created stack.go in your project directory:

// Code generated by jig; DO NOT EDIT.

//go:generate jig

package main

//jig:name StringStack

type StringStack []string

//jig:name StringStackPush

func (s *StringStack) Push(v string) {
	*s = append(*s, v)
}

//jig:name StringStackPop

func (s *StringStack) Pop() (result string) {
	slen := len(*s)
	if slen != 0 {
		result = (*s)[slen-1]
		*s = (*s)[:slen-1]
	}
	return
}

The generated code is a statically typed stack where foo has been replaced with string.

Mixing Data Types

Go's interface{} allows storing different types in the same container, but you lose static type checking. There are valid use cases for this approach:

  1. Heterogeneous containers — when you need to store int, string, and struct values in the same slice or map
  2. Large implementations — when the implementation is very large but not type-specific, you can wrap an interface{}-based implementation with strongly typed methods

jig supports specializing templates to interface{}. Simply use Stack instead of StringStack:

package main

import _ "hellojig/generic"

func main() {
	var stack Stack
	stack.Push("Hello, World!")
	stack.Push(42)  // Can mix types
	println(stack.Pop())
}

jig recognizes the absence of a reference type name and uses interface{}:

// Code generated by jig; DO NOT EDIT.

//go:generate jig

package main

//jig:name Stack

type Stack []interface{}

//jig:name StackPush

func (s *Stack) Push(v interface{}) {
	*s = append(*s, v)
}

//jig:name StackPop

func (s *Stack) Pop() (result interface{}) {
	slen := len(*s)
	if slen != 0 {
		result = (*s)[slen-1]
		*s = (*s)[:slen-1]
	}
	return
}

How does jig work?

jig works by repeatedly compiling your code. Each compile cycle may return errors about missing types or methods. jig creates type signatures for these errors and searches templates to find ones it can specialize to fix them. After generating code, jig compiles again. This continues until either no errors remain or no templates can fix the remaining errors.

jig generates the minimal amount of code needed to make your code compile. Even for large generics libraries, only the code actually needed is generated. This approach is called just-in-time-generics (jig) for Go.

Key Features

  • Automatic type discovery — no explicit specialization needed
  • Flexible specialization — templates can target specific types (int32, string) or interface{}
  • Minimal code generation — compilation drives generation
  • Working Go code — templates are buildable and testable
  • Standard distribution — template libraries are normal go get-able packages

What's a jig?

A jig holds a work in a fixed location and guides a tool to manufacture a product. A jig's primary purpose is to provide repeatability, accuracy, and interchangeability in the manufacturing process. — Wikipedia

In this project, a jig is working code written with placeholder types that generates specialized code for specific type combinations:

//jig:template <Foo>Stack Push

func (s *FooStack) Push(v foo) {
	*s = append(*s, v)
}

Note the placeholders: Foo (capitalized, for type references) and foo (lowercase, for actual type usage).

Usage

This section revisits the Getting Started example with more detail.

Command-Line

jig -h
Usage of jig [flags] [<dir>]:
  -c, --clean     Remove files generated by jig
  -m, --missing   Only generate code that is missing
  -n, --nodoc     No documentation in generated files
  -v, --verbose   Print details of what jig is doing

jig is a self-contained binary. Run it from your project directory:

  • Default behavior — removes previously generated code and regenerates everything
  • --missing / -m — only generates new code, keeps existing
  • --verbose / -v — shows what jig is doing
  • --clean / -c — removes all jig-generated files

jig discovers templates from imported packages. You must import the generics library in your code:

import _ "github.com/reactivego/jig/example/stack/generic"

To see which templates jig finds:

jig -v
removing file "stack.go"
found 4 jig templates in package "github.com/reactivego/jig/example/stack/generic"
...

Generated code includes //go:generate jig, allowing you to regenerate with go generate ./....

Writing Generics

Templates should be granular — each type, method, and function typically gets its own jig:template. This ensures:

  • Methods your code doesn't use won't be generated
  • jig can find templates for specific missing methods

Templates use placeholder types like Foo and Bar (called metasyntactic names). Here's a complete example:

package stack

type foo int

type FooStack []foo

func (s *FooStack) Push(v foo) {
	*s = append(*s, v)
}

func (s *FooStack) Pop() (foo, bool) {
	var zeroFoo foo
	if len(*s) == 0 {
		return zeroFoo, false
	}
	i := len(*s) - 1
	v := (*s)[i]
	*s = (*s)[:i]
	return v, true
}

Note:

  • This is valid Go code using placeholder type foo
  • FooStack, Push, and Pop all contain Foo in their names
  • You can use any placeholder names, not just Foo
  • Foo refers to a type reference; foo is the actual type name

This code compiles and can be tested. jig uses it as a template, replacing placeholders with actual type names — including in identifiers and comments.

To tell jig which code is part of templates, add pragmas:

package stack

type foo int

//jig:template <Foo>Stack

type FooStack []foo

//jig:template <Foo>Stack Push

func (s *FooStack) Push(v foo) {
	*s = append(*s, v)
}

//jig:template <Foo>Stack Pop

func (s *FooStack) Pop() (foo, bool) {
	var zeroFoo foo
	if len(*s) == 0 {
		return zeroFoo, false
	}
	i := len(*s) - 1
	v := (*s)[i]
	*s = (*s)[:i]
	return v, true
}

Important:

  • Three templates: <Foo>Stack, <Foo>Stack Push, <Foo>Stack Pop
  • Angle brackets (< and >) mark type variables
  • No space between // and jig:template
  • An empty line must separate the pragma from the code

This stack is available at github.com/reactivego/jig/example/stack/generic.

Using Generics

Here's a program using the generic stack:

package main

import (
	_ "github.com/reactivego/jig/example/stack/generic"
)

//jig:file stack.go

func main() {
	var s StringStack
	s.Push("Hello, World!")
}

Note:

  • The import makes the templates available to jig
  • jig:file specifies where generated code goes
  • StringStack signals we want a stack of strings
  • jig knows String means the built-in string type

After running jig, the file stack.go contains:

// Code generated by jig; DO NOT EDIT.

//go:generate jig

package main

//jig:name StringStack

type StringStack []string

//jig:name StringStackPush

func (s *StringStack) Push(v string) {
	*s = append(*s, v)
}

Note:

  • go:generate pragma enables regeneration
  • jig:name pragmas identify each fragment
  • All Foo/foo replaced with String/string
  • Pop isn't generated because it's not used!

Using Stack instead (for interface{}):

package main

import (
	"fmt"
	_ "github.com/reactivego/jig/example/stack/generic"
)

//jig:file stack.go

func main() {
	var s Stack
	s.Push("Hello, World!")
	if value, ok := s.Pop(); ok {
		fmt.Println(value)
	}
}

Running jig -v:

removing file "stack.go"
found 4 jig templates in package "github.com/reactivego/jig/example/stack/generic"
generating "Stack"
  Stack
generating "Stack Push"
  Stack Push
generating "Stack Pop"
  Stack Pop
writing file "stack.go"

Generated code:

// Code generated by jig; DO NOT EDIT.

//go:generate jig

package main

//jig:name Stack

type Stack []interface{}

var zero interface{}

//jig:name StackPush

func (s *Stack) Push(v interface{}) {
	*s = append(*s, v)
}

//jig:name StackPop

func (s *Stack) Pop() (interface{}, bool) {
	if len(*s) == 0 {
		return zero, false
	}
	i := len(*s) - 1
	v := (*s)[i]
	*s = (*s)[:i]
	return v, true
}

Now Pop is generated because it's used.

Directory Structure

Best practices for structuring a generics library:

$GOPATH/src/github.com/reactivego/jig/example/stack
├── generic/           # Template library
│   └── stack.go
├── test/              # Tests for each function/method
│   ├── Pop/
│   │   ├── doc.go
│   │   ├── example_test.go
│   │   └── stack.go
│   ├── Push/
│   └── doc.go
├── doc.go             # Package documentation
├── example_test.go    # Examples that drive code generation
└── stack.go           # Generated code

Root Directory (stack/)

Exports a package specialized on interface{}. This makes the package useful without requiring users to run jig. The generated code is heterogeneous — it accepts any type but isn't type-safe.

  • example_test.go — examples using generics that drive code generation
  • doc.go — package documentation (godoc.org doesn't show docs from test files)
  • stack.go — generated code

generic/

Contains all template code. Must be valid, compilable Go code.

Note: The package name should be stack (not generic) so jig knows what to name generated files.

No tests here — just templates.

test/

Contains tests for all aspects of the library, exercising it with different types. Each exported function or method gets its own directory for isolated testing.

Run jig in each test subdirectory to generate stack.go files.

Pragma Reference

Pragmas tell jig your intent. There are two kinds:

  1. Template Pragmas — used in template library code
  2. Generator Pragmas — used in code that consumes templates

Important: Pragmas must be separated from template code by an empty line.

Template Pragmas

Used when writing a template library.

jig:template

Defines a template. Also marks the end of any previous template.

//jig:template Subscriber

//jig:template Observable<Foo>

//jig:template Observable<Foo> Map<Bar>
  • First: template without type variables
  • Second: template for ObservableFoo with variable Foo
  • Third: template for method MapBar on ObservableFoo with variables Foo and Bar

During generation:

  • Foo and Bar → capitalized type names (Int32, String)
  • foo and bar → actual type names (int32, string)

Real example from github.com/reactivego/rx/generic:

//jig:template Observable<Foo> Map<Bar>

// MapBar transforms the items emitted by an ObservableFoo by applying a function to each item.
func (o ObservableFoo) MapBar(project func(foo) bar) ObservableBar {
	observable := func(observe BarObserveFunc, subscribeOn Scheduler, subscriber Subscriber) {
		observer := func(next foo, err error, done bool) {
			var mapped bar
			if !done {
				mapped = project(next)
			}
			observe(mapped, err, done)
		}
		o(observer, subscribeOn, subscriber)
	}
	return observable
}

jig:common

Use sparingly.

Marks a template as needed by practically every other template. Alternative to adding jig:needs to every template. Common templates must not have type variables.

//jig:common

There are usually only a few common templates for shared support code.

jig:needs

Optional pragma to speed up code generation.

Explicitly declares template dependencies. Without it, jig finds dependencies through compilation errors, which takes more iterations.

//jig:needs Observable<Foo> Concat, SubscribeOptions

This generates needed templates first, reducing compile cycles.

Note: You can't specify partially matching templates. TimeStamp<Foo>Observer must exist as an actual template to be referenced.

jig:embeds

Tells jig that a type embeds other types. If a method is missing, jig can generate it for the embedded type instead.

//jig:template Connectable<Foo>
//jig:embeds Observable<Foo>
//jig:needs SubscribeOptions

// ConnectableFoo is an ObservableFoo that has an additional method Connect()
// used to Subscribe to the parent observable and then multicasting values to
// all subscribers of ConnectableFoo.
type ConnectableFoo struct {
	ObservableFoo
	connect func(options []SubscribeOptionSetter) Subscriber
}

jig:required-vars

Prevents a template from specializing on interface{}. Use when a specific interface{} version already exists.

//jig:required-vars Foo, Bar

With this pragma, StackString and StackInt work, but Stack (suggesting interface{}) does not.

jig:end

Explicitly marks the end of a template.

//jig:end

Generator Pragmas

Used in code consuming a template library.

jig:file

Specifies where jig writes generated code.

Default: {{.package}}.go — one file per template library.

Variables available:

  • {{.Package}} / {{.package}} — template package name (title case / lowercase)
  • {{.Name}} / {{.name}} — fragment signature (title case / lowercase)

Examples (assuming package rx):

Template Result
SomeName.go SomeName.go
jig{{.Package}}.go jigRx.go
jig{{.package}}.go jigrx.go
{{.Package}}{{.Name}}.go RxObservableInt32.go
{{.package}}{{.name}}.go rxobservableint32.go

jig:type

Tells jig about unexported types.

By default, jig assumes type names start with capitals. Use jig:type for lowercase types:

type vector struct{ x, y float64 }

//jig:type Vector vector

var vstack StackVector

jig generates:

// Code generated by jig; DO NOT EDIT.
var v vector

Punctuation is allowed:

//jig:type Size size
//jig:type Points []point

jig:force-common-code-generation

You'll probably never need this.

When developing a template library, jig skips certain code in "inception" mode (generating into the library itself). This pragma forces generation of that skipped code — useful for debugging.

jig:no-doc

You'll probably never need this.

Skips generating documentation. Useful when developing templates and you want to see just the code.

jig:name

You'll never use this yourself.

Written by jig to identify generated fragments. Used when updating code to determine what's already present.

Example: ObservableInt32MapFloat32 for template Observable<Foo> Map<Bar> with int32 and float32.

Advanced Topics

Using jig inside a Template Library Package

Template code often depends on code that could also be generated. For example, in github.com/reactivego/rx/generic, mapping between observables requires both ObservableFoo and ObservableBar.

jig supports this. Configure internal generation with jig:type:

package rx

// foo is the first metasyntactic type. Use jig:type to tell jig that
// Foo references actual type foo.

//jig:type Foo foo

type foo int

// bar is the second metasyntactic type.

//jig:type Bar bar

type bar int

Type Signature Matching

jig analyzes Go compilation errors:

./main.go:11:8: undefined: StringStack

If template <Foo>Stack exists, jig replaces Foo with String and foo with string.

./main.go:12:3: s.Push undefined (type StringStack has no field or method Push)

If template <Foo>Stack Push exists, jig generates the method.

All detection uses five regular expressions matching Go errors:

^(.*):([0-9]*):([0-9]*): undeclared name: (.*)$
^(.*):([0-9]*):([0-9]*): invalid operation: .* [(]value of type [*](.*)[)] has no field or method (.*)
^(.*):([0-9]*):([0-9]*): invalid operation: .* [(]variable of type [*](.*)[)] has no field or method (.*)
^(.*):([0-9]*):([0-9]*): invalid operation: .* [(]value of type (.*)[)] has no field or method (.*)
^(.*):([0-9]*):([0-9]*): invalid operation: .* [(]variable of type (.*)[)] has no field or method (.*)

Note: These match errors from the loader package, which differ slightly from go command errors.

Revision Handling

For template libraries, the API contract works differently — users copy source fragments rather than linking compiled code.

Consider adding an exported revision function:

func Revision123() {}

Users reference this in their code. When you update to Revision124(), their code fails to build, signaling they should regenerate.

First and Higher Order Types

jig supports higher-order types that can recursively change order level:

//jig:template ObservableObservable<Foo> MergeAll

// MergeAll flattens a higher order observable by merging the observables it emits.
func (o ObservableObservableFoo) MergeAll() ObservableFoo {
	observable := func(observe FooObserveFunc, subscribeOn Scheduler, subscriber Subscriber) {
		// <SNIP>
	}
	return observable
}

Note: 2nd-order ObservableObservableFoo returns 1st-order ObservableFoo.

This template also matches 3rd-order ObservableObservableObservableFoo returning 2nd-order ObservableObservableFoo.

Available Generics Libraries

import _ "github.com/reactivego/rx/generic"
import _ "github.com/reactivego/multicast/generic"

Acknowledgements

jig is built on excellent Go tooling:

  • golang.org/x/tools/go/loader — compilation and error detection
  • text/template — code generation
  • regexp — error analysis and type signature matching

License

This library is licensed under the MIT License. See LICENSE for details.

About

Just In-time Generics for Go < v1.18

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages