Skip to content

Your first project

This guide walks through creating, developing, testing, and sharing a Nerv application. It assumes you have installed Nerv. If the syntax is new to you, start with the language guide.

The project

We will build vars, a command-line program that prints an environment variable:

nerv run -- get NERV_GUIDE_NAME

Given an environment variable named NERV_GUIDE_NAME containing lucy, it prints:

NERV_GUIDE_NAME=lucy

The standard library handles arguments and environment access. A small local library will format the output, giving us a dependency to develop and test.

Creating a project

In a directory where you keep projects, run:

nerv new vars
cd vars

The command creates this layout:

vars/
├── .gitignore
├── Nerv.toml
├── Nerv.lock
└── src/
    └── main.nerv

Nerv.toml contains the package name, version, and dependencies. Nerv.lock records resolved local packages. src/main.nerv contains your application and an initial test block. .gitignore excludes generated target/ output.

Nerv does not generate a README, a separate test directory, or a GitHub workflow. You can add those when your project needs them. Commit Nerv.toml and Nerv.lock with your source.

Running the project

Open src/main.nerv. Its generated program is:

fn main() -> i64
    println("Hello from Nerv")
    0

test "arithmetic"
    assert(20 + 22 == 42)

Run it:

nerv run

Nerv builds a native executable and starts it. It prints Hello from Nerv. The final 0 in main becomes the process exit status; it is not printed. Build output goes under target/nerv/debug/.

src/main.nerv is the default application entry file. Projects with a custom entry can set entry under [package] in Nerv.toml.

Adding dependencies

Nerv currently supports local path dependencies. There is no registry or nerv add command. Create a sibling library from inside vars:

nerv new --lib ../pairs

You now have vars and pairs beside one another. Replace pairs/src/lib.nerv with:

pairs/src/lib.nerv
fn format_pair(name: string, value: string) -> string
    nerv.string.concat(nerv.string.concat(name, "="), value)

test "formats a name and value"
    assert(format_pair("hello", "world") == "hello=world")

test "formats an empty value"
    assert(format_pair("EMPTY", "") == "EMPTY=")

The format_pair function joins a name, an equals sign, and a value. The test blocks exercise ordinary and empty values.

Edit vars/Nerv.toml:

vars/Nerv.toml
[package]
name = "vars"
version = "0.1.0"

[dependencies]
pairs = { path = "../pairs" }

The path is relative to the manifest, so ../pairs selects the sibling package. Run this from vars to resolve the change:

nerv check

This updates Nerv.lock. Commit the new lockfile with the manifest change. Afterward, nerv check --locked verifies the existing resolution without updating it. If you edit a manifest again, run without --locked to refresh the lock.

The lock records package versions and manifest checksums. It does not freeze the source of a local library; editing that library recompiles it. Keep related packages in version control and preserve their relative paths when sharing source. Registry versions, version ranges, Git URLs, and automatic dependency updates are not supported.

Using dependencies

Replace vars/src/main.nerv with:

vars/src/main.nerv
import nerv.std
import pairs

fn print_pair(name: string, value: string) -> i64
    println(pairs.format_pair(name, value))
    0

fn missing(name: string) -> i64
    stderr(nerv.string.concat("Variable not found: ", name))
    stderr("\n")
    1

fn main() -> i64
    let args = arguments()
    if nerv.arrays.len(args) != 3
        println("Usage: vars get <name>")
        return 1
    if args[1] != "get"
        println("Usage: vars get <name>")
        return 1
    let name = args[2]
    match environment(name)
        Some(value) => print_pair(name, value)
        None => missing(name)

import nerv.std loads the embedded standard-library helpers. import pairs resolves the library declared in your manifest. pairs.format_pair(...) selects its function explicitly.

arguments() returns an array whose first element is the executable path. We expect two more elements: get and a name. Check the length before indexing. Arguments after the compiler command's -- are passed to the application.

environment(name) returns Some(value) or None. The match prints a value and exits successfully when it exists. A missing variable prints to standard error and returns status 1. An empty variable is still a present value.

Set a value and try it:

$env:NERV_GUIDE_NAME = "lucy"
nerv run -- get NERV_GUIDE_NAME
export NERV_GUIDE_NAME=lucy
nerv run -- get NERV_GUIDE_NAME

Both print NERV_GUIDE_NAME=lucy. Running nerv run without program arguments prints the usage message and exits with status 1.

Testing your code

Nerv has built-in test blocks and assertions. No test-runner dependency is needed. From vars, create a tests directory and add:

vars/tests/pairs_test.nerv
import pairs

test "formats a pair through a dependency"
    assert(pairs.format_pair("hello", "world") == "hello=world")

test "keeps an empty value"
    assert(pairs.format_pair("EMPTY", "") == "EMPTY=")

Run the application's tests:

nerv test --locked

Nerv discovers test blocks in the application's source and under tests/. It runs files in separate processes. A failed assertion stops its file, while later files still run. The reported totals count files rather than individual assertions.

Dependency tests do not run automatically as part of the application's suite. Run the library's own tests explicitly:

nerv test --manifest-path ../pairs/Nerv.toml --locked

Functions are currently public without an export modifier. An internal directory has no special visibility enforcement. Organize helpers by module, and treat the library's documented functions as its supported API.

Before committing, format and check the application:

nerv fmt
nerv fmt --check
nerv check --locked

Sharing your program

Build an optimized executable:

nerv build --release --locked

Run it directly:

.\target\nerv\release\vars.exe get NERV_GUIDE_NAME
./target/nerv/release/vars get NERV_GUIDE_NAME

Share that executable with someone using a compatible system of the same operating system and architecture. They do not need Nerv, LLVM, or the pairs source. This program uses only operating-system facilities; programs that read external files must distribute those files separately.

A native build targets the machine running the compiler. Nerv does not currently offer a cross-compilation target flag. Build and test separately for each platform you want to distribute. Keep the default generic CPU setting for a wider range of machines, and check the platform baselines.

For source distribution, include both vars and pairs, their manifests, and their lockfiles. You can omit target/.

Example files

The complete example is also available as individual files:

Keep the sibling layout, then run nerv check once to generate lockfiles. For additional options, see commands and projects.