Custom dialects

Experimental. The dialect API, .synq format, codegen output, and shared-library ABI are not yet stable and can change between releases. Pin your syntaqlite version when you ship a dialect.

A syntaqlite dialect can add custom statements, functions, or clauses to SQLite. This guide follows the process from source files to a loaded shared library, using the in-tree Perfetto dialect as an example.

1. Write the grammar

A dialect is a directory of .synq node definitions plus optional .y grammar action files. .synq is syntaqlite's grammar DSL: each node declares an AST shape with typed fields, optional semantic annotations, and a fmt block for pretty-printing.

Minimal example (from dialects/perfetto/nodes/perfetto.synq):

node CreatePerfettoMacroStmt {
  name: index SqliteIdent
  args: index PerfettoMacroArgList
  returns: inline PerfettoMacroReturns
  body: index Expr

  fmt {
    group {
      "CREATE PERFETTO MACRO" line
      child(name)
      "(" nest { softline child(args) } softline ")"
      line "RETURNS" " " child(returns)
      line "AS" nest { line child(body) }
    }
  }
}

.y action files in a parallel actions/ directory hook the dialect's rules into the base SQLite grammar; simple dialects that only add functions or reserved words may not need any.

2. Generate C sources

syntaqlite dialect generate reads your .synq files and any optional .y files, then emits a single .h/.c pair containing the runtime and your dialect:

syntaqlite dialect generate \
  --name perfetto \
  --nodes-dir dialects/perfetto/nodes \
  --actions-dir dialects/perfetto/actions \
  --output-dir out \
  --output-type full

Produces:

out/syntaqlite_perfetto.h
out/syntaqlite_perfetto.c

The --name flag drives all generated symbol names: the dialect exposes syntaqlite_perfetto_dialect_template(), which is what the loader looks for at runtime.

Other --output-type values split the artifacts differently (raw for flat C files per module, runtime-only to skip the dialect body); full is the right default for shipping a self-contained shared library.

3. Compile to a shared library

The generated .c is pure C, no Rust toolchain required. Build it as a shared library for the target platform:

# Linux / macOS
cc -O2 -shared -fPIC -o libperfetto.dylib out/syntaqlite_perfetto.c

On Linux the extension is .so; on Windows, build a .dll with your C compiler's equivalent of -shared.

Confirm the loader symbol is exported:

nm libperfetto.dylib | grep dialect_template
# T _syntaqlite_perfetto_dialect_template

4. Use the dialect at runtime

Pass the library and dialect name to any syntaqlite subcommand:

syntaqlite fmt \
  --dialect ./libperfetto.dylib \
  --dialect-name perfetto \
  -e "create perfetto macro m() returns int as 42"
CREATE PERFETTO MACRO m()
RETURNS int
AS 42;

The --dialect-name matches the --name you passed to dialect generate. Omit it to resolve the default syntaqlite_dialect_template symbol, which is what an unnamed dialect exports.

To bake the choice into a project, set the dialect in syntaqlite.toml; see the config reference.

Alternative: compile the dialect into your binary

Dynamic loading is the fastest path to "works with the stock CLI", but it adds a runtime dependency and a symbol-resolution step. If you're shipping your own binary (for example, a syntaqlite-mydialect wrapper), compile the dialect in directly via the Rust CliApp trait. See syntaqlite-cli/examples/cli_wrapper.rs for a complete example.

Trade-offs:

Dynamic (--dialect)Static (CliApp)
DistributionOne shared library, stock CLISingle binary, your own name
Startup costdlopen on every invocationNone
ABI stabilityLibrary and CLI versions must matchNo separate ABI boundary
ToolingWorks with any syntaqlite installRequires the Rust toolchain to build

Next steps