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 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 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.
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.