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

kukel

package module
v0.38.0 Latest Latest
Warning

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

Go to latest
Published: Oct 30, 2026 License: MIT Imports: 38 Imported by: 0

README

kukel

A simple programming language, just for fun: Kukel, with C-like syntax, classes, closures, generators and exceptions, compiled to bytecode for a small stack machine written in Go.

// shapes.kkl
class Shape { name; Shape(name); area(); describe(); }
Shape::Shape(name) { this->name = name; }
Shape::describe() { return name + ": " + str(area()); }

class Circle : Shape { r; Circle(r); area(); }
Circle::Circle(r) { Shape::Shape("circle"); this->r = r; }
Circle::area() { return 3.14159 * r * r; }

class Square : Shape { side; Square(side); area(); }
Square::Square(side) { Shape::Shape("square"); this->side = side; }
Square::area() { return side * side; }

main(args) {
    local shapes = [new Circle(1), new Square(3), new Circle(2)];
    sort(shapes, fn(a, b) { return a->area() < b->area(); });

    for (local s in shapes) println(s->describe());

    try {
        println(shapes[5]);
    } catch (e) {
        println("caught", e->kind + ":", e->message);
    }
}
$ kukel run shapes.kkl
circle: 3.14159
square: 9
circle: 12.56636
caught RangeError: index 5 out of range for vector of length 3

What is in the language

  • ints (64-bit, wrapping), floats, strings (bytes), booleans, nil
  • vectors and maps (insertion order) with literals, indexing and for-each
  • functions with default and rest parameters, spread arguments (f(v..))
  • function literals (fn (x) { ... }) that capture variables by reference
  • classes with single inheritance, constructors, static members, virtual member functions and bound methods (p->method)
  • try/catch/finally, throw, and runtime errors as Error objects
  • switch with fall-through, labelled break/continue
  • modules: use geometry; loads geometry.kkl, reached as geometry::name; modules written in Go: db (SQL with SQLite, PostgreSQL and MySQL, and objects in tables), events (an event loop with timers), fs (open files read and written a line or a piece at a time, the standard streams, temporary files, locks, directories), json, p9 (the 9P file protocol: a client, and a server of trees the program builds and of disk directories), re (regular expressions), strings (a string builder), time, tk (desktop windows with widgets, menus, dialogs and a canvas; in a kukel built with the tag tk), term (the terminal a key at a time, and its size), utf8 (text by character instead of by byte), uuid, worker (interpreters that run in parallel and exchange messages) and zmq (ZeroMQ sockets, to talk with other programs and machines); and written in Kukel: iter (generators to make, combine and use up sequences: range, map, filter, take, zip, reduce, ...)
  • 55 built-in functions (strings, collections, math, files, ...)

New to Kukel? docs/tutorial.md is a fifteen-minute tour. The full description is in docs/reference.md; the grammar is in syntax.ebnf (and as a Lark grammar in syntax.lark). How programs are compiled and run (the virtual machine, every instruction, the .kkc format) is specified in docs/vm.md.

Installing

With Go 1.25 or newer:

go install github.com/maarten-boot/kukel/cmd/kukel@latest

or from a clone:

make            # vet, test and build bin/kukel
make install    # install kukel into $GOBIN, else $(go env GOPATH)/bin

kukel holds everything (about 25 MB; 18 MB stripped, as the release archives are). Build tags leave parts out for a smaller binary: nopostgres, nomysql, nosqlite (all three: no module db), nozmq and no9p. With all five it is about 8 MB; with SQLite as the only database about 17 MB:

go install -tags nopostgres,nomysql github.com/maarten-boot/kukel/cmd/kukel@latest
make small                    # bin/kukel-small, all five tags
make small TAGS=nozmq         # other tags

The module tk (desktop windows, see examples/notes.kkl) is the other way round: it is only in a kukel built with the tag tk, as it adds about 19 MB (stripped, as the release archives are; 25 MB with go install). It needs no C compiler and no Tcl/Tk on the system (Tcl/Tk 9 come with it); its windows need a display:

go install -tags tk github.com/maarten-boot/kukel/cmd/kukel@latest
make tk                       # bin/kukel-tk

kukel version lists the modules (and databases) a binary has.

Each release on GitHub also has ready-made archives of kukel, and of kukel-tk (with tk), for Linux, macOS and Windows (amd64 and arm64), with SHA256SUMS.

Docker

Each release is also a Docker image for linux/amd64 and linux/arm64, ghcr.io/maarten-boot/kukel (tagged with the version, and latest). It holds only kukel (on a distroless base, with CA certificates and time zones); programs come in through a volume mounted at /work, the working directory:

docker run --rm -v "$PWD:/work" ghcr.io/maarten-boot/kukel run prog.kkl -- args
docker run --rm -it -v "$PWD:/work" ghcr.io/maarten-boot/kukel repl
docker run --rm -p 5640:5640 -v "$PWD:/work" ghcr.io/maarten-boot/kukel run server.kkl

kukel runs as a user without privileges (uid 65532); to let a program write files into the mounted directory, run it as yourself: --user "$(id -u):$(id -g)". A server inside (9P, ZeroMQ) listens on tcp://*:port and is reached through -p. make docker builds the image for this machine as kukel:dev (IMAGE=... names it otherwise).

Using it

Command What it does
kukel run a.kkl b.kkl [-- args] compile the files as one program and run it
kukel a.kkl [args] the same as kukel run a.kkl -- args: a script that starts with #!/usr/bin/env kukel runs as a command
kukel check a.kkl ... compile only and report the errors
kukel build [-o prog.kkc] a.kkl ... compile to a bytecode file
kukel build -module [-o m.kkc] m.kkl compile a module on its own; use m; finds m.kkc when there is no m.kkl
kukel exec prog.kkc [--] [args] run a bytecode file
kukel disasm prog.kkc (or a.kkl ...) show the bytecode
kukel fmt [-w | -l] [-strict] a.kkl ... format source files (indentation, spacing, a blank line before if, for, ...); kukel fmt - formats standard input; -strict also adds the braces a statement of if, for, ... lacks and puts every function on several lines
kukel debug a.kkl ... [-- args] run a program under the debugger (breakpoints, stepping, values; it also stops where an exception nothing catches is thrown)
kukel profile [-o file] [-pprof file] [-cpu] a.kkl ... [-- args] run a program and report which functions and lines took the time, with call counts (also of workers); -pprof writes it for go tool pprof (flame graphs); Ctrl-C stops the program and writes the report
kukel dap the debugger for editors (the Debug Adapter Protocol): Visual Studio Code (with the extension in editors/vscode, make vscode), Neovim, Helix, Emacs; see docs/debugging.md
kukel repl [a.kkl ...] read declarations and statements and run them as they come
kukel tokens a.kkl ..., kukel ast [-pos] a.kkl ... show the tokens or the syntax tree
kukel version show the version and the modules written in Go
kukel help show the commands

