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

tono

module
v0.0.15 Latest Latest
Warning

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

Go to latest
Published: Aug 28, 2026 License: Apache-2.0

README

tono

The Tono language compiler: reads .tono files and generates idiomatic SDKs in multiple languages. Polyglot monorepo - OCaml frontend, Rust backend.

Layout

  • frontend/ - OCaml: lexer, parser, typecheck, IR
  • lsp/ - OCaml: language server (reuses the frontend)
  • backend/ - Rust: codegen engine
  • cli/ - Rust: tono binary
  • ir-schema/- serialized IR contract

Build

  • OCaml (frontend/, lsp/): dune build
  • Rust (backend/, cli/): cargo build

Install

curl -fsSL https://tono-lang.github.io/tono/install.sh | sh

Or via Homebrew:

brew install tono-lang/tono/tono

Either way you get three binaries: tono (the CLI), tono-frontend (the parser/typechecker it shells out to), and tono-lsp (the language server editors launch).

Prebuilt archives for macOS (arm64/x86_64) and Linux (x86_64/arm64) are also attached directly to each release.

Update

  • Homebrew: brew upgrade tono
  • Install script: re-run the curl command above
  • Manual: download the latest release archive for your platform

Using it

tono init                # write tono.toml and a build manifest per target
tono check src/api.tono  # parse and typecheck; check ext bindings against their libraries
tono index src/api.tono  # write the symbol index of each ext library, for completion inside #(...)
tono gen                 # generate every enabled target's SDK
tono gen --clean         # the same, clearing output the spec no longer produces

tono gen compiles the project's .tono sources (everything under project.root) and writes each enabled target under its configured out. The IR is an internal artifact, so no separate compile step is needed; pass an IR file, or pipe one in, only when you already have one.

tono check runs the frontend's diagnostics and then, for every ext block, checks each foreign binding against the real library with that target's own toolchain (go build, tsc): arity, the types a parameter is declared to cross as, the return, a handle's storage type, the Go context position. A divergence is reported at the .tono line that declared it, like any other diagnostic, instead of in a generated file. The library is resolved the way the generated SDK resolves it, from that target's out directory (a Go module requiring it, a node_modules holding it), or from --lib-root <lang>=<dir>. A binding that names one of the module's own types (#(Memo<.reading>), a parameter typed by a generated struct) is checked beside the SDK's type declarations, generated in memory for the check and written next to the probe, so nothing has to be generated first (--module <name> names the module the way tono gen would, when the file stem is not it). A library with no type source to read (no module, no .d.ts) leaves its bindings unchecked, and the report says so; Rust bindings are always reported unchecked until rustdoc's JSON output leaves nightly. Only the check needs a target toolchain; tono gen never does.

The editor runs the same check on save. tono-lsp invokes tono check <file> --json --only <ext>=<lang> for each (ext, language) pair whose text changed since its last verdict (the pair's own blocks, the parts every language shares, and its pin in tono.toml), the dirty pairs in parallel, and publishes what the command printed: the FX0001 findings at their bindings, and a note at the ext's path line for what was left unchecked. It is the command's report, never a second checker, so the editor and tono check cannot disagree; --json is that report as one object per line, --only narrows a run to the named pairs. The server finds tono through TONO_BIN, next to itself, or on PATH.

Completion inside a foreign spelling (#(...)) comes from an index of the library's symbols, one JSON file per (ext, language) under .tono/index/ beside the manifest, written by tono index. Each language has its own extractor: Go reads the package's export data through go/types from the consumer's module, TypeScript asks the compiler API for the module's exports (re-exports followed, overloads on one entry; it needs the typescript package with a scripting API beside the library, or the typescript-api alias when the tree has the native compiler), Rust parses the crate's source located through cargo metadata (what a macro produces is not in the source, and the hover of the block says so). The index carries the key it was built from (the package, the version pinned in [ext.<name>], the lockfile's digest), and the editor discards a file whose key no longer matches, so a stale suggestion is never served; tono-lsp runs tono index <file> --json --only <ext>=<lang> itself on the first open and on save for a pair whose index is missing or stale, and reads the file directly on every request. The index is a suggestion, never a verdict: what it offers is judged by tono check like any other spelling, and a library it cannot read offers nothing, with the reason on the hover of the language word that opens the block.

Renaming or deleting a module leaves its old output behind, since generation only writes. --clean sweeps each output directory of generated files the run did not produce, and removes the directories that leaves empty. Only files carrying the Code generated by tono banner are eligible, so a build manifest, a hand-written source file, or anything else you keep alongside the SDK is never touched.

Progress and results go to stderr, so stdout stays clean for the one command that produces data on it (tono fmt, which prints the formatted source).

Organizing generated SDKs

tono init scaffolds a tono.toml with one [target.<lang>] block per language you enable, each with its own out directory. out accepts any path, so how you version and publish what lands there is up to you. Three patterns cover most cases:

  • Single repo, multi-target (the init default). Each target's out (typescript/, rust/, go/, as init writes them) lives in the same repo as the .tono spec. No extra infrastructure: each target publishes straight to its own registry (npm publish, cargo publish) from a tag-triggered CI job. Go supports this natively: a module in a subdirectory is published with a tag prefixed by that subdirectory (go/v1.2.3 for the default layout), per the Go modules reference, no separate repo required.
  • One repo per language (opt-in, more mature). Most SDK generators default here instead (a monorepo is treated as the advanced setup): each language gets its own idiomatic repo (org/api-sdk-python, org/api-sdk-go), while the .tono spec and tono.toml stay the single source of truth in one place. Getting there from the single-repo default is a read-only split per package (a git subtree/splitsh-style mirror), which is separate, opt-in tooling init does not set up on its own.
  • Fully separate repo, manual sync. Always available for free, since a target's out can point anywhere, including a sibling clone or a submodule. Maximum flexibility, zero opinion from tono, entirely up to you to keep in sync.

Directories

Path Synopsis
cli
src/index/helpers command
The Go extractor: the exported symbols of one package, read from the compiler's export data through go/types, printed as the index's neutral JSON.
The Go extractor: the exported symbols of one package, read from the compiler's export data through go/types, printed as the index's neutral JSON.
examples
mathkit/probes command
Runs the generated SDK against the stand-in library for real: the client holds the interface value FromConstant returned and reads through it.
Runs the generated SDK against the stand-in library for real: the client holds the interface value FromConstant returned and reads through it.
mathkit/verify command
The Go driver of the FFI bench: constructs the generated client against the real stand-in library (no stub anywhere) and checks the values that come back through both operations.
The Go driver of the FFI bench: constructs the generated client against the real stand-in library (no stub anywhere) and checks the values that come back through both operations.
runtimes
ext-go module
http-go module

Jump to

Keyboard shortcuts

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