tdl

module
v0.2.16 Latest Latest
Warning

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

Go to latest
Published: Oct 5, 2026 License: GPL-3.0

README

tdl

CI Codecov Built with Nix Go Reference Last commit Hercules CI

TDL is a language for describing domain models: what things are, what identifies them, how they relate, and what values they may hold. It compiles a model into equivalent definitions in other formats, such as Go, protobuf, or GraphQL. It has no expressions, control flow, or runtime.

This repository holds the language specification and its reference implementation in Go.

Status

Early, incomplete, and changing.

  • Front end: done. The lexer and parser read the whole grammar.
  • Resolved model: nearly done. tdl ir resolves names, imports, mixins, class satisfaction, constraints, defaults, units, and target directives. Merging a dependency's target blocks is partial.
  • Code generation: Go, GraphQL, protobuf, Salesforce, Smithy, Thrift, and TypeScript. tdl gen also runs any tdl-gen-<name> plugin on PATH.
  • Editors: a language server, a tree-sitter grammar for Neovim, and a VS Code extension.

Documents

Document Covers
spec.md The language. Canonical.
grammar.ebnf The formal grammar.
design/workflow.md What a model author does with all of it.
design/identity.md Identity and the Entity class.
design/ir.md The resolved model backends consume.
design/plugins.md The backend plugin protocol.
design/go-backend.md The Go backend.
design/schema-backends.md The protobuf, Thrift, Smithy, GraphQL, and TypeScript backends.
design/salesforce-backend.md The Salesforce backend.
design/lsp.md The language server.
design/treesitter.md Deriving the tree-sitter grammar from the EBNF.
design/editors.md Highlighting in Neovim, VS Code, Zed, and on GitHub.
backlog.md Wanted, unscheduled work.

A *-plan.md beside a design tracks its implementation.

Example

package shop

type Order: Entity {
  id: OrderId
  customer: Customer
  shipping: Address?
  items: [LineItem] owned where { length(1..) }
  status: Status = Draft
  total: Money
}

type Customer: Entity {
  email: Email
  name: string?
}

type Address {
  line1: string
  line2: string?
  city: string
  postcode: string
  country: string
}

type Money {
  amount: decimal
  currency: Currency
}

type OrderId: uuid

type Email: string where {
  matches(/^[^@]+@[^@]+$/)
  length(3..254)
}

enum Currency { USD EUR GBP }

enum Status { Draft Placed Shipped Cancelled }

Order conforms to Entity, so it has identity that survives its contents changing; Address does not. What a code generator needs goes in a separate target block, never in the model.

Install

Run it without installing:

nix run github:UnstoppableMango/tdl

Or install it with nix profile install github:UnstoppableMango/tdl.

NixOS or home-manager

overlays.default adds pkgs.tdl and pkgs.vscode-tdl, and includes the gomod2nix overlay it builds with.

{
  inputs.tdl.url = "github:UnstoppableMango/tdl";

  # ... in a NixOS or home-manager configuration:
  nixpkgs.overlays = [ inputs.tdl.overlays.default ];
  environment.systemPackages = [ pkgs.tdl ];
}

The home-manager module (homeModules.default, also homeManagerModules.default) adds programs.tdl.enable for the CLI and programs.tdl.vscode.enable for the VS Code extension.

{
  imports = [ inputs.tdl.homeModules.default ];

  nixpkgs.overlays = [ inputs.tdl.overlays.default ];
  programs.tdl.enable = true;
}
In a project

flakeModules.default is a flake-parts module for a project that contains .tdl files. It adds devShells.tdl (pull it into your shell with inputsFrom) and checks that each model parses, is canonically formatted, and, for gen.files, that generated output on disk is current. files are strings relative to src, so include paths keep resolving.

{
  imports = [ inputs.tdl.flakeModules.default ];

  perSystem = { system, ... }: {
    _module.args.pkgs = import inputs.nixpkgs {
      inherit system;
      overlays = [ inputs.tdl.overlays.default ];
    };

    tdl = {
      enable = true;
      src = ./model;
      files = [ "orders.tdl" "billing.tdl" ];
      gen.files = [ "orders.tdl" ];
    };
  };
}