tests/run has example programs: FizzBuzz, a JSON parser, word counting, closures, exceptions and more. Each comes with its expected output.

Embedding in Go

Go programs can run Kukel code, call its functions and give it Go functions of their own:

import "github.com/maarten-boot/kukel"

app := kukel.NewModule("app").Func("notify",
    func(in *kukel.Interp, args []kukel.Value) (kukel.Value, error) {
        log.Println(args[0])
        return kukel.Nil, nil
    })
prog, err := kukel.CompileFiles([]string{"rules.kkl"}, kukel.CompileOptions{
    Modules:      []*kukel.Module{app},   // rules.kkl: use app; ... app::notify(x);
    DenyBuiltins: kukel.SystemBuiltins,   // no files, no environment, no exit
})
in, err := kukel.New(prog, kukel.Options{StepLimit: 1_000_000})
result, err := in.CallContext(ctx, "discount", kukel.Int(250))

See docs/embedding.md and the package documentation.

How it works

source ──lexer──▶ tokens ──parser──▶ syntax tree ──compiler──▶ bytecode ──VM──▶ run
                                                                  │
                                                       .kkc file ◀┘ (build / exec)
Package Role
kukel (the repository root) the Go API for embedding (docs/embedding.md)
internal/token, internal/diag token kinds and source positions; the error type all stages share
internal/lexer, internal/parser, internal/ast source to syntax tree
internal/loader finds and reads the modules a program uses
internal/native, internal/modules/... modules written in Go (db, events, fs, json, p9, re, strings, term, time, tk, utf8, uuid, worker, zmq) and native classes
internal/compiler name resolution, the language's rules, code generation
internal/bytecode instruction set, .kkc files, the verifier that checks loaded files
internal/vm the stack machine: calls, objects, closures, generators, exceptions (specified in docs/vm.md)
internal/debugger the debuggers' core: breakpoints, stepping, expressions (front ends: kukel debug, kukel dap)
internal/value, internal/builtins values with their operators, built-in functions
examples/ demo programs, such as a grep, and a Go program that embeds kukel (examples/README.md)
editors/vscode the Visual Studio Code extension: highlighting and debugging (docs/debugging.md)
editors/geany a filetype for Geany: highlighting, the symbol list, the Build menu (editors/geany/README.md)

TODO.md holds the plan the interpreter was built from, with all design decisions (Q1–Q99). Ideas for later are in docs/future.md. What changed in each release is in CHANGELOG.md; all the documents are listed in docs/README.md. How to check a change (the subsystems, what else a change touches, and when a full cross-check is due) is in docs/checks.md.

Development

Make target
make gofmt check, go vet, tests, build (each also on its own: make vet, make test, make build). The test of syntax.lark (it parses the examples, the test programs and the programs of the documents) needs Python with lark (pip install lark), else it skips
make install install kukel into $GOBIN, else $(go env GOPATH)/bin
make fmt, make clean gofmt -w the sources; remove bin/ and dist/, and the cached test results
make test-all everything: vet, the tests of the full, the smallest and the tk build, and the tests with the race detector
make small, make test-small build bin/kukel-small, or run the tests, without the optional parts (TAGS=... picks other tags for small; make vet also vets the smallest build)
make tk, make test-tk build bin/kukel-tk, or run the tests, with the module tk (make vet also vets it). Its tests need a display or Xvfb (else they skip), and click, type and drag with xdotool when it is there
make fuzz FUZZTIME=1m fuzz the lexer, parser, compiler, bytecode loader and verifier (what they accept runs in the VM with a step limit), json::decode, time parsing and kukel fmt (each for 30s by default)
make bench benchmarks
make golden rewrite the expected outputs in tests/ after an intended change
make vscode the Visual Studio Code extension (editors/vscode) as dist/kukel-vscode-<version>.vsix; needs Node.js (npx fetches vsce)
make docker the Docker image for this machine, kukel:dev (IMAGE=... for another name)
make dist VERSION=v1.2.3 the archives of a release in dist/: kukel and kukel-tk for Linux, macOS and Windows on amd64 and arm64 (PLATFORMS=... picks others), with SHA256SUMS

Releases: pushing a tag v1.2.3 runs .github/workflows/release.yml, which first runs the whole CI (ci.yml); when that passes, the archives of make dist are published as the GitHub release of the tag, with notes made from the commits since the previous tag. A release that already exists (made by hand) gets the archives added, replacing files of the same name. At the same time the Docker image is built for linux/amd64 and linux/arm64 and pushed to ghcr.io as :v1.2.3, and as :latest when the tag is the newest release (a tag with a -, such as v1.3.0-rc1, never is). The base images and BuildKit are pinned to digests; Dependabot proposes updates of the base images and of the actions (BuildKit, named in a script of the workflow, is updated by hand).

The tests of db also run on database servers when KUKEL_TEST_POSTGRES and KUKEL_TEST_MYSQL name one (a URL as db::open takes it; the tables item, tag, plain, customer, order, line and doc there are dropped: use a database for tests only), for example with Docker:

docker run -d --rm --name pg -e POSTGRES_PASSWORD=pw -p 5432:5432 postgres:16
docker run -d --rm --name maria -e MARIADB_ROOT_PASSWORD=pw -e MARIADB_DATABASE=test -p 3306:3306 mariadb:11
KUKEL_TEST_POSTGRES="postgres://postgres:pw@127.0.0.1:5432/postgres?sslmode=disable" \
KUKEL_TEST_MYSQL="mysql://root:pw@127.0.0.1:3306/test" go test ./internal/modules/dbmod

License

MIT, see LICENSE.

Documentation

Overview

