Skip to main content

Installation & Usage

Installation​

Install CLI​

Download the executable for your platform from GitHub Releases, or install using the following method:

# macOS / Linux
# 1. Download the binary
# 2. Add to PATH
sudo mv nolang /usr/local/bin/no

Install VS Code Extension​

Install the Nolang extension from the VS Code Marketplace for syntax highlighting, LSP diagnostics, go-to-definition, auto-completion, and more.

CLI Commands​

CommandDescription
no versionPrint version info
no initDefine the workspace (creates workspace.jsonc, no package.jsonc)
no new <name>Create a new package under the workspace (subdir + package.jsonc, registered in workspace.jsonc)
no fmt [-w] [-d] <file|dir>Format source code
no build [-o <file>] [-cc <s>] [-target <s>] [<file|dir>]Build (output executable)
no run [-cc <s>] [-target <s>] [<package|dir|file>]Build and run (package/dir/file)
no test [-cc <s>] [-target <s>] [<file>]Run tests
no add <pkg>Add dependency
no remove <pkg>Remove dependency
no update <pkg>Update dependency
no update-allUpdate all dependencies
no listList dependencies
no syncSync dependencies
no install [-u] [<pkg>@<version>]Install binary
no uninstall <name>Remove binary
no pub --token <token> [--registry <url>]Publish to registry

Quick Start​

Nolang uses a single-repo (project), multi-package layout: the repo root is the workspace (it only has workspace.jsonc), and each package is a subdirectory with its own package.jsonc. no init and no new are two separate steps:

  • no init —— defines the workspace in the current directory (creates workspace.jsonc only).
  • no new <name> —— creates a new package under the current workspace (generates the subdirectory ./<name>/ with its package.jsonc) and registers it in workspace.jsonc.

Initialize the Workspace and Create a Package​

# 1) Define the workspace at the repo root
no init

# 2) Create a package inside the workspace (auto-registered in workspace.jsonc)
no new foo

# 3) Enter the package directory
cd foo

# Run directly (auto-build and execute main.no)
no run

Add more packages with repeated no new:

no new bar
no new baz

workspace.jsonc then looks like:

{
"foo": "/foo",
"bar": "/bar",
"baz": "/baz"
}

Path format: Mapping values in workspace.jsonc use / prefix for workspace-relative paths (e.g. /foo → workspaceRoot/foo). ./, ../ relative jumps and OS absolute paths (e.g. /home/user/code/fork) are also supported for pointing to directories outside the workspace.

no build run from the root with no arguments builds all packages in the workspace in parallel.

Just Initialize the Workspace (no package yet)​

# Define the workspace in the current directory
no init

no init only creates workspace.jsonc (initially an empty {}); it does not generate a package.jsonc or main.no. If workspace.jsonc already exists, it is left untouched. Packages are created with no new <name>.

Build & Run​

# Build (looks for main.no by default)
no build # Build current directory
no build main.no # Build specific file
no build -o output # Specify output path
no build -cc zig # Use Zig compiler
no build -target x86_64-linux-gnu # Cross-compile (specify target platform)

# Run (build + execute)
no run # Build and execute main.no in the current directory
no run foo # Run package 'foo' from the workspace (resolves workspace.jsonc)
no run ./foo # Run the ./foo directory's main.no
no run main.no # Run a specific .no file
no run -cc zig
no run -target aarch64-macos-gnu

The no run argument is resolved in this order:

  1. an existing .no file -> run that file directly
  2. an existing directory -> run its main.no
  3. a package name registered in the nearest workspace.jsonc -> run that package's main.no

With no argument, it runs main.no in the current directory. If the current directory is a workspace root (has workspace.jsonc but no main.no), it prompts you to pick a package with no run <package>.

Cross-Compilation Targets​

The -target parameter format is <arch>-<os>-<abi>, supporting the following targets:

Target tripleDescription
x86_64-linux-gnuLinux x86_64
aarch64-linux-gnuLinux ARM64
x86_64-macos-gnumacOS x86_64
aarch64-macos-gnumacOS ARM64
x86_64-windows-gnuWindows x86_64

Automatic platform detection: no build, no run, and no test automatically detect the current host platform and compile for the native target when -target is not specified. No manual target specification needed for daily development:

