T Package Development Guide

This guide walks you through creating, developing, and publishing a package for the T language.

1. Creating a Package

You can create a new package interactively using the t init --package command.

$ t init --package advanced-stats
Initializing new T package...
Author [User]: Alice
License [EUPL-1.2]: EUPL-1.2

 Package 'advanced-stats' created successfully!

1.1 AI Agent Onboarding (Optional)

When running t init, you will be prompted to select an AI Agent Context Level. This generates two essential files in your package root:

These files are designed to be read by LLMs (like Antigravity, Claude, or ChatGPT) at the start of a session to give them immediate, high-fidelity context about your package and the specific version of T you are using.

This creates a standard directory structure:

2. Managing Dependencies

Dependencies are declared in DESCRIPTION.toml. To add a dependency on another T package (e.g., math or a git repository):

[dependencies]
# Example: depend on a git repository
my-lib = { git = "https://github.com/user/my-lib", tag = "v1.0.0" }

2.1 System Tools and LaTeX

You can also declare system-level tools and LaTeX packages required for your package development or documentation.

Additional Development Tools

Under [additional-tools], you can add any package from Nixpkgs. These tools will be available in your nix develop shell:

[additional-tools]
# Tools for building, documenting, or testing your package
packages = ["git", "jq", "gawk", "pandoc"]

LaTeX for Documentation

If your documentation requires LaTeX (e.g., for formulas), use the [latex] section. T provides texlive based on scheme-small. You only need to list additional packages:

[latex]
# LaTeX packages for math formulas or advanced formatting
packages = ["amsmath", "blindtext", "physics"]

After modifying dependencies or updating the [additional-tools] or [latex] sections, run t update to sync your flake.nix and lock file:

$ t update
Syncing 1 dependency(ies) from DESCRIPTION.toml → flake.nix...
Running nix flake update...

This regenerates flake.nix so new dependencies appear as proper flake inputs, then locks them. After updating, re-enter the development shell:

$ nix develop

3. Writing Code

Write your T code in src/. For example, src/stats_helpers.t:

-- src/stats_helpers.t

-- Public by default — importers can use this
weighted_mean = \(x, w) sum(x .* w) / sum(w)

--# Internal helper, not for public use.
--# @private
_validate_weights = \(w) {
  assert(length(w) > 0)
}

All top-level bindings in your package are public by default. To hide an internal helper, add @private to its T-Doc block.

You can test your code interactively in the REPL:

$ t repl
T> import "src/stats_helpers.t"
T> weighted_mean([1, 2, 3], [0.5, 0.3, 0.2])
1.7

4. Testing

T has a built-in test runner. Tests are .t files in the tests/ directory.

Example tests/test-mean.t:

import "src/stats.t"

-- Using specialized expect_* functions provides rich diagnostic diffs when tests fail:
assert(expect_equal(stats.mean([1, 2, 3]), 2.0))
assert(expect_equal(stats.mean([-1, -1]), -1.0))

Run all tests with:

$ t test

# Structured JSON output for agents and automation:
$ t test --json

# JUnit XML output for CI/CD:
$ t test --format junit

# Run only specific tests:
$ t test --only "stats"      # run tests matching "stats"
$ t test --not "slow"        # skip tests matching "slow"

# Stop on first failure:
$ t test --failfast

# List discovered tests without running:
$ t test --list

# Mark tests exceeding 30s as failed:
$ t test --timeout 30

# Generate coverage summary (requires instrumented build):
$ t test --coverage

Create tests/.tignore to automatically exclude test files (one pattern per line):

# tests/.tignore
slow_integration.t
*_benchmark.t
legacy/

Test Fixtures

Share expensive setup (like loading large datasets) across tests using chain():

fixture = pipeline {
  data = node(command = read_csv("data/large.csv"), serializer = ^csv)
}
test_a = pipeline { ... }
test_b = pipeline { ... }
build_pipeline(chain(fixture, parallel(test_a, test_b)))

Each node runs in an isolated Nix sandbox. The fixture’s output is available to downstream test nodes via the dependency DAG — no redundant read_csv() wrapper needed. See the API reference for a full example.

In the REPL, t_test() returns a DataFrame with test results:

results = t_test()
results |> filter($status == "failed")

-- Filter from the REPL
results = t_test(only = ["arithmetic"])
results = t_test(not = ["slow"])

Why Use expect_* Functions with assert()?

