Nanyx is a statically-typed, functional-first programming language designed for expressive workflows, clean syntax, and strong type safety. It combines the best ideas from functional programming with practical features for real-world development, featuring Hindley-Milner type inference, context-based effects, powerful pattern matching, and a unique pipeline operator for composable transformations.
- Overview
- Key Features
- Quick Start
- Language Features
- Project Structure
- Building from Source
- Running the Compiler
- Examples
- Design Principles
- Development Status
- Contributing
- License
Nanyx is built on the principle that code should be clear, composable, and correct. It takes inspiration from languages like F#, Haskell, OCaml, and Elm while maintaining its own unique identity through features like context-based effects and pipeline-centric programming.
Current Implementation Status:
- Full parser with comprehensive AST
- Hindley-Milner type inference engine
- Type checker with generics support
- JavaScript transpiler
- Command-line compiler interface
- Standard library (in progress)
- VS Code extension with syntax highlighting
The repository includes the full language specification, compiler implementation in F#, example programs, and documentation for learning the language.
Everything in Nanyx is an expression that returns a value. No statements, just composable expressions that make code flow naturally.
The \ operator lets you chain transformations in a readable, left-to-right manner:
data \parse \validate \transform \save
Exhaustive pattern matching on records, tag unions, literals, and more:
match result
| #some(value) -> process(value)
| #error(msg) -> logError(msg)
Manage side effects explicitly through contexts, providing algebraic effect handlers without the complexity:
context Console = (println: string -> ())
def greet: <Console> string -> () = { name ->
println("Hello, {name}!")
}
Full Hindley-Milner type inference means you rarely need to write type annotations, but you can when it improves clarity.
Express your domain model precisely with structural records and discriminated unions:
type User = (name: string, email: string, role: #admin | #user)
Concise syntax for common lambda patterns:
numbers \map { * 2 } -- multiply by 2
users \filter { .age > 18 } -- access property
items \map { > 10 } -- comparison
Small core with powerful abstractions. The language prioritizes having fewer features that compose well over many single-purpose features.
- .NET 9.0 or later
- (Optional) VS Code for syntax highlighting
Clone the repository:
git clone https://github.com/Richiban/nyx.git
cd nyxBuild the compiler:
cd compiler
dotnet build Nanyx.Compiler.slnRun tests to verify the installation:
dotnet test Nanyx.Compiler.slnCreate a file hello.nyx:
module Hello
def message = "Hello, Nanyx!"
def main = {
message \dbg
}
main()
Compile and type-check it:
dotnet run --project compiler/Nanyx.Compiler.Cli/NyxCompiler.Cli.fsproj hello.nyxNanyx uses clean, minimal syntax with significant indentation:
module MyModule
-- This is a single-line comment
---
This is a
multi-line comment
---
-- Value definitions
def pi = 3.14159
def greeting = "Hello"
def count = 42
def isActive = true
-- Function definition with type annotation
def add: (int, int) -> int = { x, y ->
x + y
}
Functions are first-class values in Nanyx:
-- Named function
def double: int -> int = { x -> x * 2 }
-- Multi-parameter function
def multiply: (int, int) -> int = { x, y -> x * y }
-- Higher-order function
def apply: ((a -> b), a) -> b = { f, x -> f(x) }
-- Shorthand lambda for binary operators
def doubles = [1, 2, 3] \map { * 2 }
-- Shorthand lambda for property access
def names = users \map { .name }
-- Shorthand lambda for comparison
def adults = users \filter { .age > 18 }
The pipeline operator \ passes the result of one expression as the first argument to the next function:
-- Simple pipeline
42 \double \dbg -- prints 84
-- Pipeline with additional arguments
data \process(config, options) \validate \save
-- Chaining multiple transformations
"hello world"
\split(" ")
\map { .toUpperCase() }
\join("-")
\dbg -- prints "HELLO-WORLD"
-- Pipeline in function composition
def processUser = { user ->
user
\validateEmail
\normalizeData
\saveToDatabase
\sendWelcomeEmail
}
Nanyx features Hindley-Milner type inference with optional type annotations:
-- Type inference (no annotation needed)
def add = { x, y -> x + y } -- inferred as: (number, number) -> number
-- Explicit type annotations for clarity
def length: string -> int = { s -> s.length }
-- Type aliases
type UserId = int
type Email = string
-- Record types
type Person = (
name: string
age: int
email: Email
)
-- Function types
type Validator(a) = a -> bool
type Transformer(a, b) = a -> b
Pattern matching is exhaustive and type-safe:
-- Matching on tag unions
def describe: Option(int) -> string = { opt ->
match opt
| #some(x) -> "Got value: {x}"
| #none -> "No value"
}
-- Matching on records
def greet: Person -> string = { person ->
match person
| (name = "Alice", age = age) -> "Hi Alice, age {age}!"
| (name = name, age = a) if a >= 18 -> "Hello adult {name}"
| (name = name, _) -> "Hi young {name}"
}
-- Matching on literals
def classify: int -> string = { x ->
match x
| 0 -> "zero"
| 1 -> "one"
| n if n < 0 -> "negative"
| n if n > 100 -> "large"
| _ -> "other"
}
-- Matching with comparison operators
def sign: int -> string = { x ->
match x
| < 0 -> "negative"
| > 0 -> "positive"
| _ -> "zero"
}
Records are structural types with named fields:
-- Record creation
def user = (
name = "Alice"
age = 30
email = "alice@example.com"
)
-- Record access
def userName = user.name
-- Record update (creates new record)
def olderUser = user with age = 31
-- Tuples (anonymous records with numbered fields)
def point = (10, 20)
def coords = (x = 10, y = 20) -- same as tuple but with names
-- Tuple destructuring
def (x, y) = point
Tag unions (also known as sum types or discriminated unions) represent values that can be one of several variants:
-- Simple tag union
type Result(a) =
| #some(a)
| #error(string)
-- Tag union with multiple variants
type Shape =
| #circle(float)
| #rectangle(float, float)
| #triangle(float, float)
-- Using tag unions
def area: Shape -> float = { shape ->
match shape
| #circle(r) -> 3.14159 * r * r
| #rectangle(w, h) -> w * h
| #triangle(b, h) -> 0.5 * b * h
}
-- Option type (no null in Nanyx!)
type Option(a) =
| #some(a)
| #none
def findUser: UserId -> Option(User) = { id ->
-- lookup logic
#some(user) -- or #none if not found
}
Contexts provide a way to handle effects explicitly without threading parameters through every function:
-- Define a context type
context type Console = (
println: string -> ()
readLine: () -> string
)
-- Use context in function signature
def greet: <Console> string -> () = { name ->
println("Hello, {name}!")
println("How are you?")
def response = readLine()
println("You said: {response}")
}
-- Provide context implementation
def runGreeting = {
use Console(
println = { s -> -- actual console output }
readLine = { -- actual console input }
)
greet("Alice")
}
-- State effect example
context type State(a) = (
get: () -> a
set: a -> ()
)
def counter: <State(int)> () -> int = {
def current = get()
set(current + 1)
current
}
-- Exception handling via contexts
context type Raise = (
ctx raise: string -> a
)
def safeDivide: <Raise> (int, int) -> int = { x, y ->
if y == 0 -> raise("Division by zero")
else -> x / y
}
def handleDivision = {
use Raise(
ctx raise = { msg ->
dbg("Error: {msg}")
resume 0 -- resume with default value
}
)
safeDivide(10, 0) -- Returns 0 instead of crashing
}
Nanyx supports parametric polymorphism with automatic generalization:
-- Generic function (type parameter α is inferred)
def identity: a -> a = { x -> x }
-- Generic list operations
def map: (list(a), (a -> b)) -> list(b) = { xs, f ->
match xs
| [] -> []
| [head, ...tail] -> [f(head), ...map(tail, f)]
}
-- Multiple type parameters
def zip: (list(a), list(b)) -> list((a, b)) = { xs, ys ->
match (xs, ys)
| ([], _) -> []
| (_, []) -> []
| ([x, ...xs2], [y, ...ys2]) -> [(x, y), ...zip(xs2, ys2)]
}
-- Constrained generics with traits
type Monoid(a, ~combine, ~neutral) = (
~neutral: a
~combine: (a, a) -> a
)
def fold: [Monoid(a, `+`, `0`)] (list(a)) -> a = { xs ->
xs \fold(`0`, `+`)
}
Nanyx supports refinement types for more precise specifications:
-- Constrained type definition
type Email = s :: string where isValidEmail(s)
def isValidEmail(s :: string) = s.contains("@") && s.contains(".")
-- Natural numbers
type Nat = n :: int where n >= 0
-- Ordered pairs
type OrderedPair(n :: number) = (x :: n, y :: n) where x < y
-- Age constraint
type Age = n :: int where n >= 0 && n <= 150
The Nanyx repository is organized as follows:
nyx/
├── compiler/ # Compiler implementation (F#)
│ ├── Nyx.Parser/ # FParsec-based parser
│ ├── Nanyx.Compiler/ # Type checker and inference engine
│ ├── Nanyx.Compiler.Cli/ # Command-line interface
│ ├── Nyx.Transpiler.JS/ # JavaScript code generator
│ ├── Nyx.Parser.Tests/ # Parser unit and integration tests
│ ├── Nanyx.Compiler.Tests/# Compiler and type checker tests
│ └── Nyx.Transpiler.JS.Tests/ # Transpiler tests
├── examples/ # Example Nanyx programs
│ ├── fizzbuzzbaz.nyx # FizzBuzz variant with configurable rules
│ ├── todo-web-app.nyx # Todo application example
│ ├── stringTransformation.nyx # String casing transformations
│ └── ... # Many more examples
├── docs/ # Documentation website (Next.js)
│ └── src/app/docs/ # MDX documentation pages
├── docs-md/ # Additional documentation
├── llm/ # Language specification for LLMs
├── extension/ # VS Code extension
└── README.md # This file
- Nyx.Parser: Parses
.nyxsource files into an Abstract Syntax Tree (AST) - Nanyx.Compiler: Performs type inference and type checking using Hindley-Milner algorithm
- Nyx.Transpiler.JS: Transpiles type-checked AST to JavaScript
- Nanyx.Compiler.Cli: Command-line tool for compiling Nanyx programs
- .NET SDK 9.0 or later (for parser tests)
- .NET SDK 10.0 or later (for compiler and transpiler)
-
Clone the repository:
git clone https://github.com/Richiban/nyx.git cd nyx -
Build the entire solution:
cd compiler dotnet build Nanyx.Compiler.sln -
Run all tests:
dotnet test Nanyx.Compiler.slnExpected output: All 56+ tests should pass, including:
- Parser unit tests (literals, identifiers, functions, lambdas, operators)
- Parser integration tests (full file parsing)
- Compiler tests (type inference, type checking)
- Transpiler tests (JavaScript code generation)
The Nanyx compiler CLI can parse, type-check, and report on .nyx files:
# Basic usage
dotnet run --project compiler/Nanyx.Compiler.Cli/NyxCompiler.Cli.fsproj myfile.nyx
# From the compiled binary
cd compiler/Nanyx.Compiler.Cli/bin/Debug/net10.0
./NyxCompiler.Cli myfile.nyxThe compiler will output:
- Current compilation phase
- Any type errors or warnings
- Inferred types for definitions
- Compilation success/failure status
Given example.nyx:
module Example
def add: (int, int) -> int = { x, y -> x + y }
def result = add(5, 10)
Run the compiler:
dotnet run --project compiler/Nanyx.Compiler.Cli/NyxCompiler.Cli.fsproj example.nyxOutput:
Phase: Typed
result: int
The examples/ directory contains numerous Nanyx programs demonstrating various features:
def numberGame: (Map(int, string)) -> (int -> string) = { rules ->
{ i ->
rules
\flatMap(playNumber(i))
\String.join
\match
| "" -> i.toString()
| s -> s
}
}
def fizzbuzz = numberGame(dict {
3 => "fizz"
5 => "buzz"
7 => "baz"
})
def freqs: string -> list(float) = { s ->
def lowers = list('a', 'z')
def occurs = lowers \map { c -> s \count(c \string) }
def total = occurs \sum
occurs \map { i -> percent(i, total) }
}
def uncaesar: string -> string = { s ->
def table = freqs(s)
def chitab = [0..25] \map { n ->
chisqr(table \rotate(n), english)
}
def shift = chitab \indexOf { == chitab \min } \negate
s \encode(shift)
}
type Casing = #camel | #pascal | #kebab | #snake | #title
def rejoin: (list(string), Casing) -> string = { words, casing ->
match casing
| #kebab -> words \map { .toLowerCase() } \String.join('-')
| #snake -> words \map { .toLowerCase() } \String.join('_')
| #pascal -> words \map { .capitalize() } \String.join
| #camel ->
def [first, ...rest] = words
[first.toLowerCase(), ...rest \map { .capitalize() }] \String.join
}
context type State(a) = (
get: () -> a
set: a -> ()
)
def sumdown: <State(int)> int -> int = { sum = 0 ->
def i = get()
if i <= 0 -> sum
else ->
set(i - 1)
sumdown(sum + i)
}
def runSumdown = {
state(10) { -- Initialize state with 10
sumdown() -- Returns 55 (10+9+8+...+1)
}
}
Nanyx is built on a foundation of carefully considered design principles:
The language prioritizes correctness over convenience. Static type checking, exhaustive pattern matching, and no null values ensure fewer runtime errors.
Nanyx favors conceptual simplicity over ease of initial use. The language may take longer to learn, but its consistency and orthogonality pay dividends over time.
Warnings are treated as errors in release builds, but debug builds can still execute. This maintains a tight feedback loop while ensuring production code is warning-free.
No statements, only expressions. If-then-else, match, and blocks all return values, making the language more composable.
The context system cleanly separates pure functions from effectful computations, making code easier to reason about and test.
Sane defaults, consistent syntax, and predictable behavior. When there's no obvious default, the programmer must be explicit.
No global variables or shared mutable state. All state must be explicitly threaded through the program or managed via contexts.
Modern best practices: use Option types instead of null, no runtime reflection, and no automatic type conversions.
Hindley-Milner type inference means you rarely write types, but exported functions require signatures for documentation and API stability.
Type syntax mirrors value syntax: f(a, b) for values, F(A, B) for types. Consistency makes the language easier to learn.
For the complete list of design principles, see docs-md/50.principles.md.
Nanyx is under active development. Current status:
Completed:
- Core parser with full language syntax
- Abstract Syntax Tree (AST) representation
- Hindley-Milner type inference
- Type checker with generics
- Pattern matching type checking
- JavaScript transpiler
- Basic CLI compiler
- Comprehensive test suite (56+ tests)
- Example programs
- Language documentation
In Progress:
- Standard library implementation
- Enhanced error messages
- Documentation website
- VS Code extension improvements
- REPL (Read-Eval-Print Loop)
Planned:
- Native code generation
- Package manager
- Build tool
- Debugger integration
- Language Server Protocol (LSP) implementation
- More comprehensive standard library
- Constraint/refinement type checking
Contributions to Nanyx are welcome! Here's how you can help:
- Report bugs: Open an issue describing the problem and steps to reproduce
- Suggest features: Propose new features or improvements via issues
- Write code: Fork the repository, make changes, and submit a pull request
- Improve docs: Documentation improvements are always appreciated
- Create examples: Add example programs showcasing Nanyx features
- Fork and clone the repository
- Create a feature branch:
git checkout -b feature/my-feature - Make your changes
- Run tests:
dotnet test compiler/Nanyx.Compiler.sln - Commit with clear messages
- Push to your fork
- Open a pull request
- Follow F# conventions for the compiler code
- Use clear, descriptive names
- Add tests for new features
- Update documentation as needed
- Documentation:
docs/- Next.js-based documentation site - Language Spec:
llm/README.md- Full language specification - Examples:
examples/- Sample Nanyx programs - Compiler README:
compiler/README.md- Detailed compiler documentation - Design Principles:
docs-md/50.principles.md- Language design philosophy
This project is open source. See the LICENSE file for details.
Nanyx - A language for clear, composable workflows.
Built with F#