no run hello.no # Run directly on host
no test # Run tests on host
no build -target aarch64-linux-gnu # Only specify when cross-compiling

Compiler Selection​

The -cc parameter specifies the C compiler backend:

  • clang (default) — requires LLVM installed
  • zig — requires Zig installed, suitable for cross-compilation

JavaScript Backend​

Nolang supports compiling .no source directly to JavaScript, with no LLVM toolchain required. The JS backend uses a type erasure strategy: all Nolang type annotations (int/str/bool/vec[T]/[N]T/?T etc.) are not preserved in the JS output; only the runtime behavior is generated.

Building to JavaScript​

# Compile to JS (outputs to dist/<name>.js)
no build --js main.no

# Specify the output path
no build --js -o app.js main.no

# Browser mode: generate JS + HTML wrapper
no build --js --browser main.no
# Outputs: dist/<name>.js and dist/<name>.html

Running JavaScript​

# Compile to JS and run with node
no run --js main.no

# Compile to browser JS + HTML and open in the default browser
no run --js --browser main.no

JavaScript Backend Features​

FeatureDescription
No LLVM requiredEmits JS source directly from the AST, with no dependency on the clang/LLVM toolchain
Type erasureJS is dynamically typed; Nolang type annotations are not preserved
Node.js modeDefault target; Node APIs such as require('fs') are available
Browser mode--browser generates an HTML wrapper; print() output is redirected to the #nolang-output div
Platform annotations#{js} marks JS-backend-only declarations; #{js-browser} marks browser-only code

JavaScript Backend Standard Library​

The src/js/ directory provides JS-backend-only modules, all carrying the #{js} platform annotation and effective only when compiling for the JS backend:

ModuleDescription
js/domDOM manipulation (create-element, query-selector, set-text, set-style, etc.)
js/canvasCanvas 2D drawing (fill-rect, stroke, begin-path, etc.)
js/eventsEvent handling (on-click, on-load)
js/storagelocalStorage (set-item, get-item)
js/fetchFetch API (async fetch of URL data)
js/console-logconsole.log wrapper
js/fs-read-fileNode.js fs.readFileSync wrapper
js/fs-write-fileNode.js fs.writeFileSync wrapper
js/http-fetchfetch API wrapper (Node 18+ / browser)
js/process-exitprocess.exit wrapper
js/locationLocation API (href, search, redirect)
js/historyHistory API (back, forward, push)
js/animationAnimation frames (request-frame, cancel-frame)

Browser Application Example​

; main.no — browser application
# js/dom
# js/canvas
# js/events
# js/storage

; DOM: create an element and append it to body
heading = dom.create-element('h2')
heading.set-text('Hello from Nolang!')
body = dom.body()
body.append-child(heading)

; events: button click
btn = dom.create-element('button')
btn.set-text('Click me')
body.append-child(btn)
events.on-click(btn, () {
print('button was clicked!')
})

; localStorage: save and read
storage.set-item('greeting', 'Hello from localStorage')
g = storage.get-item('greeting')
print('stored:', g)

Build and open:

no build --js --browser main.no
# Outputs: dist/main.js and dist/main.html
# Open dist/main.html in the browser

Builtin Function Mapping​

The JS backend maps Nolang builtin functions to their JavaScript equivalents:

NolangJavaScript
print(x)console.log(x)
eprint(x)console.error(x)
format(...)string concatenation
len(x)x.length
with-len(n)new Array(n)

Platform Annotations​

Use the #{js} and #{js-browser} annotations to control the platform visibility of code:

; kept only when compiling for the JS backend
#{js}
js-helper = () {
print('JS only code')
}

; kept only in browser mode
#{js-browser}
print('running in browser mode')

; kept only on the native backend
#{mac-arm64}
print('running on macOS ARM64')

Entry Point Rules​

  • main.no — Program entry point
  • lib.no — Library entry point, exports functions (see Export documentation)
  • All .no files under tests/ directory — Contains test assertions

Testing​

# Test all .no files in the tests/ directory
no test

# Run a single test file
no test my-test.no

# Use a specific compiler or target
no test -cc zig
no test -target x86_64-windows-gnu

Test notes:

  • Test files are placed in the tests/ directory
  • Each test file is built independently
  • If any test fails, a non-zero exit code is returned