Set tdl.fmt.enable = false to skip the formatting check.

Usage

tdl check ./types.tdl    # parse and report syntax errors
tdl fmt ./types.tdl      # print canonical formatting; -w writes in place
                         # --check lists what is not canonical and exits non-zero
tdl ast ./types.tdl      # print the parse tree
tdl gen ./types.tdl      # run every target block; --target narrows, -o overrides
                         # --verify checks, --clean empties first, --watch reruns
tdl ir ./types.tdl       # print the resolved model; --format json for the plugin view
                         # --prelude lowers against a replacement prelude
tdl tokens ./types.tdl   # print the token stream
tdl version              # tool and spec versions

Commands accept several files and report every failing file, not only the first. With more than one file, output is separated by ==> path <== banners. A file named - is standard input, so tdl fmt - formats an unsaved editor buffer. fmt -w and gen reject -, since there is nothing to write back to and no directory to resolve imports from.

Playground

tdl play watches a file and re-renders it on every save.

tdl play                              # scratch.tdl, created from a template if missing
tdl play ./types.tdl --views all      # source, fmt, ast, tokens, stats
tdl play ./types.tdl --once           # render and exit

Views are source, fmt, ast, tokens, stats, or all; the default is fmt,ast. Parse errors show below the panes with a caret at the column.

examples/ has files to start from.

Editor support

Highlighting comes from two grammars generated from grammar.ebnf; design/editors.md explains why there are two.

Neovim

The parser and queries are in tree-sitter/. On nvim-treesitter's main branch, register the parser and start highlighting:

vim.filetype.add({ extension = { tdl = 'tdl' } })

vim.api.nvim_create_autocmd('User', {
  pattern = 'TSUpdate',
  callback = function()
    require('nvim-treesitter.parsers').tdl = {
      install_info = {
        url = 'https://github.com/UnstoppableMango/tdl',
        location = 'tree-sitter',
        queries = 'tree-sitter/queries',
      },
    }
  end,
})

vim.api.nvim_create_autocmd('FileType', {
  pattern = 'tdl',
  callback = function() vim.treesitter.start() end,
})

Then run :TSInstall tdl (needs the tree-sitter CLI on PATH), and :TSUpdate tdl to pick up later revisions.

On the master branch, enable highlighting with highlight = { enable = true } in setup, and register the parser through require('nvim-treesitter.parsers').get_parser_configs().tdl with files = { 'src/parser.c', 'src/scanner.c' } in place of queries. That branch installs no queries for a custom parser, so copy tree-sitter/queries/highlights.scm to queries/tdl/highlights.scm on your runtimepath.

VS Code

nix build .#vscode-tdl builds the extension in editors/vscode. Install it with programs.tdl.vscode.enable, by adding pkgs.vscode-tdl to vscode-with-extensions or programs.vscode.profiles.<name>.extensions, or with make vscode-install.

The extension runs tdl lsp for diagnostics, hover, go to definition, formatting, and the outline. The nix build points it at its own tdl; otherwise it runs tdl from PATH, or the tdl.server.path setting. Without a server, highlighting still works.

Support matrix

Front end is the lexer, parser, and tdl fmt; IR is tdl ir, the resolved model a backend consumes.

Construct Front end IR
package, import Yes Yes, resolved across packages without inlining
primitive Yes Yes
alias Yes Yes
type (newtype chains) Yes Yes, constraints accumulate down the chain
type with a body, : Entity Yes Yes, the kind computed from conformance
mixin, include Yes Yes, expanded
enum, variants with fields Yes Yes
class, functional dependencies, associated types Yes Yes
instance, including conditional instances Yes Yes
Type parameters and kinds Yes Yes, parameters stay parameters
Collection and option sugar ([T], {T}, {K -> V}, T?, T | null) Yes Yes, lowered to whatever the prelude declares
where constraints Yes Yes, open set: standard names checked, others passed through
Field defaults Yes Yes, resolved against the field's type
deprecated Yes Yes
unit Yes Yes, reduced to base dimensions
target blocks Yes Partial: a dependency's declaration-level directives are not merged