Package kukel embeds the Kukel language in Go programs: compile Kukel source once, run it in as many interpreters as needed, call its functions, and give it Go functions of your own.

app := kukel.NewModule("app").
	Func("greet", func(in *kukel.Interp, args []kukel.Value) (kukel.Value, error) {
		return kukel.String("hello, " + args[0].String()), nil
	})
prog, err := kukel.CompileSource("rules.kkl", `
	use app;
	discount(total) { return total > 100 ? 0.1 : 0; }
	main() { println(app::greet("world")); }`,
	kukel.CompileOptions{Modules: []*kukel.Module{app}})
in, err := kukel.New(prog, kukel.Options{Stdout: os.Stdout, StepLimit: 1_000_000})
d, err := in.Call("discount", kukel.Int(250))   // 0.1

The language is described in docs/reference.md of the repository, the virtual machine in docs/vm.md, and this package in docs/embedding.md.

For code that is not trusted, CompileOptions limit what a program may use (modules, built-in functions such as readfile), Options.StepLimit bounds the work of every call, and CallContext stops a call when its context ends. Memory is not limited.

Example

Compile a program once and call its functions from Go.

package main

import (
	"fmt"

	"github.com/maarten-boot/kukel"
)

func main() {
	prog, err := kukel.CompileSource("pricing.kkl", `
const VAT = 0.21;
price(net, qty) {
    local total = net * qty;

    if (qty >= 10) total *= 0.9;       // volume discount
    return round(total * (1 + VAT) * 100) / 100.0;
}
main() {}`, kukel.CompileOptions{})
	if err != nil {
		fmt.Println(err)
		return
	}
	in, err := kukel.New(prog, kukel.Options{})
	if err != nil {
		fmt.Println(err)
		return
	}
	for _, qty := range []int64{1, 10} {
		p, err := in.Call("price", kukel.Float(9.99), kukel.Int(qty))
		fmt.Println(qty, p, err)
	}
}
Output:
1 12.09 <nil>
10 108.79 <nil>

Index

Examples

Constants

This section is empty.

Variables

View Source
var Nil = Value{}

Nil is the Kukel value nil.

View Source
var SystemBuiltins = []string{"readfile", "writefile", "appendfile", "readline", "getenv", "exit"}

SystemBuiltins are the built-in functions that reach outside the program: files (readfile, writefile, appendfile), standard input (readline), the environment (getenv) and ending the process (exit). CompileOptions{DenyBuiltins: kukel.SystemBuiltins} keeps them away from a program.

Functions

func Builtins

func Builtins() []string

Builtins lists the names of all built-in functions.

func StandardModules

func StandardModules() []string

StandardModules lists the modules written in Go that come with Kukel (db, events, fs, json, p9, re, strings, time, uuid, worker, zmq, and tk when built with -tags tk), less those that build tags left out.

Types

type Class added in v0.9.0

type Class struct {
	// contains filtered or unexported fields
}

Class is a class written in Go for a host module. Its objects carry a Go value (their state) and have no data members: everything goes through member functions. Kukel code makes objects with new name::C(...) when the class has a constructor, and may then derive classes from it; the derived constructor calls name::C::C(...) to set up the Go value. Without a constructor, objects come from the module's functions (Class.Object).

NewClass, New, Method, Static and Show panic on programming mistakes: a bad name, a nil function, a name used twice, or an addition after the module is in use.

func NewClass added in v0.9.0

func NewClass(name string) *Class

NewClass makes an empty class. name must be an identifier.

Example

A class written in Go: Kukel code uses it and derives a class from it that overrides a step (changed).

package main

import (
	"fmt"
	"os"

	"github.com/maarten-boot/kukel"
)

func main() {
	type counter struct{ n int64 }
	counterClass := kukel.NewClass("Counter").
		New(func(in *kukel.Interp, this *kukel.Object, args []kukel.Value) (kukel.Value, error) {
			this.SetState(&counter{}) // the constructor sets the state
			return kukel.Nil, nil
		}).
		Method("inc", func(in *kukel.Interp, this *kukel.Object, args []kukel.Value) (kukel.Value, error) {
			c := this.State().(*counter)
			c.n++
			if _, err := this.Call(in, "changed", kukel.Int(c.n)); err != nil { // overridable
				return kukel.Nil, err
			}
			return this.Value(), nil
		}).
		Method("changed", func(in *kukel.Interp, this *kukel.Object, args []kukel.Value) (kukel.Value, error) {
			return kukel.Nil, nil
		}).
		Show(func(this *kukel.Object) string {
			return fmt.Sprintf("app::Counter(%d)", this.State().(*counter).n)
		})
	app := kukel.NewModule("app").Class(counterClass)

	prog, err := kukel.CompileSource("main.kkl", `use app;

class Loud : app::Counter { Loud(); changed(n); }
Loud::Loud() { app::Counter::Counter(); }        // sets up the Go state
Loud::changed(n) { println("now", n); }

main() {
    local c = new app::Counter();
    c->inc()->inc();
    println(c);                                  // app::Counter(2)
    new Loud()->inc();                           // now 1
}`, kukel.CompileOptions{Modules: []*kukel.Module{app}})
	if err != nil {
		fmt.Println(err)
		return
	}
	in, err := kukel.New(prog, kukel.Options{Stdout: os.Stdout})
	if err != nil {
		fmt.Println(err)
		return
	}
	in.Run()
}
Output:
app::Counter(2)
now 1

func (*Class) Method added in v0.9.0

func (c *Class) Method(name string, fn Method) *Class

Method adds an instance member function.

func (*Class) New added in v0.9.0

func (c *Class) New(fn Method) *Class

New sets the constructor: new name::C(args) calls fn with the new object, whose Go value fn sets (this.SetState).

func (*Class) Object added in v0.9.0

func (c *Class) Object(state any) Value

Object makes an object of the class holding state, for a Func to return. The class must be in a module that a compiled program uses.

func (*Class) Show added in v0.9.0

func (c *Class) Show(fn func(this *Object) string) *Class

Show sets how str() and println show an object of the class (without it: "name::C{}").

func (*Class) StateOf added in v0.9.0

func (c *Class) StateOf(v Value) (any, bool)

StateOf gives the Go value of v, an object of the class or of a Kukel class derived from it whose constructor has set it up.