Install & Uninstall Binaries​

Install​

# Install the package in the current directory
no install

# Force rebuild (update)
no install -u

# Install a specific version from a remote registry
no install pkg-name@1.0

# Update an installed package
no install -u pkg-name@1.0

Installation process:

  1. Download package source (remote packages) or use current directory (local packages)
  2. Auto-execute build
  3. Copy binary to ~/no/bin/
  4. Create a symlink in /usr/local/bin/

Uninstall​

no uninstall pkg-name

Uninstalling removes the symlink from /usr/local/bin/ and the binary from ~/no/bin/.

Project Configuration​

The package.jsonc file in the project root directory describes project information:

{
"name": "my-project",
"version": "v0.1.0",
"description": "A new Nolang project",
"keywords": [],
"author": "",
"email": "",
"organization": "",
"repository": "",
"homepage": "",
"license": "MIT",
"workspace": "",
"mirrors": [],
"dependencies": {
"fmt": "*",
},
"compiler": {
"version": "v0.1.0",
},
"output": "./dist",
"ignore": [],
}

Dependency Management​

# Add dependency (version number optional, not written in repo)
no add pkg-name

# Remove dependency
no remove pkg-name

# Update dependency
no update pkg-name

# Update all dependencies
no update-all

# List dependencies
no list

# Sync dependencies (download and generate lock file)
no sync

Dependency Types & Version Rules​

Dependencies in package.jsonc are classified as local packages or remote packages. The compiler automatically determines the type using the following rules and emits a warning when a local package does not use "*" as its version.

Classification Rules​

Dependency key → lookup in workspace.jsonc (short name or full key match)
├─ Found → local package (should use "*")
└─ Not found → check path prefix (/ only)
├─ Yes → local package (should use "*")
└─ No → remote package (use version number, no warning)
  1. Lookup workspace.jsonc: First, search for the dependency key as a short name or full key in workspace.jsonc. If found, it is a local package.
  2. Check path prefix: If not found in workspace.jsonc, and the key starts with / (workspace-relative path), it is also a local package.
  3. Otherwise → remote package: Keys not matching the above conditions are treated as remote packages and should specify a version number.

Workspace boundary constraint: Local paths in package.jsonc must start with / (workspace-relative). ./, ../, and OS absolute paths are forbidden. The only escape hatch for referencing external directories is through workspace.jsonc / .workspace.jsonc mappings. See Workspace Boundary Constraints.

Local Package Reference Forms​

Local packages support the following three reference forms, all of which should use "*" as the version:

"dependencies": {
// 1. Short name: a key registered in workspace.jsonc
"test2": "*",

// 2. Workspace-relative path: starts with /, relative to workspace root
"/example/test2": "*",

// 3. Full URL: local if workspace.jsonc has a matching mapping
"github.com/lizongying/nolang/test2": "*",
}

Note:

  • Whether form 3 is a local or remote package depends on workspace.jsonc. If workspace.jsonc maps github.com/lizongying/nolang/test2 to a local path, the dependency is local (should use "*"); otherwise it is remote (should use a version number).
  • ./ and ../ prefixes are no longer allowed in package.jsonc. All local paths must start with / (workspace-relative).

Advanced: Redirecting a Remote Package to Local​

workspace.jsonc can map a remote package name to a local path, enabling a "remote-to-local" development workflow:

// workspace.jsonc
{
"test2": "/example/test2",
"github.com/lizongying/nolang/test2": "/example/test2"
}

With this mapping, a dependency referenced as github.com/lizongying/nolang/test2 in package.jsonc is resolved to the local path /example/test2 and should use "*" as its version. This is extremely convenient for local development of remote packages — simply add a mapping in workspace.jsonc to switch a remote dependency to local source code for joint debugging.

Version Warnings​

During compilation, if a local package is detected using a non-"*" version (e.g. "v0.1.0"), a warning is emitted suggesting "*". Remote packages are not subject to this restriction.

Recursive Workspace Mapping (Cross-Package Mapping Chains)​

Nolang supports recursive workspace mapping: a dependency package can carry its own workspace.jsonc with internal mapping rules, forming a natural cross-package resolution chain. This is a core differentiating capability — mainstream Go/Cargo replace/patch only applies to the current project and does not propagate into dependency packages.