tdl fmt keeps both comment forms: a /// doc comment attaches to the next declaration, and a // comment stays on its own line or at the end of its line. Inside a body, the formatter decides blank lines.

Backends

Each built-in backend also ships as a tdl-gen-<name> plugin.

Backend Generates
go Structs, entity keys, both enum shapes, newtypes, generics, classes as interfaces, Validate methods, foreign types
graphql Output types, both enum shapes, custom scalars, lists; no maps
protobuf Messages, both enum shapes, newtypes, collections, number pins, services
salesforce Salesforce DX source: a custom object per entity, Apex for values and enums
smithy Structures, both enum shapes, named collection shapes
thrift Structs, both enum shapes, newtypes as typedefs, collections, number pins
typescript JSON wire types: interfaces, both enum shapes, newtypes as aliases
debug A description of the model it was given
Anything else tdl-gen-<name> on PATH, over the plugin protocol

Development

command make build   # nix build .#
command make test    # go test ./...
command make lint    # nix flake check + golangci-lint + buf + markdownlint
command make fmt     # nix fmt + buf format

Without Nix, go build ./... and go test ./... work directly. AGENTS.md has the full command list and architecture.

Releases come from release-please; never edit a version or CHANGELOG.md by hand.

Design principles

  • The core is small. Most of what looks like a type system is TDL code in a replaceable prelude.
  • Identity is first class, and the model is pure: what a backend needs lives in a target block.
  • Constraints are syntax. The compiler parses and resolves them; backends decide what they mean.
  • The grammar is small and strict, with a hand-written lexer and parser.
  • The spec and the plain-text testdata/conformance and testdata/invalid corpora are the contract another implementation would satisfy.

Directories