func (*Class) Static added in v0.9.0

func (c *Class) Static(name string, fn Func) *Class

Static adds a static member function (name::C::f(...)).

type CompileError

type CompileError struct {
	File      string
	Line, Col int
	Message   string
	More      []*CompileError
}

CompileError is an error in Kukel source. A compile can find several: the first is the CompileError itself, the others are in More (in the order of the source), and Error() gives all of them, one per line.

func (*CompileError) Error

func (e *CompileError) Error() string

type CompileOptions

type CompileOptions struct {
	// Modules are the program's own modules, written in Go ("use app;").
	// A module here hides a standard module of the same name.
	Modules []*Module
	// StandardModules lists the standard modules written in Go the program
	// may use (nil: all of them; an empty, non-nil slice: none). Those
	// written in Kukel (iter) are always there: they can do nothing the
	// program's own code could not.
	StandardModules []string
	// DenyBuiltins lists built-in functions the program may not use
	// (using one is a compile error); see SystemBuiltins.
	DenyBuiltins []string
	// Path lists directories where "use name;" looks for name.kkl (or
	// name.kkc, a module compiled on its own), after the directory of the
	// file that uses it (CompileFiles only).
	Path []string
	// NoFileModules forbids reading program files: "use" finds only
	// modules written in Go and the standard modules written in Kukel
	// (built into kukel), and worker::startfile is refused.
	NoFileModules bool
}

CompileOptions say what a program may use beyond its own code.

type Error

type Error struct {
	Kind    string  // "TypeError", "ValueError", ...
	Message string  // the Error object's message
	Value   Value   // the thrown Error object (Nil for the step limit and cancellation)
	Trace   []Frame // the calls, innermost first
	// contains filtered or unexported fields
}

Error is an exception of a Kukel program that nothing caught, or the end of a call by its step limit (Kind "StepLimitError") or its context (Kind "CancelledError").

Example

An exception that nothing in the program catches is an *Error: its kind, message and the calls it went through.

package main

import (
	"errors"
	"fmt"

	"github.com/maarten-boot/kukel"
)

func main() {
	prog, err := kukel.CompileSource("orders.kkl", `
total(orders) { return check(orders[0]) * 2; }
check(order) {
    if (order < 0) throw new Error("negative order", "ValueError");
    return order;
}
main() {}`, kukel.CompileOptions{})
	if err != nil {
		fmt.Println(err)
		return
	}
	in, _ := kukel.New(prog, kukel.Options{})
	_, err = in.Call("total", kukel.Vector(kukel.Int(-5)))
	var e *kukel.Error
	if errors.As(err, &e) {
		fmt.Println(e.Kind, "-", e.Message)
		for _, f := range e.Trace {
			fmt.Printf("  %s (%s:%d)\n", f.Func, f.File, f.Line)
		}
	}
}
Output:
ValueError - negative order
  check (orders.kkl:4)
  total (orders.kkl:2)

func Errorf

func Errorf(kind, format string, args ...any) *Error

Errorf makes an error for a Func to return: Kukel code sees an exception (an Error object) with that kind (or "HostError" if kind is "") and message.

func (*Error) Error

func (e *Error) Error() string

Error gives the kind and message, then the trace if there is one (one " at f (file:line)" line per call).

type ExitError

type ExitError struct{ Code int }

ExitError is what Call, CallContext, CallValue and Global give when the program calls exit(code) (Run gives the code instead). A Func may return one to end the program as exit(code) does.

func (*ExitError) Error

func (e *ExitError) Error() string

type Frame

type Frame struct {
	Func string
	File string
	Line int
}

Frame is one call in a trace.

type Func

type Func func(in *Interp, args []Value) (Value, error)

Func is a Go function that Kukel code calls. in is the interpreter that calls it (to call back into Kukel with in.CallValue). The arguments are valid during the call only; keep copies (ToGo) of what is needed later. An error becomes a Kukel exception: an *Error keeps its kind (see Errorf), any other error has the kind "HostError".

type Interp

type Interp struct {
	// contains filtered or unexported fields
}

Interp is an interpreter running a Program: it has its own globals, output and state. Its methods must not be called from several goroutines at once; use one Interp per goroutine (they can share a Program). A Func may call back into the Interp that called it.

func New

func New(p *Program, opts Options) (*Interp, error)

New makes an interpreter for p. It fails if p uses a built-in or module that its CompileOptions do not allow (a program from LoadProgram).

func (*Interp) Call

func (in *Interp) Call(name string, args ...Value) (Value, error)

Call calls the program's function name ("discount", or a module's: "util::f", also a private "util::_g", or "json::encode" of a Go module the program may use) with args and gives its result. A Kukel exception that nothing catches is an *Error; exit(code) gives an *ExitError.

func (*Interp) CallContext

func (in *Interp) CallContext(ctx context.Context, name string, args ...Value) (Value, error)

CallContext is Call, stopped when ctx ends: the program stops at its next step (a function written in Go that waits, such as time::sleep, returns at once) with an *Error of kind "CancelledError" that it cannot catch. This also holds for a call a Func makes back into Kukel. Workers started during the call are cancelled if ctx ends while it runs; Close cancels those that outlive it.

Example

Run code that is not trusted: no files, no workers, a step budget and a time limit.

package main

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/maarten-boot/kukel"
)

func main() {
	opts := kukel.CompileOptions{
		DenyBuiltins:    kukel.SystemBuiltins,
		StandardModules: []string{"json", "strings"},
		NoFileModules:   true,
	}
	if _, err := kukel.CompileSource("evil.kkl", `main() { println(readfile("/etc/passwd")); }`, opts); err != nil {
		fmt.Println(err)
	}

	prog, _ := kukel.CompileSource("slow.kkl", `spin() { while (true) {} } main() {}`, opts)
	in, _ := kukel.New(prog, kukel.Options{})
	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
	defer cancel()
	_, err := in.CallContext(ctx, "spin")
	fmt.Println(strings.SplitN(err.Error(), "\n", 2)[0])
}
Output:
evil.kkl:1:18: built-in function readfile is not available in this program
CancelledError: the program was cancelled: context deadline exceeded

func (*Interp) CallValue

func (in *Interp) CallValue(fn Value, args ...Value) (Value, error)

CallValue calls a Kukel function value (such as a callback a Func received) with args.