Use cases:

  • Unified internal aliases for base libraries
  • Standardized import paths
  • Multi-layer library compatibility migration

How it works:

When resolving a dependency key, the compiler:

  1. Looks up the key in the current workspace.jsonc
  2. If found, checks whether the target directory has its own workspace.jsonc
  3. If so, continues looking up the same key, resolving layer by layer
  4. Until no further mapping is found

Example:

workspace/ ← top-level workspace
├── workspace.jsonc ← {"testkey": "/pkgA"}
├── pkgA/
│ ├── workspace.jsonc ← {"testkey": "/pkgB"}
│ └── pkgB/
│ ├── workspace.jsonc ← {"testkey": "/pkgC"}
│ └── pkgC/ ← final target (no workspace.jsonc)

Resolving testkey:

  1. Top-level workspace.jsonc maps → /pkgA (workspace-relative)
  2. pkgA/workspace.jsonc maps → /pkgB (relative to pkgA workspace)
  3. pkgB/workspace.jsonc maps → /pkgC (relative to pkgB workspace)
  4. pkgC has no workspace.jsonc → final resolution to pkgC

Cycle detection:

The compiler maintains a resolution visit stack tracking visited workspace root directories. If a circular mapping is detected (e.g. A→B, B→A), it immediately errors with the full chain:

Error: circular workspace mapping detected: /path/to/A → /path/to/B → /path/to/A

Note: This capability only supports natural cross-package recursive mapping (dependency packages carrying their own workspace.jsonc). It does not support manual chain shorthand within a single file.

Mirror Configuration​

Configure mirror URLs in the mirrors array in package.jsonc to accelerate remote package downloads:

"mirrors": [
"https://mirror.example.com/"
]

Workspace Configuration (workspace.jsonc)​

workspace.jsonc sits at the repo root and describes a single-repo, multi-package workspace as a map of package name -> relative path. When no build is run with no arguments and workspace.jsonc exists, all listed packages are built in parallel; no run executes a single workspace package by name (e.g. no run foo).

{
"foo": "/foo",
"bar": "/bar"
}

no init defines the workspace: if workspace.jsonc is missing it generates an empty object {} and does not create any package.jsonc; if it already exists, it is preserved and not overwritten. Each subsequent no new <name> automatically registers "<name>": "/<name>" into workspace.jsonc.

Private Local Configuration (.workspace.jsonc)​

In addition to the shared workspace.jsonc, Nolang supports a private local configuration file .workspace.jsonc (note the leading .). This file should be added to .gitignore and is used for developer-specific temporary dependency overrides.

Loading logic:

  1. Load the public config workspace.jsonc first
  2. Then load the private config .workspace.jsonc
  3. Same keys: private config overrides public config
  4. New keys: merged together
workspace/
├── workspace.jsonc ← shared project config (version-controlled)
├── .workspace.jsonc ← private local config (add to .gitignore)
├── foo/
└── bar/

Use cases:

  • Team-wide aliases and dependency forwarding rules go in workspace.jsonc (version-controlled)
  • Developers needing to temporarily point a dependency to a local fork can override it in .workspace.jsonc
  • Prevents temporary fork mappings from polluting the project's public config, balancing large-team collaboration with individual development flexibility

Example:

// workspace.jsonc (public, version-controlled)
{
"foo": "/foo",
"bar": "/bar"
}

// .workspace.jsonc (private, local debugging)
{
// Override public config: point foo to a local fork (within workspace)
"foo": "/my-fork-foo",
// Add a private mapping: point to an external fork (OS absolute path, the only escape hatch)
"github.com/lizongying/nolang/core": "/home/lzy/code/fork/core",
// Add a private mapping (within workspace)
"experimental": "/experimental"
}

Effective mappings: foo → /my-fork-foo (private override), bar → /bar (from public), github.com/lizongying/nolang/core → /home/lzy/code/fork/core (private addition, escapes workspace), experimental → /experimental (private addition).

Workspace Boundary Constraints​

Nolang enforces strict workspace boundary controls on path references to ensure project portability and security.

Rule Summary​