Path Synopsis
Package ast defines the TDL parse tree, which mirrors source text 1:1 with names left unresolved.
Package ast defines the TDL parse tree, which mirrors source text 1:1 with names left unresolved.
backend
debug
Package debug is a backend that describes the model it was given.
Package debug is a backend that describes the model it was given.
golang
Package golang generates Go source from a resolved model.
Package golang generates Go source from a resolved model.
graphql
Package graphql generates a GraphQL schema of output types from a resolved model.
Package graphql generates a GraphQL schema of output types from a resolved model.
internal/emit
Package emit holds what every code generator in backend/ shares: which declarations are the model's own, reading directives for one target, reporting what cannot be generated, and resolving a type reference to the prelude's shapes.
Package emit holds what every code generator in backend/ shares: which declarations are the model's own, reading directives for one target, reporting what cannot be generated, and resolving a type reference to the prelude's shapes.
internal/irtest
Package irtest builds ir.Model values by hand for backend tests, so a backend test failure is the backend's and not lowering's.
Package irtest builds ir.Model values by hand for backend tests, so a backend test failure is the backend's and not lowering's.
protobuf
Package protobuf generates a proto3 schema, or one under the edition an `edition` directive names, from a resolved model.
Package protobuf generates a proto3 schema, or one under the edition an `edition` directive names, from a resolved model.
salesforce
Package salesforce generates Salesforce DX source, one file per component, from a resolved model: a custom object for each entity, and an Apex class or enum for each value, mixin, and enum.
Package salesforce generates Salesforce DX source, one file per component, from a resolved model: a custom object for each entity, and an Apex class or enum for each value, mixin, and enum.
smithy
Package smithy generates a Smithy IDL 2.0 model from a resolved model.
Package smithy generates a Smithy IDL 2.0 model from a resolved model.
thrift
Package thrift generates a Thrift IDL file from a resolved model.
Package thrift generates a Thrift IDL file from a resolved model.
typescript
Package typescript generates TypeScript declarations of JSON wire types from a resolved model: interfaces and type aliases, with no runtime code.
Package typescript generates TypeScript declarations of JSON wire types from a resolved model: interfaces and type aliases, with no runtime code.
cli module
cmd
tdl command
Command tdl parses and formats Type Description Language source files.
Command tdl parses and formats Type Description Language source files.
tdl-gen-debug command
Command tdl-gen-debug is the debug backend as a plugin.
Command tdl-gen-debug is the debug backend as a plugin.
tdl-gen-go command
Command tdl-gen-go is the Go backend as a plugin.
Command tdl-gen-go is the Go backend as a plugin.
tdl-gen-graphql command
Command tdl-gen-graphql is the GraphQL backend as a plugin.
Command tdl-gen-graphql is the GraphQL backend as a plugin.
tdl-gen-protobuf command
Command tdl-gen-protobuf is the protobuf backend as a plugin.
Command tdl-gen-protobuf is the protobuf backend as a plugin.
tdl-gen-salesforce command
Command tdl-gen-salesforce is the Salesforce backend as a plugin.
Command tdl-gen-salesforce is the Salesforce backend as a plugin.
tdl-gen-smithy command
Command tdl-gen-smithy is the Smithy backend as a plugin.
Command tdl-gen-smithy is the Smithy backend as a plugin.
tdl-gen-thrift command
Command tdl-gen-thrift is the Thrift backend as a plugin.
Command tdl-gen-thrift is the Thrift backend as a plugin.
tdl-gen-typescript command
Command tdl-gen-typescript is the TypeScript backend as a plugin.
Command tdl-gen-typescript is the TypeScript backend as a plugin.
e2e module
gen module
internal
cli
Package cli wires up the tdl command-line interface.
Package cli wires up the tdl command-line interface.
ebnf
Package ebnf lints the grammar files under docs/, which are Wirth syntax notation as read by golang.org/x/exp/ebnf.
Package ebnf lints the grammar files under docs/, which are Wirth syntax notation as read by golang.org/x/exp/ebnf.
gen
Package gen is the compiler side of the plugin protocol: which backends exist, how a request is built, and what happens to the files that come back.
Package gen is the compiler side of the plugin protocol: which backends exist, how a request is built, and what happens to the files that come back.
lsp
Package lsp serves the Language Server Protocol for TDL: diagnostics, go to definition, hover, document symbols, and formatting.
Package lsp serves the Language Server Protocol for TDL: diagnostics, go to definition, hover, document symbols, and formatting.
sema
Package sema lowers a parse tree to the resolved semantic model in the ir package: it resolves names, lowers sugar to prelude types, and interns type references.
Package sema lowers a parse tree to the resolved semantic model in the ir package: it resolves names, lowers sugar to prelude types, and interns type references.
textmate
Package textmate turns the annotated grammar into a TextMate grammar for VS Code.
Package textmate turns the annotated grammar into a TextMate grammar for VS Code.
treesitter
Package treesitter turns the grammar internal/ebnf read into a tree-sitter grammar.js.
Package treesitter turns the grammar internal/ebnf read into a tree-sitter grammar.js.
Package ir is the resolved semantic model backends consume.
Package ir is the resolved semantic model backends consume.
Package lex implements the TDL lexer, turning source text into a stream of tokens for the parser.
Package lex implements the TDL lexer, turning source text into a stream of tokens for the parser.
Package parser is a recursive-descent parser turning TDL source into an ast.File.
Package parser is a recursive-descent parser turning TDL source into an ast.File.
pkg module
Package plugin is the wire protocol a TDL backend speaks, and the SDK for writing one.
Package plugin is the wire protocol a TDL backend speaks, and the SDK for writing one.
Package prelude embeds the standard prelude, the TDL source declaring every type, including `List` and `Option`, that sugar such as `[T]` and `T?` names.
Package prelude embeds the standard prelude, the TDL source declaring every type, including `List` and `Option`, that sugar such as `[T]` and `T?` names.
tools
textmate command
Command textmate derives the VS Code TextMate grammar from docs/grammar.ebnf.
Command textmate derives the VS Code TextMate grammar from docs/grammar.ebnf.
treesitter command
Command treesitter derives tree-sitter/grammar.js from docs/grammar.ebnf.
Command treesitter derives tree-sitter/grammar.js from docs/grammar.ebnf.

Jump to

Keyboard shortcuts

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