Example

A Go function that gets a Kukel function as a callback calls it with CallValue.

package main

import (
	"fmt"
	"os"

	"github.com/maarten-boot/kukel"
)

func main() {
	app := kukel.NewModule("app").Func("each", func(in *kukel.Interp, args []kukel.Value) (kukel.Value, error) {
		sum := int64(0)
		for _, item := range []string{"pen", "ink"} {
			r, err := in.CallValue(args[0], kukel.String(item))
			if err != nil {
				return kukel.Nil, err // an exception of the callback goes on
			}
			n, _ := r.AsInt()
			sum += n
		}
		return kukel.Int(sum), nil
	})
	prog, err := kukel.CompileSource("each.kkl", `use app;
main() {
    println(app::each(fn(item) { println("got", item); return len(item); }));
}`, kukel.CompileOptions{Modules: []*kukel.Module{app}})
	if err != nil {
		fmt.Println(err)
		return
	}
	in, _ := kukel.New(prog, kukel.Options{Stdout: os.Stdout})
	in.Run()
}
Output:
got pen
got ink
6

func (*Interp) Close added in v0.2.1

func (in *Interp) Close()

Close stops the interpreter for good: workers it started that are still running are cancelled, and what they write later is dropped; files the program opened with fs::open and did not close are closed. Calls after Close give an error. Close may be called more than once.

func (*Interp) Generator added in v0.37.0

func (in *Interp) Generator(name string, next func() (Value, bool, error)) (Value, error)

Generator makes a generator (an object of the class Generator) whose values come from next, a Go function: it gives the next value, or false at the end; an error ends the generator and is thrown where the value was asked for, as a Func's error is. A member function iter() of a host class that gives one makes its objects iterable with for. name is how the generator shows ("shop::Cart::iter"). Kukel code steps it as one written in Kukel, but send gives it no value and close only ends it.

func (*Interp) Global

func (in *Interp) Global(name string) (Value, error)

Global gives the value of a global of the program (a variable, function or class), after the global initializers have run.

Example

Read a global variable after the global initializers have run.

package main

import (
	"fmt"

	"github.com/maarten-boot/kukel"
)

func main() {
	prog, err := kukel.CompileSource("config.kkl", `
global limits = {"orders": 10, "items": 3 * 4};
main() {}`, kukel.CompileOptions{})
	if err != nil {
		fmt.Println(err)
		return
	}
	in, _ := kukel.New(prog, kukel.Options{})
	limits, err := in.Global("limits")
	fmt.Println(limits.Get(kukel.String("items")), err)
	_, err = in.Global("nosuch")
	fmt.Println(err)
}
Output:
12 <nil>
kukel: the program has no global nosuch

func (*Interp) Run

func (in *Interp) Run(args ...string) (int, error)

Run runs the program's main, with args as its parameter (a vector of strings) if it has one, and gives the exit code: main's result if that is an int, else 0; or the code of exit(code). If the program fails, Run gives 1 and the error. The global initializers run first, the first time anything is called; if they throw an exception, every call gives it (a step limit or cancellation lets the next call run them again). Run does not wait for workers the program started (see Close).

func (*Interp) RunContext

func (in *Interp) RunContext(ctx context.Context, args ...string) (int, error)

RunContext is Run, stopped when ctx ends (see CallContext).

type Method added in v0.9.0

type Method func(in *Interp, this *Object, args []Value) (Value, error)

Method is a member function written in Go: this is the object it is called on (also an object of a Kukel class derived from the Go class). For the constructor (Class.New), it sets the object's Go value with this.SetState; its result is not used.

type Module

type Module struct {
	// contains filtered or unexported fields
}

Module is a module written in Go for one program: Kukel code that says "use name;" reaches its functions and constants as name::f and name::K. Add everything before the module is first used in CompileOptions.

func NewModule

func NewModule(name string) *Module

NewModule makes an empty module. name must be a Kukel identifier that is not a keyword. NewModule, Func and Const panic on such programming mistakes: a bad name, a nil function, a name used twice, a constant that is not nil, a boolean, a number or a string, or an addition after the module is in use.

Example

Give Kukel code functions written in Go, in a module of its own.

package main

import (
	"fmt"
	"os"

	"github.com/maarten-boot/kukel"
)

func main() {
	stock := map[string]int64{"pen": 12, "ink": 0}
	shop := kukel.NewModule("shop").
		Const("currency", kukel.String("EUR")).
		Func("stock", func(in *kukel.Interp, args []kukel.Value) (kukel.Value, error) {
			item, ok := args[0].AsString()
			if !ok {
				return kukel.Nil, kukel.Errorf("TypeError", "an item is a string")
			}
			n, known := stock[item]
			if !known {
				return kukel.Nil, kukel.Errorf("KeyError", "no item %q", item)
			}
			return kukel.Int(n), nil
		})
	prog, err := kukel.CompileSource("shop.kkl", `use shop;
main() {
    for (local item in ["pen", "ink", "cap"]) {
        try {
            println(item, shop::stock(item) > 0 ? "in stock" : "sold out");
        } catch (e) {
            println(item, e->kind, e->message);
        }
    }
    println("prices in", shop::currency);
}`, kukel.CompileOptions{Modules: []*kukel.Module{shop}})
	if err != nil {
		fmt.Println(err)
		return
	}
	in, _ := kukel.New(prog, kukel.Options{Stdout: os.Stdout})
	in.Run()
}
Output:
pen in stock
ink sold out
cap KeyError no item "cap"
prices in EUR

func (*Module) Class added in v0.9.0

func (m *Module) Class(c *Class) *Module

Class adds a class written in Go (see NewClass); Kukel code reaches it as name::C: new name::C(...), class D : name::C, name::C::f(...).

func (*Module) Const

func (m *Module) Const(name string, v Value) *Module

Const adds a constant: nil, a boolean, a number or a string. Kukel code can use it in constant expressions.

func (*Module) Func

func (m *Module) Func(name string, fn Func) *Module

Func adds a function; it gives m, so that calls can be chained.

type Object added in v0.9.0

type Object struct {
	// contains filtered or unexported fields
}

Object is an object, as a Method gets it (this) or Value.Object gives it.

func (*Object) Call added in v0.9.0

func (o *Object) Call(in *Interp, name string, args ...Value) (Value, error)