Config layer/ workspace-relativeOS absolute path./ ../ relative jump
package.jsonc dependency key✅ Allowed❌ Forbidden❌ Forbidden
workspace.jsonc mapping value✅ Allowed✅ Allowed (escape hatch)✅ Allowed (escape hatch)
.workspace.jsonc mapping value✅ Allowed✅ Allowed (escape hatch)✅ Allowed (escape hatch)
Source code # import path✅ Allowed❌ Forbidden❌ Forbidden

1. package.jsonc (strictly constrained, no workspace escape)​

package.jsonc is the standard baseline configuration for project publishing, CI, and team collaboration, committed to the repository.

"dependencies": {
// ✅ Valid: starts with /, relative to workspace root
"/vendor/test": "*",
// ❌ Forbidden: OS absolute path /home/xxx/...
// ❌ Forbidden: ./ ../ relative jump
}

Constraints:

  • Local paths must start with /xxx, base = workspace root
  • OS absolute paths are not accepted
  • ./ and ../ are not supported
  • Cannot directly reference any directory outside the workspace

Purpose: If external paths were allowed, it would easily lead to "compiles on my machine but fails for others", and also poses security risks of accidentally loading unfamiliar external code.

2. workspace.jsonc + .workspace.jsonc (the only escape hatch)​

Mapping targets support three local path forms:

  • /xxx → workspace-internal directory (same semantics as package.jsonc)
  • ./xxx, ../xxx → resolved relative to workspace root, allowed to escape workspace boundary
  • OS absolute path → allowed to point outside the workspace (forks, external components)
// .workspace.jsonc private config, for local development
{
// Within workspace
"foo": "/vendor/foo",
// Relative jump: point to an external fork
"github.com/lizongying/nolang/core": "../fork/core",
// OS absolute path: point to any external directory
"github.com/lizongying/nolang/utils": "/home/lzy/code/utils"
}

Key logic: package.jsonc has no ability to directly reference external code; to mount source code outside the repository, users must explicitly configure a mapping in the workspace config. This effectively adds a clear, visible switch for "cross-directory loading", with all external dependencies controlled in one place.

3. Source Code Import Constraints​

Source code cannot use OS absolute paths in imports; all external code references must go through workspace aliases or remote package identifiers. ./ and ../ relative jumps are also forbidden.

; ✅ Valid: workspace-relative path
# /vendor/utils.helper

; ✅ Valid: remote package identifier
# github.com/lizongying/nolang/core.process

; ✅ Valid: standard library
# std/fs

; ❌ Forbidden: relative jump
; # ./utils.helper
; # ../lib/helper.process

; ❌ Forbidden: OS absolute path
; # /home/user/code/core.process

Data Flow Demo​

Scenario: A project with workspace root at /code/project wants to use external /code/fork/core

❌ This won't work (package.jsonc forbids it):

// package.jsonc
"/code/fork/core": "*" // invalid: cannot escape workspace

✅ Standard approach (break through boundary via workspace mapping):

// .workspace.jsonc
"github.com/lizongying/nolang/core": "/code/fork/core"
; Source code imports normally
# github.com/lizongying/nolang/core

Workspace Flow​

The compile/execute entry directory is always the workspace directory (where workspace.jsonc resides). The overall flow is:

  1. User runs no build or no run <package-name> from the workspace directory
  2. Compiler reads workspace.jsonc and looks up the package's subdirectory path by name
  3. Loads the package.jsonc in that subdirectory (the package root) and builds/runs relative to it
  4. All import paths (use /path/to/module) are resolved relative to the workspace root (see the path-resolution convention), i.e. the directory containing workspace.jsonc — no longer relative to the importing package.
workspace/ ← workspace directory (workspace.jsonc lives here)
├── workspace.jsonc ← package name -> path mapping
├── foo/ ← package foo
│ ├── package.jsonc ← foo's package root
│ ├── main.no
│ └── lib.no
└── bar/ ← package bar
├── package.jsonc ← bar's package root
└── main.no

Note: Import paths are resolved relative to the workspace root (directory containing workspace.jsonc), not the package's package.jsonc directory. If a nested package.jsonc exists in a subdirectory within a package, LoadPackage searches upward and uses the nearest package.jsonc as the package root, but import resolution is uniformly based on the workspace root.


Projects​

  • notools — A collection of common Unix command-line tools implemented in Nolang, including cat, ls, grep, wc, head, tail, and more. Demonstrates Nolang's real-world system programming capabilities.