While a plain boolean check like assert(colnames(df) == ["a", "b", "c"]) works, it only evaluates to true or false. When it fails, assert produces a generic error (AssertionError: expression evaluated to false), providing no detail on what differed.

By contrast, expect_* functions (such as expect_equal, expect_colnames, expect_nrow, expect_type, etc.) perform deep structural comparisons and provide rich diagnostic diffs:

-- Plain assert: fails with unhelpful generic "expression evaluated to false"
assert(colnames(df) == ["a", "b", "c"])

-- Recommended: produces exact structural diff on failure (e.g. expected "b" at index 2, got "x")
assert(expect_colnames(df, ["a", "b", "c"]))
assert(expect_equal(colnames(df), ["a", "b", "c"]))

expect_* functions also return first-class Expect values (Expect_pass, Expect_stop msg, Expect_hold msg) that allow soft failure, NA handling, or programmatic inspection before passing to assert().

Skipping Pipeline Tests Conditionally

Since pipeline nodes are compiled and executed via Nix, tests that build or run pipelines might fail or block in sandboxed environments or systems lacking Nix. You can conditionally skip the execution of specific pipeline nodes using the noop parameter with any T expression.

Because noop accepts T expressions (evaluated at runtime), you can pass conditions referencing environment variables:

import my_package

p = pipeline {
  heavy_node = node(
    command = <{ run_heavy_nix_job() }>,
    # Skip execution if not running in CI
    noop = (get(env("CI"), "") == "")
  )
}

res = build_pipeline(p)

# Because errors are first-class values in T, skipped nodes propagate VError values cleanly.
# You can check if the node was skipped using expect_error or custom checks:
assert(expect_error(read_node(res.heavy_node), class = "TypeError", message = "was skipped"))

5. Documentation

T packages use T-Doc, a comment-based documentation system. Documentation lives in source files close to the code and is generated into Markdown.

Writing Documentation

Use --# comments above your functions to document them.

--# Calculate the square of a number.
--#
--# @param x :: Integer
--#   The input number.
--#
--# @return :: Integer
--#   The squared result.
--#
--# @example
--#   square(4)
--#   -- 16
--#
--# @export
fn square(x) {
  x * x
}

Supported Tags: - @param <name> :: <type> <description>: Document a parameter. - @return :: <type> <description>: Document the return value. - @example: Start a code example block. - @seealso <func1>, <func2>: Link to related functions. - @family <name>: Group related functions together. - @private: Mark the function as private — it will not be visible to importers. - @export: Explicitly mark as public (this is the default, so usually not needed).

Generating Documentation

To generate the documentation files in docs/reference/:

$ t doc --parse --generate

This will: 1. Scan your src/ directory for --# blocks. 2. Generate Markdown files for each function in docs/reference/. 3. Generate a docs/reference/index.md listing all exported functions.

Viewing Documentation

You can view your documentation locally using:

$ t docs

This opens docs/index.md (or README.md) in your system viewer. You can link to your reference documentation from there.

6. Quality Control

Before publishing, run t doctor to check your package for common issues:

$ t doctor
 Everything looks good!

It checks for:

7. Publishing

When you are ready to release a version:

  1. Ensure DESCRIPTION.toml has the correct version.
  2. Update CHANGELOG.md with release notes for that version.
  3. Commit all changes to git.
  4. Run t publish.
$ t publish
Preparing to publish version 0.1.0...

 Validation complete.
Proceed to tag and push v0.1.0? [y/N] y
 Tag v0.1.0 pushed to remote.

This will run your tests, verify the changelog, and push a git tag to your repository.

8. How Others Import Your Package

Once published, other packages or projects can depend on yours by adding it to their DESCRIPTION.toml or tproject.toml:

[dependencies]
advanced-stats = { git = "https://github.com/user/advanced-stats", tag = "v0.1.0" }

Then in their T code, they can import your package:

-- Import everything (all public functions)
import advanced_stats

-- Import only specific functions
import advanced_stats[weighted_mean]

-- Import with aliases
import advanced_stats[wmean=weighted_mean]

Functions marked with @private in your package are not visible to importers.


Next Steps

Now that you know how to build packages, explore how to ensure your work is reproducible and understand T’s underlying architecture:

  1. Reproducibility Guide — Deep dive into T’s commitment to reproducible research.
  2. Architecture — Understand the internal design and execution model of T.
  3. Project Development — Master T’s project structure and dependency management.
  4. API Reference — Complete function reference by package.