Call calls the member function name on the object, as p->name(args) does: a Kukel class derived from a Go class may override it.

func (*Object) ClassName added in v0.9.0

func (o *Object) ClassName() string

ClassName gives the name of the object's class ("app::Counter", or the Kukel class derived from it).

func (*Object) SetState added in v0.9.0

func (o *Object) SetState(state any)

SetState sets the object's Go value.

func (*Object) State added in v0.9.0

func (o *Object) State() any

State gives the object's Go value (nil before the constructor sets it).

func (*Object) Value added in v0.9.0

func (o *Object) Value() Value

Value gives the object as a Value.

type Options

type Options struct {
	Stdout io.Writer // what print writes to; nil: discarded
	Stderr io.Writer // what eprint writes to; nil: discarded
	Stdin  io.Reader // what readline reads; nil: empty

	// Program is what program() gives: the program's name or path, as
	// C's argv[0]; "" by default. Workers started with worker::start get
	// it too.
	Program string

	// StepLimit bounds each call from Go (Run, Call, CallValue): after that
	// many steps (backward jumps, calls and throws, so every loop counts)
	// the call ends with an *Error of kind "StepLimitError" that the
	// program cannot catch. Calls a Func makes back into Kukel share the
	// budget of the call that runs the Func. 0: no limit. Workers the
	// program starts get the same limit for their whole run.
	StepLimit int

	// Profile, if not nil, profiles the calls of the interpreter from Go
	// (each outermost Run, Call, ...: the time between them does not
	// count), and the workers they start.
	Profile *Profile
}

Options configure an interpreter.

Example (StepLimit)

A step limit stops code that runs too long (each call from Go gets the whole budget), and a panic in a Go function is a HostError for Kukel.

package main

import (
	"errors"
	"fmt"

	"github.com/maarten-boot/kukel"
)

func main() {
	app := kukel.NewModule("app").Func("crash", func(in *kukel.Interp, args []kukel.Value) (kukel.Value, error) {
		var m map[string]int
		m["x"] = 1 // panics
		return kukel.Nil, nil
	})
	prog, err := kukel.CompileSource("limits.kkl", `use app;
spin() { while (true) {} }
count(n) { local i = 0; while (i < n) i++; return i; }
crash() { try { app::crash(); } catch (e) { return e->kind; } }
main() {}`, kukel.CompileOptions{Modules: []*kukel.Module{app}})
	if err != nil {
		fmt.Println(err)
		return
	}
	in, _ := kukel.New(prog, kukel.Options{StepLimit: 10000})
	_, err = in.Call("spin")
	var e *kukel.Error
	if errors.As(err, &e) {
		fmt.Println(e.Kind + ": " + e.Message)
	}
	fmt.Println(in.Call("count", kukel.Int(5000))) // a new budget for this call
	fmt.Println(in.Call("crash"))
}
Output:
StepLimitError: more than 10000 steps
5000 <nil>
HostError <nil>

type Profile added in v0.30.0

type Profile struct {
	// contains filtered or unexported fields
}

Profile collects where the time of interpreters' calls goes: which functions and lines of the program, with their calls (also of workers). Give it to New in Options.Profile; then the calls of that interpreter from Go (Run, Call, ...) are profiled, and WriteReport and WritePprof show what they gave (of a call or worker still running: its samples so far, its calls up to its latest sample). Several interpreters may share a Profile (the first is "main", the others "interp 2", ...); lines of their programs stay apart also when two have a file of the same name (the report then names the interpreter). Its methods are safe for concurrent use.

Example

Profile the calls of an interpreter: where their time goes, by function and line, with call counts (the time between the calls does not count).

package main

import (
	"os"

	"github.com/maarten-boot/kukel"
)

func main() {
	prog, _ := kukel.CompileSource("work.kkl", `
work(n) { local t = 0; for (local i = 0; i < n; i++) t += i % 7; return t; }
main() {}`, kukel.CompileOptions{})
	prof := kukel.NewProfile(kukel.ProfileOptions{})
	in, _ := kukel.New(prog, kukel.Options{Profile: prof})
	for range 3 {
		in.Call("work", kukel.Int(100000))
	}
	prof.WriteReport(os.Stderr) // or WritePprof, for go tool pprof
}

func NewProfile added in v0.30.0

func NewProfile(opts ProfileOptions) *Profile

NewProfile makes a Profile.

func (*Profile) WritePprof added in v0.30.0

func (p *Profile) WritePprof(w io.Writer) error

WritePprof writes the profile in Go's pprof format, for go tool pprof (flame graphs, call graphs, the source with the time of each line).

func (*Profile) WriteReport added in v0.30.0

func (p *Profile) WriteReport(w io.Writer) error

WriteReport writes the text report kukel profile writes: the functions by their own time and their total time (with what they call), with their calls; the slowest lines with their source; the calls of built-ins and Go functions.

type ProfileOptions added in v0.30.0

type ProfileOptions struct {
	// Interval is the time between samples (0 or less: a millisecond).
	Interval time.Duration
	// CPU leaves out the time spent waiting in the standard Go functions
	// that wait (time::sleep, events::run(), receive, readline, ...): by
	// default each sample weighs the time since the one before, so that
	// waiting shows on the line that waits. The time of the Go functions
	// of your modules counts in full, also when they wait.
	CPU bool
}

ProfileOptions configure a Profile.

type Program

type Program struct {
	// contains filtered or unexported fields
}

Program is a compiled program. It does not change when it runs, so any number of interpreters (New) may run it, also at the same time.

func CompileFiles

func CompileFiles(files []string, opts CompileOptions) (*Program, error)

CompileFiles compiles source files as one program, with the modules they use. An error in the source is a *CompileError.

func CompileSource

func CompileSource(name, src string, opts CompileOptions) (*Program, error)

CompileSource compiles a program given as text; name is used in messages. Modules written in Kukel are looked for in opts.Path only.

func LoadProgram

func LoadProgram(data []byte, opts CompileOptions) (*Program, error)

LoadProgram reads a program from the bytes of a .kkc file (Encode, or kukel build). opts say what it may use; New refuses a program that uses more.

Example

Compile once, keep the bytes of a .kkc file, and load them later without compiling. A .kkc file names the functions of host modules but does not contain them: LoadProgram must be given the modules again.

package main

import (
	"fmt"

	"github.com/maarten-boot/kukel"
)

func main() {
	host := func() *kukel.Module {
		return kukel.NewModule("host").Func("double", func(in *kukel.Interp, args []kukel.Value) (kukel.Value, error) {
			n, _ := args[0].AsInt()
			return kukel.Int(2 * n), nil
		})
	}
	prog, err := kukel.CompileSource("calc.kkl", `use host;
calc(n) { return host::double(n) + 1; }
main() {}`, kukel.CompileOptions{Modules: []*kukel.Module{host()}})
	if err != nil {
		fmt.Println(err)
		return
	}
	data, err := prog.Encode() // to store, as kukel build would
	if err != nil {
		fmt.Println(err)
		return
	}

	loaded, err := kukel.LoadProgram(data, kukel.CompileOptions{Modules: []*kukel.Module{host()}})
	if err != nil {
		fmt.Println(err)
		return
	}
	in, _ := kukel.New(loaded, kukel.Options{})
	fmt.Println(in.Call("calc", kukel.Int(20)))
}
Output:
41 <nil>

func (*Program) Encode

func (p *Program) Encode() ([]byte, error)

Encode gives the program as the bytes of a .kkc file.

type Value

type Value struct {
	// contains filtered or unexported fields
}

Value is a Kukel value: nil, a boolean, an int (64 bits), a float, a string (bytes), a vector, a map, a function, a class or an object. The zero Value is nil.

Vectors, maps and objects are shared, not copied: a Value taken from an interpreter refers to the interpreter's data. Use it only while that interpreter does not run on another goroutine.

func Bool

func Bool(b bool) Value

Bool makes a boolean.

func Float

func Float(f float64) Value

Float makes a float.

func FromGo

func FromGo(x any) (Value, error)

FromGo makes a Value of a Go value: nil, any bool, integer, float or string type, []byte (a string), a Value, a slice or array (a vector) or a map with string keys (a map, keys in sorted order), also nested.

Example

Convert between Go and Kukel values.

package main

import (
	"fmt"

	"github.com/maarten-boot/kukel"
)

func main() {
	v, _ := kukel.FromGo(map[string]any{"name": "Kukel", "tags": []string{"small", "fun"}})
	fmt.Println(v, v.Get(kukel.String("tags")).Len())
	g, _ := v.ToGo()
	fmt.Printf("%v\n", g)
}
Output:
{"name": "Kukel", "tags": ["small", "fun"]} 2
map[name:Kukel tags:[small fun]]

func Int

func Int(i int64) Value

Int makes an int.

func MapOf

func MapOf(m map[string]Value) Value

MapOf makes a new map of m, with the keys in sorted order (Kukel maps keep the order in which keys were added).

func NewMap

func NewMap() Value

NewMap makes a new, empty map; add entries with SetKey.

func String

func String(s string) Value

String makes a string.

func Vector

func Vector(elems ...Value) Value

Vector makes a new vector of the elements.

func (Value) AsBool

func (v Value) AsBool() (b, ok bool)

AsBool gives a boolean's value.

func (Value) AsFloat

func (v Value) AsFloat() (float64, bool)

AsFloat gives a float's value, or an int's as a float.

func (Value) AsInt

func (v Value) AsInt() (int64, bool)

AsInt gives an int's value.

func (Value) AsString

func (v Value) AsString() (string, bool)

AsString gives a string's value.

func (Value) Get

func (v Value) Get(key Value) Value

Get gives the value of key in a map (Nil if v is not a map or has no such key).

func (Value) Index

func (v Value) Index(i int) Value

Index gives element i of a vector (Nil if v is not a vector or i is out of range).

func (Value) IsNil

func (v Value) IsNil() bool

IsNil reports whether v is nil.

func (Value) Keys

func (v Value) Keys() []Value

Keys gives a map's keys, in order.

func (Value) Kind

func (v Value) Kind() string

Kind is what typeof gives in Kukel: "nil", "bool", "int", "float", "string", "vector", "map", "function", "class" or "object".

func (Value) Len

func (v Value) Len() int

Len is the length of a string (in bytes), vector or map, else -1.

func (Value) Object added in v0.9.0

func (v Value) Object() (*Object, bool)

Object gives v as an object (of a class written in Kukel or in Go).

func (Value) Push

func (v Value) Push(e Value) error

Push appends e to a vector.

func (Value) SetKey

func (v Value) SetKey(key, val Value) error

SetKey sets key to val in a map. Keys are strings and numbers.

func (Value) String

func (v Value) String() string

String gives v as Kukel's print and str() show it.

func (Value) ToGo

func (v Value) ToGo() (any, error)

ToGo gives v as a Go value: nil, bool, int64, float64, string, []any for a vector and map[string]any for a map (keys as str() shows them), nested. A function, class or object stays a Value. A vector or map that contains itself is an error, and so is a map with two keys that give the same Go key (1 and "1").

func (Value) Truthy

func (v Value) Truthy() bool

Truthy reports whether v counts as true in a condition (everything but nil, false, 0 and 0.0).

Directories

Path Synopsis
cmd
kukel command
Command kukel compiles and runs Kukel programs.
Command kukel compiles and runs Kukel programs.
examples
embed command
Command embed is an example of a Go program that runs Kukel code (see docs/embedding.md): it prices orders, and the discount of each order comes from rules that users write as a Kukel script.
Command embed is an example of a Go program that runs Kukel code (see docs/embedding.md): it prices orders, and the discount of each order comes from rules that users write as a Kukel script.
internal
ast
Package ast declares the syntax tree of Kukel programs (syntax.ebnf, sections 1-3).
Package ast declares the syntax tree of Kukel programs (syntax.ebnf, sections 1-3).
builtins
Package builtins implements Kukel's built-in functions.
Package builtins implements Kukel's built-in functions.
bytecode
Package bytecode defines the instruction set of the Kukel stack machine and the compiled program image.
Package bytecode defines the instruction set of the Kukel stack machine and the compiled program image.
compiler
Package compiler translates syntax trees into bytecode for the stack machine.
Package compiler translates syntax trees into bytecode for the stack machine.
debugger
Package debugger is the core of Kukel's debuggers (TODO.md Q60, Q77): breakpoints, stepping, the stop at an exception nothing catches, and reading and changing the program's values.
Package debugger is the core of Kukel's debuggers (TODO.md Q60, Q77): breakpoints, stepping, the stop at an exception nothing catches, and reading and changing the program's values.
diag
Package diag defines the error type shared by the lexer, parser and compiler: a message at a source position.
Package diag defines the error type shared by the lexer, parser and compiler: a message at a source position.
format
Package format formats Kukel source (kukel fmt): it re-indents lines and normalizes the spaces between tokens.
Package format formats Kukel source (kukel fmt): it re-indents lines and normalizes the spaces between tokens.
lexer
Package lexer turns Kukel source text into tokens (syntax.ebnf, section 4).
Package lexer turns Kukel source text into tokens (syntax.ebnf, section 4).
loader
Package loader reads the source files of a program together with the modules they use ("use name;"), directly or through other modules.
Package loader reads the source files of a program together with the modules they use ("use name;"), directly or through other modules.
modules/dbmod
Package dbmod is the native module "db": SQL databases through Go's database/sql: SQLite (modernc.org/sqlite), PostgreSQL (jackc/pgx) and MySQL or MariaDB (go-sql-driver/mysql), all pure Go, so no C library is needed.
Package dbmod is the native module "db": SQL databases through Go's database/sql: SQLite (modernc.org/sqlite), PostgreSQL (jackc/pgx) and MySQL or MariaDB (go-sql-driver/mysql), all pure Go, so no C library is needed.
modules/eventsmod
Package eventsmod is the native module "events": an event loop that calls Kukel functions when timers expire or other modules post events.
Package eventsmod is the native module "events": an event loop that calls Kukel functions when timers expire or other modules post events.
modules/fsmod
Package fsmod is the native module "fs": directories and files of the local file system (TODO.md Q61).
Package fsmod is the native module "fs": directories and files of the local file system (TODO.md Q61).
modules/jsonmod
Package jsonmod is the native module "json": encode Kukel values as JSON text and decode JSON text into Kukel values.
Package jsonmod is the native module "json": encode Kukel values as JSON text and decode JSON text into Kukel values.
modules/p9mod
Package p9mod is the native module "p9": the 9P file protocol of Plan 9 and Linux (TODO.md Q62), as a client and as a server, in both versions: 9P2000 and 9P2000.L.
Package p9mod is the native module "p9": the 9P file protocol of Plan 9 and Linux (TODO.md Q62), as a client and as a server, in both versions: 9P2000 and 9P2000.L.
modules/remod
Package remod is the native module "re": regular expressions, with Go's RE2 syntax (TODO.md Q59).
Package remod is the native module "re": regular expressions, with Go's RE2 syntax (TODO.md Q59).
modules/stringsmod
Package stringsmod is the native module "strings": a Builder class that builds a long string from many pieces.
Package stringsmod is the native module "strings": a Builder class that builds a long string from many pieces.
modules/termmod
Package termmod is the native module "term": the terminal a key at a time, and its size (TODO.md Q99), for programs that draw the screen themselves with ANSI escape codes, such as examples/vi.kkl.
Package termmod is the native module "term": the terminal a key at a time, and its size (TODO.md Q99), for programs that draw the screen themselves with ANSI escape codes, such as examples/vi.kkl.
modules/timemod
Package timemod is the native module "time": the class time::Time (an instant with a time zone), and sleep.
Package timemod is the native module "time": the class time::Time (an instant with a time zone), and sleep.
modules/tkmod
Package tkmod is the native module "tk": desktop windows with Tk widgets whose event handlers are Kukel functions (TODO.md Q67).
Package tkmod is the native module "tk": desktop windows with Tk widgets whose event handlers are Kukel functions (TODO.md Q67).
modules/utf8mod
Package utf8mod is the native module "utf8": text as characters (code points) instead of bytes (TODO.md Q93).
Package utf8mod is the native module "utf8": text as characters (code points) instead of bytes (TODO.md Q93).
modules/uuidmod
Package uuidmod is the native module "uuid": new UUIDs as strings (TODO.md Q58).
Package uuidmod is the native module "uuid": new UUIDs as strings (TODO.md Q58).
modules/workermod
Package workermod is the native module "worker": workers are Kukel interpreters that run in parallel, each on its own goroutine, and talk to the interpreter that started them only through messages.
Package workermod is the native module "worker": workers are Kukel interpreters that run in parallel, each on its own goroutine, and talk to the interpreter that started them only through messages.
modules/zmqmod
Package zmqmod is the native module "zmq": ZeroMQ sockets, to talk with other programs (in any language) and other machines.
Package zmqmod is the native module "zmq": ZeroMQ sockets, to talk with other programs (in any language) and other machines.
native
Package native lets Go code define Kukel modules: functions, constants and native classes, whose objects carry a Go value and whose member functions are written in Go.
Package native lets Go code define Kukel modules: functions, constants and native classes, whose objects carry a Go value and whose member functions are written in Go.
parser
Package parser builds the syntax tree of a Kukel source file (syntax.ebnf, sections 1-3) by recursive descent.
Package parser builds the syntax tree of a Kukel source file (syntax.ebnf, sections 1-3) by recursive descent.
profile
Package profile turns what a vm.Profiler collected into reports (TODO.md Q78): a text report of the functions and lines that took the time, for kukel profile and the Go API.
Package profile turns what a vm.Profiler collected into reports (TODO.md Q78): a text report of the functions and lines that took the time, for kukel profile and the Go API.
stdlib
Package stdlib holds the standard modules written in Kukel (Q88), built into kukel: "use iter;" finds iter.kkl here, after the modules written in Go and before files on disk.
Package stdlib holds the standard modules written in Kukel (Q88), built into kukel: "use iter;" finds iter.kkl here, after the modules written in Go and before files on disk.
token
Package token defines the lexical tokens of Kukel (syntax.ebnf, section 4).
Package token defines the lexical tokens of Kukel (syntax.ebnf, section 4).
value
Package value defines Kukel's runtime values and the semantics of the operators on them.
Package value defines Kukel's runtime values and the semantics of the operators on them.
vm
Package vm executes compiled Kukel programs on a stack machine.
Package vm executes compiled Kukel programs on a stack machine.

Jump to

Keyboard shortcuts

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